From 943cfaea26d9a2803d8c12073ec5d1cd04643523 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Thu, 5 Mar 2026 22:30:44 +0400 Subject: [PATCH 01/10] feat: add Solana blockchain support with HTLC client and wallet integration - Implemented SolanaHTLCClient and registration functions in index.ts - Created login module for Solana wallet signing - Added transaction builder for user lock transactions - Defined types for Solana signer and HTLC client configuration - Introduced utility function for converting secrets to buffers - Configured TypeScript settings for the Solana package - Updated pnpm lockfile to include Solana dependencies --- .../ConnectedWallets/ConnectWalletButton.tsx | 2 +- .../Input/Address/AddressPicker/index.tsx | 2 +- .../Swap/AtomicChat/Actions/UserActions.tsx | 4 +- .../components/WalletModal/ConnectorsList.tsx | 12 +- .../WalletModal/MultichainConnectorPicker.tsx | 4 +- apps/app/context/swapAccounts.tsx | 14 +- apps/app/helpers/getSettings.ts | 41 + apps/app/hooks/htlc/useHTLCWriteClient.ts | 22 +- apps/app/hooks/htlc/useUserLockPolling.tsx | 1 - apps/app/hooks/useConnectors.ts | 4 +- apps/app/hooks/useFee.ts | 27 + apps/app/lib/NetworkSettings.ts | 6 + apps/app/lib/gases/gasResolver.ts | 2 + .../lib/gases/providers/solanaGasProvider.ts | 55 +- apps/app/lib/knownIds.ts | 9 +- apps/app/lib/wallets/evm/useEVM.ts | 8 +- .../lib/wallets/solana/nativeAnchorHTLC.ts | 817 ---- .../app/lib/wallets/solana/tokenAnchorHTLC.ts | 1220 ------ .../lib/wallets/solana/transactionBuilder.ts | 216 - apps/app/lib/wallets/solana/useAtomicSVM.ts | 260 -- apps/app/lib/wallets/solana/useSVM.tsx | 2 +- apps/app/package.json | 1 + apps/app/pages/_app.js | 1 + packages/blockchains/solana/package.json | 43 + packages/blockchains/solana/src/client.ts | 514 +++ .../blockchains/solana/src/idl/trainHtlc.ts | 3611 +++++++++++++++++ packages/blockchains/solana/src/index.ts | 31 + .../blockchains/solana/src/login/index.ts | 1 + .../solana/src/login/wallet-sign.ts | 28 + .../solana/src/transactionBuilder.ts | 126 + packages/blockchains/solana/src/types.ts | 19 + packages/blockchains/solana/src/utils.ts | 7 + packages/blockchains/solana/tsconfig.json | 17 + pnpm-lock.yaml | 28 + 34 files changed, 4579 insertions(+), 2576 deletions(-) delete mode 100644 apps/app/lib/wallets/solana/nativeAnchorHTLC.ts delete mode 100644 apps/app/lib/wallets/solana/tokenAnchorHTLC.ts delete mode 100644 apps/app/lib/wallets/solana/transactionBuilder.ts delete mode 100644 apps/app/lib/wallets/solana/useAtomicSVM.ts create mode 100644 packages/blockchains/solana/package.json create mode 100644 packages/blockchains/solana/src/client.ts create mode 100644 packages/blockchains/solana/src/idl/trainHtlc.ts create mode 100644 packages/blockchains/solana/src/index.ts create mode 100644 packages/blockchains/solana/src/login/index.ts create mode 100644 packages/blockchains/solana/src/login/wallet-sign.ts create mode 100644 packages/blockchains/solana/src/transactionBuilder.ts create mode 100644 packages/blockchains/solana/src/types.ts create mode 100644 packages/blockchains/solana/src/utils.ts create mode 100644 packages/blockchains/solana/tsconfig.json diff --git a/apps/app/components/Input/Address/AddressPicker/ConnectedWallets/ConnectWalletButton.tsx b/apps/app/components/Input/Address/AddressPicker/ConnectedWallets/ConnectWalletButton.tsx index 76819e71..d5ed0158 100644 --- a/apps/app/components/Input/Address/AddressPicker/ConnectedWallets/ConnectWalletButton.tsx +++ b/apps/app/components/Input/Address/AddressPicker/ConnectedWallets/ConnectWalletButton.tsx @@ -25,7 +25,7 @@ const ConnectWalletButton: FC = ({ provider, onConnect }) => { ) diff --git a/apps/app/context/swapAccounts.tsx b/apps/app/context/swapAccounts.tsx index 4a1c77c5..f7cadc74 100644 --- a/apps/app/context/swapAccounts.tsx +++ b/apps/app/context/swapAccounts.tsx @@ -52,10 +52,10 @@ export function SwapAccountsProvider({ children }: PickerAccountsProviderProps) if (!hasWallet(provider)) return null; const selectedWallet = provider.connectedWallets?.find(wallet => wallet.id === selectedSourceAccounts.find(acc => - acc.providerName === provider.name && wallet.addresses.some(a => a === acc.address))?.id && wallet.addresses) + acc.providerName === provider.id && wallet.addresses.some(a => a === acc.address))?.id && wallet.addresses) const wallet = selectedWallet || provider.activeWallet; - const selectedAccountAddress = selectedWallet ? selectedSourceAccounts.find(acc => acc.providerName === provider.name && acc.id === selectedWallet.id)?.address : undefined + const selectedAccountAddress = selectedWallet ? selectedSourceAccounts.find(acc => acc.providerName === provider.id && acc.id === selectedWallet.id)?.address : undefined const address = selectedAccountAddress ? selectedAccountAddress : wallet.address; const res = ResolveWalletSwapAccount(provider, wallet, address); @@ -79,7 +79,7 @@ export function SwapAccountsProvider({ children }: PickerAccountsProviderProps) const destinationAccounts: AccountIdentity[] = useMemo(() => { return providers.map(provider => { const manuallyAdded = selectedDestAccounts.find( - acc => acc.providerName === provider.name && acc.id === 'manually_added' + acc => acc.providerName === provider.id && acc.id === 'manually_added' ); if (manuallyAdded) { @@ -89,10 +89,10 @@ export function SwapAccountsProvider({ children }: PickerAccountsProviderProps) if (!hasWallet(provider)) return null; const selectedWallet = provider.connectedWallets?.find(wallet => wallet.id === selectedDestAccounts.find(acc => - acc.providerName === provider.name && wallet.addresses.some(a => a === acc.address))?.id && wallet.addresses) + acc.providerName === provider.id && wallet.addresses.some(a => a === acc.address))?.id && wallet.addresses) const wallet = selectedWallet || provider.activeWallet; - const selectedAccountAddress = selectedWallet ? selectedDestAccounts.find(acc => acc.providerName === provider.name && acc.id === selectedWallet.id)?.address : undefined + const selectedAccountAddress = selectedWallet ? selectedDestAccounts.find(acc => acc.providerName === provider.id && acc.id === selectedWallet.id)?.address : undefined const address = selectedAccountAddress ? selectedAccountAddress : wallet.address; return ResolveWalletSwapAccount(provider, wallet, address); @@ -206,7 +206,7 @@ function ResolveWalletSwapAccount(provider: WalletProvider, wallet: Wallet, addr return { address, provider, - providerName: provider.name, + providerName: provider.id, id: wallet.id, walletWithdrawalSupportedNetworks: wallet.withdrawalSupportedNetworks, walletAutofillSupportedNetworks: wallet.autofillSupportedNetworks, @@ -221,7 +221,7 @@ function ResolveManualSwapAccount(provider: WalletProvider, address: string): Ac return { address, provider, - providerName: provider.name, + providerName: provider.id, id: 'manually_added', displayName: "Manual", addresses: [address], diff --git a/apps/app/helpers/getSettings.ts b/apps/app/helpers/getSettings.ts index 19e3756f..0d00f58a 100644 --- a/apps/app/helpers/getSettings.ts +++ b/apps/app/helpers/getSettings.ts @@ -56,6 +56,31 @@ export async function getServerSideProps(context) { } as any) } + // Inject Solana devnet if the API doesn't return it + const hasSolanaDevnet = resolvedNetworks.some(n => n.caip2Id === KnownInternalNames.Networks.SolanaDevnet) + if (!hasSolanaDevnet) { + const solanaMock = mockData.data.find(n => n.caip2Id === KnownInternalNames.Networks.SolanaDevnet) + resolvedNetworks.push({ + caip2Id: KnownInternalNames.Networks.SolanaDevnet, + displayName: "Solana Devnet", + chainId: 'devnet', + nativeTokenAddress: null, + type: { name: "solana" }, + logoUrl: 'https://raw.githubusercontent.com/TrainProtocol/icons/main/networks/solana.png', + tokens: [{ + symbol: "SOL", + contractAddress: null, + decimals: 9, + priceInUsd: prices["solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp:So11111111111111111111111111111111111111112"] + ?? prices["SOLANA_MAINNET:So11111111111111111111111111111111111111112"] + ?? 150, + }], + nodes: solanaMock?.nodes ?? [], + contracts: (solanaMock?.contracts as NetworkContract[]) ?? [], + metadata: [], + } as any) + } + const settings = { networks: resolvedNetworks, } @@ -143,6 +168,22 @@ const mockData = { "address": "0x2b9192d4571cceb33c689f750bcf380a7baae350846cc55616a278523cfd0dfc" } ], + }, + { + "caip2Id": KnownInternalNames.Networks.SolanaDevnet, + "nodes": [ + { + "providerName": "solana-devnet", + "url": "https://api.devnet.solana.com", + "protocol": "Http" + } + ], + "contracts": [ + { + "type": "Train", + "address": "6zasug6x5AY93zNVjPZPGoqQfdTBd3C1w6CU9NDKtNH8" + } + ], } ] } \ No newline at end of file diff --git a/apps/app/hooks/htlc/useHTLCWriteClient.ts b/apps/app/hooks/htlc/useHTLCWriteClient.ts index b7aafaa9..9a8cf209 100644 --- a/apps/app/hooks/htlc/useHTLCWriteClient.ts +++ b/apps/app/hooks/htlc/useHTLCWriteClient.ts @@ -2,9 +2,11 @@ import { useCallback } from 'react' import { useConfig } from 'wagmi' import { getWalletClient } from 'wagmi/actions' import { getConnections } from '@wagmi/core' -import { createHTLCClient as createClient, getRegisteredNamespaces, IHTLCClient, TrainApiClient as SdkTrainApiClient } from '@train-protocol/sdk' +import { createHTLCClient as createClient, IHTLCClient, TrainApiClient as SdkTrainApiClient } from '@train-protocol/sdk' import type { EvmSigner } from '@train-protocol/evm' import type { AztecSigner } from '@train-protocol/aztec' +import type { SolanaSigner } from '@train-protocol/solana' +import { useWallet, useConnection } from '@solana/wallet-adapter-react' import { Network } from '../../Models/Network' import { Wallet } from '@/Models/WalletProvider' import { useRpcConfigStore } from '@/stores/rpcConfigStore' @@ -19,11 +21,27 @@ export function useHTLCWriteClient() { const config = useConfig() const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls) const { wallet: aztecWallet, accountAddress: aztecAccountAddress } = useAztecWalletContext() + const { connection: solanaConnection } = useConnection() + const { publicKey: solanaPublicKey, sendTransaction: solanaSendTransaction } = useWallet() return useCallback(async (network: Network, wallet?: Wallet): Promise => { const chainType = network.caip2Id.split(':')[0] const rpcUrl = getEffectiveRpcUrls(network)[0] ?? network.nodes?.[0]?.url ?? '' + // Solana chain path + if (chainType === 'solana') { + let signer: SolanaSigner | undefined + if (solanaPublicKey && solanaSendTransaction) { + signer = { + publicKey: solanaPublicKey.toBase58(), + sendTransaction: async (tx) => solanaSendTransaction(tx as any, solanaConnection), + } + } else { + console.error('[useHTLCWriteClient] Solana signer unavailable', { hasPubkey: !!solanaPublicKey, hasSendTx: !!solanaSendTransaction }) + } + return createClient(chainType, { rpcUrl: solanaConnection.rpcEndpoint, signer, apiClient }) + } + // Aztec chain path if (chainType === 'aztec') { let signer: AztecSigner | undefined @@ -82,5 +100,5 @@ export function useHTLCWriteClient() { } return createClient(chainType, { rpcUrl, signer, apiClient }) - }, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress]) + }, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress, solanaPublicKey, solanaSendTransaction, solanaConnection]) } diff --git a/apps/app/hooks/htlc/useUserLockPolling.tsx b/apps/app/hooks/htlc/useUserLockPolling.tsx index 81ec4ad3..f6701e2d 100644 --- a/apps/app/hooks/htlc/useUserLockPolling.tsx +++ b/apps/app/hooks/htlc/useUserLockPolling.tsx @@ -1,4 +1,3 @@ -import { useEffect, useRef } from "react" import useSWR from "swr" import { Network, Token } from "../../Models/Network" import { LockDetails } from "../../Models/phtlc/PHTLC" diff --git a/apps/app/hooks/useConnectors.ts b/apps/app/hooks/useConnectors.ts index ab5c78a3..f42dee75 100644 --- a/apps/app/hooks/useConnectors.ts +++ b/apps/app/hooks/useConnectors.ts @@ -23,7 +23,7 @@ export function useConnectors({ .map((provider) => provider.availableWalletsForConnect ?.filter(v => searchValue ? v.name.toLowerCase().includes(searchValue.toLowerCase()) : true) - .map((connector) => ({ ...connector, providerName: provider.name })) + .map((connector) => ({ ...connector, providerName: provider.id })) ) .flat() as InternalConnector[], [featuredProviders, searchValue] @@ -38,7 +38,7 @@ export function useConnectors({ (searchValue ? v.name.toLowerCase().includes(searchValue.toLowerCase()) : true) && !featuredWalletsIds.includes(v.id.toLowerCase()) ) - .map((connector) => ({ ...connector, providerName: provider.name, isHidden: true })) + .map((connector) => ({ ...connector, providerName: provider.id, isHidden: true })) ) .flat() as InternalConnector[], [featuredProviders, searchValue] diff --git a/apps/app/hooks/useFee.ts b/apps/app/hooks/useFee.ts index e76c2554..e00aabee 100644 --- a/apps/app/hooks/useFee.ts +++ b/apps/app/hooks/useFee.ts @@ -136,6 +136,33 @@ export function useQuoteData(formValues: Props | undefined, refreshInterval?: nu setLoading(true) } + // Mock quote for Solana devnet — real API doesn't support it yet + const urlParams = new URLSearchParams(url.split('?')[1]) + if (urlParams.get('sourceNetwork')?.startsWith('solana:')) { + setKey(url) + setLoading(false) + const amount = urlParams.get('amount') ?? '1000000000' + return { + quote: { + signature: 'mock-solana-devnet-quote', + totalFee: '5000000', + receiveAmount: String(BigInt(amount) * 95n / 100n), + sourceSolverAddress: '9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM', + destinationSolverAddress: '0x0000000000000000000000000000000000000001', + quoteExpirationTimestampInSeconds: Math.floor(Date.now() / 1000) + 3600, + route: { + source: { networkSlug: urlParams.get('sourceNetwork')!, tokenSymbol: 'SOL', tokenContract: '', tokenDecimals: 9 }, + destination: { networkSlug: urlParams.get('destinationNetwork')!, tokenSymbol: 'ETH', tokenContract: '', tokenDecimals: 18 }, + minAmountInSource: '10000000', + maxAmountInSource: '10000000000000', + }, + timelock: { timelockTimeSpanInSeconds: 69 }, + reward: { amount: '0', rewardTimelockTimeSpanInSeconds: 3600, rewardToken: '', rewardRecipientAddress: '' }, + }, + solverId: 'mock-solver', + } + } + const response = await apiClient.fetcher(url) as { data?: AggregatedQuoteResponse; error?: { message: string } } setKey(url) diff --git a/apps/app/lib/NetworkSettings.ts b/apps/app/lib/NetworkSettings.ts index 8f1ffec7..b933bccc 100644 --- a/apps/app/lib/NetworkSettings.ts +++ b/apps/app/lib/NetworkSettings.ts @@ -39,6 +39,7 @@ const sourceOrder = [ KnownInternalNames.Networks.BNBChainMainnet, KnownInternalNames.Networks.OptimismMainnet, KnownInternalNames.Networks.SolanaMainnet, + KnownInternalNames.Networks.SolanaDevnet, KnownInternalNames.Networks.ZksyncEraMainnet, KnownInternalNames.Networks.PolygonMainnet, KnownInternalNames.Networks.AvalancheMainnet, @@ -117,6 +118,11 @@ export default class NetworkSettings { ChainId: 'aztec-devnet', TransactionExplorerTemplate: 'https://aztecexplorer.xyz/tx/{0}', }; + NetworkSettings.KnownSettings[KnownInternalNames.Networks.SolanaDevnet] = { + ChainId: 'devnet', + TransactionExplorerTemplate: 'https://explorer.solana.com/tx/{0}?cluster=devnet', + AccountExplorerTemplate: 'https://explorer.solana.com/address/{0}?cluster=devnet', + }; for (var k in NetworkSettings.KnownSettings) { diff --git a/apps/app/lib/gases/gasResolver.ts b/apps/app/lib/gases/gasResolver.ts index e7f7423e..b02c17b8 100644 --- a/apps/app/lib/gases/gasResolver.ts +++ b/apps/app/lib/gases/gasResolver.ts @@ -1,10 +1,12 @@ import { GasProps } from "../../Models/Balance"; import { EVMGasProvider } from "./providers/evmGasProvider"; +import { SolanaGasProvider } from "./providers/solanaGasProvider"; export class GasResolver { private providers = [ new EVMGasProvider(), + new SolanaGasProvider(), ]; getGas({ address, network, token }: GasProps) { diff --git a/apps/app/lib/gases/providers/solanaGasProvider.ts b/apps/app/lib/gases/providers/solanaGasProvider.ts index e5c14b08..4d87f7df 100644 --- a/apps/app/lib/gases/providers/solanaGasProvider.ts +++ b/apps/app/lib/gases/providers/solanaGasProvider.ts @@ -1,44 +1,39 @@ import { GasProps } from "../../../Models/Balance"; -import { Network, getNativeToken } from "../../../Models/Network"; +import { Network, getNativeToken, NetworkContractType } from "../../../Models/Network"; import { formatUnits } from "viem"; -import KnownInternalNames from "../../knownIds"; + export class SolanaGasProvider { supportsNetwork(network: Network): boolean { - return KnownInternalNames.Networks.SolanaMainnet.includes(network.caip2Id) + return network.caip2Id.toLowerCase().startsWith('solana') } getGas = async ({ address, network, token }: GasProps) => { - if (!address) - return - const { PublicKey, Connection } = await import("@solana/web3.js"); - - const walletPublicKey = new PublicKey(address) + if (!address) return - const connection = new Connection( - `${network.nodes?.[0]?.url}`, - "confirmed" - ); + const atomicContract = network.contracts?.find(c => c.type === NetworkContractType.Train)?.address + if (!atomicContract) return - if (!walletPublicKey) return + const nativeToken = getNativeToken(network) + if (!nativeToken) return try { - const transactionBuilder = ((await import("../../wallets/solana/transactionBuilder")).transactionBuilder); - - const transaction = await transactionBuilder(network, token, walletPublicKey) - - const nativeToken = getNativeToken(network) - - if (!transaction || !nativeToken) return - - const message = transaction.compileMessage(); - const result = await connection.getFeeForMessage(message) - - const formatedGas = result.value ? Number(formatUnits(BigInt(result.value), nativeToken.decimals)) : undefined - - return formatedGas - } - catch (e) { - console.log(e) + const { SolanaHTLCClient } = await import("@train-protocol/solana") + + const client = new SolanaHTLCClient({ + rpcUrl: network.nodes?.[0]?.url ?? '', + }) + + const lamports = await client.estimateGas({ + contractAddress: atomicContract, + address, + tokenSymbol: token.symbol, + tokenContractAddress: token.contractAddress, + decimals: token.decimals ?? 6, + }) + + return lamports ? Number(formatUnits(BigInt(lamports), nativeToken.decimals)) : undefined + } catch (e) { + console.error(e) } } } \ No newline at end of file diff --git a/apps/app/lib/knownIds.ts b/apps/app/lib/knownIds.ts index 1d6ab0f5..ff116e93 100644 --- a/apps/app/lib/knownIds.ts +++ b/apps/app/lib/knownIds.ts @@ -5,9 +5,10 @@ export default class KnownInternalNames { public static readonly EthereumSepolia: string = "eip155:11155111"; public static readonly BaseSepolia: string = "eip155:84532"; - + public static readonly AztecDevnet: string = "aztec:aztec-devnet" - + + public static readonly SolanaDevnet: string = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; public static readonly CronosMainnet: string = "CRONOS_MAINNET"; @@ -85,8 +86,6 @@ export default class KnownInternalNames { public static readonly SolanaTestnet: string = "SOLANA_TESTNET"; - public static readonly SolanaDevnet: string = "SOLANA_DEVNET"; - public static readonly SoonMainnet: string = "SOON_MAINNET"; public static readonly SoonTestnet: string = "SOON_TESTNET"; @@ -212,7 +211,7 @@ export default class KnownInternalNames { public static readonly MorphMainnet: string = "MORPH_MAINNET"; public static readonly SeiMainnet: string = "SEI_MAINNET"; - + public static readonly GravityMainnet: string = "GRAVITY_MAINNET"; public static readonly BitcoinMainnet: string = "BITCOIN_MAINNET" diff --git a/apps/app/lib/wallets/evm/useEVM.ts b/apps/app/lib/wallets/evm/useEVM.ts index bf820b61..fcd82726 100644 --- a/apps/app/lib/wallets/evm/useEVM.ts +++ b/apps/app/lib/wallets/evm/useEVM.ts @@ -16,7 +16,7 @@ import sleep from "../utils/sleep" import { useEvmConnectors, HIDDEN_WALLETCONNECT_ID } from "@/context/evmConnectorsContext" import { useActiveEvmAccount } from "@/components/WalletProviders/ActiveEvmAccount" -const name = 'eip155' +const name = 'EVM' const id = 'eip155' // Storage key for dynamic wallet metadata @@ -158,7 +158,7 @@ export default function useEVM(): WalletProvider { installUrl: walletConnectWallet?.installUrl, hasBrowserExtension: walletConnectWallet?.hasBrowserExtension, extensionNotFound: walletConnectWallet?.hasBrowserExtension ? (type == 'walletConnect' && !isMobilePlatform) : false, - providerName: name + providerName: id } }) }, [allConnectors, walletConnectConnectors]) @@ -276,7 +276,7 @@ export default function useEVM(): WalletProvider { autofill: autofillSupportedNetworks, withdrawal: withdrawalSupportedNetworks }, - providerName: name + providerName: id }) return wallet @@ -310,7 +310,7 @@ export default function useEVM(): WalletProvider { autofill: autofillSupportedNetworks, withdrawal: withdrawalSupportedNetworks }, - providerName: name + providerName: id }) return wallet diff --git a/apps/app/lib/wallets/solana/nativeAnchorHTLC.ts b/apps/app/lib/wallets/solana/nativeAnchorHTLC.ts deleted file mode 100644 index 0271b08a..00000000 --- a/apps/app/lib/wallets/solana/nativeAnchorHTLC.ts +++ /dev/null @@ -1,817 +0,0 @@ -import { Idl } from "@coral-xyz/anchor" - -export const NativeAnchorHtlc = (address: string): Idl => ({ - "address": address, - "metadata": { - "name": "native_htlc", - "version": "0.1.0", - "spec": "0.1.0", - "description": "Created with Anchor" - }, - "instructions": [ - { - "name": "add_lock", - "docs": [ - "@dev Called by the sender to add hashlock to the HTLC", - "", - "@param Id of the HTLC to addLock.", - "@param hashlock of the HTLC to be locked." - ], - "discriminator": [ - 242, - 102, - 183, - 107, - 109, - 168, - 82, - 140 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "add_lock_sig", - "docs": [ - "@dev Called by the solver to add hashlock to the HTLC", - "", - "@param Id of the HTLC.", - "@param hashlock to be added." - ], - "discriminator": [ - 145, - 171, - 87, - 95, - 168, - 39, - 158, - 180 - ], - "accounts": [ - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "ix_sysvar", - "docs": [ - "the supplied Sysvar could be anything else.", - "The Instruction Sysvar has not been implemented", - "in the Anchor framework yet, so this is the safe approach." - ], - "address": "Sysvar1nstructions1111111111111111111111111" - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "signature", - "type": { - "array": [ - "u8", - 64 - ] - } - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "commit", - "docs": [ - "@dev Sender / Payer sets up a new pre-hash time lock contract depositing the", - "funds and providing the src_receiver and terms.", - "@param src_receiver src_receiver of the funds.", - "@param timelock UNIX epoch seconds time that the lock expires at.", - "Refunds can be made after this time.", - "@return Id of the new HTLC. This is needed for subsequent calls." - ], - "discriminator": [ - 223, - 140, - 142, - 165, - 229, - 208, - 156, - 74 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hopChains", - "type": { - "vec": "string" - } - }, - { - "name": "hopAssets", - "type": { - "vec": "string" - } - }, - { - "name": "hopAddresses", - "type": { - "vec": "string" - } - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "dst_address", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "src_receiver", - "type": "pubkey" - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "amount", - "type": "u64" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "getDetails", - "docs": [ - "@dev Get HTLC details.", - "@param Id of the HTLC." - ], - "discriminator": [ - 185, - 254, - 236, - 165, - 213, - 30, - 224, - 250 - ], - "accounts": [ - { - "name": "htlc", - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ], - "returns": { - "defined": { - "name": "HTLC" - } - } - }, - { - "name": "lock", - "docs": [ - "@dev Sender / Payer sets up a new hash time lock contract depositing the", - "funds and providing the reciever and terms.", - "@param src_receiver receiver of the funds.", - "@param hashlock A sha-256 hash hashlock.", - "@param timelock UNIX epoch seconds time that the lock expires at.", - "Refunds can be made after this time.", - "@return Id of the new HTLC. This is needed for subsequent calls." - ], - "discriminator": [ - 21, - 19, - 208, - 43, - 237, - 62, - 255, - 87 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_address", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "src_receiver", - "type": "pubkey" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "lock_reward", - "docs": [ - "@dev Solver / Payer sets the reward for claiming the funds.", - "@param reward the amount of the reward token.", - "@param reward_timelock After this time the rewards can be claimed." - ], - "discriminator": [ - 66, - 69, - 228, - 16, - 76, - 50, - 65, - 157 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true, - "relations": [ - "htlc" - ] - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "reward_timelock", - "type": "u64" - }, - { - "name": "reward", - "type": "u64" - } - ], - "returns": "bool" - }, - { - "name": "redeem", - "docs": [ - "@dev Called by the src_receiver once they know the secret of the hashlock.", - "This will transfer the locked funds to the HTLC's src_receiver's address.", - "", - "@param Id of the HTLC.", - "@param secret sha256(secret) should equal the contract hashlock." - ], - "discriminator": [ - 184, - 12, - 86, - 149, - 70, - 196, - 97, - 225 - ], - "accounts": [ - { - "name": "user_signing", - "writable": true, - "signer": true - }, - { - "name": "sender", - "writable": true, - "relations": [ - "htlc" - ] - }, - { - "name": "src_receiver", - "writable": true, - "relations": [ - "htlc" - ] - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "secret", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ], - "returns": "bool" - }, - { - "name": "refund", - "docs": [ - "@dev Called by the sender if there was no redeem AND the time lock has", - "expired. This will refund the contract amount.", - "", - "@param Id of the HTLC to refund from." - ], - "discriminator": [ - 2, - 96, - 183, - 251, - 63, - 208, - 46, - 46 - ], - "accounts": [ - { - "name": "user_signing", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "sender", - "writable": true, - "relations": [ - "htlc" - ] - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ], - "returns": "bool" - } - ], - "accounts": [ - { - "name": "HTLC", - "discriminator": [ - 172, - 245, - 108, - 24, - 224, - 199, - 55, - 177 - ] - } - ], - "errors": [ - { - "code": 6000, - "name": "InvalidTimeLock", - "msg": "Invalid TimeLock." - }, - { - "code": 6001, - "name": "NotPastTimeLock", - "msg": "Not Past TimeLock." - }, - { - "code": 6002, - "name": "InvalidRewardTimeLock", - "msg": "Invalid Reward TimeLock." - }, - { - "code": 6003, - "name": "HashlockNotSet", - "msg": "Hashlock Is Not Set." - }, - { - "code": 6004, - "name": "HashlockNoMatch", - "msg": "Does Not Match the Hashlock." - }, - { - "code": 6005, - "name": "HashlockAlreadySet", - "msg": "Hashlock Already Set." - }, - { - "code": 6006, - "name": "AlreadyClaimed", - "msg": "Funds Are Alredy Claimed." - }, - { - "code": 6007, - "name": "FundsNotSent", - "msg": "Funds Can Not Be Zero." - }, - { - "code": 6008, - "name": "UnauthorizedAccess", - "msg": "Unauthorized Access." - }, - { - "code": 6009, - "name": "NotOwner", - "msg": "Not The Owner." - }, - { - "code": 6010, - "name": "NotSender", - "msg": "Not The Sender." - }, - { - "code": 6011, - "name": "NotReciever", - "msg": "Not The Reciever." - }, - { - "code": 6012, - "name": "SigVerificationFailed", - "msg": "Signature verification failed." - }, - { - "code": 6013, - "name": "RewardAlreadyExists", - "msg": "Reward Already Exists." - } - ], - "types": [ - { - "name": "HTLC", - "type": { - "kind": "struct", - "fields": [ - { - "name": "dst_address", - "type": "string" - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "sender", - "type": "pubkey" - }, - { - "name": "src_receiver", - "type": "pubkey" - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "secret", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "reward", - "type": "u64" - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "reward_timelock", - "type": "u64" - }, - { - "name": "claimed", - "type": "u8" - } - ] - } - } - ] - }) \ No newline at end of file diff --git a/apps/app/lib/wallets/solana/tokenAnchorHTLC.ts b/apps/app/lib/wallets/solana/tokenAnchorHTLC.ts deleted file mode 100644 index c809384a..00000000 --- a/apps/app/lib/wallets/solana/tokenAnchorHTLC.ts +++ /dev/null @@ -1,1220 +0,0 @@ -import { Idl } from "@coral-xyz/anchor" - -export const TokenAnchorHtlc = (address: string): Idl => ({ - "address": address, - "metadata": { - "name": "anchor_htlc", - "version": "0.1.0", - "spec": "0.1.0", - "description": "Created with Anchor" - }, - "instructions": [ - { - "name": "add_lock", - "docs": [ - "@dev Called by the sender to add hashlock to the HTLC", - "", - "@param Id of the HTLC.", - "@param hashlock to be added." - ], - "discriminator": [ - 242, - 102, - 183, - 107, - 109, - 168, - 82, - 140 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "add_lock_sig", - "docs": [ - "@dev Called by the solver to add hashlock to the HTLC", - "", - "@param Id of the HTLC.", - "@param hashlock to be added." - ], - "discriminator": [ - 145, - 171, - 87, - 95, - 168, - 39, - 158, - 180 - ], - "accounts": [ - { - "name": "payer", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "ix_sysvar", - "docs": [ - "the supplied Sysvar could be anything else.", - "The Instruction Sysvar has not been implemented", - "in the Anchor framework yet, so this is the safe approach." - ], - "address": "Sysvar1nstructions1111111111111111111111111" - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "signature", - "type": { - "array": [ - "u8", - 64 - ] - } - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "commit", - "docs": [ - "@dev Sender / Payer sets up a new pre-hash time lock contract depositing the", - "funds and providing the reciever/src_receiver and terms.", - "@param src_receiver reciever of the funds.", - "@param timelock UNIX epoch seconds time that the lock expires at.", - "Refunds can be made after this time.", - "@return Id of the new HTLC. This is needed for subsequent calls." - ], - "discriminator": [ - 223, - 140, - 142, - 165, - 229, - 208, - 156, - 74 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "htlc_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 104, - 116, - 108, - 99, - 95, - 116, - 111, - 107, - 101, - 110, - 95, - 97, - 99, - 99, - 111, - 117, - 110, - 116 - ] - }, - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "token_contract" - }, - { - "name": "sender_token_account", - "writable": true - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hopChains", - "type": { - "vec": "string" - } - }, - { - "name": "hopAssets", - "type": { - "vec": "string" - } - }, - { - "name": "hopAddress", - "type": { - "vec": "string" - } - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "dst_address", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "src_receiver", - "type": "pubkey" - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "amount", - "type": "u64" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "getDetails", - "docs": [ - "@dev Get HTLC details.", - "@param Id of the HTLC." - ], - "discriminator": [ - 185, - 254, - 236, - 165, - 213, - 30, - 224, - 250 - ], - "accounts": [ - { - "name": "htlc", - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ], - "returns": { - "defined": { - "name": "HTLC" - } - } - }, - { - "name": "lock", - "docs": [ - "@dev Sender / Payer sets up a new hash time lock contract depositing the", - "funds and providing the reciever and terms.", - "@param src_receiver receiver of the funds.", - "@param hashlock A sha-256 hash hashlock.", - "@param timelock UNIX epoch seconds time that the lock expires at.", - "Refunds can be made after this time.", - "@return Id of the new HTLC. This is needed for subsequent calls." - ], - "discriminator": [ - 21, - 19, - 208, - 43, - 237, - 62, - 255, - 87 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "htlc_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 104, - 116, - 108, - 99, - 95, - 116, - 111, - 107, - 101, - 110, - 95, - 97, - 99, - 99, - 111, - 117, - 110, - 116 - ] - }, - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "token_contract" - }, - { - "name": "sender_token_account", - "writable": true - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_address", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "src_receiver", - "type": "pubkey" - }, - { - "name": "amount", - "type": "u64" - } - ], - "returns": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "lock_reward", - "discriminator": [ - 66, - 69, - 228, - 16, - 76, - 50, - 65, - 157 - ], - "accounts": [ - { - "name": "sender", - "writable": true, - "signer": true, - "relations": [ - "htlc" - ] - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "htlc_token_account" - }, - { - "name": "token_contract" - }, - { - "name": "sender_token_account", - "writable": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "reward_timelock", - "type": "u64" - }, - { - "name": "reward", - "type": "u64" - } - ], - "returns": "bool" - }, - { - "name": "redeem", - "docs": [ - "@dev Called by the src_receiver once they know the secret of the hashlock.", - "This will transfer the locked funds to the HTLC's src_receiver's address.", - "", - "@param Id of the HTLC.", - "@param secret sha256(secret) should equal the contract hashlock." - ], - "discriminator": [ - 184, - 12, - 86, - 149, - 70, - 196, - 97, - 225 - ], - "accounts": [ - { - "name": "user_signing", - "writable": true, - "signer": true - }, - { - "name": "sender", - "writable": true, - "relations": [ - "htlc" - ] - }, - { - "name": "src_receiver", - "relations": [ - "htlc" - ] - }, - { - "name": "token_contract", - "relations": [ - "htlc" - ] - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "htlc_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 104, - 116, - 108, - 99, - 95, - 116, - 111, - 107, - 101, - 110, - 95, - 97, - 99, - 99, - 111, - 117, - 110, - 116 - ] - }, - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "sender_token_account", - "writable": true - }, - { - "name": "src_receiver_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "account", - "path": "src_receiver" - }, - { - "kind": "const", - "value": [ - 6, - 221, - 246, - 225, - 215, - 101, - 161, - 147, - 217, - 203, - 225, - 70, - 206, - 235, - 121, - 172, - 28, - 180, - 133, - 237, - 95, - 91, - 55, - 145, - 58, - 140, - 245, - 133, - 126, - 255, - 0, - 169 - ] - }, - { - "kind": "account", - "path": "token_contract" - } - ], - "program": { - "kind": "const", - "value": [ - 140, - 151, - 37, - 143, - 78, - 36, - 137, - 241, - 187, - 61, - 16, - 41, - 20, - 142, - 13, - 131, - 11, - 90, - 19, - 153, - 218, - 255, - 16, - 132, - 4, - 142, - 123, - 216, - 219, - 233, - 248, - 89 - ] - } - } - }, - { - "name": "reward_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "account", - "path": "user_signing" - }, - { - "kind": "const", - "value": [ - 6, - 221, - 246, - 225, - 215, - 101, - 161, - 147, - 217, - 203, - 225, - 70, - 206, - 235, - 121, - 172, - 28, - 180, - 133, - 237, - 95, - 91, - 55, - 145, - 58, - 140, - 245, - 133, - 126, - 255, - 0, - 169 - ] - }, - { - "kind": "account", - "path": "token_contract" - } - ], - "program": { - "kind": "const", - "value": [ - 140, - 151, - 37, - 143, - 78, - 36, - 137, - 241, - 187, - 61, - 16, - 41, - 20, - 142, - 13, - 131, - 11, - 90, - 19, - 153, - 218, - 255, - 16, - 132, - 4, - 142, - 123, - 216, - 219, - 233, - 248, - 89 - ] - } - } - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "associated_token_program", - "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "secret", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "htlc_bump", - "type": "u8" - } - ], - "returns": "bool" - }, - { - "name": "refund", - "docs": [ - "@dev Called by the sender if there was no redeem AND the time lock has", - "expired. This will refund the contract amount.", - "", - "@param Id of the HTLC to refund from." - ], - "discriminator": [ - 2, - 96, - 183, - 251, - 63, - 208, - 46, - 46 - ], - "accounts": [ - { - "name": "user_signing", - "writable": true, - "signer": true - }, - { - "name": "htlc", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "htlc_token_account", - "writable": true, - "pda": { - "seeds": [ - { - "kind": "const", - "value": [ - 104, - 116, - 108, - 99, - 95, - 116, - 111, - 107, - 101, - 110, - 95, - 97, - 99, - 99, - 111, - 117, - 110, - 116 - ] - }, - { - "kind": "arg", - "path": "Id" - } - ] - } - }, - { - "name": "sender", - "writable": true, - "relations": [ - "htlc" - ] - }, - { - "name": "token_contract", - "relations": [ - "htlc" - ] - }, - { - "name": "sender_token_account", - "writable": true - }, - { - "name": "system_program", - "address": "11111111111111111111111111111111" - }, - { - "name": "token_program", - "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - }, - { - "name": "rent", - "address": "SysvarRent111111111111111111111111111111111" - } - ], - "args": [ - { - "name": "Id", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "htlc_bump", - "type": "u8" - } - ], - "returns": "bool" - } - ], - "accounts": [ - { - "name": "HTLC", - "discriminator": [ - 172, - 245, - 108, - 24, - 224, - 199, - 55, - 177 - ] - } - ], - "errors": [ - { - "code": 6000, - "name": "InvalidTimeLock", - "msg": "Invalid TimeLock." - }, - { - "code": 6001, - "name": "NotPastTimeLock", - "msg": "Not Past TimeLock." - }, - { - "code": 6002, - "name": "InvalidRewardTimeLock", - "msg": "Invalid Reward TimeLock." - }, - { - "code": 6003, - "name": "HashlockNotSet", - "msg": "Hashlock Is Not Set." - }, - { - "code": 6004, - "name": "HashlockNoMatch", - "msg": "Does Not Match the Hashlock." - }, - { - "code": 6005, - "name": "HashlockAlreadySet", - "msg": "Hashlock Already Set." - }, - { - "code": 6006, - "name": "AlreadyClaimed", - "msg": "Funds Are Alredy Claimed." - }, - { - "code": 6007, - "name": "FundsNotSent", - "msg": "Funds Can Not Be Zero." - }, - { - "code": 6008, - "name": "UnauthorizedAccess", - "msg": "Unauthorized Access." - }, - { - "code": 6009, - "name": "NotOwner", - "msg": "Not The Owner." - }, - { - "code": 6010, - "name": "NotSender", - "msg": "Not The Sender." - }, - { - "code": 6011, - "name": "NotReciever", - "msg": "Not The Reciever." - }, - { - "code": 6012, - "name": "NoToken", - "msg": "Wrong Token." - }, - { - "code": 6013, - "name": "SigVerificationFailed", - "msg": "Signature verification failed." - }, - { - "code": 6014, - "name": "RewardAlreadyExists", - "msg": "Reward Already Exists." - } - ], - "types": [ - { - "name": "HTLC", - "type": { - "kind": "struct", - "fields": [ - { - "name": "dst_address", - "type": "string" - }, - { - "name": "dst_chain", - "type": "string" - }, - { - "name": "dst_asset", - "type": "string" - }, - { - "name": "src_asset", - "type": "string" - }, - { - "name": "sender", - "type": "pubkey" - }, - { - "name": "src_receiver", - "type": "pubkey" - }, - { - "name": "hashlock", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "secret", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "timelock", - "type": "u64" - }, - { - "name": "reward", - "type": "u64" - }, - { - "name": "reward_timelock", - "type": "u64" - }, - { - "name": "token_contract", - "type": "pubkey" - }, - { - "name": "token_wallet", - "type": "pubkey" - }, - { - "name": "claimed", - "type": "u8" - } - ] - } - } - ] -}) \ No newline at end of file diff --git a/apps/app/lib/wallets/solana/transactionBuilder.ts b/apps/app/lib/wallets/solana/transactionBuilder.ts deleted file mode 100644 index 920c551d..00000000 --- a/apps/app/lib/wallets/solana/transactionBuilder.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { Connection, PublicKey, SystemProgram, Transaction, TransactionInstruction } from "@solana/web3.js"; -import { createAssociatedTokenAccountInstruction, createTransferInstruction, getAccount, getAssociatedTokenAddress } from '@solana/spl-token'; -import { Network, Token } from "../../../Models/Network"; -import { LockParams, UserLockParams, OldLockParams } from "../../../Models/phtlc"; -import { BN, Idl, Program } from "@coral-xyz/anchor"; -import { createHash } from "crypto"; - -export const transactionBuilder = async (network: Network, token: Token, walletPublicKey: PublicKey, recipientAddress?: string | undefined) => { - - const connection = new Connection( - `${network.nodes?.[0]?.url}`, - "confirmed" - ); - const recipientPublicKey = new PublicKey(recipientAddress || new Array(32).fill(0)); - - if (token.contractAddress) { - const sourceToken = new PublicKey(token?.contractAddress); - - const transactionInstructions: TransactionInstruction[] = []; - const associatedTokenFrom = await getAssociatedTokenAddress( - sourceToken, - walletPublicKey - ); - const fromAccount = await getAccount(connection, associatedTokenFrom); - const associatedTokenTo = await getAssociatedTokenAddress( - sourceToken, - recipientPublicKey - ); - - if (!(await connection.getAccountInfo(associatedTokenTo))) { - transactionInstructions.push( - createAssociatedTokenAccountInstruction( - walletPublicKey, - associatedTokenTo, - recipientPublicKey, - sourceToken - ) - ); - } - transactionInstructions.push( - createTransferInstruction( - fromAccount.address, - associatedTokenTo, - walletPublicKey, - 2000000 * Math.pow(10, Number(token?.decimals)) - ) - ); - const result = await connection.getLatestBlockhash() - - const transaction = new Transaction({ - feePayer: walletPublicKey, - blockhash: result.blockhash, - lastValidBlockHeight: result.lastValidBlockHeight - }).add(...transactionInstructions); - - return transaction - } - else { - const transaction = new Transaction(); - const amountInLamports = 20000 * Math.pow(10, Number(token?.decimals)); - - const transferInstruction = SystemProgram.transfer({ - fromPubkey: walletPublicKey, - toPubkey: recipientPublicKey, - lamports: amountInLamports - }); - transaction.add(transferInstruction); - - const { blockhash } = await connection.getLatestBlockhash(); - transaction.recentBlockhash = blockhash; - transaction.feePayer = walletPublicKey; - - return transaction - } -} - -export const phtlcTransactionBuilder = async (params: UserLockParams & { program: Program, connection: Connection, walletPublicKey: PublicKey, network: Network }) => { - - const { destinationChain, destinationAsset, sourceAsset, srcLpAddress: lpAddress, destinationAddress, amount, atomicContract, chainId, program, walletPublicKey, connection, network } = params - - if (!walletPublicKey) { - throw Error("Wallet not connected") - } - if (!lpAddress) { - throw Error("No LP address") - } - if (!atomicContract) { - throw Error("No contract address") - } - - const bnTimelock = new BN(0); - - const lpAddressPublicKey = new PublicKey(lpAddress); - - const bnAmount = new BN(Number(amount) * Math.pow(10, 6)); - - try { - function generateBytes32Hex() { - const bytes = new Uint8Array(32); // 32 bytes = 64 hex characters - crypto.getRandomValues(bytes); - return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join(''); - } - const hashlock = Buffer.from(generateBytes32Hex(), 'hex') - - let [htlcTokenAccount, _] = hashlock && PublicKey.findProgramAddressSync( - [Buffer.from("htlc_token_account"), hashlock], - program.programId - ); - let [htlc] = hashlock && PublicKey.findProgramAddressSync( - [hashlock], - program.programId - ); - - - const hopChains = [destinationChain] - const hopAssets = [destinationAsset] - const hopAddresses = [lpAddress] - - let commit = new Transaction(); - - - if (sourceAsset.contractAddress) { - const senderTokenAddress = await getAssociatedTokenAddress(new PublicKey(sourceAsset.contractAddress), walletPublicKey); - const tokenContract = new PublicKey(sourceAsset.contractAddress); - - const commitTx = await program.methods - .commit(hashlock, hopChains, hopAssets, hopAddresses, destinationChain, destinationAsset, destinationAddress, sourceAsset.symbol, lpAddressPublicKey, bnTimelock, bnAmount) - .accountsPartial({ - sender: walletPublicKey, - htlc: htlc, - htlcTokenAccount: htlcTokenAccount, - tokenContract: tokenContract, - senderTokenAccount: senderTokenAddress - }) - .transaction(); - - commit.add(commitTx); - } else { - const commitTx = await program.methods - .commit(hashlock, hopChains, hopAssets, hopAddresses, destinationChain, destinationAsset, destinationAddress, sourceAsset.symbol, lpAddressPublicKey, bnTimelock, bnAmount) - .accountsPartial({ - sender: walletPublicKey, - htlc: htlc, - }) - .transaction(); - - commit.add(commitTx); - } - - const blockHash = await connection.getLatestBlockhash(); - - commit.recentBlockhash = blockHash.blockhash; - commit.lastValidBlockHeight = blockHash.lastValidBlockHeight; - commit.feePayer = walletPublicKey; - - return { initAndCommit: commit, hashlock } - } - catch (error) { - - if (error?.simulationResponse?.err === 'AccountNotFound') { - throw new Error('Not enough SOL balance') - } - - throw new Error(error) - } - - -} - -export const lockTransactionBuilder = async (params: LockParams & OldLockParams & { program: Program, walletPublicKey: PublicKey }) => { - const { walletPublicKey, id, hashlock, program } = params - - if (!program) { - throw Error("No program") - } - if (!walletPublicKey) { - throw Error("No Wallet public key") - } - - const timelock = 0; - const bnTimelock = new BN(timelock); - - const commitIdBuffer = Buffer.from(id.replace('0x', ''), 'hex'); - const hashlockBuffer = Buffer.from(hashlock.replace('0x', ''), 'hex'); - - const TIMELOCK_LE = Buffer.alloc(8); - TIMELOCK_LE.writeBigUInt64LE(BigInt(bnTimelock.toString())); - const MSG = createHash("sha256").update(commitIdBuffer).update(hashlockBuffer).update(Buffer.from(TIMELOCK_LE)).digest(); - - const signingDomain = Buffer.from("\xffsolana offchain", "ascii") - const headerVersion = Buffer.from([0]); - const applicationDomain = Buffer.alloc(32); - applicationDomain.write("Train"); - const messageFormat = Buffer.from([0]); - const signerCount = Buffer.from([1]); - const signerPublicKey = walletPublicKey.toBytes(); - - const messageLength = Buffer.alloc(2); - messageLength.writeUInt16LE(MSG.length, 0); - - const finalMessage = Buffer.concat([ - signingDomain, - headerVersion, - applicationDomain, - messageFormat, - signerCount, - signerPublicKey, - messageLength, - MSG, - ]); - const hexString = finalMessage.toString('hex'); - - const data = new TextEncoder().encode(hexString); - - return { lockCommit: data, lockId: hashlockBuffer, timelock } -} \ No newline at end of file diff --git a/apps/app/lib/wallets/solana/useAtomicSVM.ts b/apps/app/lib/wallets/solana/useAtomicSVM.ts deleted file mode 100644 index 9533f21a..00000000 --- a/apps/app/lib/wallets/solana/useAtomicSVM.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { AnchorProvider, Program } from "@coral-xyz/anchor" -import { Connection, PublicKey } from "@solana/web3.js" -import { Network } from "../../../Models/Network" -import { UserLockParams, LockParams, OldLockParams, RefundParams, RedeemSolverParams } from "../../../Models/phtlc" -import { TokenAnchorHtlc } from "./tokenAnchorHTLC" -import { NativeAnchorHtlc } from "./nativeAnchorHTLC" -import { lockTransactionBuilder, phtlcTransactionBuilder } from "./transactionBuilder" -import TrainApiClient from "../../trainApiClient" -import { toHex } from "viem" -import { AnchorWallet } from "@solana/wallet-adapter-react" -import { useSecretDerivation } from "@/context/secretDerivationContext" -import { LockDetails } from "@/Models/phtlc/PHTLC" - -export interface UseAtomicSVMParams { - connection: Connection - signTransaction: ((transaction: any) => Promise) | undefined - signMessage: ((message: Uint8Array) => Promise) | undefined - publicKey: PublicKey | null - network: Network | undefined - anchorProvider: AnchorProvider | undefined -} - -export default function useAtomicSVM(params: UseAtomicSVMParams) { - const { connection, signTransaction, signMessage, publicKey, network, anchorProvider } = params - const { deriveSecret } = useSecretDerivation() - - const userLock = async (params: UserLockParams): Promise<{ hash: string; hashlock: string; } | null | undefined> => { - const { atomicContract, sourceAsset } = params - const program = (anchorProvider && atomicContract) ? new Program(sourceAsset.contractAddress ? TokenAnchorHtlc(atomicContract) : NativeAnchorHtlc(atomicContract), anchorProvider) : null; - - if (!program || !publicKey || !network) return null - - // Secret derivation for HTLC with hashlock - const solanaWallet = { signMessage }; - const secret = await deriveSecret({ - wallet: { metadata: { wallet: solanaWallet }, providerName: 'solana' } as any - }); - // const hashlock = secretToHashlock(secret); - - // Note: Add hashlock to transaction params when contract supports it - const transaction = await phtlcTransactionBuilder({ connection, program, walletPublicKey: publicKey, network, ...params }) - - const signed = transaction?.initAndCommit && signTransaction && await signTransaction(transaction.initAndCommit); - const signature = signed && await connection.sendRawTransaction(signed.serialize()); - - if (signature) { - const blockHash = await connection.getLatestBlockhash(); - - const res = await connection.confirmTransaction({ - blockhash: blockHash.blockhash, - lastValidBlockHeight: blockHash.lastValidBlockHeight, - signature - }); - - if (res?.value.err) { - throw new Error(res.value.err.toString()) - } - - return { hash: signature, hashlock: `0x${toHexString(transaction.hashlock)}` } - } - - } - - const getDetails = async (params: LockParams) => { - const solanaAddress = '4hLwFR5JpxztsYMyy574mcWsfYc9sbfeAx5FKMYfw8vB' - const { contractAddress, id, type } = params - - if (!solanaAddress) throw new Error("No LP address") - - const idBuffer = Buffer.from(id.replace('0x', ''), 'hex'); - - const lpAnchorWallet = { publicKey: new PublicKey(solanaAddress) } - const provider = new AnchorProvider(connection, lpAnchorWallet as AnchorWallet); - const lpProgram = (provider && contractAddress) ? new Program(type === 'erc20' ? TokenAnchorHtlc(contractAddress) : NativeAnchorHtlc(contractAddress), provider) : null; - - if (!lpProgram) { - throw new Error("Could not initiate a program") - } - - let [htlc, _] = idBuffer && PublicKey.findProgramAddressSync( - [idBuffer], - lpProgram.programId - ); - - // Check if the HTLC account exists before calling getDetails - const accountInfo = await connection.getAccountInfo(htlc); - if (!accountInfo) { - // Account doesn't exist yet, return null - return null; - } - - try { - const result = await lpProgram?.methods.getDetails(Array.from(idBuffer)).accountsPartial({ htlc }).view(); - - if (!result) return null - - const parsedResult = { - ...result, - hashlock: (result?.hashlock && toHexString(result.hashlock) !== '0000000000000000000000000000000000000000000000000000000000000000') && `0x${toHexString(result.hashlock)}`, - amount: Number(result.amount) / Math.pow(10, 6), - timelock: Number(result.timelock), - sender: new PublicKey(result.sender).toString(), - srcReceiver: new PublicKey(result.srcReceiver).toString(), - secret: result.secret, - tokenContract: result.tokenContract ? new PublicKey(result.tokenContract).toString() : undefined, - tokenWallet: result.tokenWallet ? new PublicKey(result.tokenWallet).toString() : undefined, - } - - return parsedResult - } - catch (e) { - console.error('Error fetching HTLC details:', e) - // If account exists but getDetails fails, return null instead of throwing - // This allows the polling mechanism to retry later - return null - } - } - - const addLock = async (params: LockParams & OldLockParams) => { - - const { contractAddress } = params - const program = (anchorProvider && contractAddress) ? new Program(TokenAnchorHtlc(contractAddress), anchorProvider) : null; - - if (!program || !publicKey) return null - - const { lockCommit, lockId, timelock } = await lockTransactionBuilder({ program, walletPublicKey: publicKey, ...params }) - - const hexLockId = `0x${toHexString(lockId)}` - - if (!signMessage) { - throw new Error("Wallet does not support message signing!"); - } - - try { - - const signature = await signMessage(lockCommit) - - if (signature) { - const sigBase64 = Buffer.from(signature).toString("base64"); - const apiClient = new TrainApiClient() - - // AddLockSig not supported in Station API — Solana chain not yet migrated - return { hash: sigBase64, result: hexLockId } - - } else { - return null - } - } catch (e) { - throw new Error("Failed to add lock") - } - - } - - const refund = async (params: RefundParams) => { - const { id, sourceAsset, contractAddress } = params - const program = (anchorProvider && contractAddress) ? new Program(sourceAsset.contractAddress ? TokenAnchorHtlc(contractAddress) : NativeAnchorHtlc(contractAddress), anchorProvider) : null; - - if (!program || !sourceAsset?.contractAddress || !publicKey) return null - - const getAssociatedTokenAddress = (await import('@solana/spl-token')).getAssociatedTokenAddress; - - const idBuffer = Buffer.from(id.replace('0x', ''), 'hex'); - - let [htlc, htlcBump] = idBuffer && PublicKey.findProgramAddressSync( - [idBuffer], - program.programId - ); - - if (sourceAsset.contractAddress) { - let [htlcTokenAccount, _] = idBuffer && PublicKey.findProgramAddressSync( - [Buffer.from("htlc_token_account"), idBuffer], - program.programId - ); - - const senderTokenAddress = await getAssociatedTokenAddress(new PublicKey(sourceAsset.contractAddress), publicKey); - const tokenContract = new PublicKey(sourceAsset.contractAddress); - - return await program.methods.refund(Array.from(idBuffer), Number(htlcBump)).accountsPartial({ - userSigning: publicKey, - htlc, - htlcTokenAccount, - sender: publicKey, - tokenContract: tokenContract, - senderTokenAccount: senderTokenAddress, - }).rpc(); - } else { - return await program.methods.refund(Array.from(idBuffer), Number(htlcBump)).accountsPartial({ - userSigning: publicKey, - htlc, - sender: publicKey, - }).rpc(); - } - } - - const redeemSolver = async (params: RedeemSolverParams) => { - const { sourceAsset, id, secret, contractAddress, destLpAddress } = params - const program = (anchorProvider && contractAddress) ? new Program(sourceAsset.contractAddress ? TokenAnchorHtlc(contractAddress) : NativeAnchorHtlc(contractAddress), anchorProvider) : null; - - const lpAddress = new PublicKey(destLpAddress); - - if (!program || !publicKey) return - - const idBuffer = Buffer.from(id.replace('0x', ''), 'hex'); - const secretBuffer = Buffer.from(toHex(secret).toString().replace('0x', ''), 'hex'); - - let [htlc, htlcBump] = idBuffer && PublicKey.findProgramAddressSync( - [idBuffer], - program.programId - ); - - if (sourceAsset.contractAddress) { - const tokenContract = new PublicKey(sourceAsset.contractAddress); - - let [htlcTokenAccount, _] = idBuffer && PublicKey.findProgramAddressSync( - [Buffer.from("htlc_token_account"), idBuffer], - program.programId - ); - - const getAssociatedTokenAddress = (await import('@solana/spl-token')).getAssociatedTokenAddress; - const senderTokenAddress = await getAssociatedTokenAddress(new PublicKey(sourceAsset.contractAddress), lpAddress); - - return await program.methods.redeem(idBuffer, secretBuffer, htlcBump). - accountsPartial({ - userSigning: publicKey, - htlc: htlc, - htlcTokenAccount: htlcTokenAccount, - sender: lpAddress, - tokenContract: tokenContract, - srcReceiverTokenAccount: senderTokenAddress, - }) - .rpc(); - } - else { - return await program.methods.redeem(idBuffer, secretBuffer). - accountsPartial({ - userSigning: publicKey, - htlc: htlc, - sender: lpAddress, - srcReceiver: publicKey, - }) - .rpc(); - } - } - - return { - userLock, - getUserLockDetails: getDetails, - refund, - redeemSolver, - getSolverLockDetails: function (params: LockParams): Promise { - throw new Error("Function not implemented.") - } - } -} - -function toHexString(byteArray: Uint8Array | number[] | any): string { - return Array.from(byteArray, (byte: any) => - ('0' + (byte & 0xFF).toString(16)).slice(-2) - ).join('') -} \ No newline at end of file diff --git a/apps/app/lib/wallets/solana/useSVM.tsx b/apps/app/lib/wallets/solana/useSVM.tsx index 6cc8adc9..9b587144 100644 --- a/apps/app/lib/wallets/solana/useSVM.tsx +++ b/apps/app/lib/wallets/solana/useSVM.tsx @@ -161,7 +161,7 @@ function resolveSupportedNetworks(supportedNetworks: string[], connectorId: stri const supportedNetworksForWallet: string[] = []; supportedNetworks.forEach((network) => { - const networkName = network.split("_")[0].toLowerCase(); + const networkName = network.split(":")[0].split("_")[0].toLowerCase(); if (networkName === "solana") { supportedNetworksForWallet.push(networkName); } else if (networkSupport[networkName] && networkSupport[networkName].includes(connectorId?.toLowerCase())) { diff --git a/apps/app/package.json b/apps/app/package.json index f7be36f2..3103e195 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -62,6 +62,7 @@ "@train-protocol/sdk": "workspace:^", "@train-protocol/aztec": "workspace:^", "@train-protocol/evm": "workspace:^", + "@train-protocol/solana": "workspace:^", "@uidotdev/usehooks": "^2.4.1", "@vercel/analytics": "^1.5.0", "@walletconnect/ethereum-provider": "2.21.8", diff --git a/apps/app/pages/_app.js b/apps/app/pages/_app.js index 5ae3e690..e5c98f17 100644 --- a/apps/app/pages/_app.js +++ b/apps/app/pages/_app.js @@ -15,6 +15,7 @@ import { registerEvmSdk } from '@train-protocol/evm'; if (typeof window !== 'undefined') { registerEvmSdk(); import('@train-protocol/aztec').then(m => m.registerAztecSdk()); + import('@train-protocol/solana').then(m => m.registerSolanaSdk()); } const progress = new ProgressBar({ diff --git a/packages/blockchains/solana/package.json b/packages/blockchains/solana/package.json new file mode 100644 index 00000000..9d0360a4 --- /dev/null +++ b/packages/blockchains/solana/package.json @@ -0,0 +1,43 @@ +{ + "name": "@train-protocol/solana", + "version": "0.1.0", + "description": "Train Protocol SDK — Solana 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": false, + "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": { + "@coral-xyz/anchor": "^0.30.1", + "@solana/spl-token": "^0.4.14", + "@solana/web3.js": "^1.98.4" + }, + "peerDependencies": { + "@train-protocol/sdk": "workspace:^" + }, + "devDependencies": { + "@train-protocol/sdk": "workspace:^", + "@types/node": "^20", + "rimraf": "^6.0.1", + "typescript": "catalog:" + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/blockchains/solana/src/client.ts b/packages/blockchains/solana/src/client.ts new file mode 100644 index 00000000..8ba593f8 --- /dev/null +++ b/packages/blockchains/solana/src/client.ts @@ -0,0 +1,514 @@ +import { AnchorProvider, Program } from '@coral-xyz/anchor' +import { Connection, PublicKey } from '@solana/web3.js' +import { + HTLCClient, + UserLockParams, + LockParams, + RefundParams, + RedeemSolverParams, + LockDetails, + LockStatus, + AtomicResult, + RecoveredSwapData, + formatUnits, + bytesToHex, +} from '@train-protocol/sdk' +import type { SolanaHTLCClientConfig, SolanaSigner } from './types.js' +import { TrainHtlc } from './idl/trainHtlc.js' +import { userLockTransactionBuilder } from './transactionBuilder.js' +import { secretToBuffer } from './utils.js' + + +export class SolanaHTLCClient extends HTLCClient { + private connection: Connection + private signer: SolanaSigner | undefined + + constructor(config: SolanaHTLCClientConfig) { + super(config.apiClient as any) + this.connection = new Connection(config.rpcUrl, 'confirmed') + this.signer = config.signer + } + + // ── Write Operations ──────────────────────────────────────────────── + + async userLock(params: UserLockParams): Promise { + const signer = this.requireSigner() + const { atomicContract, sourceAsset, hashlock: hashlockHex, timelockDelta, rewardTimelockDelta } = params + + if (!atomicContract) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const program = this.buildProgram(atomicContract, walletPublicKey) + const hashlock = Buffer.from(hashlockHex.replace('0x', ''), 'hex') + + const { transaction, blockhash, lastValidBlockHeight } = await userLockTransactionBuilder({ + connection: this.connection, + program, + walletPublicKey, + hashlock, + sourceChain: params.sourceChain, + destinationChain: params.destinationChain, + destinationAsset: params.destinationAsset, + destinationAddress: params.destinationAddress, + destinationAmount: params.destinationAmount, + lpAddress: params.srcLpAddress, + sourceAsset: { + symbol: sourceAsset.symbol, + contractAddress: sourceAsset.contractAddress, + }, + amount: params.amount, + decimals: params.decimals, + timelockDelta: timelockDelta || 0, + quoteExpiry: params.quoteExpiry ?? Math.floor(Date.now() / 1000) + 86400, + rewardAmount: params.rewardAmount ?? '0', + rewardToken: params.rewardToken ?? '', + rewardRecipient: params.rewardRecipient ?? '', + rewardTimelockDelta: rewardTimelockDelta || 0, + solverData: params.solverData, + nonce: params.nonce, + }) + + let signature: string + try { + signature = await signer.sendTransaction(transaction) + } catch (e: any) { + console.error('[SolanaHTLC] sendTransaction failed', e?.message ?? String(e), e?.logs ?? []) + throw e + } + + const res = await this.connection.confirmTransaction({ + blockhash, + lastValidBlockHeight, + signature, + }) + + if (res?.value.err) { + console.error('[SolanaHTLC] confirmTransaction error', res.value.err.toString()) + throw new Error(res.value.err.toString()) + } + + return { hash: signature, hashlock: hashlockHex } + } + + async refund(params: RefundParams): Promise { + const signer = this.requireSigner() + const { id, sourceAsset, contractAddress } = params + + if (!contractAddress) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const hashlockBuffer = Buffer.from(id.replace('0x', ''), 'hex') + const hashlockArray = Array.from(hashlockBuffer) + const program = this.buildProgram(contractAddress, walletPublicKey) + + const [userLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("user_lock"), hashlockBuffer], + program.programId + ) + + try { + let tx + if (sourceAsset.contractAddress) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(sourceAsset.contractAddress) + const senderTokenAccount = await getAssociatedTokenAddress(tokenMint, walletPublicKey) + const [vault] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), hashlockBuffer], + program.programId + ) + + tx = await program.methods + .refundUserToken(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + sender: walletPublicKey, + tokenMint, + vault, + senderTokenAccount, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction() + } else { + tx = await program.methods + .refundUserSol(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + sender: walletPublicKey, + }) + .transaction() + } + + const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() + tx.recentBlockhash = blockhash + tx.lastValidBlockHeight = lastValidBlockHeight + tx.feePayer = walletPublicKey + + const signature = await signer.sendTransaction(tx) + + const res = await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }) + if (res?.value.err) { + throw new Error(res.value.err.toString()) + } + + // Fire-and-forget: reclaim rent after refund is confirmed + this.closeUserLockAccount(program, hashlockArray, userLockPda, walletPublicKey, signer).catch((e: any) => + console.warn('[SolanaHTLC] closeUserLock skipped:', e?.message ?? String(e)) + ) + + return signature + } catch (error: any) { + console.error('[SolanaHTLC] refund failed', error?.logs ?? error) + throw error + } + } + + async redeemSolver(params: RedeemSolverParams): Promise { + const signer = this.requireSigner() + const { sourceAsset, id, secret, contractAddress, destinationAddress } = params + + if (!contractAddress) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const hashlockBuffer = Buffer.from(id.replace('0x', ''), 'hex') + const hashlockArray = Array.from(hashlockBuffer) + const secretArray = Array.from(secretToBuffer(secret)) + const lockIndex = params.index ?? 1 + + const indexBuffer = Buffer.alloc(8) + indexBuffer.writeBigUInt64LE(BigInt(lockIndex)) + + const program = this.buildProgram(contractAddress, walletPublicKey) + + const [solverLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("solver_lock"), hashlockBuffer, indexBuffer], + program.programId + ) + + try { + const solverLockAccount = await (program.account as any).solverLock.fetch(solverLockPda) + const rewardRecipient: PublicKey = new PublicKey(solverLockAccount.rewardRecipient) + + const recipient = destinationAddress + ? new PublicKey(destinationAddress) + : walletPublicKey + + let tx + if (sourceAsset.contractAddress) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(sourceAsset.contractAddress) + const [vault] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), hashlockBuffer, indexBuffer], + program.programId + ) + const recipientTokenAccount = await getAssociatedTokenAddress(tokenMint, recipient) + const rewardRecipientTokenAccount = await getAssociatedTokenAddress(tokenMint, rewardRecipient) + const callerTokenAccount = await getAssociatedTokenAddress(tokenMint, walletPublicKey) + + tx = await program.methods + .redeemSolverToken(hashlockArray, lockIndex, secretArray) + .accounts({ + caller: walletPublicKey, + solverLock: solverLockPda, + recipient, + rewardRecipient, + tokenMint, + vault, + recipientTokenAccount, + rewardRecipientTokenAccount, + callerTokenAccount, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .transaction() + } else { + tx = await program.methods + .redeemSolverSol(hashlockArray, lockIndex, secretArray) + .accounts({ + caller: walletPublicKey, + solverLock: solverLockPda, + recipient, + rewardRecipient, + }) + .transaction() + } + + const blockHash = await this.connection.getLatestBlockhash() + tx.recentBlockhash = blockHash.blockhash + tx.lastValidBlockHeight = blockHash.lastValidBlockHeight + tx.feePayer = walletPublicKey + + return signer.sendTransaction(tx) + } catch (error) { + console.error('Error in redeemSolver:', error) + throw error + } + } + + // ── Read Operations ───────────────────────────────────────────────── + + async getUserLockDetails(params: LockParams): Promise { + const { contractAddress, id } = params + + if (!contractAddress) throw new Error('No contract address') + + const hashlockBuffer = Buffer.from(id.replace('0x', ''), 'hex') + const program = this.buildProgram(contractAddress) + + const [userLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("user_lock"), hashlockBuffer], + program.programId + ) + + // Fetch log data from tx independently — needed even if account isn't indexed yet + const logsPromise = params.txId + ? this.findUserDataFromLogs(params.txId, id, program) + : Promise.resolve({} as { userData?: string; blockTimestamp?: number }) + + const accountInfo = await this.connection.getAccountInfo(userLockPda) + if (!accountInfo) return null + + try { + const result = await (program.account as any).userLock.fetch(userLockPda) + + if (!result) return null + + const { userData, blockTimestamp } = await logsPromise + + const details: LockDetails = { + hashlock: `0x${id.replace('0x', '')}`, + amount: Number(formatUnits(BigInt(result.amount.toString()), params.decimals ?? 6)), + timelock: Number(result.timelock), + sender: new PublicKey(result.sender).toString(), + recipient: new PublicKey(result.recipient).toString(), + secret: this.parseSecret(result.secret), + token: result.tokenMint && result.tokenMint.toString() !== '11111111111111111111111111111111' + ? result.tokenMint.toString() + : undefined, + status: Number(result.status) as LockStatus, + userData, + blockTimestamp, + } + return details + } catch (e) { + console.error('[SolanaHTLC][getUserLockDetails] fetch failed', e) + return null + } + } + + async _getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { + const { contractAddress, id } = params + + if (!contractAddress) throw new Error('No contract address') + + const connection = new Connection(nodeUrl, 'confirmed') + const hashlockBuffer = Buffer.from(id.replace('0x', ''), 'hex') + + const pk = this.signer ? new PublicKey(this.signer.publicKey) : new PublicKey('11111111111111111111111111111111') + const provider = new AnchorProvider( + connection, + { publicKey: pk, signTransaction: async (tx: any) => tx, signAllTransactions: async (txs: any[]) => txs } as any, + AnchorProvider.defaultOptions() + ) + const program = new Program(TrainHtlc(contractAddress), provider) + + // Count-then-loop: iterate PDAs from index 1 until getAccountInfo returns null + for (let i = 1; ; i++) { + const indexBuffer = Buffer.alloc(8) + indexBuffer.writeBigUInt64LE(BigInt(i)) + + const [solverLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("solver_lock"), hashlockBuffer, indexBuffer], + program.programId + ) + + const accountInfo = await connection.getAccountInfo(solverLockPda) + if (!accountInfo) return null + + try { + const result = await (program.account as any).solverLock.fetch(solverLockPda) + + if (!result) continue + + // Skip empty slots + const sender = new PublicKey(result.sender).toString() + if (sender === '11111111111111111111111111111111') continue + + // Filter by solver address if provided + if (params.solverAddress && sender.toLowerCase() !== params.solverAddress.toLowerCase()) continue + + return { + hashlock: `0x${id.replace('0x', '')}`, + amount: Number(formatUnits(BigInt(result.amount.toString()), params.decimals ?? 6)), + reward: Number(formatUnits(BigInt(result.reward.toString()), params.decimals ?? 6)), + timelock: Number(result.timelock), + rewardTimelock: Number(result.rewardTimelock), + sender, + recipient: new PublicKey(result.recipient).toString(), + rewardRecipient: new PublicKey(result.rewardRecipient).toString(), + secret: this.parseSecret(result.secret), + token: result.tokenMint && result.tokenMint.toString() !== '11111111111111111111111111111111' + ? result.tokenMint.toString() + : undefined, + rewardToken: result.rewardTokenMint && result.rewardTokenMint.toString() !== '11111111111111111111111111111111' + ? result.rewardTokenMint.toString() + : undefined, + status: Number(result.status) as LockStatus, + index: i, + } + } catch (e) { + console.error('Error fetching Solana solver lock details:', e) + return null + } + } + } + + async recoverSwap(_txHash: string): Promise { + throw new Error('recoverSwap is not supported for Solana') + } + + // ── Private Helpers ───────────────────────────────────────────────── + + private requireSigner(): SolanaSigner { + if (!this.signer) throw new Error('Solana signer not configured') + return this.signer + } + + private parseSecret(secretBytes: Uint8Array): bigint | undefined { + return Array.from(secretBytes).some(b => b !== 0) + ? BigInt(bytesToHex(Array.from(secretBytes))) + : undefined + } + + private buildReadOnlyProvider(publicKey: PublicKey): AnchorProvider { + const wallet = { + publicKey, + signTransaction: async (tx: any) => tx, + signAllTransactions: async (txs: any[]) => txs, + } + return new AnchorProvider(this.connection, wallet as any, AnchorProvider.defaultOptions()) + } + + private async closeUserLockAccount( + program: Program, + hashlockArray: number[], + userLockPda: PublicKey, + walletPublicKey: PublicKey, + signer: SolanaSigner + ): Promise { + const closeTx = await program.methods + .closeUserLock(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + }) + .transaction() + + const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() + closeTx.recentBlockhash = blockhash + closeTx.lastValidBlockHeight = lastValidBlockHeight + closeTx.feePayer = walletPublicKey + + const sig = await signer.sendTransaction(closeTx) + await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature: sig }) + console.log('[SolanaHTLC][closeUserLock] account closed, rent reclaimed', sig) + } + + private buildProgram(contractAddress: string, readerKey?: PublicKey): Program { + const pk = readerKey + ?? (this.signer ? new PublicKey(this.signer.publicKey) : new PublicKey('11111111111111111111111111111111')) + const provider = this.buildReadOnlyProvider(pk) + return new Program(TrainHtlc(contractAddress), provider) + } + + private async findUserDataFromLogs( + txId: string, + id: string, + program: Program + ): Promise<{ userData?: string; blockTimestamp?: number }> { + try { + let tx = await this.connection.getTransaction(txId, { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0, + }) + // Some RPCs omit logMessages at 'confirmed' — retry at 'finalized' + if (!tx || !tx.meta?.logMessages?.length) { + tx = await this.connection.getTransaction(txId, { + commitment: 'finalized', + maxSupportedTransactionVersion: 0, + }) + } + if (!tx) return {} + + const blockTimestamp = tx.blockTime ? tx.blockTime * 1000 : undefined + const logs = tx.meta?.logMessages ?? [] + + const PROGRAM_DATA_PREFIX = 'Program data: ' + const PROGRAM_LOG_PREFIX = 'Program log: ' + for (const log of logs) { + const isData = log.startsWith(PROGRAM_DATA_PREFIX) + const isLog = log.startsWith(PROGRAM_LOG_PREFIX) + if (!isData && !isLog) continue + + const b64 = isData ? log.slice(PROGRAM_DATA_PREFIX.length) : log.slice(PROGRAM_LOG_PREFIX.length) + const event = program.coder.events.decode(b64) + if (!event || event.name.toLowerCase() !== 'userlocked') continue + + const hashlockBytes: number[] = Array.from(event.data.hashlock as number[]) + const eventHashlock = '0x' + Buffer.from(hashlockBytes).toString('hex') + if (eventHashlock.toLowerCase() !== `0x${id.replace('0x', '')}`.toLowerCase()) continue + + const userDataBytes: number[] = Array.from((event.data as any).userData as Buffer ?? []) + const userData = userDataBytes.length > 0 + ? Buffer.from(userDataBytes).toString('utf8').replace(/\0/g, '').trim() || undefined + : undefined + + return { userData, blockTimestamp } + } + + return { blockTimestamp } + } catch (e) { + console.error('Error fetching userData from Solana logs:', e) + return {} + } + } + + async estimateGas(params: { + contractAddress: string + address: string + tokenSymbol: string + tokenContractAddress?: string | null + decimals: number + }): Promise { + const walletPublicKey = new PublicKey(params.address) + const program = this.buildProgram(params.contractAddress, walletPublicKey) + + const { transaction } = await userLockTransactionBuilder({ + connection: this.connection, + program, + walletPublicKey, + hashlock: Buffer.alloc(32), + sourceChain: 'solana', + destinationChain: 'eip155:1', + destinationAsset: 'ETH', + destinationAddress: params.address, + destinationAmount: '1', + lpAddress: params.address, + sourceAsset: { symbol: params.tokenSymbol, contractAddress: params.tokenContractAddress }, + amount: '1', + decimals: params.decimals, + timelockDelta: 69, + quoteExpiry: Math.floor(Date.now() / 1000) + 3600, + rewardAmount: '0', + rewardToken: '', + rewardRecipient: '', + rewardTimelockDelta: 34, + }) + + const message = transaction.compileMessage() + const result = await this.connection.getFeeForMessage(message) + return result.value ?? undefined + } +} diff --git a/packages/blockchains/solana/src/idl/trainHtlc.ts b/packages/blockchains/solana/src/idl/trainHtlc.ts new file mode 100644 index 00000000..bc46cd6a --- /dev/null +++ b/packages/blockchains/solana/src/idl/trainHtlc.ts @@ -0,0 +1,3611 @@ +import type { Idl } from '@coral-xyz/anchor' + +export const TrainHtlc = (address: string): Idl => ({ + "address": address, + "metadata": { + "name": "train_htlc", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Train HTLC program for cross-chain atomic swaps" + }, + "instructions": [ + { + "name": "close_solver_lock", + "discriminator": [ + 155, + 212, + 31, + 231, + 73, + 217, + 169, + 229 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "_hashlock" + }, + { + "kind": "arg", + "path": "_index" + } + ] + } + } + ], + "args": [ + { + "name": "_hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "_index", + "type": "u64" + } + ] + }, + { + "name": "close_user_lock", + "discriminator": [ + 106, + 156, + 216, + 210, + 55, + 80, + 233, + 113 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "_hashlock" + } + ] + } + } + ], + "args": [ + { + "name": "_hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "get_solver_lock", + "discriminator": [ + 112, + 193, + 20, + 65, + 13, + 198, + 117, + 75 + ], + "accounts": [ + { + "name": "solver_lock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "_hashlock" + }, + { + "kind": "arg", + "path": "_index" + } + ] + } + } + ], + "args": [ + { + "name": "_hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "_index", + "type": "u64" + } + ], + "returns": { + "defined": { + "name": "SolverLockData" + } + } + }, + { + "name": "get_solver_lock_count", + "discriminator": [ + 88, + 88, + 167, + 23, + 36, + 78, + 199, + 154 + ], + "accounts": [ + { + "name": "counter", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "arg", + "path": "_hashlock" + } + ] + } + } + ], + "args": [ + { + "name": "_hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ], + "returns": "u64" + }, + { + "name": "get_user_lock", + "discriminator": [ + 160, + 232, + 28, + 5, + 133, + 31, + 165, + 226 + ], + "accounts": [ + { + "name": "user_lock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "_hashlock" + } + ] + } + } + ], + "args": [ + { + "name": "_hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ], + "returns": { + "defined": { + "name": "UserLockData" + } + } + }, + { + "name": "redeem_solver_sol", + "discriminator": [ + 158, + 244, + 167, + 180, + 50, + 185, + 114, + 43 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "recipient", + "writable": true + }, + { + "name": "reward_recipient", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "redeem_solver_token", + "discriminator": [ + 198, + 1, + 109, + 168, + 143, + 32, + 179, + 249 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "recipient" + }, + { + "name": "reward_recipient" + }, + { + "name": "token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "reward_recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "reward_recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "caller_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "caller" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "redeem_solver_token_diff_reward", + "discriminator": [ + 95, + 153, + 26, + 142, + 42, + 33, + 204, + 17 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "recipient" + }, + { + "name": "reward_recipient" + }, + { + "name": "token_mint" + }, + { + "name": "reward_token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "reward_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 114, + 101, + 119, + 97, + 114, + 100, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "reward_recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "reward_recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "reward_token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "caller_reward_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "caller" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "reward_token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "redeem_user_sol", + "discriminator": [ + 63, + 101, + 248, + 158, + 196, + 124, + 156, + 183 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "recipient", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "redeem_user_token", + "discriminator": [ + 29, + 218, + 215, + 128, + 14, + 81, + 152, + 69 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "recipient" + }, + { + "name": "token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "recipient_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "recipient" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "refund_solver_sol", + "discriminator": [ + 50, + 141, + 169, + 25, + 246, + 101, + 74, + 6 + ], + "accounts": [ + { + "name": "caller", + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "_index" + } + ] + } + }, + { + "name": "sender", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "_index", + "type": "u64" + } + ] + }, + { + "name": "refund_solver_token", + "discriminator": [ + 60, + 218, + 158, + 221, + 178, + 70, + 15, + 135 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "sender", + "writable": true + }, + { + "name": "token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "sender_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + } + ] + }, + { + "name": "refund_solver_token_diff_reward", + "discriminator": [ + 115, + 154, + 45, + 30, + 60, + 240, + 141, + 122 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "sender", + "writable": true + }, + { + "name": "token_mint" + }, + { + "name": "reward_token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "reward_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 114, + 101, + 119, + 97, + 114, + 100, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "sender_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "sender_reward_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "reward_token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + } + ] + }, + { + "name": "refund_user_sol", + "discriminator": [ + 30, + 19, + 18, + 219, + 217, + 139, + 0, + 228 + ], + "accounts": [ + { + "name": "caller", + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "sender", + "writable": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "refund_user_token", + "discriminator": [ + 86, + 78, + 244, + 214, + 30, + 222, + 125, + 195 + ], + "accounts": [ + { + "name": "caller", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "sender", + "writable": true + }, + { + "name": "token_mint" + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "sender_token_account", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "sender" + }, + { + "kind": "account", + "path": "token_program" + }, + { + "kind": "account", + "path": "token_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "token_program" + }, + { + "name": "associated_token_program", + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + }, + { + "name": "solver_lock_sol", + "discriminator": [ + 35, + 247, + 168, + 201, + 87, + 39, + 138, + 122 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "counter", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "timelock_delta", + "type": "u64" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "data", + "type": "bytes" + } + ] + }, + { + "name": "solver_lock_token", + "discriminator": [ + 95, + 75, + 155, + 114, + 253, + 146, + 126, + 66 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "counter", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "token_mint" + }, + { + "name": "sender_token_account", + "writable": true + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "timelock_delta", + "type": "u64" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "data", + "type": "bytes" + } + ] + }, + { + "name": "solver_lock_token_diff_reward", + "discriminator": [ + 58, + 7, + 9, + 111, + 250, + 37, + 106, + 29 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "counter", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 99, + 111, + 117, + 110, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "solver_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "token_mint" + }, + { + "name": "reward_token_mint" + }, + { + "name": "sender_token_account", + "writable": true + }, + { + "name": "sender_reward_token_account", + "writable": true + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "reward_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 111, + 108, + 118, + 101, + 114, + 95, + 114, + 101, + 119, + 97, + 114, + 100, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "timelock_delta", + "type": "u64" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "data", + "type": "bytes" + } + ] + }, + { + "name": "user_lock_sol", + "discriminator": [ + 214, + 198, + 105, + 134, + 57, + 232, + 113, + 180 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timelock_delta", + "type": "u64" + }, + { + "name": "quote_expiry", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "reward_amount", + "type": "u64" + }, + { + "name": "reward_token", + "type": "string" + }, + { + "name": "reward_recipient", + "type": "string" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "user_data", + "type": "bytes" + }, + { + "name": "solver_data", + "type": "bytes" + } + ] + }, + { + "name": "user_lock_token", + "discriminator": [ + 239, + 183, + 70, + 101, + 167, + 166, + 171, + 238 + ], + "accounts": [ + { + "name": "signer", + "writable": true, + "signer": true + }, + { + "name": "user_lock", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 108, + 111, + 99, + 107 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "token_mint" + }, + { + "name": "sender_token_account", + "writable": true + }, + { + "name": "vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 115, + 101, + 114, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "arg", + "path": "hashlock" + } + ] + } + }, + { + "name": "token_program" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timelock_delta", + "type": "u64" + }, + { + "name": "quote_expiry", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "reward_amount", + "type": "u64" + }, + { + "name": "reward_token", + "type": "string" + }, + { + "name": "reward_recipient", + "type": "string" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "user_data", + "type": "bytes" + }, + { + "name": "solver_data", + "type": "bytes" + } + ] + } + ], + "accounts": [ + { + "name": "SolverLock", + "discriminator": [ + 243, + 127, + 29, + 186, + 176, + 44, + 12, + 85 + ] + }, + { + "name": "SolverLockCounter", + "discriminator": [ + 192, + 165, + 115, + 87, + 4, + 102, + 174, + 95 + ] + }, + { + "name": "UserLock", + "discriminator": [ + 107, + 42, + 69, + 173, + 232, + 188, + 205, + 98 + ] + } + ], + "events": [ + { + "name": "SolverLocked", + "discriminator": [ + 124, + 169, + 104, + 162, + 255, + 178, + 136, + 197 + ] + }, + { + "name": "SolverRedeemed", + "discriminator": [ + 175, + 134, + 83, + 184, + 150, + 62, + 172, + 196 + ] + }, + { + "name": "SolverRefunded", + "discriminator": [ + 78, + 144, + 230, + 203, + 161, + 247, + 151, + 37 + ] + }, + { + "name": "UserLocked", + "discriminator": [ + 113, + 246, + 107, + 252, + 122, + 82, + 241, + 159 + ] + }, + { + "name": "UserRedeemed", + "discriminator": [ + 158, + 165, + 185, + 4, + 94, + 204, + 37, + 237 + ] + }, + { + "name": "UserRefunded", + "discriminator": [ + 217, + 44, + 189, + 196, + 215, + 248, + 130, + 200 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "ZeroAmount", + "msg": "Amount must be greater than zero." + }, + { + "code": 6001, + "name": "ZeroTimelockDelta", + "msg": "Timelock delta must be greater than zero." + }, + { + "code": 6002, + "name": "QuoteExpired", + "msg": "Quote has expired." + }, + { + "code": 6003, + "name": "NotPending", + "msg": "Lock is not in Pending status." + }, + { + "code": 6004, + "name": "TimelockNotExpired", + "msg": "Timelock has not expired yet." + }, + { + "code": 6005, + "name": "HashlockMismatch", + "msg": "Secret does not match hashlock." + }, + { + "code": 6006, + "name": "RewardTimelockNotLessThanTimelock", + "msg": "Reward timelock delta must be less than timelock delta." + }, + { + "code": 6007, + "name": "InvalidIndex", + "msg": "Invalid index: must equal current count + 1." + }, + { + "code": 6008, + "name": "WrongToken", + "msg": "Wrong token mint provided." + }, + { + "code": 6009, + "name": "WrongSender", + "msg": "Wrong sender address." + }, + { + "code": 6010, + "name": "WrongRecipient", + "msg": "Wrong recipient address." + }, + { + "code": 6011, + "name": "StillPending", + "msg": "Lock is still pending." + }, + { + "code": 6012, + "name": "Overflow", + "msg": "Arithmetic overflow." + } + ], + "types": [ + { + "name": "SolverLock", + "type": { + "kind": "struct", + "fields": [ + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "reward_timelock", + "type": "u64" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "token_mint", + "type": "pubkey" + }, + { + "name": "reward_token_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "SolverLockCounter", + "type": { + "kind": "struct", + "fields": [ + { + "name": "count", + "type": "u64" + } + ] + } + }, + { + "name": "SolverLockData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "reward_timelock", + "type": "u64" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "token_mint", + "type": "pubkey" + }, + { + "name": "reward_token_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "SolverLocked", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "token_mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "reward", + "type": "u64" + }, + { + "name": "reward_token_mint", + "type": "pubkey" + }, + { + "name": "reward_recipient", + "type": "pubkey" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "reward_timelock", + "type": "u64" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "data", + "type": "bytes" + } + ] + } + }, + { + "name": "SolverRedeemed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + }, + { + "name": "redeemer", + "type": "pubkey" + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "SolverRefunded", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "index", + "type": "u64" + } + ] + } + }, + { + "name": "UserLock", + "type": { + "kind": "struct", + "fields": [ + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "token_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "UserLockData", + "type": { + "kind": "struct", + "fields": [ + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "status", + "type": "u8" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "token_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "UserLocked", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sender", + "type": "pubkey" + }, + { + "name": "recipient", + "type": "pubkey" + }, + { + "name": "src_chain", + "type": "string" + }, + { + "name": "token_mint", + "type": "pubkey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "timelock", + "type": "u64" + }, + { + "name": "dst_chain", + "type": "string" + }, + { + "name": "dst_address", + "type": "string" + }, + { + "name": "dst_amount", + "type": "u64" + }, + { + "name": "dst_token", + "type": "string" + }, + { + "name": "reward_amount", + "type": "u64" + }, + { + "name": "reward_token", + "type": "string" + }, + { + "name": "reward_recipient", + "type": "string" + }, + { + "name": "reward_timelock_delta", + "type": "u64" + }, + { + "name": "quote_expiry", + "type": "u64" + }, + { + "name": "user_data", + "type": "bytes" + }, + { + "name": "solver_data", + "type": "bytes" + } + ] + } + }, + { + "name": "UserRedeemed", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "redeemer", + "type": "pubkey" + }, + { + "name": "secret", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "UserRefunded", + "type": { + "kind": "struct", + "fields": [ + { + "name": "hashlock", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + } + ] +}) diff --git a/packages/blockchains/solana/src/index.ts b/packages/blockchains/solana/src/index.ts new file mode 100644 index 00000000..59bba087 --- /dev/null +++ b/packages/blockchains/solana/src/index.ts @@ -0,0 +1,31 @@ +import { registerHTLCClient, registerWalletSign } from '@train-protocol/sdk' +import { SolanaHTLCClient } from './client.js' +import { deriveKeyFromSolanaWallet } from './login/index.js' +import type { SolanaWalletLike } from './login/index.js' +import type { SolanaSigner } from './types.js' + +export { SolanaHTLCClient } from './client.js' +export type { SolanaHTLCClientConfig, SolanaSigner } from './types.js' +export { deriveKeyFromSolanaWallet } from './login/index.js' +export type { SolanaWalletLike } from './login/index.js' +export { userLockTransactionBuilder as phtlcTransactionBuilder } from './transactionBuilder.js' +export type { UserLockParams as PhtlcParams } from './transactionBuilder.js' +export { TrainHtlc } from './idl/trainHtlc.js' +export const TRAIN_HTLC_PROGRAM_ID = '6zasug6x5AY93zNVjPZPGoqQfdTBd3C1w6CU9NDKtNH8' + +let registered = false + +export function registerSolanaSdk(): void { + if (registered) return + registered = true + + registerHTLCClient('solana', (config) => new SolanaHTLCClient({ + rpcUrl: config.rpcUrl as string, + signer: config.signer as SolanaSigner | undefined, + apiClient: config.apiClient, + })) + + registerWalletSign('solana', async (config) => { + return deriveKeyFromSolanaWallet(config.wallet as SolanaWalletLike) + }) +} diff --git a/packages/blockchains/solana/src/login/index.ts b/packages/blockchains/solana/src/login/index.ts new file mode 100644 index 00000000..b04a4379 --- /dev/null +++ b/packages/blockchains/solana/src/login/index.ts @@ -0,0 +1 @@ +export * from './wallet-sign.js' diff --git a/packages/blockchains/solana/src/login/wallet-sign.ts b/packages/blockchains/solana/src/login/wallet-sign.ts new file mode 100644 index 00000000..1477409e --- /dev/null +++ b/packages/blockchains/solana/src/login/wallet-sign.ts @@ -0,0 +1,28 @@ +import { deriveKeyMaterial, IDENTITY_SALT } from '@train-protocol/sdk' + +/** + * Minimal interface for the Solana wallet needed by the login flow. + */ +export interface SolanaWalletLike { + signMessage(message: Uint8Array): Promise +} + +/** + * Derive a deterministic login key from a Solana wallet. + * + * Signs a fixed UTF-8 message, then derives key material from the resulting + * signature — analogous to the EVM approach using eth_signTypedData_v4. + */ +export const deriveKeyFromSolanaWallet = async ( + wallet: SolanaWalletLike, +): Promise => { + if (!wallet?.signMessage) { + throw new Error('Solana wallet does not support message signing') + } + + const message = new TextEncoder().encode('I am using TRAIN') + const signature = await wallet.signMessage(message) + + const identitySalt = Buffer.from(IDENTITY_SALT, 'utf8') + return Buffer.from(deriveKeyMaterial(signature, identitySalt)) +} diff --git a/packages/blockchains/solana/src/transactionBuilder.ts b/packages/blockchains/solana/src/transactionBuilder.ts new file mode 100644 index 00000000..d89349bb --- /dev/null +++ b/packages/blockchains/solana/src/transactionBuilder.ts @@ -0,0 +1,126 @@ +import { Connection, PublicKey, Transaction } from "@solana/web3.js" +import { BN, Idl, Program } from "@coral-xyz/anchor" + +export type UserLockParams = { + connection: Connection + program: Program + walletPublicKey: PublicKey + hashlock: Buffer + sourceChain: string + destinationChain: string + destinationAsset: string + destinationAddress: string + destinationAmount: string + lpAddress: string + sourceAsset: { symbol: string; contractAddress?: string | null } + amount: string + decimals: number + timelockDelta: number + quoteExpiry: number + rewardAmount: string + rewardToken: string + rewardRecipient: string + rewardTimelockDelta: number + solverData?: string + nonce?: number +} + +export type TransactionResult = { + transaction: Transaction + blockhash: string + lastValidBlockHeight: number +} + +function toBaseUnits(amount: string, decimals: number): BN { + const [int, frac = ''] = amount.split('.') + const fracPadded = frac.padEnd(decimals, '0').slice(0, decimals) + return new BN(int + fracPadded) +} + +export const userLockTransactionBuilder = async (params: UserLockParams): Promise => { + const { + connection, program, walletPublicKey, hashlock, + sourceChain, destinationChain, destinationAsset, destinationAddress, destinationAmount, + lpAddress, sourceAsset, amount, decimals, + timelockDelta, quoteExpiry, rewardAmount, rewardToken, rewardRecipient, rewardTimelockDelta, + solverData, nonce, + } = params + + if (!walletPublicKey) throw new Error("Wallet not connected") + if (!lpAddress) throw new Error("No LP address") + + const bnAmount = toBaseUnits(amount, decimals) + const bnDstAmount = toBaseUnits(destinationAmount, decimals) + const bnRewardAmount = toBaseUnits(rewardAmount || '0', decimals) + const bnTimelockDelta = new BN(timelockDelta) + const bnRewardTimelockDelta = new BN(rewardTimelockDelta) + const bnQuoteExpiry = new BN(quoteExpiry) + const lpPublicKey = new PublicKey(lpAddress) + const hashlockArray = Array.from(hashlock) + const userData = nonce != null ? Buffer.from(nonce.toString(), 'utf8') : Buffer.from([]) + const solverDataBytes = solverData ? Buffer.from(solverData, 'utf8') : Buffer.from([]) + + const [userLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("user_lock"), hashlock], + program.programId + ) + + const tx = new Transaction() + + if (sourceAsset.contractAddress) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(sourceAsset.contractAddress) + const senderTokenAccount = await getAssociatedTokenAddress(tokenMint, walletPublicKey) + const [vault] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), hashlock], + program.programId + ) + + const lockTx = await program.methods + .userLockToken( + hashlockArray, + bnAmount, bnTimelockDelta, bnQuoteExpiry, + walletPublicKey, lpPublicKey, + sourceChain, destinationChain, destinationAddress, + bnDstAmount, destinationAsset, + bnRewardAmount, rewardToken, rewardRecipient, bnRewardTimelockDelta, + userData, solverDataBytes + ) + .accounts({ + signer: walletPublicKey, + userLock: userLockPda, + tokenMint, + senderTokenAccount, + vault, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .transaction() + + tx.add(lockTx) + } else { + const lockTx = await program.methods + .userLockSol( + hashlockArray, + bnAmount, bnTimelockDelta, bnQuoteExpiry, + walletPublicKey, lpPublicKey, + sourceChain, destinationChain, destinationAddress, + bnDstAmount, destinationAsset, + bnRewardAmount, rewardToken, rewardRecipient, bnRewardTimelockDelta, + userData, solverDataBytes + ) + .accounts({ + signer: walletPublicKey, + userLock: userLockPda, + }) + .transaction() + + tx.add(lockTx) + } + + const blockHash = await connection.getLatestBlockhash() + tx.recentBlockhash = blockHash.blockhash + tx.lastValidBlockHeight = blockHash.lastValidBlockHeight + tx.feePayer = walletPublicKey + + return { transaction: tx, blockhash: blockHash.blockhash, lastValidBlockHeight: blockHash.lastValidBlockHeight } +} diff --git a/packages/blockchains/solana/src/types.ts b/packages/blockchains/solana/src/types.ts new file mode 100644 index 00000000..20e0bdca --- /dev/null +++ b/packages/blockchains/solana/src/types.ts @@ -0,0 +1,19 @@ +import type { Transaction, VersionedTransaction } from '@solana/web3.js' +import type { BaseHTLCClientConfig } from '@train-protocol/sdk' + +/** + * Framework-agnostic Solana signer. + * The app wraps @solana/wallet-adapter-react hooks into this interface. + */ +export interface SolanaSigner { + /** Base58 public key string */ + publicKey: string + sendTransaction(tx: Transaction | VersionedTransaction): Promise +} + +export type SolanaHTLCClientConfig = { + rpcUrl: string + signer?: SolanaSigner + /** Required for revealSecret; may be omitted for read-only / gas-estimation use. */ + apiClient?: BaseHTLCClientConfig['apiClient'] +} diff --git a/packages/blockchains/solana/src/utils.ts b/packages/blockchains/solana/src/utils.ts new file mode 100644 index 00000000..76ce4e53 --- /dev/null +++ b/packages/blockchains/solana/src/utils.ts @@ -0,0 +1,7 @@ +export function secretToBuffer(secret: string | bigint): Buffer { + if (typeof secret === 'bigint') { + const hex = secret.toString(16).padStart(64, '0') + return Buffer.from(hex, 'hex') + } + return Buffer.from(secret.replace('0x', ''), 'hex') +} diff --git a/packages/blockchains/solana/tsconfig.json b/packages/blockchains/solana/tsconfig.json new file mode 100644 index 00000000..4fbcf18c --- /dev/null +++ b/packages/blockchains/solana/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "module": "ESNext", + "moduleResolution": "Bundler", + "declaration": true, + "noEmit": false, + "resolveJsonModule": true, + "types": ["node"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3d0aad3..92e29ea0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,6 +182,9 @@ importers: '@train-protocol/sdk': specifier: workspace:^ version: link:../../packages/sdk + '@train-protocol/solana': + specifier: workspace:^ + version: link:../../packages/blockchains/solana '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -393,6 +396,31 @@ importers: specifier: ^4.0.18 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.34)(jiti@2.6.1)(lightningcss@1.31.1) + packages/blockchains/solana: + dependencies: + '@coral-xyz/anchor': + specifier: ^0.30.1 + version: 0.30.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/spl-token': + specifier: ^0.4.14 + version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/web3.js': + specifier: ^1.98.4 + version: 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + devDependencies: + '@train-protocol/sdk': + specifier: workspace:^ + version: link:../../sdk + '@types/node': + specifier: ^20 + version: 20.19.34 + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/sdk: devDependencies: '@noble/hashes': From 1d51485da647a286005db631c3ba55efddeb6f30 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Fri, 6 Mar 2026 14:34:56 +0400 Subject: [PATCH 02/10] refactor: streamline refund process in SolanaHTLCClient by replacing transaction handling with instruction-based approach --- packages/blockchains/solana/src/client.ts | 52 +++++++---------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/packages/blockchains/solana/src/client.ts b/packages/blockchains/solana/src/client.ts index 8ba593f8..22d3cf0c 100644 --- a/packages/blockchains/solana/src/client.ts +++ b/packages/blockchains/solana/src/client.ts @@ -1,5 +1,5 @@ import { AnchorProvider, Program } from '@coral-xyz/anchor' -import { Connection, PublicKey } from '@solana/web3.js' +import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' import { HTLCClient, UserLockParams, @@ -107,7 +107,7 @@ export class SolanaHTLCClient extends HTLCClient { ) try { - let tx + let refundIx: TransactionInstruction if (sourceAsset.contractAddress) { const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } = await import('@solana/spl-token') const tokenMint = new PublicKey(sourceAsset.contractAddress) @@ -117,7 +117,7 @@ export class SolanaHTLCClient extends HTLCClient { program.programId ) - tx = await program.methods + refundIx = await program.methods .refundUserToken(hashlockArray) .accounts({ caller: walletPublicKey, @@ -129,22 +129,32 @@ export class SolanaHTLCClient extends HTLCClient { tokenProgram: TOKEN_PROGRAM_ID, associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, }) - .transaction() + .instruction() } else { - tx = await program.methods + refundIx = await program.methods .refundUserSol(hashlockArray) .accounts({ caller: walletPublicKey, userLock: userLockPda, sender: walletPublicKey, }) - .transaction() + .instruction() } + const closeIx = await program.methods + .closeUserLock(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + }) + .instruction() + const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() + const tx = new Transaction() tx.recentBlockhash = blockhash tx.lastValidBlockHeight = lastValidBlockHeight tx.feePayer = walletPublicKey + tx.add(refundIx, closeIx) const signature = await signer.sendTransaction(tx) @@ -153,11 +163,6 @@ export class SolanaHTLCClient extends HTLCClient { throw new Error(res.value.err.toString()) } - // Fire-and-forget: reclaim rent after refund is confirmed - this.closeUserLockAccount(program, hashlockArray, userLockPda, walletPublicKey, signer).catch((e: any) => - console.warn('[SolanaHTLC] closeUserLock skipped:', e?.message ?? String(e)) - ) - return signature } catch (error: any) { console.error('[SolanaHTLC] refund failed', error?.logs ?? error) @@ -391,31 +396,6 @@ export class SolanaHTLCClient extends HTLCClient { return new AnchorProvider(this.connection, wallet as any, AnchorProvider.defaultOptions()) } - private async closeUserLockAccount( - program: Program, - hashlockArray: number[], - userLockPda: PublicKey, - walletPublicKey: PublicKey, - signer: SolanaSigner - ): Promise { - const closeTx = await program.methods - .closeUserLock(hashlockArray) - .accounts({ - caller: walletPublicKey, - userLock: userLockPda, - }) - .transaction() - - const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() - closeTx.recentBlockhash = blockhash - closeTx.lastValidBlockHeight = lastValidBlockHeight - closeTx.feePayer = walletPublicKey - - const sig = await signer.sendTransaction(closeTx) - await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature: sig }) - console.log('[SolanaHTLC][closeUserLock] account closed, rent reclaimed', sig) - } - private buildProgram(contractAddress: string, readerKey?: PublicKey): Program { const pk = readerKey ?? (this.signer ? new PublicKey(this.signer.publicKey) : new PublicKey('11111111111111111111111111111111')) From 54723a3c002cd22550e2772d55ef24e19589e08f Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Fri, 6 Mar 2026 16:35:33 +0400 Subject: [PATCH 03/10] refactor: enhance Solana gas estimation and streamline HTLC client methods --- .../lib/gases/providers/solanaGasProvider.ts | 59 +++++++- apps/app/lib/wallets/solana/useSVM.tsx | 4 +- packages/blockchains/solana/src/client.ts | 137 ++++++++++-------- 3 files changed, 133 insertions(+), 67 deletions(-) diff --git a/apps/app/lib/gases/providers/solanaGasProvider.ts b/apps/app/lib/gases/providers/solanaGasProvider.ts index 4d87f7df..4efff892 100644 --- a/apps/app/lib/gases/providers/solanaGasProvider.ts +++ b/apps/app/lib/gases/providers/solanaGasProvider.ts @@ -17,13 +17,8 @@ export class SolanaGasProvider { if (!nativeToken) return try { - const { SolanaHTLCClient } = await import("@train-protocol/solana") - - const client = new SolanaHTLCClient({ + const lamports = await estimateSolanaGas({ rpcUrl: network.nodes?.[0]?.url ?? '', - }) - - const lamports = await client.estimateGas({ contractAddress: atomicContract, address, tokenSymbol: token.symbol, @@ -31,9 +26,59 @@ export class SolanaGasProvider { decimals: token.decimals ?? 6, }) - return lamports ? Number(formatUnits(BigInt(lamports), nativeToken.decimals)) : undefined + const gas = lamports ? Number(formatUnits(BigInt(lamports), nativeToken.decimals)) : undefined + return gas !== undefined ? { gas, token: nativeToken } : undefined } catch (e) { console.error(e) } } +} + +async function estimateSolanaGas(params: { + rpcUrl: string + contractAddress: string + address: string + tokenSymbol: string + tokenContractAddress?: string | null + decimals: number +}): Promise { + const { Connection, PublicKey } = await import('@solana/web3.js') + const { AnchorProvider, Program } = await import('@coral-xyz/anchor') + const { phtlcTransactionBuilder, TrainHtlc } = await import('@train-protocol/solana') + + const connection = new Connection(params.rpcUrl, 'confirmed') + const walletPublicKey = new PublicKey(params.address) + const wallet = { + publicKey: walletPublicKey, + signTransaction: async (tx: any) => tx, + signAllTransactions: async (txs: any) => txs, + } + const provider = new AnchorProvider(connection, wallet as any, AnchorProvider.defaultOptions()) + const program = new Program(TrainHtlc(params.contractAddress), provider) + + const { transaction } = await phtlcTransactionBuilder({ + connection, + program, + walletPublicKey, + hashlock: Buffer.alloc(32), + sourceChain: 'solana', + destinationChain: 'eip155:1', + destinationAsset: 'ETH', + destinationAddress: params.address, + destinationAmount: '1', + lpAddress: params.address, + sourceAsset: { symbol: params.tokenSymbol, contractAddress: params.tokenContractAddress }, + amount: '1', + decimals: params.decimals, + timelockDelta: 69, + quoteExpiry: Math.floor(Date.now() / 1000) + 3600, + rewardAmount: '0', + rewardToken: '', + rewardRecipient: '', + rewardTimelockDelta: 34, + }) + + const message = transaction.compileMessage() + const result = await connection.getFeeForMessage(message) + return result.value ?? undefined } \ No newline at end of file diff --git a/apps/app/lib/wallets/solana/useSVM.tsx b/apps/app/lib/wallets/solana/useSVM.tsx index 9b587144..2c0d3be1 100644 --- a/apps/app/lib/wallets/solana/useSVM.tsx +++ b/apps/app/lib/wallets/solana/useSVM.tsx @@ -163,9 +163,9 @@ function resolveSupportedNetworks(supportedNetworks: string[], connectorId: stri supportedNetworks.forEach((network) => { const networkName = network.split(":")[0].split("_")[0].toLowerCase(); if (networkName === "solana") { - supportedNetworksForWallet.push(networkName); + supportedNetworksForWallet.push(network); } else if (networkSupport[networkName] && networkSupport[networkName].includes(connectorId?.toLowerCase())) { - supportedNetworksForWallet.push(networkName); + supportedNetworksForWallet.push(network); } }); diff --git a/packages/blockchains/solana/src/client.ts b/packages/blockchains/solana/src/client.ts index 22d3cf0c..961b6a53 100644 --- a/packages/blockchains/solana/src/client.ts +++ b/packages/blockchains/solana/src/client.ts @@ -1,7 +1,8 @@ -import { AnchorProvider, Program } from '@coral-xyz/anchor' -import { Connection, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js' +import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor' +import { Connection, PublicKey, Transaction, TransactionInstruction, VersionedTransaction } from '@solana/web3.js' import { HTLCClient, + TrainApiClient, UserLockParams, LockParams, RefundParams, @@ -18,13 +19,42 @@ import { TrainHtlc } from './idl/trainHtlc.js' import { userLockTransactionBuilder } from './transactionBuilder.js' import { secretToBuffer } from './utils.js' +// Raw Anchor-deserialized on-chain account shapes (u64 → BN, pubkey → PublicKey, [u8;32] → number[]) +interface UserLockData { + amount: BN + timelock: BN + sender: PublicKey + recipient: PublicKey + secret: number[] + tokenMint: PublicKey + status: number +} + +interface SolverLockData { + amount: BN + reward: BN + timelock: BN + rewardTimelock: BN + sender: PublicKey + recipient: PublicKey + rewardRecipient: PublicKey + secret: number[] + tokenMint: PublicKey + rewardTokenMint: PublicKey + status: number +} + +type TypedProgramAccounts = { + userLock: { fetch(pda: PublicKey): Promise } + solverLock: { fetch(pda: PublicKey): Promise } +} export class SolanaHTLCClient extends HTLCClient { private connection: Connection private signer: SolanaSigner | undefined constructor(config: SolanaHTLCClientConfig) { - super(config.apiClient as any) + super(config.apiClient as TrainApiClient) this.connection = new Connection(config.rpcUrl, 'confirmed') this.signer = config.signer } @@ -106,6 +136,8 @@ export class SolanaHTLCClient extends HTLCClient { program.programId ) + console.log('[SolanaHTLC][refund] start', { id, userLockPda: userLockPda.toBase58(), isToken: !!sourceAsset.contractAddress }) + try { let refundIx: TransactionInstruction if (sourceAsset.contractAddress) { @@ -117,6 +149,7 @@ export class SolanaHTLCClient extends HTLCClient { program.programId ) + console.log('[SolanaHTLC][refund] building refundUserToken ix', { tokenMint: tokenMint.toBase58(), vault: vault.toBase58(), senderTokenAccount: senderTokenAccount.toBase58() }) refundIx = await program.methods .refundUserToken(hashlockArray) .accounts({ @@ -131,6 +164,7 @@ export class SolanaHTLCClient extends HTLCClient { }) .instruction() } else { + console.log('[SolanaHTLC][refund] building refundUserSol ix') refundIx = await program.methods .refundUserSol(hashlockArray) .accounts({ @@ -143,13 +177,11 @@ export class SolanaHTLCClient extends HTLCClient { const closeIx = await program.methods .closeUserLock(hashlockArray) - .accounts({ - caller: walletPublicKey, - userLock: userLockPda, - }) + .accounts({ caller: walletPublicKey, userLock: userLockPda }) .instruction() const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() + console.log('[SolanaHTLC][refund] sending tx', { blockhash, lastValidBlockHeight }) const tx = new Transaction() tx.recentBlockhash = blockhash tx.lastValidBlockHeight = lastValidBlockHeight @@ -157,15 +189,17 @@ export class SolanaHTLCClient extends HTLCClient { tx.add(refundIx, closeIx) const signature = await signer.sendTransaction(tx) + console.log('[SolanaHTLC][refund] tx sent', { signature }) const res = await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }) + console.log('[SolanaHTLC][refund] confirmed', { signature, err: res?.value.err ?? null }) if (res?.value.err) { throw new Error(res.value.err.toString()) } return signature } catch (error: any) { - console.error('[SolanaHTLC] refund failed', error?.logs ?? error) + console.error('[SolanaHTLC] refund failed', error?.message ?? error, error?.logs ?? []) throw error } } @@ -193,7 +227,7 @@ export class SolanaHTLCClient extends HTLCClient { ) try { - const solverLockAccount = await (program.account as any).solverLock.fetch(solverLockPda) + const solverLockAccount = await (program.account as TypedProgramAccounts).solverLock.fetch(solverLockPda) const rewardRecipient: PublicKey = new PublicKey(solverLockAccount.rewardRecipient) const recipient = destinationAddress @@ -273,10 +307,32 @@ export class SolanaHTLCClient extends HTLCClient { : Promise.resolve({} as { userData?: string; blockTimestamp?: number }) const accountInfo = await this.connection.getAccountInfo(userLockPda) - if (!accountInfo) return null + if (!accountInfo) { + const sigs = await this.connection.getSignaturesForAddress(userLockPda, { limit: 1 }).catch(() => []) + if (!sigs.length) return null + const closedTx = await this.connection.getTransaction(sigs[0].signature, { commitment: 'confirmed', maxSupportedTransactionVersion: 0 }) + const PROGRAM_DATA_PREFIX = 'Program data: ' + const PROGRAM_LOG_PREFIX = 'Program log: ' + for (const log of closedTx?.meta?.logMessages ?? []) { + const isData = log.startsWith(PROGRAM_DATA_PREFIX) + if (!isData && !log.startsWith(PROGRAM_LOG_PREFIX)) continue + const event = program.coder.events.decode(isData ? log.slice(PROGRAM_DATA_PREFIX.length) : log.slice(PROGRAM_LOG_PREFIX.length)) + if (!event) continue + const name = event.name.toLowerCase() + if (name === 'userrefunded' || name === 'userredeemed') { + return { + hashlock: `0x${id.replace('0x', '')}`, + amount: 0, timelock: 0, secret: undefined, + status: name === 'userrefunded' ? LockStatus.Refunded : LockStatus.Redeemed, + blockTimestamp: closedTx?.blockTime ? closedTx.blockTime * 1000 : undefined, + } + } + } + return null + } try { - const result = await (program.account as any).userLock.fetch(userLockPda) + const result = await (program.account as TypedProgramAccounts).userLock.fetch(userLockPda) if (!result) return null @@ -312,11 +368,12 @@ export class SolanaHTLCClient extends HTLCClient { const hashlockBuffer = Buffer.from(id.replace('0x', ''), 'hex') const pk = this.signer ? new PublicKey(this.signer.publicKey) : new PublicKey('11111111111111111111111111111111') - const provider = new AnchorProvider( - connection, - { publicKey: pk, signTransaction: async (tx: any) => tx, signAllTransactions: async (txs: any[]) => txs } as any, - AnchorProvider.defaultOptions() - ) + const wallet = { + publicKey: pk, + signTransaction: async (tx: T): Promise => tx, + signAllTransactions: async (txs: T[]): Promise => txs, + } + const provider = new AnchorProvider(connection, wallet as Wallet, AnchorProvider.defaultOptions()) const program = new Program(TrainHtlc(contractAddress), provider) // Count-then-loop: iterate PDAs from index 1 until getAccountInfo returns null @@ -333,7 +390,7 @@ export class SolanaHTLCClient extends HTLCClient { if (!accountInfo) return null try { - const result = await (program.account as any).solverLock.fetch(solverLockPda) + const result = await (program.account as TypedProgramAccounts).solverLock.fetch(solverLockPda) if (!result) continue @@ -381,7 +438,7 @@ export class SolanaHTLCClient extends HTLCClient { return this.signer } - private parseSecret(secretBytes: Uint8Array): bigint | undefined { + private parseSecret(secretBytes: Uint8Array | number[]): bigint | undefined { return Array.from(secretBytes).some(b => b !== 0) ? BigInt(bytesToHex(Array.from(secretBytes))) : undefined @@ -390,10 +447,10 @@ export class SolanaHTLCClient extends HTLCClient { private buildReadOnlyProvider(publicKey: PublicKey): AnchorProvider { const wallet = { publicKey, - signTransaction: async (tx: any) => tx, - signAllTransactions: async (txs: any[]) => txs, + signTransaction: async (tx: T): Promise => tx, + signAllTransactions: async (txs: T[]): Promise => txs, } - return new AnchorProvider(this.connection, wallet as any, AnchorProvider.defaultOptions()) + return new AnchorProvider(this.connection, wallet as Wallet, AnchorProvider.defaultOptions()) } private buildProgram(contractAddress: string, readerKey?: PublicKey): Program { @@ -440,7 +497,7 @@ export class SolanaHTLCClient extends HTLCClient { const eventHashlock = '0x' + Buffer.from(hashlockBytes).toString('hex') if (eventHashlock.toLowerCase() !== `0x${id.replace('0x', '')}`.toLowerCase()) continue - const userDataBytes: number[] = Array.from((event.data as any).userData as Buffer ?? []) + const userDataBytes: number[] = Array.from((event.data as Record).userData as Buffer ?? []) const userData = userDataBytes.length > 0 ? Buffer.from(userDataBytes).toString('utf8').replace(/\0/g, '').trim() || undefined : undefined @@ -455,40 +512,4 @@ export class SolanaHTLCClient extends HTLCClient { } } - async estimateGas(params: { - contractAddress: string - address: string - tokenSymbol: string - tokenContractAddress?: string | null - decimals: number - }): Promise { - const walletPublicKey = new PublicKey(params.address) - const program = this.buildProgram(params.contractAddress, walletPublicKey) - - const { transaction } = await userLockTransactionBuilder({ - connection: this.connection, - program, - walletPublicKey, - hashlock: Buffer.alloc(32), - sourceChain: 'solana', - destinationChain: 'eip155:1', - destinationAsset: 'ETH', - destinationAddress: params.address, - destinationAmount: '1', - lpAddress: params.address, - sourceAsset: { symbol: params.tokenSymbol, contractAddress: params.tokenContractAddress }, - amount: '1', - decimals: params.decimals, - timelockDelta: 69, - quoteExpiry: Math.floor(Date.now() / 1000) + 3600, - rewardAmount: '0', - rewardToken: '', - rewardRecipient: '', - rewardTimelockDelta: 34, - }) - - const message = transaction.compileMessage() - const result = await this.connection.getFeeForMessage(message) - return result.value ?? undefined - } } From f2a065f90a619af8b1be6a4dc1f86144af688c71 Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Fri, 6 Mar 2026 17:36:34 +0400 Subject: [PATCH 04/10] feat: implement Solana wallet login integration and add tests for HTLC client --- apps/app/context/walletLoginContext.tsx | 14 +++- .../lib/wallets/solana/useSolanaWalletRef.ts | 19 ++++++ packages/blockchains/solana/package.json | 6 +- .../src/__tests__/registerSolanaSdk.test.ts | 65 +++++++++++++++++++ packages/blockchains/solana/src/client.ts | 7 -- packages/blockchains/solana/vitest.config.ts | 7 ++ pnpm-lock.yaml | 3 + 7 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 apps/app/lib/wallets/solana/useSolanaWalletRef.ts create mode 100644 packages/blockchains/solana/src/__tests__/registerSolanaSdk.test.ts create mode 100644 packages/blockchains/solana/vitest.config.ts diff --git a/apps/app/context/walletLoginContext.tsx b/apps/app/context/walletLoginContext.tsx index f98da721..5f60ceb3 100644 --- a/apps/app/context/walletLoginContext.tsx +++ b/apps/app/context/walletLoginContext.tsx @@ -5,6 +5,7 @@ import { deriveKeyFromEvmSignature } from '@/lib/htlc/secretDerivation/walletSig import { deriveKeyFromWallet } from '@train-protocol/sdk' import { deriveKeyFromStarknetSignature } from '@/lib/htlc/secretDerivation/walletSign/starknet' import useWallet from '@/hooks/useWallet' +import { useSolanaWalletRef } from '@/lib/wallets/solana/useSolanaWalletRef' interface WalletLoginContextValue { deriveKey: (providerName: string, address: string) => Promise } @@ -15,6 +16,7 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) { const evmConfig = useConfig() const { getWallet: getAztecWallet } = useAztecWalletContext() const { wallets } = useWallet() + const solanaWalletRef = useSolanaWalletRef() const deriveKey = useCallback( async (providerName: string, address: string): Promise => { @@ -41,9 +43,19 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) { return deriveKeyFromStarknetSignature(starknetAccount, address) } + if (provider === 'solana') { + let { signMessage } = solanaWalletRef.current + if (!signMessage) { + await new Promise(resolve => setTimeout(resolve, 0)) + signMessage = solanaWalletRef.current.signMessage + } + if (!signMessage) throw new Error('Solana wallet does not support message signing') + return deriveKeyFromWallet('solana', { wallet: { signMessage } }) + } + throw new Error(`Unsupported wallet provider for login: ${providerName}`) }, - [evmConfig, getAztecWallet], + [evmConfig, getAztecWallet, wallets], ) return ( diff --git a/apps/app/lib/wallets/solana/useSolanaWalletRef.ts b/apps/app/lib/wallets/solana/useSolanaWalletRef.ts new file mode 100644 index 00000000..6eda5475 --- /dev/null +++ b/apps/app/lib/wallets/solana/useSolanaWalletRef.ts @@ -0,0 +1,19 @@ +import { useRef } from 'react' +import { useWallet } from '@solana/wallet-adapter-react' + +/** + * Returns a ref that always holds the latest Solana wallet hook state. + * + * Assigning ref.current synchronously during render (rather than in useEffect) + * is the documented React escape hatch for reading fresh state inside stable + * callbacks — see https://react.dev/learn/referencing-values-with-refs. + * This lets old useCallback closures read the current adapter without needing + * the wallet as a dependency (which would cascade re-renders through all + * consuming contexts). + */ +export function useSolanaWalletRef() { + const wallet = useWallet() + const ref = useRef(wallet) + ref.current = wallet + return ref +} diff --git a/packages/blockchains/solana/package.json b/packages/blockchains/solana/package.json index 9d0360a4..2a7749e9 100644 --- a/packages/blockchains/solana/package.json +++ b/packages/blockchains/solana/package.json @@ -21,7 +21,8 @@ "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" + "check:types": "tsc --noEmit", + "test": "vitest run" }, "dependencies": { "@coral-xyz/anchor": "^0.30.1", @@ -35,7 +36,8 @@ "@train-protocol/sdk": "workspace:^", "@types/node": "^20", "rimraf": "^6.0.1", - "typescript": "catalog:" + "typescript": "catalog:", + "vitest": "^4.0.18" }, "engines": { "node": ">=18" diff --git a/packages/blockchains/solana/src/__tests__/registerSolanaSdk.test.ts b/packages/blockchains/solana/src/__tests__/registerSolanaSdk.test.ts new file mode 100644 index 00000000..58e6c59b --- /dev/null +++ b/packages/blockchains/solana/src/__tests__/registerSolanaSdk.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { + getRegisteredNamespaces, + getRegisteredWalletSignProviders, + createHTLCClient, + deriveKeyFromWallet, + type TrainApiClient, +} from '@train-protocol/sdk' +import { registerSolanaSdk } from '../index.js' + +const mockApiClient = {} as TrainApiClient + +describe('registerSolanaSdk', () => { + // Note: because the registry is a global singleton and registerSolanaSdk is + // idempotent (guarded by a module-level flag), these tests run in sequence + // and the first call registers while subsequent calls are no-ops. + + beforeEach(() => { + registerSolanaSdk() + }) + + it('registers the solana HTLC client', () => { + expect(getRegisteredNamespaces()).toContain('solana') + }) + + it('registers the solana wallet-sign provider', () => { + expect(getRegisteredWalletSignProviders()).toContain('solana') + }) + + it('createHTLCClient works for solana after registration', () => { + const client = createHTLCClient('solana', { rpcUrl: 'https://api.devnet.solana.com', apiClient: mockApiClient }) + expect(client).toBeDefined() + expect(typeof client.getUserLockDetails).toBe('function') + expect(typeof client.getSolverLockDetails).toBe('function') + expect(typeof client.userLock).toBe('function') + expect(typeof client.refund).toBe('function') + expect(typeof client.redeemSolver).toBe('function') + }) + + it('deriveKeyFromWallet is callable for solana after registration', async () => { + // We can't fully exercise wallet signing without a real Solana wallet, + // but we verify the factory is wired up (it rejects with a signing error, + // not a "No wallet sign registered" error). + await expect( + deriveKeyFromWallet('solana', { wallet: { signMessage: null } }) + ).rejects.toThrow() + + // Confirm the error is NOT "No wallet sign registered" — that would mean + // registration didn't work. Any other error means the factory was found. + try { + await deriveKeyFromWallet('solana', { wallet: { signMessage: null } }) + } catch (e: unknown) { + expect((e as Error).message).not.toContain('No wallet sign registered') + } + }) + + it('multiple calls do not throw or double-register', () => { + // Call again — should be a no-op + expect(() => registerSolanaSdk()).not.toThrow() + + // Still only one solana entry (Map.set overwrites, but the guard prevents even that) + const namespaces = getRegisteredNamespaces().filter(ns => ns === 'solana') + expect(namespaces).toHaveLength(1) + }) +}) diff --git a/packages/blockchains/solana/src/client.ts b/packages/blockchains/solana/src/client.ts index 961b6a53..9276c79c 100644 --- a/packages/blockchains/solana/src/client.ts +++ b/packages/blockchains/solana/src/client.ts @@ -136,8 +136,6 @@ export class SolanaHTLCClient extends HTLCClient { program.programId ) - console.log('[SolanaHTLC][refund] start', { id, userLockPda: userLockPda.toBase58(), isToken: !!sourceAsset.contractAddress }) - try { let refundIx: TransactionInstruction if (sourceAsset.contractAddress) { @@ -149,7 +147,6 @@ export class SolanaHTLCClient extends HTLCClient { program.programId ) - console.log('[SolanaHTLC][refund] building refundUserToken ix', { tokenMint: tokenMint.toBase58(), vault: vault.toBase58(), senderTokenAccount: senderTokenAccount.toBase58() }) refundIx = await program.methods .refundUserToken(hashlockArray) .accounts({ @@ -164,7 +161,6 @@ export class SolanaHTLCClient extends HTLCClient { }) .instruction() } else { - console.log('[SolanaHTLC][refund] building refundUserSol ix') refundIx = await program.methods .refundUserSol(hashlockArray) .accounts({ @@ -181,7 +177,6 @@ export class SolanaHTLCClient extends HTLCClient { .instruction() const { blockhash, lastValidBlockHeight } = await this.connection.getLatestBlockhash() - console.log('[SolanaHTLC][refund] sending tx', { blockhash, lastValidBlockHeight }) const tx = new Transaction() tx.recentBlockhash = blockhash tx.lastValidBlockHeight = lastValidBlockHeight @@ -189,10 +184,8 @@ export class SolanaHTLCClient extends HTLCClient { tx.add(refundIx, closeIx) const signature = await signer.sendTransaction(tx) - console.log('[SolanaHTLC][refund] tx sent', { signature }) const res = await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }) - console.log('[SolanaHTLC][refund] confirmed', { signature, err: res?.value.err ?? null }) if (res?.value.err) { throw new Error(res.value.err.toString()) } diff --git a/packages/blockchains/solana/vitest.config.ts b/packages/blockchains/solana/vitest.config.ts new file mode 100644 index 00000000..72a92608 --- /dev/null +++ b/packages/blockchains/solana/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['src/__tests__/**/*.test.ts'], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e2082f0..10913a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -426,6 +426,9 @@ importers: typescript: specifier: 'catalog:' version: 5.9.3 + vitest: + specifier: ^4.0.18 + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.34)(jiti@2.6.1)(lightningcss@1.31.1) packages/blockchains/starknet: dependencies: From 741ff088927f92265b30d1c7530f9b7c0cafbcbd Mon Sep 17 00:00:00 2001 From: yasha-meursault Date: Fri, 6 Mar 2026 19:17:49 +0400 Subject: [PATCH 05/10] refactor: update provider identifier from id to name across wallet connection components --- apps/app/components/WalletModal/ConnectorsList.tsx | 12 ++++++------ .../WalletModal/MultichainConnectorPicker.tsx | 2 +- apps/app/context/evmConnectorsContext.tsx | 2 +- apps/app/hooks/useConnectors.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/app/components/WalletModal/ConnectorsList.tsx b/apps/app/components/WalletModal/ConnectorsList.tsx index b05e907f..9a4b96ef 100644 --- a/apps/app/components/WalletModal/ConnectorsList.tsx +++ b/apps/app/components/WalletModal/ConnectorsList.tsx @@ -66,15 +66,15 @@ const ConnectorsList: FC<{ onFinish: (result: Wallet | undefined) => void }> = ( if (result && connector && provider) { setRecentConnectors((prev) => { - const next = [{ providerName: provider.id, connectorName: connector.name }]; + const next = [{ providerName: provider.name, connectorName: connector.name }]; const counts = new Map(); - counts.set(provider.id, 1); + counts.set(provider.name, 1); (prev || []).forEach(item => { if ( item.providerName && item.connectorName && - !(item.providerName === provider.id && item.connectorName === connector.name) + !(item.providerName === provider.name && item.connectorName === connector.name) ) { const count = counts.get(item.providerName) ?? 0; if (count < 3) { @@ -151,7 +151,7 @@ const ConnectorsList: FC<{ onFinish: (result: Wallet | undefined) => void }> = ( }, [hasMoreToLoad, isLoadingMore, loadMore, selectedConnector, selectedMultiChainConnector]); if (selectedConnector?.extensionNotFound && !selectedConnector?.showQrCode && !isMobilePlatfrom) { - const provider = featuredProviders.find(p => p.id === selectedConnector?.providerName) + const provider = featuredProviders.find(p => p.name === selectedConnector?.providerName) return { connect(connector, provider!) }} /> } if (selectedConnector?.qr?.state && (!selectedConnector?.hasBrowserExtension || selectedConnector?.showQrCode)) { @@ -159,7 +159,7 @@ const ConnectorsList: FC<{ onFinish: (result: Wallet | undefined) => void }> = ( } if (selectedConnector) { - const provider = featuredProviders.find(p => p.id === selectedConnector?.providerName) + const provider = featuredProviders.find(p => p.name === selectedConnector?.providerName) return { (selectedConnector && provider) && connect(selectedConnector, provider) }} selectedConnector={selectedConnector} @@ -204,7 +204,7 @@ const ConnectorsList: FC<{ onFinish: (result: Wallet | undefined) => void }> = (
{ displayedConnectors.map(item => { - const provider = featuredProviders.find(p => p.id === item.providerName) + const provider = featuredProviders.find(p => p.name === item.providerName) const isRecent = recentConnectors?.some(v => v.connectorName === item.name) return ( = ({ s }, new Map()) .values() ).map((connector, index) => { - const provider = providers.find(p => p.id === connector?.providerName) + const provider = providers.find(p => p.name === connector?.providerName) return (