From d93fa3d75ffd222c8eecdd486cffdf19b52aa2a6 Mon Sep 17 00:00:00 2001 From: 0xtxbi Date: Thu, 19 Feb 2026 11:04:00 +0100 Subject: [PATCH 1/4] update types --- packages/sdk/src/types/Requests.ts | 8 ++++++++ packages/sdk/src/types/index.ts | 1 + 2 files changed, 9 insertions(+) create mode 100644 packages/sdk/src/types/Requests.ts diff --git a/packages/sdk/src/types/Requests.ts b/packages/sdk/src/types/Requests.ts new file mode 100644 index 000000000..673a62f1b --- /dev/null +++ b/packages/sdk/src/types/Requests.ts @@ -0,0 +1,8 @@ +import type { paths } from './api.js' + +export type UserTransactionsResponse = + paths['/requests/v2']['get']['responses']['200']['content']['application/json'] + +export type RelayTransaction = NonNullable< + NonNullable['requests'] +>[0] diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index 74ed31814..4e4cc22ce 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -5,3 +5,4 @@ export * from './TransactionStepItem.js' export * from './AdaptedWallet.js' export * from './RelayChain.js' export * from './Progress.js' +export * from './Requests.js' From 5179aeec561a4d36362c90894ccc644ceea5781e Mon Sep 17 00:00:00 2001 From: 0xtxbi Date: Thu, 19 Feb 2026 11:04:42 +0100 Subject: [PATCH 2/4] add callback --- packages/sdk/src/actions/execute.ts | 140 +++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 3 deletions(-) diff --git a/packages/sdk/src/actions/execute.ts b/packages/sdk/src/actions/execute.ts index 49cf585c2..66bd372b3 100644 --- a/packages/sdk/src/actions/execute.ts +++ b/packages/sdk/src/actions/execute.ts @@ -1,20 +1,29 @@ -import type { AdaptedWallet, ProgressData, Execute } from '../types/index.js' +import type { + AdaptedWallet, + ProgressData, + Execute, + RelayTransaction +} from '../types/index.js' import { getClient } from '../client.js' import { executeSteps, adaptViemWallet, getCurrentStepData, - safeStructuredClone + safeStructuredClone, + request as requestApi, + getApiKeyHeader } from '../utils/index.js' import { type WalletClient } from 'viem' import { isViemWalletClient } from '../utils/viemWallet.js' import { isDeadAddress } from '../constants/address.js' +import { extractDepositRequestId } from '../utils/websocket.js' export type ExecuteActionParameters = { quote: Execute wallet: AdaptedWallet | WalletClient depositGasLimit?: string onProgress?: (data: ProgressData) => any + onTransactionReceived?: (transaction: RelayTransaction) => any } /** @@ -23,6 +32,7 @@ export type ExecuteActionParameters = { * @param data.depositGasLimit A gas limit to use in base units (wei, etc) * @param data.wallet Wallet object that adheres to the AdaptedWakket interface or a viem WalletClient * @param data.onProgress Callback to update UI state as execution progresses + * @param data.onTransactionReceived Callback fired when /requests metadata is available * @param abortController Optional AbortController to cancel the execution */ export function execute(data: ExecuteActionParameters): Promise<{ @@ -31,7 +41,8 @@ export function execute(data: ExecuteActionParameters): Promise<{ }> & { abortController: AbortController } { - const { quote, wallet, depositGasLimit, onProgress } = data + const { quote, wallet, depositGasLimit, onProgress, onTransactionReceived } = + data const client = getClient() if (!client.baseApiUrl || !client.baseApiUrl.length) { @@ -113,6 +124,12 @@ export function execute(data: ExecuteActionParameters): Promise<{ ) .then((data) => { resolve({ data, abortController }) + void hydrateTransactionMetadataAndNotify({ + data, + abortController, + onProgress, + onTransactionReceived + }) }) .catch(reject) }) @@ -128,3 +145,120 @@ export function execute(data: ExecuteActionParameters): Promise<{ throw err } } + +async function hydrateTransactionMetadataAndNotify({ + data, + abortController, + onProgress, + onTransactionReceived +}: { + data: Execute + abortController: AbortController + onProgress?: (data: ProgressData) => any + onTransactionReceived?: (transaction: RelayTransaction) => any +}) { + try { + const requestId = extractDepositRequestId(data.steps) + if (!requestId) { + return + } + + const transaction = await pollRequestById(requestId) + if (!transaction) { + return + } + + onTransactionReceived?.(transaction) + + const metadata = transaction.data?.metadata + const existingCurrencyOut = data.details?.currencyOut + const nextCurrencyOut = metadata?.currencyOut + + if (!nextCurrencyOut) { + return + } + + const amountChanged = + nextCurrencyOut.amount !== existingCurrencyOut?.amount || + nextCurrencyOut.amountFormatted !== existingCurrencyOut?.amountFormatted || + nextCurrencyOut.amountUsd !== existingCurrencyOut?.amountUsd + + if (!amountChanged) { + return + } + + data.details = { + ...data.details, + sender: metadata?.sender ?? data.details?.sender, + recipient: metadata?.recipient ?? data.details?.recipient, + currencyIn: metadata?.currencyIn ?? data.details?.currencyIn, + currencyOut: nextCurrencyOut, + currencyGasTopup: metadata?.currencyGasTopup ?? data.details?.currencyGasTopup + } + + if (!onProgress || abortController.signal.aborted) { + return + } + + const { currentStep, currentStepItem, txHashes } = getCurrentStepData( + data.steps + ) + onProgress({ + steps: data.steps, + fees: data.fees, + breakdown: data.breakdown, + details: data.details, + currentStep, + currentStepItem, + txHashes, + refunded: data.refunded, + error: data.error + }) + } catch { + return + } +} + +async function pollRequestById( + requestId: string +): Promise { + const client = getClient() + const requestConfig = { + url: `${client.baseApiUrl}/requests/v2`, + method: 'get' as const, + params: { + id: requestId, + limit: 1, + sortBy: 'updatedAt' as const, + sortDirection: 'desc' as const + }, + headers: { + 'Content-Type': 'application/json', + ...getApiKeyHeader(client, client.baseApiUrl), + 'relay-sdk-version': client.version ?? 'unknown' + } + } + + const maxAttempts = 5 + const pollingInterval = 1000 + let transaction: RelayTransaction | undefined = undefined + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const res = (await requestApi(requestConfig)) as { + data?: { + requests?: RelayTransaction[] + } + } + transaction = res.data?.requests?.[0] + + if (transaction?.data?.metadata?.currencyOut) { + break + } + + if (attempt < maxAttempts - 1) { + await new Promise((resolve) => setTimeout(resolve, pollingInterval)) + } + } + + return transaction +} From 6fbbb4a695f7630d9c41c501a83571c3aea33b8d Mon Sep 17 00:00:00 2001 From: 0xtxbi Date: Thu, 19 Feb 2026 11:05:12 +0100 Subject: [PATCH 3/4] add test --- packages/sdk/src/actions/execute.test.ts | 70 ++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/packages/sdk/src/actions/execute.test.ts b/packages/sdk/src/actions/execute.test.ts index 5eede6929..60cda3f80 100644 --- a/packages/sdk/src/actions/execute.test.ts +++ b/packages/sdk/src/actions/execute.test.ts @@ -6,6 +6,7 @@ import { MAINNET_RELAY_API } from '../constants' import { executeBridge } from '../../tests/data/executeBridge' import type { AdaptedWallet, Execute } from '../types' import { evmDeadAddress } from '../constants/address' +import { axios } from '../utils' let client: RelayClient | undefined let wallet: AdaptedWallet = { @@ -174,4 +175,73 @@ describe('Should test the execute action.', () => { }) ).toThrow('Recipient should never be burn address') }) + + it('Should emit settled metadata through onProgress and onTransactionReceived', async () => { + client = createClient({ + baseApiUrl: MAINNET_RELAY_API + }) + + const onProgress = vi.fn() + const onTransactionReceived = vi.fn() + const settledAmount = '1002000000000000' + + executeStepsSpy.mockImplementation( + ( + chainId: any, + request: any, + wallet: any, + progress: any, + clonedQuote: Execute, + options?: any + ) => { + progress({ + steps: clonedQuote.steps, + fees: clonedQuote.fees, + breakdown: clonedQuote.breakdown, + details: clonedQuote.details + }) + return Promise.resolve(clonedQuote) + } + ) + + const axiosRequestSpy = vi.spyOn(axios, 'request').mockResolvedValue({ + data: { + requests: [ + { + id: '0xabc', + data: { + metadata: { + currencyOut: { + ...quote.details?.currencyOut, + amount: settledAmount, + amountFormatted: '0.001002' + } + } + } + } + ] + } + } as any) + + await client?.actions?.execute({ + wallet, + quote, + onProgress, + onTransactionReceived + }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(onTransactionReceived).toHaveBeenCalledWith( + expect.objectContaining({ + id: '0xabc' + }) + ) + expect(onProgress).toHaveBeenCalled() + expect(onProgress.mock.calls.at(-1)?.[0]?.details?.currencyOut?.amount).toBe( + settledAmount + ) + + axiosRequestSpy.mockRestore() + }) }) From af33f006e035b943b84e7fd922ffb4a8b2981152 Mon Sep 17 00:00:00 2001 From: 0xtxbi Date: Thu, 19 Feb 2026 11:07:51 +0100 Subject: [PATCH 4/4] feat: changeset --- .changeset/quiet-candies-act.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-candies-act.md diff --git a/.changeset/quiet-candies-act.md b/.changeset/quiet-candies-act.md new file mode 100644 index 000000000..d94c37dc6 --- /dev/null +++ b/.changeset/quiet-candies-act.md @@ -0,0 +1,5 @@ +--- +'@relayprotocol/relay-sdk': patch +--- + +Fix execute() to return settled currencyOut; add onTransactionReceived