From 1e43b217f5df5b5723d4e67d49bb6bbb2e20f2e5 Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Tue, 11 Nov 2025 20:27:09 +0100 Subject: [PATCH 1/8] feat: api v2 examples --- src/evm/index.ts | 234 +++++++++++++++++++++++++------------------- src/solana/index.ts | 200 ++++++++++++++++++------------------- 2 files changed, 235 insertions(+), 199 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index fdea4aa..f030714 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -1,112 +1,146 @@ -import axios from 'axios'; -import { createPublicClient, createWalletClient, http } from 'viem'; +import { createWalletClient, createPublicClient, http, type TypedDataDefinition } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { mainnet } from 'viem/chains'; +import { optimism } from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); -// Replace with your actual private key - NEVER hardcode in production code -const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as `0x${string}` +const API = 'https://stargate.finance/api/unstable'; +const API_KEY = process.env.STARGATE_API_KEY!; +const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as `0x${string}`; const account = privateKeyToAccount(PRIVATE_KEY); +const wallet = createWalletClient({ account, chain: optimism, transport: http() }); +const client = createPublicClient({ chain: optimism, transport: http() }); -// Initialize clients -const ethereumClient = createPublicClient({ - chain: mainnet, - transport: http() -}); - -const walletClient = createWalletClient({ - account, - chain: mainnet, - transport: http() -}); - -async function fetchStargateRoutes() { - try { - // Fetching route for USDC transfer from Ethereum to Polygon - https://docs.stargate.finance - const response = await axios.get('https://stargate.finance/api/v1/quotes', { - params: { - srcToken: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', // USDC on Ethereum - dstToken: '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', // USDC on Polygon - srcAddress: '0x0C0d18aa99B02946C70EAC6d47b8009b993c9BfF', - dstAddress: '0x0C0d18aa99B02946C70EAC6d47b8009b993c9BfF', - srcChainKey: 'ethereum', // All chainKeys - https://stargate.finance/api/v1/chains - dstChainKey: 'polygon', - srcAmount: '1000000', // 1 USDC (6 decimals) - dstAmountMin: '900000' // Amount to receive deducted by Stargate fees (max 0.15%) - } - }); - - console.log('Stargate quotes data:', response.data); - return response.data; - } catch (error) { - if (axios.isAxiosError(error)) { - console.error('Axios error:', error.message); - if (error.response) { - console.error('Response data:', error.response.data); - } - } else { - console.error('Unexpected error:', error); - } - throw error; - } +type AmountType = 'EXACT_SRC_AMOUNT'; +type FeeTolerance = { type: 'PERCENT'; amount?: number }; + +type GetQuotesInput = { + srcTokenAddress: string; + dstTokenAddress: string; + srcChainKey: string; + dstChainKey: string; + amount: string | bigint; + srcWalletAddress: string; + dstWalletAddress: string; + options: { + amountType?: AmountType; + feeTolerance?: FeeTolerance; + dstNativeDropAmount?: number | bigint; + }; +}; + +type QuoteHead = { id: string }; +type GetQuotesResult = { quotes: QuoteHead[] }; + +type EvmEncodedTx = { + chainId: number; + to: `0x${string}`; + data?: `0x${string}`; + value?: string | bigint; + from?: `0x${string}`; + gasLimit?: string | bigint; +}; + +type TransactionStep = { + type: 'TRANSACTION'; + chainKey: string; + chainType: 'EVM'; + description: string; + signerAddress: `0x${string}`; + transaction: { encoded: EvmEncodedTx }; +}; + +type SignatureStep = { + type: 'SIGNATURE'; + description: string; + chainKey?: string; + signerAddress: `0x${string}`; + signature: { type: 'EIP712'; typedData: TypedDataDefinition }; +}; + +type UserStep = TransactionStep | SignatureStep; + +type BuildUserStepsResult = { + body: { userSteps: UserStep[] }; +}; + +async function postJson(path: string, body: unknown): Promise { + const res = await fetch(`${API}${path}`, { + method: 'POST', + headers: { + 'x-api-key': API_KEY, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json() as Promise; } -async function executeStargateTransaction() { - try { - // 1. Fetch quotes data - const routesData = await fetchStargateRoutes(); - - // 2. Get the first route (or implement your own selection logic) - // Here you can select from all the supported routes including StargateV2:Taxi, StargateBus or CCTP - // Supported routes are different for each token - // Each route contains all transactions required to execute the transfer given in executable order - const selectedRoute = routesData.quotes[0]; - if (!selectedRoute) { - throw new Error('No quotes available'); - } - - console.log('Selected route:', selectedRoute); - - // Execute all transactions in the route steps - for (let i = 0; i < selectedRoute.steps.length; i++) { - const executableTransaction = selectedRoute.steps[i].transaction; - console.log(`Executing step ${i + 1}/${selectedRoute.steps.length}:`, executableTransaction); - - // Create transaction object, only include value if it exists and is not empty - const txParams: Record = { - account, - to: executableTransaction.to, - data: executableTransaction.data, - }; - - // Only add value if it exists and is not empty - if (executableTransaction.value && executableTransaction.value !== '0') { - txParams.value = BigInt(executableTransaction.value); - } - - // Execute the transaction - const txHash = await walletClient.sendTransaction(txParams); - console.log(`Step ${i + 1} transaction hash: ${txHash}`); - - // Wait for transaction to be mined - const receipt = await ethereumClient.waitForTransactionReceipt({ hash: txHash }); - console.log(`Step ${i + 1} transaction confirmed:`, receipt); +async function fetchQuotes(): Promise { + const payload: GetQuotesInput = { + srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', + srcChainKey: 'optimism', + dstChainKey: 'arbitrum', + amount: '1000000000000000', + srcWalletAddress: account.address, + dstWalletAddress: account.address, + options: { + amountType: 'EXACT_SRC_AMOUNT', + feeTolerance: { type: 'PERCENT', amount: 20 }, + dstNativeDropAmount: 0, + }, + }; + return postJson('/quotes', payload); +} + +async function buildUserSteps(quoteId: string) { + return postJson('/build-user-steps', { quoteId }); +} + +async function submitSignature(quoteId: string, signatures: string[]) { + await postJson>('/submit-signature', { quoteId, signatures }); +} + +function toBigIntOrUndefined(v: string | bigint | undefined): bigint | undefined { + if (v === undefined) return undefined; + return typeof v === 'string' ? BigInt(v) : v; +} + +async function executeEvmTransaction(step: TransactionStep) { + const tx = step.transaction.encoded; + + const hash = await wallet.sendTransaction({ + account, + to: tx.to, + data: tx.data, + value: toBigIntOrUndefined(tx.value) ?? 0n, + }); + + await client.waitForTransactionReceipt({ hash }); +} + +async function signEip712(step: SignatureStep) { + const typed = step.signature.typedData; + const signature = await wallet.signTypedData(typed); + return signature; +} + +async function run() { + const quotes = await fetchQuotes(); + const quote = quotes.quotes?.[0]; + if (!quote) throw new Error('No quote'); + + const { body } = await buildUserSteps(quote.id); + for (const step of body.userSteps) { + if (step.type === 'SIGNATURE') { + const sig = await signEip712(step); + await submitSignature(quote.id, [sig]); + } else if (step.type === 'TRANSACTION') { + await executeEvmTransaction(step); } - - console.log('All steps executed successfully'); - return true; - } catch (error) { - console.error('Error executing Stargate transaction:', error); - throw error; } } -// Execute the transaction -void executeStargateTransaction() - .then(() => { - console.log('Successfully executed Stargate transaction'); - }) - .catch((err) => { - console.error('Failed to execute Stargate transaction:', err); - }); \ No newline at end of file +void run(); \ No newline at end of file diff --git a/src/solana/index.ts b/src/solana/index.ts index f46ae01..deda334 100644 --- a/src/solana/index.ts +++ b/src/solana/index.ts @@ -1,116 +1,118 @@ -import axios from 'axios'; import * as web3 from '@solana/web3.js'; +import bs58 from 'bs58'; import * as dotenv from 'dotenv'; -import * as bs58 from 'bs58'; dotenv.config(); -// Initialize Solana connection and wallet -const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY as string; +const API = 'https://stargate.finance/api/v2'; +const API_KEY = process.env.STARGATE_API_KEY!; +const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!; +const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed'); -// Convert private key from string to Uint8Array -let privateKeyBytes; -try { - // Try to interpret as hex string (without 0x prefix) - privateKeyBytes = Buffer.from(PRIVATE_KEY.replace(/^0x/, ''), 'hex'); - - // Check if length is correct (should be 64 bytes for Ed25519) - if (privateKeyBytes.length !== 64) { - // Alternatively, try as base58 (Solana CLI format) - privateKeyBytes = Buffer.from(bs58.default.decode(PRIVATE_KEY)); +type FeeTolerance = { type: 'PERCENT'; amount?: number }; +type GetQuotesInput = { + srcChainKey: string; + dstChainKey: string; + srcTokenAddress: string; + dstTokenAddress: string; + srcWalletAddress: string; + dstWalletAddress: string; + amount: string | bigint; + options: { + amountType?: 'EXACT_SRC_AMOUNT'; + feeTolerance?: FeeTolerance; + dstNativeDropAmount?: number | bigint; + }; +}; +type Quote = { id: string }; +type GetQuotesResult = { quotes: Quote[] }; +type SolanaTxEncoded = { encoding: 'base64'; data: string }; +type UserTransactionStep = { + type: 'TRANSACTION'; + transaction: { encoded: SolanaTxEncoded }; +}; +type BuildUserStepsResult = { body: { userSteps: UserTransactionStep[] } }; + +function assert(v: T | undefined | null, msg: string): T { + if (v === undefined || v === null) throw new Error(msg); + return v; +} + +function parseSolanaSecretKey(raw: string): Uint8Array { + const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw); + if (isHex) { + const hex = raw.replace(/^0x/, ''); + const buf = Buffer.from(hex, 'hex'); + if (buf.length !== 64) throw new Error(`Expected 64-byte hex key, got ${buf.length}`); + return new Uint8Array(buf); } -} catch (error) { - console.error('Error parsing private key:', error); - throw new Error('Invalid private key format. Must be hex or base58 encoded.'); + const decoded = bs58.decode(raw); + if (decoded.length !== 64) throw new Error(`Expected 64-byte base58 key, got ${decoded.length}`); + return decoded; +} + +const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(PRIVATE_KEY)); + +async function postJson(path: string, body: unknown): Promise { + const res = await fetch(`${API}${path}`, { + method: 'POST', + headers: { + 'x-api-key': assert(API_KEY, 'Missing STARGATE_API_KEY'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!res.ok) throw new Error(await res.text()); + return res.json() as Promise; } -// Make sure the key has correct length -if (privateKeyBytes.length !== 64) { - throw new Error(`Bad secret key size: ${privateKeyBytes.length}. Expected 64 bytes.`); +async function fetchQuote(): Promise { + const payload: GetQuotesInput = { + srcChainKey: 'solana', + dstChainKey: 'optimism', + srcTokenAddress: 'DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT', + dstTokenAddress: '0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34', + srcWalletAddress: keypair.publicKey.toBase58(), + dstWalletAddress: '0x9F1473c484Ce6b227538765b1c996DDfEc853DAA', + amount: '3308758007', + options: { amountType: 'EXACT_SRC_AMOUNT', feeTolerance: { type: 'PERCENT', amount: 20 }, dstNativeDropAmount: 0 }, + }; + + const result = await postJson('/quotes', payload); + const quote = result.quotes?.[0]; + if (!quote) throw new Error('No quote'); + return quote; +} + +async function buildUserSteps(quoteId: string) { + return postJson('/build-user-steps', { quoteId }); } -const keypair = web3.Keypair.fromSecretKey(privateKeyBytes); +async function executeSolanaSteps(steps: UserTransactionStep[]) { + for (const step of steps) { + if (step.type !== 'TRANSACTION') continue; + + const tx = step.transaction.encoded; + if (tx.encoding !== 'base64') continue; -// Initialize Solana connection -const connection = new web3.Connection( - web3.clusterApiUrl('mainnet-beta'), - 'confirmed' -); + const raw = Buffer.from(tx.data, 'base64'); + const msg = web3.VersionedMessage.deserialize(raw); + const vtx = new web3.VersionedTransaction(msg); + vtx.sign([keypair]); -// For sending FROM Solana TO another chain (e.g., Ethereum) -async function sendFromSolanaToOtherChain() { - try { - // This would require fetching a quotes with Solana as the source chain - - // 1. First, we would fetch the appropriate route - const response = await axios.get('https://stargate.finance/api/v1/quotes', { - params: { - srcToken: 'DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT', // Token on Solana - dstToken: '0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34', // Token on destination chain - srcAddress: '9hWTHmE8T2fTeuog1K2ZzBtg8pfKhh3fcYAJUo54Vz37', // Source address - dstAddress: '0x9F1473c484Ce6b227538765b1c996DDfEc853DAA', // Destination address - srcChainKey: 'solana', - dstChainKey: 'optimism', - srcAmount: '3308758007', // Amount to send - dstAmountMin: '3215670426000000000' // Minimum to receive after fees - } + const sig = await connection.sendTransaction(vtx); + const latest = await connection.getLatestBlockhash(); + await connection.confirmTransaction({ + signature: sig, + blockhash: latest.blockhash, + lastValidBlockHeight: latest.lastValidBlockHeight, }); - - const quotesData = response.data; - console.log('Quotes for sending from Solana:', quotesData); - - if (!quotesData.quotes || quotesData.quotes.length === 0) { - throw new Error('No quotes available for sending from Solana'); - } - - const quote = quotesData.quotes[0]; - - // 2. Execute each step in the quote - for (let i = 0; i < quote.steps.length; i++) { - const step = quote.steps[i]; - console.log(`Executing step ${i + 1}/${quote.steps.length}:`, step); - - // For Solana, the transaction would typically be provided as serialized data - // We would deserialize, sign, and send it - if (step.transaction && step.transaction.data) { - // The format depends on Stargate's API response for Solana transactions - // This is a simplified example - actual implementation would depend on the API response format - - // If data is provided as a serialized transaction - const transactionBuffer = Buffer.from(step.transaction.data, 'base64'); - - // Use VersionedMessage.deserialize instead of Transaction.from - const versionedMessage = web3.VersionedMessage.deserialize(transactionBuffer); - const transaction = new web3.VersionedTransaction(versionedMessage); - - // Sign and send the transaction - transaction.sign([keypair]); - const signature = await connection.sendTransaction(transaction); - - // Wait for confirmation - const latestBlockHash = await connection.getLatestBlockhash(); - await connection.confirmTransaction({ - signature, - blockhash: latestBlockHash.blockhash, - lastValidBlockHeight: latestBlockHash.lastValidBlockHeight - }); - - console.log(`Transaction signature: ${signature}`); - } - } - - return true; - } catch (error) { - console.error('Error sending from Solana:', error); - throw error; } } +async function run() { + const quote = await fetchQuote(); + const { body } = await buildUserSteps(quote.id); + await executeSolanaSteps(body.userSteps); +} -void sendFromSolanaToOtherChain() - .then(() => { - console.log('Successfully sent tokens from Solana'); - }) - .catch((err) => { - console.error('Failed to send tokens from Solana:', err); - }); - +void run(); \ No newline at end of file From b59badeb2be1aa60e5c2255c30a152715106f2f9 Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Tue, 11 Nov 2025 21:26:49 +0100 Subject: [PATCH 2/8] refactor: code improvements --- src/evm/index.ts | 13 ++++--------- src/solana/index.ts | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index f030714..c925b34 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -4,7 +4,7 @@ import { optimism } from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); -const API = 'https://stargate.finance/api/unstable'; +const API = 'https://stargate.finance/api/v2'; const API_KEY = process.env.STARGATE_API_KEY!; const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as `0x${string}`; const account = privateKeyToAccount(PRIVATE_KEY); @@ -103,11 +103,6 @@ async function submitSignature(quoteId: string, signatures: string[]) { await postJson>('/submit-signature', { quoteId, signatures }); } -function toBigIntOrUndefined(v: string | bigint | undefined): bigint | undefined { - if (v === undefined) return undefined; - return typeof v === 'string' ? BigInt(v) : v; -} - async function executeEvmTransaction(step: TransactionStep) { const tx = step.transaction.encoded; @@ -115,7 +110,7 @@ async function executeEvmTransaction(step: TransactionStep) { account, to: tx.to, data: tx.data, - value: toBigIntOrUndefined(tx.value) ?? 0n, + value: BigInt(tx.value ?? 0n), }); await client.waitForTransactionReceipt({ hash }); @@ -135,8 +130,8 @@ async function run() { const { body } = await buildUserSteps(quote.id); for (const step of body.userSteps) { if (step.type === 'SIGNATURE') { - const sig = await signEip712(step); - await submitSignature(quote.id, [sig]); + const signature = await signEip712(step); + await submitSignature(quote.id, [signature]); } else if (step.type === 'TRANSACTION') { await executeEvmTransaction(step); } diff --git a/src/solana/index.ts b/src/solana/index.ts index deda334..6848dca 100644 --- a/src/solana/index.ts +++ b/src/solana/index.ts @@ -99,10 +99,10 @@ async function executeSolanaSteps(steps: UserTransactionStep[]) { const vtx = new web3.VersionedTransaction(msg); vtx.sign([keypair]); - const sig = await connection.sendTransaction(vtx); + const signature = await connection.sendTransaction(vtx); const latest = await connection.getLatestBlockhash(); await connection.confirmTransaction({ - signature: sig, + signature, blockhash: latest.blockhash, lastValidBlockHeight: latest.lastValidBlockHeight, }); From 37aafca1b1d574cf410e041e1498306282e8704c Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Wed, 12 Nov 2025 10:41:01 +0100 Subject: [PATCH 3/8] feat: implement get status, fix userSteps types --- src/evm/index.ts | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index c925b34..8c9a8c9 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -59,11 +59,13 @@ type SignatureStep = { }; type UserStep = TransactionStep | SignatureStep; - type BuildUserStepsResult = { - body: { userSteps: UserStep[] }; + userSteps: UserStep[]; }; +type Status = 'PENDING' | 'PROCESSING' | 'SUCCEEDED' | 'FAILED' | 'UNKNOWN'; +type GetStatusResult = { status: Status; explorerUrl?: string }; + async function postJson(path: string, body: unknown): Promise { const res = await fetch(`${API}${path}`, { method: 'POST', @@ -77,18 +79,27 @@ async function postJson(path: string, body: unknown): Promise { return res.json() as Promise; } +async function getJson(path: string): Promise { + const res = await fetch(`${API}${path}`, { + method: 'GET', + headers: { 'x-api-key': API_KEY }, + }); + if (!res.ok) throw new Error(await res.text()); + return res.json() as Promise; +} + async function fetchQuotes(): Promise { const payload: GetQuotesInput = { srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', srcChainKey: 'optimism', dstChainKey: 'arbitrum', - amount: '1000000000000000', + amount: '100000000000000', srcWalletAddress: account.address, dstWalletAddress: account.address, options: { amountType: 'EXACT_SRC_AMOUNT', - feeTolerance: { type: 'PERCENT', amount: 20 }, + feeTolerance: { type: 'PERCENT', amount: 1 }, dstNativeDropAmount: 0, }, }; @@ -103,6 +114,21 @@ async function submitSignature(quoteId: string, signatures: string[]) { await postJson>('/submit-signature', { quoteId, signatures }); } +async function getStatus(quoteId: string, txHash?: `0x${string}`) { + const query = txHash ? `?txHash=${txHash}` : ''; + return getJson(`/status/${encodeURIComponent(quoteId)}${query}`); +} + +async function pollStatus(quoteId: string, txHash?: `0x${string}`) { + const deadline = Date.now() + 5 * 60_000; + for (;;) { + const { status } = await getStatus(quoteId, txHash); + if (status === 'SUCCEEDED' || status === 'FAILED' || status === 'UNKNOWN') return status; + if (Date.now() > deadline) return 'UNKNOWN'; + await new Promise((r) => setTimeout(r, 4_000)); + } +} + async function executeEvmTransaction(step: TransactionStep) { const tx = step.transaction.encoded; @@ -114,6 +140,7 @@ async function executeEvmTransaction(step: TransactionStep) { }); await client.waitForTransactionReceipt({ hash }); + return hash; } async function signEip712(step: SignatureStep) { @@ -124,18 +151,23 @@ async function signEip712(step: SignatureStep) { async function run() { const quotes = await fetchQuotes(); + // You can implement a logic to choose the best quote here const quote = quotes.quotes?.[0]; if (!quote) throw new Error('No quote'); - const { body } = await buildUserSteps(quote.id); - for (const step of body.userSteps) { + const {userSteps} = await buildUserSteps(quote.id); + let txHash: `0x${string}` | undefined; + for (const step of userSteps) { if (step.type === 'SIGNATURE') { const signature = await signEip712(step); await submitSignature(quote.id, [signature]); } else if (step.type === 'TRANSACTION') { - await executeEvmTransaction(step); + txHash = await executeEvmTransaction(step); } } + + const status = await pollStatus(quote.id, txHash); + console.log('Final status:', status); } void run(); \ No newline at end of file From 4e1cbbc8191c5073008ff4df82f652c5b682d56b Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Wed, 12 Nov 2025 12:48:20 +0100 Subject: [PATCH 4/8] feat: support intents --- src/evm/index.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index 8c9a8c9..3ef4c21 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -1,4 +1,4 @@ -import { createWalletClient, createPublicClient, http, type TypedDataDefinition } from 'viem'; +import { createWalletClient, createPublicClient, http, type TypedDataDefinition, getAddress, verifyTypedData } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { optimism } from 'viem/chains'; import * as dotenv from 'dotenv'; @@ -143,9 +143,33 @@ async function executeEvmTransaction(step: TransactionStep) { return hash; } +// Normalizes message fields for correct EIP-712 signing. +export function mapMessageTypes( + message: any, +) { + return { + offerer: message.offerer, + recipient: message.recipient, + inputToken: message.inputToken, + outputToken: message.outputToken, + inputAmount: BigInt(message.inputAmount), + outputAmount: BigInt(message.outputAmount), + startTime: BigInt(message.startTime), + endTime: BigInt(message.endTime), + srcEid: message.srcEid, + dstEid: message.dstEid + } +} + async function signEip712(step: SignatureStep) { const typed = step.signature.typedData; - const signature = await wallet.signTypedData(typed); + const signature = await wallet.signTypedData({ + account, + domain: typed.domain, + types: typed.types, + primaryType: typed.primaryType, + message: mapMessageTypes(typed.message), + }); return signature; } @@ -154,8 +178,8 @@ async function run() { // You can implement a logic to choose the best quote here const quote = quotes.quotes?.[0]; if (!quote) throw new Error('No quote'); - const {userSteps} = await buildUserSteps(quote.id); + let txHash: `0x${string}` | undefined; for (const step of userSteps) { if (step.type === 'SIGNATURE') { From 903e05c33675dd17bc58d2f7cf68908fe6c7f9a8 Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Thu, 13 Nov 2025 15:34:48 +0100 Subject: [PATCH 5/8] refactor: code improvements --- src/evm/index.ts | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index 3ef4c21..4d4b2fa 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -1,15 +1,16 @@ -import { createWalletClient, createPublicClient, http, type TypedDataDefinition, getAddress, verifyTypedData } from 'viem'; +import { createWalletClient, createPublicClient, http, type TypedDataDefinition, getAddress, verifyTypedData, Hex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { optimism } from 'viem/chains'; +import { base } from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); +// Setup: initialize wallet and client const API = 'https://stargate.finance/api/v2'; const API_KEY = process.env.STARGATE_API_KEY!; -const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as `0x${string}`; +const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as Hex; const account = privateKeyToAccount(PRIVATE_KEY); -const wallet = createWalletClient({ account, chain: optimism, transport: http() }); -const client = createPublicClient({ chain: optimism, transport: http() }); +const wallet = createWalletClient({ account, chain: base, transport: http() }); +const client = createPublicClient({ chain: base, transport: http() }); type AmountType = 'EXACT_SRC_AMOUNT'; type FeeTolerance = { type: 'PERCENT'; amount?: number }; @@ -34,10 +35,10 @@ type GetQuotesResult = { quotes: QuoteHead[] }; type EvmEncodedTx = { chainId: number; - to: `0x${string}`; - data?: `0x${string}`; + to: Hex; + data?: Hex; value?: string | bigint; - from?: `0x${string}`; + from?: Hex; gasLimit?: string | bigint; }; @@ -46,7 +47,7 @@ type TransactionStep = { chainKey: string; chainType: 'EVM'; description: string; - signerAddress: `0x${string}`; + signerAddress: Hex; transaction: { encoded: EvmEncodedTx }; }; @@ -54,7 +55,7 @@ type SignatureStep = { type: 'SIGNATURE'; description: string; chainKey?: string; - signerAddress: `0x${string}`; + signerAddress: Hex; signature: { type: 'EIP712'; typedData: TypedDataDefinition }; }; @@ -66,6 +67,7 @@ type BuildUserStepsResult = { type Status = 'PENDING' | 'PROCESSING' | 'SUCCEEDED' | 'FAILED' | 'UNKNOWN'; type GetStatusResult = { status: Status; explorerUrl?: string }; +// Helper functions for API requests async function postJson(path: string, body: unknown): Promise { const res = await fetch(`${API}${path}`, { method: 'POST', @@ -88,18 +90,19 @@ async function getJson(path: string): Promise { return res.json() as Promise; } +// Core API calls async function fetchQuotes(): Promise { const payload: GetQuotesInput = { srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', dstTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', - srcChainKey: 'optimism', - dstChainKey: 'arbitrum', + srcChainKey: 'base', + dstChainKey: 'optimism', amount: '100000000000000', srcWalletAddress: account.address, dstWalletAddress: account.address, options: { amountType: 'EXACT_SRC_AMOUNT', - feeTolerance: { type: 'PERCENT', amount: 1 }, + feeTolerance: { type: 'PERCENT', amount: 50 }, dstNativeDropAmount: 0, }, }; @@ -114,12 +117,13 @@ async function submitSignature(quoteId: string, signatures: string[]) { await postJson>('/submit-signature', { quoteId, signatures }); } -async function getStatus(quoteId: string, txHash?: `0x${string}`) { +async function getStatus(quoteId: string, txHash?: Hex) { const query = txHash ? `?txHash=${txHash}` : ''; return getJson(`/status/${encodeURIComponent(quoteId)}${query}`); } -async function pollStatus(quoteId: string, txHash?: `0x${string}`) { +// Execution logic +async function pollStatus(quoteId: string, txHash?: Hex) { const deadline = Date.now() + 5 * 60_000; for (;;) { const { status } = await getStatus(quoteId, txHash); @@ -131,19 +135,17 @@ async function pollStatus(quoteId: string, txHash?: `0x${string}`) { async function executeEvmTransaction(step: TransactionStep) { const tx = step.transaction.encoded; - const hash = await wallet.sendTransaction({ account, to: tx.to, data: tx.data, value: BigInt(tx.value ?? 0n), }); - await client.waitForTransactionReceipt({ hash }); return hash; } -// Normalizes message fields for correct EIP-712 signing. +// Normalizes message fields for EIP-712 signing. export function mapMessageTypes( message: any, ) { @@ -178,10 +180,10 @@ async function run() { // You can implement a logic to choose the best quote here const quote = quotes.quotes?.[0]; if (!quote) throw new Error('No quote'); - const {userSteps} = await buildUserSteps(quote.id); - - let txHash: `0x${string}` | undefined; - for (const step of userSteps) { + // NOTE: build user steps is not supported yet + // const {userSteps} = await buildUserSteps(quote.id); + let txHash: Hex | undefined; + for (const step of (quote as unknown as {userSteps: UserStep[]}).userSteps) { if (step.type === 'SIGNATURE') { const signature = await signEip712(step); await submitSignature(quote.id, [signature]); From 96ccadda4c139b7433a0fff141d851347576101c Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Thu, 13 Nov 2025 16:09:30 +0100 Subject: [PATCH 6/8] feat: add api v2 on solana example --- src/solana/index.ts | 77 ++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/src/solana/index.ts b/src/solana/index.ts index 6848dca..6e4a344 100644 --- a/src/solana/index.ts +++ b/src/solana/index.ts @@ -6,6 +6,7 @@ dotenv.config(); const API = 'https://stargate.finance/api/v2'; const API_KEY = process.env.STARGATE_API_KEY!; const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!; + const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed'); type FeeTolerance = { type: 'PERCENT'; amount?: number }; @@ -23,8 +24,10 @@ type GetQuotesInput = { dstNativeDropAmount?: number | bigint; }; }; + type Quote = { id: string }; type GetQuotesResult = { quotes: Quote[] }; + type SolanaTxEncoded = { encoding: 'base64'; data: string }; type UserTransactionStep = { type: 'TRANSACTION'; @@ -32,10 +35,8 @@ type UserTransactionStep = { }; type BuildUserStepsResult = { body: { userSteps: UserTransactionStep[] } }; -function assert(v: T | undefined | null, msg: string): T { - if (v === undefined || v === null) throw new Error(msg); - return v; -} +type Status = 'PENDING' | 'PROCESSING' | 'SUCCEEDED' | 'FAILED' | 'UNKNOWN'; +type GetStatusResult = { status: Status; explorerUrl?: string }; function parseSolanaSecretKey(raw: string): Uint8Array { const isHex = /^0x[0-9a-fA-F]+$/.test(raw) || /^[0-9a-fA-F]+$/.test(raw); @@ -56,7 +57,7 @@ async function postJson(path: string, body: unknown): Promise { const res = await fetch(`${API}${path}`, { method: 'POST', headers: { - 'x-api-key': assert(API_KEY, 'Missing STARGATE_API_KEY'), + 'x-api-key': API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify(body), @@ -65,29 +66,54 @@ async function postJson(path: string, body: unknown): Promise { return res.json() as Promise; } -async function fetchQuote(): Promise { +async function getJson(path: string): Promise { + const res = await fetch(`${API}${path}`, { + method: 'GET', + headers: { 'x-api-key': API_KEY }, + }); + if (!res.ok) throw new Error(await res.text()); + return res.json() as Promise; +} + +async function fetchQuotes(): Promise { const payload: GetQuotesInput = { srcChainKey: 'solana', - dstChainKey: 'optimism', - srcTokenAddress: 'DEkqHyPN7GMRJ5cArtQFAWefqbZb33Hyf6s5iCwjEonT', - dstTokenAddress: '0x5d3a1Ff2b6BAb83b63cd9AD0787074081a52ef34', + dstChainKey: 'arbitrum', + srcTokenAddress: 'CAW777xcHVTQZ4CRwVQGB8CV1BVKPm5bNVxFJHWFKiH8', + dstTokenAddress: '0x16f1967565aaD72DD77588a332CE445e7cEF752b', srcWalletAddress: keypair.publicKey.toBase58(), - dstWalletAddress: '0x9F1473c484Ce6b227538765b1c996DDfEc853DAA', - amount: '3308758007', - options: { amountType: 'EXACT_SRC_AMOUNT', feeTolerance: { type: 'PERCENT', amount: 20 }, dstNativeDropAmount: 0 }, + dstWalletAddress: '0x6d9798053f498451bec79c0397f7f95b079bdcd6', + amount: '1000000000000', + options: { + amountType: 'EXACT_SRC_AMOUNT', + feeTolerance: { type: 'PERCENT', amount: 20 }, + dstNativeDropAmount: 0, + }, }; - - const result = await postJson('/quotes', payload); - const quote = result.quotes?.[0]; - if (!quote) throw new Error('No quote'); - return quote; + return postJson('/quotes', payload); } async function buildUserSteps(quoteId: string) { return postJson('/build-user-steps', { quoteId }); } +async function getStatus(quoteId: string, txSig?: string) { + const query = txSig ? `?txHash=${encodeURIComponent(txSig)}` : ''; + return getJson(`/status/${encodeURIComponent(quoteId)}${query}`); +} + +async function pollStatus(quoteId: string, txSig?: string) { + const deadline = Date.now() + 5 * 60_000; + for (;;) { + const { status } = await getStatus(quoteId, txSig); + if (status === 'SUCCEEDED' || status === 'FAILED' || status === 'UNKNOWN') return status; + if (Date.now() > deadline) return 'UNKNOWN'; + await new Promise((r) => setTimeout(r, 4_000)); + } +} + async function executeSolanaSteps(steps: UserTransactionStep[]) { + let signature: string | undefined; for (const step of steps) { if (step.type !== 'TRANSACTION') continue; @@ -99,7 +125,8 @@ async function executeSolanaSteps(steps: UserTransactionStep[]) { const vtx = new web3.VersionedTransaction(msg); vtx.sign([keypair]); - const signature = await connection.sendTransaction(vtx); + signature = await connection.sendTransaction(vtx); + const latest = await connection.getLatestBlockhash(); await connection.confirmTransaction({ signature, @@ -107,12 +134,20 @@ async function executeSolanaSteps(steps: UserTransactionStep[]) { lastValidBlockHeight: latest.lastValidBlockHeight, }); } + + return signature; } async function run() { - const quote = await fetchQuote(); - const { body } = await buildUserSteps(quote.id); - await executeSolanaSteps(body.userSteps); + const quotes = await fetchQuotes(); + const quote = quotes.quotes?.[0]; + if (!quote) throw new Error('No quote'); + + const { body } = await buildUserSteps(quote.id); // Solana requires user steps to get tx + const txSig = await executeSolanaSteps(body.userSteps); + + const status = await pollStatus(quote.id, txSig); + console.log('Final status:', status); } void run(); \ No newline at end of file From c220df312145dda0a5ce602419979e165a7fed92 Mon Sep 17 00:00:00 2001 From: konrad-w3 Date: Fri, 14 Nov 2025 14:32:19 +0100 Subject: [PATCH 7/8] feat: add comments --- src/evm/index.ts | 16 ++++++++++++++-- src/solana/index.ts | 15 +++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index 4d4b2fa..28e3c08 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -4,6 +4,7 @@ import { base } from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); +// The code demonstrates how to use the Stargate API to fetch quotes and execute transactions. // Setup: initialize wallet and client const API = 'https://stargate.finance/api/v2'; const API_KEY = process.env.STARGATE_API_KEY!; @@ -90,7 +91,9 @@ async function getJson(path: string): Promise { return res.json() as Promise; } -// Core API calls +// Core API operations for Stargate cross-chain transfers. +// Here, we fetch quotes for sending native tokens (ETH) from Base to Optimism. +// The options specify to use the exact source amount and include a fee tolerance of 2%. async function fetchQuotes(): Promise { const payload: GetQuotesInput = { srcTokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', @@ -102,21 +105,30 @@ async function fetchQuotes(): Promise { dstWalletAddress: account.address, options: { amountType: 'EXACT_SRC_AMOUNT', - feeTolerance: { type: 'PERCENT', amount: 50 }, + feeTolerance: { type: 'PERCENT', amount: 2 }, dstNativeDropAmount: 0, }, }; return postJson('/quotes', payload); } +// Builds user-interactive steps required to complete the transaction. +// Useful for both signature requests (like EIP-712) and direct EVM transactions. +// Can be integrated into a UI to guide users through signing messages or submitting transactions. async function buildUserSteps(quoteId: string) { return postJson('/build-user-steps', { quoteId }); } +// Submits signatures for a given quote. +// Required for EIP-712 messages that need to be signed by the user. +// Can be integrated into a UI to submit signatures after users have signed messages. async function submitSignature(quoteId: string, signatures: string[]) { await postJson>('/submit-signature', { quoteId, signatures }); } +// Checks the status of a transaction. +// Useful for monitoring the progress of a transaction. +// Can be integrated into a UI to display the status of a transaction. async function getStatus(quoteId: string, txHash?: Hex) { const query = txHash ? `?txHash=${txHash}` : ''; return getJson(`/status/${encodeURIComponent(quoteId)}${query}`); diff --git a/src/solana/index.ts b/src/solana/index.ts index 6e4a344..775dc87 100644 --- a/src/solana/index.ts +++ b/src/solana/index.ts @@ -3,10 +3,10 @@ import bs58 from 'bs58'; import * as dotenv from 'dotenv'; dotenv.config(); +// Setup: initialize wallet and client const API = 'https://stargate.finance/api/v2'; const API_KEY = process.env.STARGATE_API_KEY!; const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!; - const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed'); type FeeTolerance = { type: 'PERCENT'; amount?: number }; @@ -53,6 +53,7 @@ function parseSolanaSecretKey(raw: string): Uint8Array { const keypair = web3.Keypair.fromSecretKey(parseSolanaSecretKey(PRIVATE_KEY)); +// Helper functions for API requests async function postJson(path: string, body: unknown): Promise { const res = await fetch(`${API}${path}`, { method: 'POST', @@ -75,6 +76,9 @@ async function getJson(path: string): Promise { return res.json() as Promise; } +// Core API operations for Stargate cross-chain transfers. +// Here, we fetch quotes for sending OFT tokens (CAW) from Solana to Arbitrum. +// The options specify to use the exact source amount and include a fee tolerance of 2%. async function fetchQuotes(): Promise { const payload: GetQuotesInput = { srcChainKey: 'solana', @@ -86,22 +90,29 @@ async function fetchQuotes(): Promise { amount: '1000000000000', options: { amountType: 'EXACT_SRC_AMOUNT', - feeTolerance: { type: 'PERCENT', amount: 20 }, + feeTolerance: { type: 'PERCENT', amount: 2 }, dstNativeDropAmount: 0, }, }; return postJson('/quotes', payload); } +// Builds user-interactive steps required to complete the transaction. +// Useful for both signature requests (like EIP-712) and direct Solana transactions. +// Can be integrated into a UI to guide users through signing messages or submitting transactions. async function buildUserSteps(quoteId: string) { return postJson('/build-user-steps', { quoteId }); } +// Checks the status of a transaction. +// Useful for monitoring the progress of a transaction. +// Can be integrated into a UI to display the status of a transaction. async function getStatus(quoteId: string, txSig?: string) { const query = txSig ? `?txHash=${encodeURIComponent(txSig)}` : ''; return getJson(`/status/${encodeURIComponent(quoteId)}${query}`); } +// Execution logic async function pollStatus(quoteId: string, txSig?: string) { const deadline = Date.now() + 5 * 60_000; for (;;) { From 2ab57f602f6bfb9adafd284b272e76961bb4025f Mon Sep 17 00:00:00 2001 From: Piotr Date: Wed, 26 Nov 2025 14:28:25 -0800 Subject: [PATCH 8/8] updating api to v1 --- src/evm/index.ts | 4 ++-- src/solana/index.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/evm/index.ts b/src/evm/index.ts index 28e3c08..9fa71a3 100644 --- a/src/evm/index.ts +++ b/src/evm/index.ts @@ -4,9 +4,9 @@ import { base } from 'viem/chains'; import * as dotenv from 'dotenv'; dotenv.config(); -// The code demonstrates how to use the Stargate API to fetch quotes and execute transactions. +// The code demonstrates how to use the LayerZero API to fetch quotes and execute transactions. // Setup: initialize wallet and client -const API = 'https://stargate.finance/api/v2'; +const API = 'https://transfer.layerzero-api.com/v1'; const API_KEY = process.env.STARGATE_API_KEY!; const PRIVATE_KEY = process.env.EVM_PRIVATE_KEY as Hex; const account = privateKeyToAccount(PRIVATE_KEY); diff --git a/src/solana/index.ts b/src/solana/index.ts index 775dc87..92ec63d 100644 --- a/src/solana/index.ts +++ b/src/solana/index.ts @@ -4,7 +4,7 @@ import * as dotenv from 'dotenv'; dotenv.config(); // Setup: initialize wallet and client -const API = 'https://stargate.finance/api/v2'; +const API = 'https://transfer.layerzero-api.com/v1'; const API_KEY = process.env.STARGATE_API_KEY!; const PRIVATE_KEY = process.env.SOLANA_PRIVATE_KEY!; const connection = new web3.Connection(web3.clusterApiUrl('mainnet-beta'), 'confirmed');