Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions apps/app/components/Swap/AtomicChat/Actions/ManualClaim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import useWallet from "@/hooks/useWallet";
import { WalletActionButton } from "../../buttons";
import posthog from "posthog-js";
import { SwapViewType } from ".";
import { useWalletClient } from "wagmi";
import { createHTLCClient } from "@/lib/htlc/createHTLCClient";
import { useRpcConfigStore } from "@/stores/rpcConfigStore";
import { useHTLCWriteClient } from "@/hooks/htlc/useHTLCWriteClient";

export const ManualClaimAction: FC<{ type: SwapViewType }> = ({ type }) => {
const {
Expand All @@ -24,8 +22,7 @@ export const ManualClaimAction: FC<{ type: SwapViewType }> = ({ type }) => {

const { provider } = useWallet(destination_network, 'withdrawal');
const wallet = provider?.activeWallet;
const { data: walletClient } = useWalletClient();
const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls);
const createWriteClient = useHTLCWriteClient();

const handleManualClaim = async () => {
try {
Expand All @@ -35,12 +32,10 @@ export const ManualClaimAction: FC<{ type: SwapViewType }> = ({ type }) => {
if (!destination_asset) throw new Error("No destination asset");
if (!destAtomicContract) throw new Error("No destination contract");
if (!address) throw new Error("No destination address");
if (!walletClient) throw new Error("No wallet client");

if (provider?.activeWallet && (provider.activeWallet.chainId != destination_network.chainId) && provider.switchChain)
await provider.switchChain(provider.activeWallet, destination_network.chainId);

const writeClient = createHTLCClient(destination_network, getEffectiveRpcUrls, walletClient);
const writeClient = await createWriteClient(destination_network, wallet);

const txHash = await writeClient.claim({
type: destination_asset.contractAddress ? 'erc20' : 'native',
Expand Down
80 changes: 28 additions & 52 deletions apps/app/components/Swap/AtomicChat/Actions/UserActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import { LockStatus } from "@/Models/phtlc/PHTLC";
import { SwapQuote } from "@/lib/trainApiClient";
import { useSwapStore } from "@/stores/swapStore";
import { SwapViewType } from ".";
import { useConfig, useWalletClient } from "wagmi";
import { useConfig } from "wagmi";
import { useSecretDerivation } from "@/context/secretDerivationContext";
import { secretToHashlock } from "@train-protocol/sdk";
import { createHTLCClient } from "@/lib/htlc/createHTLCClient";
import { useRpcConfigStore } from "@/stores/rpcConfigStore";
import { useHTLCWriteClient } from "@/hooks/htlc/useHTLCWriteClient";
import { useSelectedAccount } from "@/context/swapAccounts";
import { Address } from "@/lib/address";

Expand All @@ -24,10 +23,9 @@ export const UserCommitAction: FC<UserCommitActionProps> = ({ quote, type }) =>
const { source_network, destination_network, amount, address, source_asset, destination_asset, onUserLock, hashlock, setError, srcAtomicContract } = useAtomicState();
const { provider } = useWallet(source_network, 'withdrawal')
const wallet = provider?.activeWallet
const { data: walletClient } = useWalletClient()
const { deriveSecret } = useSecretDerivation()
const config = useConfig()
const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls)
const createWriteClient = useHTLCWriteClient()
const sourceAccount = useSelectedAccount('from', source_network?.caip2Id)
const sourceWallet = (sourceAccount?.address && source_network) ? provider?.connectedWallets?.find(w => Address.equals(w.address, sourceAccount?.address, source_network)) : undefined

Expand All @@ -37,48 +35,20 @@ export const UserCommitAction: FC<UserCommitActionProps> = ({ quote, type }) =>

const handleUserLock = async () => {
try {
if (!amount) {
throw new Error("No amount specified")
}
if (!address) {
throw new Error("Please enter a valid address")
}
if (!destination_network) {
throw new Error("No destination chain")
}
if (!source_network) {
throw new Error("No source chain")
}
if (!source_asset) {
throw new Error("No source asset")
}
if (!destination_asset) {
throw new Error("No destination asset")
}
if (!provider) {
throw new Error("No source_provider")
}
if (!atomicContract) {
throw new Error("No atomic contract")
}
if (!destLpAddress || !srcLpAddress) {
throw new Error("No lp address")
}
if (!walletClient) {
throw new Error("No wallet client")
}
if(!quote || !source_network || !sourceWallet || !provider?.activeWallet || !amount || !address || !destination_network || !destination_asset || !source_asset || !atomicContract || !destLpAddress || !srcLpAddress) throw new Error("Missing params")

if (provider && sourceWallet && (sourceWallet.chainId != source_network.chainId) && provider.switchChain) await provider.switchChain(sourceWallet, source_network.chainId)

const { secret, nonce } = await deriveSecret({
wallet: provider?.activeWallet,
wallet: provider.activeWallet,
config
})
const htlcHashlock = secretToHashlock(secret)
const hashlock = secretToHashlock(secret)

const writeClient = createHTLCClient(source_network, getEffectiveRpcUrls, walletClient)
const writeClient = await createWriteClient(source_network, sourceWallet)

const result = await writeClient.createHTLC({
...resolveQuote(quote),
address,
amount: amount.toString(),
destinationChain: destination_network.caip2Id,
Expand All @@ -91,16 +61,8 @@ export const UserCommitAction: FC<UserCommitActionProps> = ({ quote, type }) =>
decimals: source_asset.decimals,
atomicContract,
chainId: source_network.chainId,
solverData: quote?.signature,
quoteExpiry: quote?.quoteExpirationTimestampInSeconds,
rewardToken: quote?.reward ? quote?.reward.rewardToken : undefined,
rewardRecipient: quote?.reward ? quote?.reward.rewardRecipientAddress : undefined,
rewardAmount: quote?.reward ? quote?.reward.amount : undefined,
rewardTimelockDelta: quote?.reward ? quote?.reward.rewardTimelockTimeSpanInSeconds : undefined,
destinationAmount: quote?.receiveAmount,
timelockDelta: quote?.timelock.timelockTimeSpanInSeconds,
hashlock: htlcHashlock,
nonce,
hashlock,
nonce
})
if (result?.hashlock && result?.hash) {
onUserLock(
Expand Down Expand Up @@ -143,12 +105,26 @@ export const UserCommitAction: FC<UserCommitActionProps> = ({ quote, type }) =>
</div>
}

const resolveQuote = (quote: SwapQuote) => {
return {
solverData: quote?.signature,
quoteExpiry: quote?.quoteExpirationTimestampInSeconds,
rewardToken: quote?.reward ? quote?.reward.rewardToken : undefined,
rewardRecipient: quote?.reward ? quote?.reward.rewardRecipientAddress : undefined,
rewardAmount: quote?.reward ? quote?.reward.amount : undefined,
rewardTimelockDelta: quote?.reward ? quote?.reward.rewardTimelockTimeSpanInSeconds : undefined,
destinationAmount: quote?.receiveAmount,
timelockDelta: quote?.timelock.timelockTimeSpanInSeconds,
}
}

export const UserRefundAction: FC<{ type: SwapViewType }> = ({ type }) => {
const { source_network, hashlock, sourceDetails, source_asset, setError, refundTxId, srcAtomicContract } = useAtomicState()
const { provider: source_provider } = useWallet(source_network, 'withdrawal')
const { data: walletClient } = useWalletClient()
const sourceAccount = useSelectedAccount('from', source_network?.caip2Id)
const sourceWallet = (sourceAccount?.address && source_network) ? source_provider?.connectedWallets?.find(w => Address.equals(w.address, sourceAccount?.address, source_network)) : undefined
const updateSwap = useSwapStore(s => s.updateSwap)
const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls)
const createWriteClient = useHTLCWriteClient()

const [requestedRefund, setRequestedRefund] = useState(false)

Expand All @@ -161,12 +137,12 @@ export const UserRefundAction: FC<{ type: SwapViewType }> = ({ type }) => {
if (!sourceDetails) throw new Error("No commitment")
if (!source_asset) throw new Error("No source asset")
if (!srcAtomicContract) throw new Error("No atomic contract")
if (!walletClient) throw new Error("No wallet client")
if (!sourceWallet) throw new Error("No wallet client")

if (source_provider?.activeWallet && (source_provider.activeWallet.chainId != source_network.chainId) && source_provider.switchChain)
await source_provider.switchChain(source_provider.activeWallet, source_network.chainId)

const writeClient = createHTLCClient(source_network, getEffectiveRpcUrls, walletClient)
const writeClient = await createWriteClient(source_network, sourceWallet)

const res = await writeClient.refund({
type: (source_asset?.contractAddress && source_asset.contractAddress !== '0x0000000000000000000000000000000000000000') ? 'erc20' : 'native',
Expand Down
2 changes: 1 addition & 1 deletion apps/app/context/secretDerivationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
registerPasskey,
PrfSupportResult
} from '@/lib/htlc/secretDerivation';
import { deriveKeyFromEvmSignature } from '@/lib/htlc/secretDerivation/walletSign/evm';
import { useSecretDerivationStore, DerivationStatus } from '@/stores/secretDerivationStore';
import { DerivationMethod, deriveSecretFromTimelock } from '@train-protocol/sdk';
import { deriveKeyFromEvmSignature } from '@/lib/htlc/secretDerivation/walletSign/evm';

interface SecretDerivationContextValue {
method: DerivationMethod | null;
Expand Down
44 changes: 44 additions & 0 deletions apps/app/hooks/htlc/useHTLCWriteClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useCallback } from 'react'
import { useConfig } from 'wagmi'
import { getWalletClient } from 'wagmi/actions'
import { createHTLCClient as createClient, IHTLCClient } from '@train-protocol/sdk'
import '@train-protocol/sdk-evm'
import type { EvmSigner } from '@train-protocol/sdk-evm'
import { Network } from '../../Models/Network'
import { Wallet } from '@/Models/WalletProvider'
import { useRpcConfigStore } from '@/stores/rpcConfigStore'
import resolveChain from '@/lib/resolveChain'

/** Hook that returns a factory for creating HTLC write clients with a signer */
export function useHTLCWriteClient() {
const config = useConfig()
const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls)

return useCallback(async (network: Network, wallet?: Wallet): Promise<IHTLCClient> => {
const chainType = network.caip2Id.split(':')[0]
const rpcUrl = getEffectiveRpcUrls(network)[0] ?? network.nodes?.[0]?.url ?? ''
const chain = resolveChain(network)

let signer: EvmSigner | undefined
if (wallet?.address && chain) {
const walletClient = await getWalletClient(config, {
chainId: chain.id,
account: wallet.address as `0x${string}`,
})
signer = {
address: walletClient.account.address,
sendTransaction: async (tx) => {
return walletClient.sendTransaction({
to: tx.to as `0x${string}`,
data: tx.data as `0x${string}`,
value: tx.value,
chain,
account: walletClient.account,
})
},
}
}

return createClient(chainType, { rpcUrl, signer })
}, [config, getEffectiveRpcUrls])
}
12 changes: 3 additions & 9 deletions apps/app/lib/htlc/createHTLCClient.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
import { WalletClient } from 'viem'
import { EvmHTLCClient, IHTLCClient } from '@train-protocol/sdk'
import { createHTLCClient as createClient, IHTLCClient } from '@train-protocol/sdk'
import '@train-protocol/sdk-evm' // side-effect: registers eip155 htlc client
import { Network } from '../../Models/Network'

export function createHTLCClient(
network: Network,
getEffectiveRpcUrls: (network: Network) => string[],
walletClient?: WalletClient
): IHTLCClient {
const chainType = network.caip2Id.split(':')[0]
const rpcUrl = getEffectiveRpcUrls(network)[0] ?? network.nodes?.[0]?.url ?? ''

switch (chainType) {
case 'eip155':
return new EvmHTLCClient({ rpcUrl, walletClient })
default:
throw new Error(`Unsupported chain type: ${chainType} for network ${network.caip2Id}`)
}
return createClient(chainType, { rpcUrl })
}
16 changes: 8 additions & 8 deletions apps/app/lib/htlc/secretDerivation/walletSign/evm.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Compatibility shim — wraps @train-protocol/sdk deriveKeyFromEvmSignature with the old wagmi Config API
// Compatibility shim — wraps SDK wallet sign registry with the Wagmi Config API
import { getAccount } from '@wagmi/core'
import { Config } from 'wagmi'
import {
deriveKeyFromEvmSignature as sdkDeriveKey,
getEvmTypedData as sdkGetEvmTypedData,
} from '@train-protocol/sdk'
import { deriveKeyFromWallet } from '@train-protocol/sdk'
import '@train-protocol/sdk-evm' // side-effect: registers evm wallet sign
import { getEvmTypedData as sdkGetEvmTypedData } from '@train-protocol/sdk-evm'

const isSandbox = process.env.NEXT_PUBLIC_API_VERSION === 'sandbox'

Expand All @@ -21,8 +20,9 @@ export const deriveKeyFromEvmSignature = async (
request: (args: { method: string; params: unknown[] }) => Promise<unknown>
}

return sdkDeriveKey(provider, address, {
sandbox: isSandbox,
currentChainId: account.chainId,
return deriveKeyFromWallet('eip155', {
provider,
address,
options: { sandbox: isSandbox, currentChainId: account.chainId },
})
}
1 change: 1 addition & 0 deletions apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"@ton/ton": "^13.11.1",
"@tonconnect/ui-react": "^2.0.0-beta.4",
"@train-protocol/sdk": "workspace:^",
"@train-protocol/sdk-evm": "workspace:^",
"@uidotdev/usehooks": "^2.4.1",
"@vercel/analytics": "^1.5.0",
"@walletconnect/ethereum-provider": "2.21.8",
Expand Down
43 changes: 43 additions & 0 deletions packages/sdk-evm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@train-protocol/sdk-evm",
"version": "0.1.0",
"description": "Train Protocol SDK — EVM HTLC client",
"type": "module",
"main": "dist/esm/index.js",
"types": "dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
}
},
"sideEffects": [
"./dist/esm/index.js"
],
"files": [
"dist",
"dist/**/*"
],
"scripts": {
"build": "pnpm clean && pnpm build:esm+types",
"build:esm+types": "tsc --project tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types",
"clean": "rimraf dist tsconfig.tsbuildinfo",
"dev": "tsc --project tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types --watch",
"check:types": "tsc --noEmit"
},
"dependencies": {
"ox": "^0.13.2"
},
"peerDependencies": {
"@train-protocol/sdk": "workspace:^"
},
"devDependencies": {
"@train-protocol/sdk": "workspace:^",
"@types/node": "^20",
"rimraf": "^6.0.1",
"typescript": "catalog:"
},
"engines": {
"node": ">=18"
}
}
27 changes: 27 additions & 0 deletions packages/sdk-evm/src/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { AbiFunction, AbiEvent } from 'ox'
import HTLCAbi from './abis/EVM_HTLC.json'

// Pre-parse HTLC contract functions from ABI
export const htlcFunctions = {
getUserLock: AbiFunction.fromAbi(HTLCAbi, 'getUserLock'),
getSolverLock: AbiFunction.fromAbi(HTLCAbi, 'getSolverLock'),
getSolverLockCount: AbiFunction.fromAbi(HTLCAbi, 'getSolverLockCount'),
userLock: AbiFunction.fromAbi(HTLCAbi, 'userLock'),
refundUser: AbiFunction.fromAbi(HTLCAbi, 'refundUser'),
redeemSolver: AbiFunction.fromAbi(HTLCAbi, 'redeemSolver'),
}

// Pre-parse HTLC events from ABI
export const htlcEvents = {
UserLocked: AbiEvent.fromAbi(HTLCAbi, 'UserLocked'),
}

// ERC20 functions — inline instead of importing the broken StarkNet ABI
export const erc20Functions = {
allowance: AbiFunction.from(
'function allowance(address owner, address spender) view returns (uint256)'
),
approve: AbiFunction.from(
'function approve(address spender, uint256 amount) returns (bool)'
),
}
Loading