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/evmConnectorsContext.tsx b/apps/app/context/evmConnectorsContext.tsx index 2f786147..faed1fd2 100644 --- a/apps/app/context/evmConnectorsContext.tsx +++ b/apps/app/context/evmConnectorsContext.tsx @@ -75,7 +75,7 @@ export function EvmConnectorsProvider({ children }) { const initialRecentConnectors = useMemo(() => { const evmRecentConnectors = recentConnectors.filter(c => - c.providerName === 'eip155' + c.providerName === 'EVM' && c.connectorName && !featuredWalletsIds.includes(c.connectorName.toLowerCase()) ) diff --git a/apps/app/context/walletLoginContext.tsx b/apps/app/context/walletLoginContext.tsx index f98da721..f26edba8 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 { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react' interface WalletLoginContextValue { deriveKey: (providerName: string, address: string) => Promise } @@ -15,12 +16,13 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) { const evmConfig = useConfig() const { getWallet: getAztecWallet } = useAztecWalletContext() const { wallets } = useWallet() + const { wallets: solanaAdapterWallets } = useSolanaWallet() const deriveKey = useCallback( async (providerName: string, address: string): Promise => { const provider = providerName.toLowerCase() - if (provider === 'eip155') { + if (provider === 'evm') { return deriveKeyFromEvmSignature(evmConfig, address as `0x${string}`) } @@ -41,9 +43,16 @@ export function WalletLoginProvider({ children }: { children: ReactNode }) { return deriveKeyFromStarknetSignature(starknetAccount, address) } + if (provider === 'solana') { + const connectedAdapter = solanaAdapterWallets.find(w => w.adapter.connected)?.adapter + const signMessage = connectedAdapter && 'signMessage' in connectedAdapter ? (msg: Uint8Array) => (connectedAdapter as any).signMessage(msg) : undefined + if (!signMessage) throw new Error('Solana wallet is not connected') + return deriveKeyFromWallet('solana', { wallet: { signMessage } }) + } + throw new Error(`Unsupported wallet provider for login: ${providerName}`) }, - [evmConfig, getAztecWallet], + [evmConfig, getAztecWallet, wallets, solanaAdapterWallets], ) return ( diff --git a/apps/app/helpers/getSettings.ts b/apps/app/helpers/getSettings.ts index 3343205f..fa724b10 100644 --- a/apps/app/helpers/getSettings.ts +++ b/apps/app/helpers/getSettings.ts @@ -79,6 +79,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, } @@ -182,6 +207,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 b549ae76..0c81982b 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 type { StarknetSigner } from '@train-protocol/starknet' import { Network } from '../../Models/Network' import { Wallet } from '@/Models/WalletProvider' @@ -20,11 +22,29 @@ export function useHTLCWriteClient() { const config = useConfig() const getEffectiveRpcUrls = useRpcConfigStore(s => s.getEffectiveRpcUrls) const { wallet: aztecWallet, accountAddress: aztecAccountAddress } = useAztecWalletContext() + const { connection: solanaConnection } = useConnection() + const { wallets: solanaWallets } = 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 + const connectedWallet = solanaWallets.find(w => w.adapter.connected) + const connectedPublicKey = connectedWallet?.adapter.publicKey + if (connectedWallet && connectedPublicKey) { + signer = { + publicKey: connectedPublicKey.toBase58(), + sendTransaction: async (tx) => connectedWallet.adapter.sendTransaction(tx as any, solanaConnection), + } + } else { + console.error('[useHTLCWriteClient] Solana signer unavailable', { hasPubkey: !!connectedPublicKey }) + } + return createClient(chainType, { rpcUrl, signer, apiClient }) + } + // Aztec chain path if (chainType === 'aztec') { let signer: AztecSigner | undefined @@ -96,5 +116,5 @@ export function useHTLCWriteClient() { } return createClient(chainType, { rpcUrl, signer, apiClient }) - }, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress]) + }, [config, getEffectiveRpcUrls, aztecWallet, aztecAccountAddress, solanaWallets, 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/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 5b316f6c..4abaf62c 100644 --- a/apps/app/lib/gases/gasResolver.ts +++ b/apps/app/lib/gases/gasResolver.ts @@ -1,11 +1,13 @@ import { GasProps } from "../../Models/Balance"; import { EVMGasProvider } from "./providers/evmGasProvider"; +import { SolanaGasProvider } from "./providers/solanaGasProvider"; import { StarknetGasProvider } from "./providers/starknetGasProvider"; export class GasResolver { private providers = [ new EVMGasProvider(), + new SolanaGasProvider(), new StarknetGasProvider(), ]; diff --git a/apps/app/lib/gases/providers/solanaGasProvider.ts b/apps/app/lib/gases/providers/solanaGasProvider.ts index e5c14b08..66b56730 100644 --- a/apps/app/lib/gases/providers/solanaGasProvider.ts +++ b/apps/app/lib/gases/providers/solanaGasProvider.ts @@ -1,44 +1,84 @@ 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 lamports = await estimateSolanaGas({ + rpcUrl: network.nodes?.[0]?.url ?? '', + contractAddress: atomicContract, + address, + tokenSymbol: token.symbol, + tokenContractAddress: token.contractAddress, + decimals: token.decimals ?? 6, + }) - const nativeToken = getNativeToken(network) + const gas = lamports ? Number(formatUnits(BigInt(lamports), nativeToken.decimals)) : undefined + return gas !== undefined ? { gas, token: nativeToken } : undefined + } catch (e) { + console.error(e) + } + } +} - if (!transaction || !nativeToken) return +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 { userLockTransactionBuilder, TrainHtlc } = await import('@train-protocol/solana') - const message = transaction.compileMessage(); - const result = await connection.getFeeForMessage(message) + 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 formatedGas = result.value ? Number(formatUnits(BigInt(result.value), nativeToken.decimals)) : undefined + const { transaction } = await userLockTransactionBuilder({ + connection, + program, + walletPublicKey, + hashlock: '0x' + Buffer.alloc(32).toString('hex'), + sourceChain: 'solana', + destinationChain: 'eip155:1', + destinationAsset: 'ETH', + destinationAddress: params.address, + destinationAmount: '1', + srcLpAddress: 'bD5zQpd6RkbNJDtW7cBf1mw6wHzFxZ71wPCMwAmMh6n', + sourceAsset: { symbol: params.tokenSymbol, contractAddress: params.tokenContractAddress ?? '', decimals: params.decimals }, + amount: '1', + decimals: params.decimals, + timelockDelta: 69, + quoteExpiry: Math.floor(Date.now() / 1000) + 3600, + rewardAmount: '0', + rewardToken: '', + rewardRecipient: '', + rewardTimelockDelta: 34, + } as any) - return formatedGas - } - catch (e) { - console.log(e) - } - } + 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/knownIds.ts b/apps/app/lib/knownIds.ts index d108b0a3..de2534ae 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..74fadd2b 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 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..f7562b33 100644 --- a/apps/app/lib/wallets/solana/useSVM.tsx +++ b/apps/app/lib/wallets/solana/useSVM.tsx @@ -22,7 +22,7 @@ export default function useSVM(): WalletProvider { const name = 'Solana' const id = 'solana' - const { disconnect, wallet: solanaWallet, select, wallets, signTransaction, signMessage } = useWallet(); + const { disconnect, wallet: solanaWallet, select, wallets, signTransaction } = useWallet(); const publicKey = solanaWallet?.adapter.publicKey const connectedWallet = wallets.find(w => w.adapter.connected === true) @@ -37,6 +37,7 @@ export default function useSVM(): WalletProvider { if (anchorProvider) setProvider(anchorProvider); }, [anchorProvider]); + const connectedWallets = useMemo(() => { if (solanaWallet?.adapter.connected === true) { const wallet: Wallet | undefined = (connectedAddress && connectedAdapterName) ? { @@ -73,6 +74,7 @@ export default function useSVM(): WalletProvider { const newConnectedWallet = wallets.find(w => w.adapter.connected === true) const connectedAddress = newConnectedWallet?.adapter.publicKey?.toBase58() + const wallet: Wallet | undefined = connectedAddress && newConnectedWallet ? { id: newConnectedWallet.adapter.name, address: connectedAddress, @@ -161,11 +163,11 @@ function resolveSupportedNetworks(supportedNetworks: string[], connectorId: stri const supportedNetworksForWallet: string[] = []; supportedNetworks.forEach((network) => { - const networkName = network.split("_")[0].toLowerCase(); + const networkName = network.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/apps/app/package.json b/apps/app/package.json index b2f65bad..cf9e943e 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:^", "@train-protocol/starknet": "workspace:^", "@uidotdev/usehooks": "^2.4.1", "@vercel/analytics": "^1.5.0", diff --git a/apps/app/pages/_app.js b/apps/app/pages/_app.js index 70b90c7d..5e4b6f7e 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()); import('@train-protocol/starknet').then(m => m.registerStarknetSdk()); } diff --git a/packages/blockchains/evm/src/__tests__/registerEvmSdk.test.ts b/packages/blockchains/evm/src/__tests__/registerEvmSdk.test.ts index bfde7277..8af03b15 100644 --- a/packages/blockchains/evm/src/__tests__/registerEvmSdk.test.ts +++ b/packages/blockchains/evm/src/__tests__/registerEvmSdk.test.ts @@ -19,15 +19,15 @@ describe('registerEvmSdk', () => { registerEvmSdk() }) - it('registers the eip155 HTLC client', () => { + it('registers the evm HTLC client', () => { expect(getRegisteredNamespaces()).toContain('eip155') }) - it('registers the eip155 wallet-sign provider', () => { + it('registers the evm wallet-sign provider', () => { expect(getRegisteredWalletSignProviders()).toContain('eip155') }) - it('createHTLCClient works for eip155 after registration', () => { + it('createHTLCClient works for evm after registration', () => { const client = createHTLCClient('eip155', { rpcUrl: 'https://example.com', apiClient: mockApiClient }) expect(client).toBeDefined() expect(typeof client.getUserLockDetails).toBe('function') @@ -37,7 +37,7 @@ describe('registerEvmSdk', () => { expect(typeof client.redeemSolver).toBe('function') }) - it('deriveKeyFromWallet is callable for eip155 after registration', async () => { + it('deriveKeyFromWallet is callable for evm after registration', async () => { // We can't fully exercise wallet signing without a real EIP-1193 provider, // but we verify the factory is wired up (it rejects with a provider error, // not a "no wallet sign registered" error). @@ -58,7 +58,7 @@ describe('registerEvmSdk', () => { // Call again — should be a no-op expect(() => registerEvmSdk()).not.toThrow() - // Still only one eip155 entry (Map.set overwrites, but the guard prevents even that) + // Still only one evm entry (Map.set overwrites, but the guard prevents even that) const namespaces = getRegisteredNamespaces().filter(ns => ns === 'eip155') expect(namespaces).toHaveLength(1) }) diff --git a/packages/blockchains/solana/package.json b/packages/blockchains/solana/package.json new file mode 100644 index 00000000..2a7749e9 --- /dev/null +++ b/packages/blockchains/solana/package.json @@ -0,0 +1,45 @@ +{ + "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", + "test": "vitest run" + }, + "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:", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/blockchains/solana/src/__tests__/client.test.ts b/packages/blockchains/solana/src/__tests__/client.test.ts new file mode 100644 index 00000000..35beeaf8 --- /dev/null +++ b/packages/blockchains/solana/src/__tests__/client.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { LockStatus, type TrainApiClient } from '@train-protocol/sdk' +import { BN } from '@coral-xyz/anchor' +import { PublicKey } from '@solana/web3.js' +import { SolanaHTLCClient } from '../client.js' + +// ── Constants ──────────────────────────────────────────────────────────────── + +const BLOCKHASH = 'BlockHash1111111111111111111111111111111111' +const SIGNATURE = 'TxSig111111111111111111111111111111111111111' +const CONTRACT = '11111111111111111111111111111111' +const HASHLOCK = '0x' + 'ab'.repeat(32) +const SIGNER_KEY = 'So11111111111111111111111111111111111111112' +const SENDER_KEY = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' +const RECIPIENT_KEY = '4NSREK36nAr32vooa3L7US9byT11xGJhbxPZfY4iBhEj' +const TOKEN_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' + +// ── Mocks ──────────────────────────────────────────────────────────────────── + +const mocks = vi.hoisted(() => { + const makeChain = () => { + const chain: any = { + accounts: vi.fn(), + instruction: vi.fn().mockResolvedValue({}), + } + chain.accounts.mockReturnValue(chain) + return chain + } + + return { + program: { + methods: { + refundUserSol: vi.fn().mockReturnValue(makeChain()), + refundUserToken: vi.fn().mockReturnValue(makeChain()), + closeUserLock: vi.fn().mockReturnValue(makeChain()), + }, + account: { userLock: { fetch: vi.fn() } }, + programId: null as any, + coder: { events: { decode: vi.fn().mockReturnValue(null) } }, + }, + connection: { + getAccountInfo: vi.fn().mockResolvedValue(null), + getSignaturesForAddress: vi.fn().mockResolvedValue([]), + getTransaction: vi.fn().mockResolvedValue(null), + getLatestBlockhash: vi.fn().mockResolvedValue({ + blockhash: 'BlockHash1111111111111111111111111111111111', + lastValidBlockHeight: 100, + }), + confirmTransaction: vi.fn().mockResolvedValue({ value: { err: null } }), + }, + makeChain, + } +}) + +vi.mock('@solana/web3.js', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, Connection: vi.fn().mockImplementation(function () { return mocks.connection }) } +}) + +vi.mock('@coral-xyz/anchor', async (importOriginal) => { + const actual = await importOriginal() + const MockAnchorProvider: any = vi.fn().mockImplementation(function () { return {} }) + MockAnchorProvider.defaultOptions = actual.AnchorProvider.defaultOptions + return { + ...actual, + AnchorProvider: MockAnchorProvider, + Program: vi.fn().mockImplementation(function () { return mocks.program }), + } +}) + +vi.mock('@solana/spl-token', () => ({ + getAssociatedTokenAddress: vi.fn().mockResolvedValue('MockTokenAccount'), + TOKEN_PROGRAM_ID: 'MockTokenProgram', + ASSOCIATED_TOKEN_PROGRAM_ID: 'MockAssocTokenProgram', +})) + +// ── Tests ───────────────────────────────────────────────────────────────────── + +const mockApiClient = {} as TrainApiClient + +describe('SolanaHTLCClient', () => { + let client: SolanaHTLCClient + let sendTransaction: ReturnType + + beforeEach(() => { + mocks.program.programId = new PublicKey(CONTRACT) + vi.clearAllMocks() + mocks.connection.getAccountInfo.mockResolvedValue(null) + mocks.connection.getSignaturesForAddress.mockResolvedValue([]) + mocks.connection.getTransaction.mockResolvedValue(null) + mocks.connection.getLatestBlockhash.mockResolvedValue({ blockhash: BLOCKHASH, lastValidBlockHeight: 100 }) + mocks.connection.confirmTransaction.mockResolvedValue({ value: { err: null } }) + mocks.program.coder.events.decode.mockReturnValue(null) + mocks.program.methods.refundUserSol.mockReturnValue(mocks.makeChain()) + mocks.program.methods.refundUserToken.mockReturnValue(mocks.makeChain()) + mocks.program.methods.closeUserLock.mockReturnValue(mocks.makeChain()) + + sendTransaction = vi.fn().mockResolvedValue(SIGNATURE) + client = new SolanaHTLCClient({ + rpcUrl: 'https://api.devnet.solana.com', + apiClient: mockApiClient, + signer: { publicKey: SIGNER_KEY, sendTransaction: sendTransaction as any }, + }) + }) + + // ── getUserLockDetails ─────────────────────────────────────────────────── + + describe('getUserLockDetails', () => { + const baseParams = { + id: HASHLOCK, + contractAddress: CONTRACT, + chainId: null, + decimals: 6, + } + + it('throws when contractAddress is missing', async () => { + await expect( + client.getUserLockDetails({ ...baseParams, contractAddress: '' }) + ).rejects.toThrow('No contract address') + }) + + it('returns null when account not found and no past signatures', async () => { + const result = await client.getUserLockDetails(baseParams) + expect(result).toBeNull() + }) + + it('returns LockDetails for an active SOL lock', async () => { + mocks.connection.getAccountInfo.mockResolvedValue({ data: Buffer.from([]) }) + mocks.program.account.userLock.fetch.mockResolvedValue({ + amount: new BN(2_000_000), + timelock: new BN(1_700_000_000), + sender: new PublicKey(SENDER_KEY), + recipient: new PublicKey(RECIPIENT_KEY), + secret: new Array(32).fill(0), + tokenMint: new PublicKey('11111111111111111111111111111111'), + status: LockStatus.Pending, + }) + + const result = await client.getUserLockDetails(baseParams) + + expect(result).toMatchObject({ + hashlock: HASHLOCK, + amount: 2, + timelock: 1_700_000_000, + sender: SENDER_KEY, + recipient: RECIPIENT_KEY, + secret: undefined, + token: undefined, + status: LockStatus.Pending, + }) + }) + + it('returns LockDetails with token address for a token lock', async () => { + mocks.connection.getAccountInfo.mockResolvedValue({ data: Buffer.from([]) }) + mocks.program.account.userLock.fetch.mockResolvedValue({ + amount: new BN(5_000_000), + timelock: new BN(1_700_000_000), + sender: new PublicKey(SENDER_KEY), + recipient: new PublicKey(RECIPIENT_KEY), + secret: new Array(32).fill(0), + tokenMint: new PublicKey(TOKEN_MINT), + status: 0, + }) + + const result = await client.getUserLockDetails(baseParams) + + expect(result?.token).toBe(TOKEN_MINT) + expect(result?.amount).toBe(5) + }) + + it('returns Refunded status from closed account logs (Program data prefix)', async () => { + mocks.connection.getSignaturesForAddress.mockResolvedValue([{ signature: 'closedTxSig' }]) + mocks.connection.getTransaction.mockResolvedValue({ + blockTime: 1_700_000_000, + meta: { logMessages: ['Program data: encodedEvent'] }, + }) + mocks.program.coder.events.decode.mockReturnValue({ name: 'UserRefunded', data: {} }) + + const result = await client.getUserLockDetails(baseParams) + + expect(result).toMatchObject({ + hashlock: HASHLOCK, + status: LockStatus.Refunded, + blockTimestamp: 1_700_000_000_000, + }) + }) + + it('returns Redeemed status from closed account logs (Program log prefix)', async () => { + mocks.connection.getSignaturesForAddress.mockResolvedValue([{ signature: 'closedTxSig' }]) + mocks.connection.getTransaction.mockResolvedValue({ + blockTime: 1_700_000_000, + meta: { logMessages: ['Program log: encodedEvent'] }, + }) + mocks.program.coder.events.decode.mockReturnValue({ name: 'UserRedeemed', data: {} }) + + const result = await client.getUserLockDetails(baseParams) + + expect(result?.status).toBe(LockStatus.Redeemed) + }) + + it('returns null when account is closed but logs contain no matching event', async () => { + mocks.connection.getSignaturesForAddress.mockResolvedValue([{ signature: 'closedTxSig' }]) + mocks.connection.getTransaction.mockResolvedValue({ + blockTime: 1_700_000_000, + meta: { logMessages: ['Program data: encodedEvent'] }, + }) + // decode returns null — no matching event + mocks.program.coder.events.decode.mockReturnValue(null) + + const result = await client.getUserLockDetails(baseParams) + + expect(result).toBeNull() + }) + + it('returns null when program fetch throws', async () => { + mocks.connection.getAccountInfo.mockResolvedValue({ data: Buffer.from([]) }) + mocks.program.account.userLock.fetch.mockRejectedValue(new Error('RPC error')) + + const result = await client.getUserLockDetails(baseParams) + + expect(result).toBeNull() + }) + }) + + // ── refund ─────────────────────────────────────────────────────────────── + + describe('refund', () => { + const baseParams = { + type: 'native' as const, + id: HASHLOCK, + contractAddress: CONTRACT, + chainId: null, + sourceAsset: { symbol: 'SOL', contractAddress: '', decimals: 9 }, + } + + it('throws when signer is not configured', async () => { + const noSignerClient = new SolanaHTLCClient({ + rpcUrl: 'https://api.devnet.solana.com', + apiClient: mockApiClient, + }) + await expect(noSignerClient.refund(baseParams)).rejects.toThrow('Solana signer not configured') + }) + + it('throws when contractAddress is missing', async () => { + await expect( + client.refund({ ...baseParams, contractAddress: '' }) + ).rejects.toThrow('No contract address') + }) + + it('returns signature for SOL refund', async () => { + const result = await client.refund(baseParams) + + expect(result).toBe(SIGNATURE) + expect(mocks.program.methods.refundUserSol).toHaveBeenCalled() + expect(mocks.program.methods.closeUserLock).toHaveBeenCalled() + expect(sendTransaction).toHaveBeenCalledOnce() + }) + + it('returns signature for token refund', async () => { + const result = await client.refund({ + ...baseParams, + type: 'erc20', + sourceAsset: { symbol: 'USDC', contractAddress: TOKEN_MINT, decimals: 6 }, + }) + + expect(result).toBe(SIGNATURE) + expect(mocks.program.methods.refundUserToken).toHaveBeenCalled() + expect(mocks.program.methods.closeUserLock).toHaveBeenCalled() + }) + + it('throws when transaction confirmation reports an error', async () => { + mocks.connection.confirmTransaction.mockResolvedValue({ value: { err: 'InstructionError' } }) + + await expect(client.refund(baseParams)).rejects.toThrow('InstructionError') + }) + }) +}) 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 new file mode 100644 index 00000000..167c66fc --- /dev/null +++ b/packages/blockchains/solana/src/client.ts @@ -0,0 +1,368 @@ +import { AnchorProvider, BN, Program, Wallet } from '@coral-xyz/anchor' +import { Connection, PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js' +import { + HTLCClient, + UserLockParams, + LockParams, + RefundParams, + RedeemSolverParams, + LockDetails, + LockStatus, + AtomicResult, + RecoveredSwapData, + formatUnits, + bytesToHex, +} from '@train-protocol/sdk' +import { NATIVE_SOL_ADDRESS } from './constants.js' +import type { SolanaHTLCClientConfig, SolanaSigner } from './types.js' +import { TrainHtlc } from './idl/trainHtlc.js' +import { userLockTransactionBuilder, refundTransactionBuilder, redeemSolverTransactionBuilder } from './transactionBuilder.js' + +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) + this.connection = new Connection(config.rpcUrl, 'confirmed') + this.signer = config.signer + } + + async userLock(params: UserLockParams): Promise { + const signer = this.requireSigner() + + if (!params.atomicContract) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const program = this.buildProgram(params.atomicContract, walletPublicKey) + + const { transaction, blockhash, lastValidBlockHeight } = await userLockTransactionBuilder({ + ...params, + connection: this.connection, + program, + walletPublicKey, + quoteExpiry: params.quoteExpiry ?? Math.floor(Date.now() / 1000) + 86400, + }) + + 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: params.hashlock } + } + + async refund(params: RefundParams): Promise { + const signer = this.requireSigner() + + if (!params.contractAddress) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const program = this.buildProgram(params.contractAddress, walletPublicKey) + + try { + const { transaction, blockhash, lastValidBlockHeight } = await refundTransactionBuilder({ + ...params, + connection: this.connection, + program, + walletPublicKey, + }) + + const signature = await signer.sendTransaction(transaction) + + const res = await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }) + if (res?.value.err) { + throw new Error(res.value.err.toString()) + } + + return signature + } catch (error: any) { + console.error('[SolanaHTLC] refund failed', error?.message ?? error, error?.logs ?? []) + throw error + } + } + + async redeemSolver(params: RedeemSolverParams): Promise { + const signer = this.requireSigner() + + if (!params.contractAddress) throw new Error('No contract address') + + const walletPublicKey = new PublicKey(signer.publicKey) + const program = this.buildProgram(params.contractAddress, walletPublicKey) + + try { + const { transaction, blockhash, lastValidBlockHeight } = await redeemSolverTransactionBuilder({ + ...params, + connection: this.connection, + program, + walletPublicKey, + }) + + const signature = await signer.sendTransaction(transaction) + + const res = await this.connection.confirmTransaction({ blockhash, lastValidBlockHeight, signature }) + if (res?.value.err) { + throw new Error(res.value.err.toString()) + } + + return signature + } catch (error) { + console.error('Error in redeemSolver:', error) + throw error + } + } + + 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 + ) + + const accountInfo = await this.connection.getAccountInfo(userLockPda) + 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 }) + for (const event of this.parseLogEvents(closedTx?.meta?.logMessages ?? [], program)) { + 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 TypedProgramAccounts).userLock.fetch(userLockPda) + + if (!result) return null + + const { userData, blockTimestamp } = params.txId ? await this.findUserDataFromLogs(params.txId, id, program) : {} + + 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() !== NATIVE_SOL_ADDRESS + ? 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 program = this.buildProgram(contractAddress, undefined, connection) + + const hashlockArray = Array.from(hashlockBuffer) + const count = Number(await program.methods.getSolverLockCount(hashlockArray).view()) + if (count === 0) return null + + for (let i = 1; i <= count; i++) { + const indexBuffer = Buffer.alloc(8) + indexBuffer.writeBigUInt64LE(BigInt(i)) + + const [solverLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("solver_lock"), hashlockBuffer, indexBuffer], + program.programId + ) + + try { + const result = await (program.account as TypedProgramAccounts).solverLock.fetch(solverLockPda) + + if (!result) continue + + // Skip empty slots + const sender = new PublicKey(result.sender).toString() + if (sender === NATIVE_SOL_ADDRESS) 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() !== NATIVE_SOL_ADDRESS + ? result.tokenMint.toString() + : undefined, + rewardToken: result.rewardTokenMint && result.rewardTokenMint.toString() !== NATIVE_SOL_ADDRESS + ? 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 + } + } + + 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 | number[]): bigint | undefined { + return Array.from(secretBytes).some(b => b !== 0) ? BigInt(bytesToHex(Array.from(secretBytes))) : undefined + } + + private buildReadOnlyProvider(publicKey: PublicKey, connection?: Connection): AnchorProvider { + const wallet = { + publicKey, + signTransaction: async (tx: T): Promise => tx, + signAllTransactions: async (txs: T[]): Promise => txs, + } + return new AnchorProvider(connection ?? this.connection, wallet as Wallet, AnchorProvider.defaultOptions()) + } + + private buildProgram(contractAddress: string, readerKey?: PublicKey, connection?: Connection): Program { + const pk = readerKey ?? (this.signer ? new PublicKey(this.signer.publicKey) : new PublicKey(NATIVE_SOL_ADDRESS)) + const provider = this.buildReadOnlyProvider(pk, connection) + return new Program(TrainHtlc(contractAddress), provider) + } + + private parseLogEvents(logs: string[], program: Program): Array<{ name: string; data: Record }> { + const PROGRAM_DATA_PREFIX = 'Program data: ' + const PROGRAM_LOG_PREFIX = 'Program log: ' + const events: Array<{ name: string; data: Record }> = [] + for (const log of logs) { + 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) events.push(event as { name: string; data: Record }) + } + return events + } + + 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 ?? [] + + for (const event of this.parseLogEvents(logs, program)) { + if (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 Record).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 {} + } + } + + +} diff --git a/packages/blockchains/solana/src/constants.ts b/packages/blockchains/solana/src/constants.ts new file mode 100644 index 00000000..090a4684 --- /dev/null +++ b/packages/blockchains/solana/src/constants.ts @@ -0,0 +1 @@ +export const NATIVE_SOL_ADDRESS = '11111111111111111111111111111111' 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..f14dac4c --- /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, refundTransactionBuilder, redeemSolverTransactionBuilder } from './transactionBuilder.js' +export type { UserLockParams, RefundTxParams, RedeemSolverTxParams } 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..06ac49a0 --- /dev/null +++ b/packages/blockchains/solana/src/transactionBuilder.ts @@ -0,0 +1,240 @@ +import { Connection, PublicKey, Transaction, TransactionInstruction } from "@solana/web3.js" +import { BN, Idl, Program } from "@coral-xyz/anchor" +import { UserLockParams as SdkUserLockParams, RefundParams, RedeemSolverParams, parseUnits } from '@train-protocol/sdk' +import { NATIVE_SOL_ADDRESS } from './constants.js' + +type SolanaContext = { + connection: Connection + program: Program + walletPublicKey: PublicKey +} + +export type UserLockParams = SdkUserLockParams & SolanaContext +export type RefundTxParams = RefundParams & SolanaContext +export type RedeemSolverTxParams = RedeemSolverParams & SolanaContext + +export type TransactionResult = { + transaction: Transaction + blockhash: string + lastValidBlockHeight: number +} + +function toBaseUnits(amount: string, decimals: number): BN { + return new BN(parseUnits(amount, decimals).toString()) +} + +function secretToBuffer(secret: string | bigint): Buffer { + if (typeof secret === 'bigint') { + return Buffer.from(secret.toString(16).padStart(64, '0'), 'hex') + } + return Buffer.from(secret.replace('0x', ''), 'hex') +} + +export const userLockTransactionBuilder = async (params: UserLockParams): Promise => { + const { connection, program, walletPublicKey } = params + + if (!walletPublicKey) throw new Error("Wallet not connected") + if (!params.srcLpAddress) throw new Error("No LP address") + + const hashlock = Buffer.from(params.hashlock.replace('0x', ''), 'hex') + const bnAmount = toBaseUnits(params.amount, params.decimals) + const bnDstAmount = toBaseUnits(params.destinationAmount, params.decimals) + const bnRewardAmount = toBaseUnits(params.rewardAmount || '0', params.decimals) + const bnTimelockDelta = new BN(params.timelockDelta || 0) + const bnRewardTimelockDelta = new BN(params.rewardTimelockDelta || 0) + const bnQuoteExpiry = new BN(params.quoteExpiry) + const lpPublicKey = new PublicKey(params.srcLpAddress) + const hashlockArray = Array.from(hashlock) + const userData = params.nonce != null ? Buffer.from(params.nonce.toString(), 'utf8') : Buffer.from([]) + const solverDataBytes = params.solverData ? Buffer.from(params.solverData, 'utf8') : Buffer.from([]) + + const [userLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("user_lock"), hashlock], + program.programId + ) + + const tx = new Transaction() + + if (params.sourceAsset.contractAddress && params.sourceAsset.contractAddress !== NATIVE_SOL_ADDRESS) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(params.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, + params.sourceChain, params.destinationChain, params.destinationAddress, + bnDstAmount, params.destinationAsset, + bnRewardAmount, params.rewardToken ?? '', params.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, + params.sourceChain, params.destinationChain, params.destinationAddress, + bnDstAmount, params.destinationAsset, + bnRewardAmount, params.rewardToken ?? '', params.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 } +} + +export const refundTransactionBuilder = async (params: RefundTxParams): Promise => { + const { connection, program, walletPublicKey } = params + const hashlockBuffer = Buffer.from(params.id.replace('0x', ''), 'hex') + const hashlockArray = Array.from(hashlockBuffer) + + const [userLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("user_lock"), hashlockBuffer], + program.programId + ) + + let refundIx: TransactionInstruction + if (params.sourceAsset.contractAddress && params.sourceAsset.contractAddress !== NATIVE_SOL_ADDRESS) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(params.sourceAsset.contractAddress) + const senderTokenAccount = await getAssociatedTokenAddress(tokenMint, walletPublicKey) + const [vault] = PublicKey.findProgramAddressSync( + [Buffer.from("vault"), hashlockBuffer], + program.programId + ) + + refundIx = await program.methods + .refundUserToken(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + sender: walletPublicKey, + tokenMint, + vault, + senderTokenAccount, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .instruction() + } else { + refundIx = await program.methods + .refundUserSol(hashlockArray) + .accounts({ + caller: walletPublicKey, + userLock: userLockPda, + sender: walletPublicKey, + }) + .instruction() + } + + const closeIx = await program.methods + .closeUserLock(hashlockArray) + .accounts({ caller: walletPublicKey, userLock: userLockPda }) + .instruction() + + const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash() + const tx = new Transaction() + tx.recentBlockhash = blockhash + tx.lastValidBlockHeight = lastValidBlockHeight + tx.feePayer = walletPublicKey + tx.add(refundIx, closeIx) + + return { transaction: tx, blockhash, lastValidBlockHeight } +} + +export const redeemSolverTransactionBuilder = async (params: RedeemSolverTxParams): Promise => { + const { connection, program, walletPublicKey } = params + const hashlockBuffer = Buffer.from(params.id.replace('0x', ''), 'hex') + const hashlockArray = Array.from(hashlockBuffer) + const secretArray = Array.from(secretToBuffer(params.secret)) + const lockIndex = params.index ?? 1 + + const indexBuffer = Buffer.alloc(8) + indexBuffer.writeBigUInt64LE(BigInt(lockIndex)) + + const [solverLockPda] = PublicKey.findProgramAddressSync( + [Buffer.from("solver_lock"), hashlockBuffer, indexBuffer], + program.programId + ) + + const solverLockAccount = await (program.account as any).solverLock.fetch(solverLockPda) + const rewardRecipient = new PublicKey(solverLockAccount.rewardRecipient) + const recipient = params.destinationAddress ? new PublicKey(params.destinationAddress) : walletPublicKey + + let tx: Transaction + if (params.sourceAsset.contractAddress && params.sourceAsset.contractAddress !== NATIVE_SOL_ADDRESS) { + const { getAssociatedTokenAddress, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } = await import('@solana/spl-token') + const tokenMint = new PublicKey(params.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 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..4c70aa8c --- /dev/null +++ b/packages/blockchains/solana/src/types.ts @@ -0,0 +1,17 @@ +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 = BaseHTLCClientConfig & { + rpcUrl: string + signer?: SolanaSigner +} \ No newline at end of file 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/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 6f7f4f0a..10913a1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -185,6 +185,9 @@ importers: '@train-protocol/sdk': specifier: workspace:^ version: link:../../packages/sdk + '@train-protocol/solana': + specifier: workspace:^ + version: link:../../packages/blockchains/solana '@train-protocol/starknet': specifier: workspace:^ version: link:../../packages/blockchains/starknet @@ -399,6 +402,34 @@ 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 + 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: starknet: