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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-candies-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@relayprotocol/relay-sdk': patch
---

Fix execute() to return settled currencyOut; add onTransactionReceived
70 changes: 70 additions & 0 deletions packages/sdk/src/actions/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()
})
})
140 changes: 137 additions & 3 deletions packages/sdk/src/actions/execute.ts
Original file line number Diff line number Diff line change
@@ -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
}

/**
Expand All @@ -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<{
Expand All @@ -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) {
Expand Down Expand Up @@ -113,6 +124,12 @@ export function execute(data: ExecuteActionParameters): Promise<{
)
.then((data) => {
resolve({ data, abortController })
void hydrateTransactionMetadataAndNotify({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this void here? Seems odd

data,
abortController,
onProgress,
onTransactionReceived
})
})
.catch(reject)
})
Expand All @@ -128,3 +145,120 @@ export function execute(data: ExecuteActionParameters): Promise<{
throw err
}
}

async function hydrateTransactionMetadataAndNotify({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hydrate feels like a strange word placement here. What we're doing feels more like enriching

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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we do this today somewhere, maybe the ui client. Do we no longer need to do that in the ui and instead pass it up to the swapwidget/tokenwidget?

requestId: string
): Promise<RelayTransaction | undefined> {
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
}
8 changes: 8 additions & 0 deletions packages/sdk/src/types/Requests.ts
Original file line number Diff line number Diff line change
@@ -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<UserTransactionsResponse>['requests']
>[0]
1 change: 1 addition & 0 deletions packages/sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './TransactionStepItem.js'
export * from './AdaptedWallet.js'
export * from './RelayChain.js'
export * from './Progress.js'
export * from './Requests.js'