Skip to content

Commit 86626ef

Browse files
authored
Merge pull request #816 from relayprotocol/feature/improved-transaction-steps
Improved transaction steps
2 parents 82e6689 + c71a562 commit 86626ef

33 files changed

+1924
-584
lines changed

packages/sdk/src/types/Execute.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,11 @@ export type QuoteStepId = NonNullable<
1919
paths['/quote']['post']['responses']['200']['content']['application/json']['steps']
2020
>['0']['id']
2121

22-
export type TransactionStepState =
23-
| 'confirming'
24-
| 'validating'
25-
| 'validating_delayed'
26-
| 'complete'
22+
export type TransactionStepState = 'confirming' | 'validating' | 'complete'
2723
export type SignatureStepState =
2824
| 'signing'
2925
| 'posting'
3026
| 'validating'
31-
| 'validating_delayed'
3227
| 'complete'
3328

3429
export type Execute = {
@@ -53,7 +48,6 @@ export type Execute = {
5348
receipt?: TransactionReceipt | SvmReceipt | SuiReceipt
5449
checkStatus?:
5550
| 'refund'
56-
| 'delayed'
5751
| 'waiting'
5852
| 'failure'
5953
| 'pending'

packages/sdk/src/types/SignatureStepItem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type SignatureStepItem = Pick<
1111
| 'internalTxHashes'
1212
| 'check'
1313
| 'isValidatingSignature'
14+
| 'checkStatus'
1415
> & {
1516
data?: {
1617
sign?: {

packages/sdk/src/types/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,7 +4115,7 @@ export interface paths {
41154115
content: {
41164116
"application/json": {
41174117
/** @enum {string} */
4118-
status?: "refund" | "waiting" | "failure" | "pending" | "success" | "delayed";
4118+
status?: "refund" | "waiting" | "failure" | "pending" | "success";
41194119
details?: string;
41204120
/** @description Incoming transaction hashes */
41214121
inTxHashes?: string[];
@@ -4705,7 +4705,7 @@ export interface paths {
47054705
* @description Note that fallback is returned in the case of a refund
47064706
* @enum {string}
47074707
*/
4708-
status?: "refund" | "waiting" | "failure" | "pending" | "success" | "delayed";
4708+
status?: "refund" | "waiting" | "failure" | "pending" | "success";
47094709
user?: string;
47104710
recipient?: string;
47114711
subsidizedRequest?: boolean;

packages/sdk/src/utils/executeSteps/executeSteps.test.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ describe('Should test a signature step.', () => {
813813
it('Should validate a signature step item', async () => {
814814
let progressState = ''
815815
let isValidating = false
816+
let checkStatus = ''
816817

817818
vi.spyOn(axios, 'request').mockImplementation((config) => {
818819
if (config.url?.includes('/intents/status')) {
@@ -836,18 +837,20 @@ describe('Should test a signature step.', () => {
836837
const signatureStepItem = signatureStep?.items?.[0]
837838
progressState = signatureStepItem?.progressState ?? ''
838839
isValidating = signatureStepItem?.isValidatingSignature ? true : false
840+
checkStatus = signatureStepItem?.checkStatus ?? ''
839841
},
840842
bridgeData,
841843
undefined
842844
).catch((e) => {})
843845

844846
await vi.waitFor(() => {
845-
if (progressState !== 'validating' || !isValidating) {
846-
throw 'Waiting for signature validation'
847+
if (checkStatus !== 'pending') {
848+
throw 'Waiting for pending status'
847849
}
848850
})
849-
expect(progressState).toBe('validating')
850-
expect(isValidating).toBeTruthy()
851+
expect(checkStatus).toBe('pending')
852+
expect(progressState).toBe('')
853+
expect(isValidating).toBeFalsy()
851854
})
852855
it('Should set txHashes returned by check request', async () => {
853856
let signatureStep = bridgeData.steps.find(
@@ -857,7 +860,14 @@ describe('Should test a signature step.', () => {
857860
signatureStep?.items?.[0] as any
858861
const checkEndpoint = signatureStepItem.check?.endpoint ?? ''
859862
vi.spyOn(axios, 'request').mockImplementation((config) => {
860-
if (config.url?.includes(checkEndpoint)) {
863+
// Handle both the original endpoint and the v3 transformation
864+
const isCheckEndpoint =
865+
config.url?.includes(checkEndpoint) ||
866+
config.url?.includes(
867+
checkEndpoint.replace('/intents/status', '/intents/status/v3')
868+
)
869+
870+
if (isCheckEndpoint) {
861871
return Promise.resolve({
862872
data: {
863873
status: 'success',
@@ -916,7 +926,14 @@ describe('Should test a signature step.', () => {
916926
const checkEndpoint = signatureStepItem.check?.endpoint ?? ''
917927
let errorMessage: string | undefined
918928
vi.spyOn(axios, 'request').mockImplementation((config) => {
919-
if (config.url?.includes(checkEndpoint)) {
929+
// Handle both the original endpoint and the v3 transformation
930+
const isCheckEndpoint =
931+
config.url?.includes(checkEndpoint) ||
932+
config.url?.includes(
933+
checkEndpoint.replace('/intents/status', '/intents/status/v3')
934+
)
935+
936+
if (isCheckEndpoint) {
920937
return Promise.resolve({
921938
data: { status: 'failure', details: 'Failed to check' },
922939
status: 400

packages/sdk/src/utils/executeSteps/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,7 @@ export async function executeSteps(
216216
isLastStep &&
217217
isStepIncomplete &&
218218
!statusControl.websocketActive &&
219-
chainId !== 8253038 &&
220-
json?.details?.currencyOut?.currency?.chainId !== 8253038
219+
chainId !== 8253038
221220
) {
222221
statusControl.websocketActive = true
223222

packages/sdk/src/utils/executeSteps/signatureStep.ts

Lines changed: 135 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -90,19 +90,13 @@ export async function handleSignatureStepItem({
9090
}
9191

9292
try {
93-
const getData = async function () {
94-
let response = await axios.request({
95-
url: postOrderUrl.href,
96-
data: postData.body ? JSON.stringify(postData.body) : undefined,
97-
method: postData.method,
98-
params: request.params,
99-
headers
100-
})
101-
102-
return response
103-
}
104-
105-
const res = await getData()
93+
const res = await axios.request({
94+
url: postOrderUrl.href,
95+
data: postData.body ? JSON.stringify(postData.body) : undefined,
96+
method: postData.method,
97+
params: request.params,
98+
headers
99+
})
106100

107101
// Append new steps if returned in response
108102
if (res.data && res.data.steps && Array.isArray(res.data.steps)) {
@@ -150,15 +144,9 @@ export async function handleSignatureStepItem({
150144
// If check, poll check until validated
151145
if (stepItem?.check) {
152146
stepItem.progressState = 'validating'
153-
setState({
154-
steps: [...json.steps],
155-
fees: { ...json?.fees },
156-
breakdown: json?.breakdown,
157-
details: json?.details
158-
})
159147
stepItem.isValidatingSignature = true
160148
setState({
161-
steps: [...json?.steps],
149+
steps: [...json.steps],
162150
fees: { ...json?.fees },
163151
breakdown: json?.breakdown,
164152
details: json?.details
@@ -193,60 +181,154 @@ export async function handleSignatureStepItem({
193181

194182
// Start polling for signature validation
195183
const pollWithCancellation = async () => {
184+
// Helper to update state
185+
const updateState = () => {
186+
setState({
187+
steps: [...json?.steps],
188+
fees: { ...json?.fees },
189+
breakdown: json?.breakdown,
190+
details: json?.details
191+
})
192+
}
193+
194+
// Helper to extract and set origin txHashes
195+
const extractOriginTxHashes = (
196+
inTxHashesData: string[],
197+
originChainId?: number
198+
) => {
199+
const chainInTxHashes: NonNullable<
200+
Execute['steps'][0]['items']
201+
>[0]['txHashes'] = inTxHashesData.map((hash: string) => ({
202+
txHash: hash,
203+
chainId: originChainId ?? chain?.id
204+
}))
205+
stepItem.internalTxHashes = chainInTxHashes
206+
}
207+
208+
// Helper to extract and set destination txHashes
209+
const extractDestinationTxHashes = (
210+
txHashesData: string[],
211+
destinationChainId?: number
212+
) => {
213+
const chainTxHashes: NonNullable<
214+
Execute['steps'][0]['items']
215+
>[0]['txHashes'] = txHashesData.map((hash: string) => ({
216+
txHash: hash,
217+
chainId: destinationChainId ?? chain?.id
218+
}))
219+
stepItem.txHashes = chainTxHashes
220+
}
221+
196222
let attemptCount = 0
197223
while (attemptCount < maximumAttempts) {
198224
try {
225+
let endpoint = stepItem?.check?.endpoint || ''
226+
227+
// Override v2 status endpoint to v3 to get 'submitted' status
228+
if (
229+
endpoint.includes('/intents/status') &&
230+
!endpoint.includes('/v3')
231+
) {
232+
endpoint = endpoint.replace('/intents/status', '/intents/status/v3')
233+
}
234+
199235
const res = await axios.request({
200-
url: `${request.baseURL}${stepItem?.check?.endpoint}`,
236+
url: `${request.baseURL}${endpoint}`,
201237
method: stepItem?.check?.method,
202-
headers
238+
headers,
239+
validateStatus: (status) => status < 500 // Don't throw on 4xx responses
203240
})
204241

205-
client.log(
206-
[`Execute Steps: Polling execute status to check if indexed`, res],
207-
LogLevel.Verbose
208-
)
209-
210242
// Check status
211-
if (res?.data?.status === 'success' && res?.data?.txHashes) {
212-
const chainTxHashes: NonNullable<
213-
Execute['steps'][0]['items']
214-
>[0]['txHashes'] = res.data?.txHashes?.map((hash: string) => {
215-
return {
216-
txHash: hash,
217-
chainId: res.data.destinationChainId ?? chain?.id
218-
}
219-
})
243+
if (res?.data?.status === 'pending') {
244+
// Extract origin txHashes if provided
245+
if (res?.data?.inTxHashes && res.data.inTxHashes.length > 0) {
246+
extractOriginTxHashes(res.data.inTxHashes, res.data.originChainId)
247+
}
248+
249+
stepItem.checkStatus = 'pending'
250+
stepItem.progressState = undefined
251+
stepItem.isValidatingSignature = false
252+
updateState()
253+
client.log(
254+
['Origin tx confirmed, backend processing'],
255+
LogLevel.Verbose
256+
)
257+
} else if (res?.data?.status === 'submitted') {
258+
// Extract destination txHashes if provided
259+
if (res?.data?.txHashes && res.data.txHashes.length > 0) {
260+
extractDestinationTxHashes(
261+
res.data.txHashes,
262+
res.data.destinationChainId
263+
)
264+
}
265+
266+
// Extract origin txHashes if provided
267+
if (res?.data?.inTxHashes && res.data.inTxHashes.length > 0) {
268+
extractOriginTxHashes(res.data.inTxHashes, res.data.originChainId)
269+
}
220270

271+
stepItem.checkStatus = 'submitted'
272+
stepItem.progressState = undefined
273+
stepItem.isValidatingSignature = false
274+
updateState()
275+
client.log(
276+
['Destination tx submitted, continuing validation'],
277+
LogLevel.Verbose
278+
)
279+
} else if (res?.data?.status === 'success' && res?.data?.txHashes) {
280+
// Extract destination txHashes
281+
extractDestinationTxHashes(
282+
res.data.txHashes,
283+
res.data.destinationChainId
284+
)
285+
286+
// Extract origin txHashes if provided (keeping original chainId order)
221287
if (res?.data?.inTxHashes) {
222288
const chainInTxHashes: NonNullable<
223289
Execute['steps'][0]['items']
224-
>[0]['txHashes'] = res.data.inTxHashes.map((hash: string) => {
225-
return {
226-
txHash: hash,
227-
chainId: chain?.id ?? res.data.originChainId
228-
}
229-
})
290+
>[0]['txHashes'] = res.data.inTxHashes.map((hash: string) => ({
291+
txHash: hash,
292+
chainId: chain?.id ?? res.data.originChainId
293+
}))
230294
stepItem.internalTxHashes = chainInTxHashes
231295
}
232-
stepItem.txHashes = chainTxHashes
296+
297+
stepItem.checkStatus = 'success'
298+
stepItem.status = 'complete'
299+
stepItem.progressState = 'complete'
300+
updateState()
301+
client.log(['Transaction completed successfully'], LogLevel.Verbose)
233302
return // Success - exit polling
234303
} else if (res?.data?.status === 'failure') {
235304
throw Error(res?.data?.details || 'Transaction failed')
236-
} else if (res?.data?.status === 'delayed') {
237-
stepItem.progressState = 'validating_delayed'
238-
setState({
239-
steps: [...json?.steps],
240-
fees: { ...json?.fees },
241-
breakdown: json?.breakdown,
242-
details: json?.details
243-
})
305+
} else if (res.status >= 400) {
306+
// Handle HTTP error responses that don't have our expected data structure
307+
throw Error(
308+
res?.data?.details || res?.data?.message || 'Failed to check'
309+
)
310+
}
311+
312+
attemptCount++
313+
await new Promise((resolve) => setTimeout(resolve, pollingInterval))
314+
} catch (error: any) {
315+
// If it's a deliberate failure response, re-throw immediately
316+
if (
317+
error.message &&
318+
(error.message.includes('Transaction failed') ||
319+
error.message.includes('Failed to check') ||
320+
error.message === 'Failed to check')
321+
) {
322+
throw error
244323
}
245324

325+
// For network errors or other recoverable issues, continue polling
326+
client.log(
327+
['Check request failed, retrying...', error],
328+
LogLevel.Verbose
329+
)
246330
attemptCount++
247331
await new Promise((resolve) => setTimeout(resolve, pollingInterval))
248-
} catch (error) {
249-
throw error
250332
}
251333
}
252334

packages/sdk/src/utils/executeSteps/transactionStep.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,8 @@ export async function handleTransactionStepItem({
100100
request,
101101
undefined,
102102
crossChainIntentChainId,
103-
(res) => {
104-
if (res && res.data.status === 'delayed') {
105-
stepItem.progressState = 'validating_delayed'
106-
} else {
107-
stepItem.progressState = 'validating'
108-
}
103+
() => {
104+
stepItem.progressState = 'validating'
109105
setState({
110106
steps: [...json.steps],
111107
fees: { ...json?.fees },
@@ -126,6 +122,13 @@ export async function handleTransactionStepItem({
126122
(checkStatus) => {
127123
if (checkStatus != stepItem.checkStatus) {
128124
stepItem.checkStatus = checkStatus
125+
if (
126+
checkStatus === 'pending' ||
127+
checkStatus === 'submitted' ||
128+
checkStatus === 'success'
129+
) {
130+
stepItem.progressState = undefined
131+
}
129132
setState({
130133
steps: [...json.steps],
131134
fees: { ...json?.fees },

0 commit comments

Comments
 (0)