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/signature-check-final-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@relayprotocol/relay-sdk': patch
---

Stop signature steps from polling on after the status check reports `failure` or `refund`. A `failure` whose `details` did not contain "Transaction failed" was retried until the polling limit and then surfaced as a generic "Failed to get an ok response" timeout, and a `refund` was never treated as final. `execute` now rejects on the first such status, with the reported `details` for a failure and "Transaction failed: Refunded" (so `refunded` is set) for a refund, matching transaction steps.
52 changes: 52 additions & 0 deletions packages/sdk/src/utils/executeSteps/signatureStep.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,55 @@ describe('handleSignatureStepItem (HyperCore)', () => {
expect(stepItem.progressState).toBe('validating')
})
})

describe('handleSignatureStepItem (status polling)', () => {
beforeEach(() => {
vi.mocked(findHyperliquidSendHash).mockResolvedValue(undefined)
})
afterEach(() => {
vi.restoreAllMocks()
})

it('Should fail right away with the failure details from the check.', async () => {
const request = pollReturns({
status: 'failure',
details: 'Deposit transaction reverted'
})
const { params } = setup()
params.maximumAttempts = 5

await expect(handleSignatureStepItem(params)).rejects.toThrow(
'Deposit transaction reverted'
)
expect(request).toHaveBeenCalledOnce()
})

it('Should fail right away as refunded when the check reports a refund.', async () => {
const request = pollReturns({ status: 'refund' })
const { stepItem, params } = setup()
params.maximumAttempts = 5

await expect(handleSignatureStepItem(params)).rejects.toThrow(
'Transaction failed: Refunded'
)
expect(request).toHaveBeenCalledOnce()
expect(stepItem.checkStatus).toBe('refund')
})

it('Should keep polling when the check request itself errors.', async () => {
const request = vi
.spyOn(axios, 'request')
.mockRejectedValueOnce(new Error('Network Error'))
.mockResolvedValue({
status: 200,
data: { status: 'success', txHashes: [FILL_HASH] }
} as never)
const { stepItem, params } = setup()
params.maximumAttempts = 2

await handleSignatureStepItem(params)

expect(request).toHaveBeenCalledTimes(2)
expect(stepItem.checkStatus).toBe('success')
})
})
22 changes: 17 additions & 5 deletions packages/sdk/src/utils/executeSteps/signatureStep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ import {
postHyperliquidSignature
} from '../hyperliquid.js'

/**
* A failure or refund reported by the check endpoint. It is final, whatever
* its message says, so polling stops instead of retrying.
*/
class CheckStatusError extends Error {}

/**
* Handles the execution of a signature step item, including signing, posting, and validation.
*/
Expand Down Expand Up @@ -362,7 +368,12 @@ export async function handleSignatureStepItem({
client.log(['Transaction completed successfully'], LogLevel.Verbose)
return // Success - exit polling
} else if (res?.data?.status === 'failure') {
throw Error(res?.data?.details || 'Transaction failed')
throw new CheckStatusError(
res?.data?.details || 'Transaction failed'
)
} else if (res?.data?.status === 'refund') {
stepItem.checkStatus = 'refund'
throw new CheckStatusError('Transaction failed: Refunded')
Comment thread
greptile-apps[bot] marked this conversation as resolved.
} else if (res.status >= 400) {
// Handle HTTP error responses that don't have our expected data structure
throw Error(
Expand All @@ -375,10 +386,11 @@ export async function handleSignatureStepItem({
} catch (error: any) {
// If it's a deliberate failure response, re-throw immediately
if (
error.message &&
(error.message.includes('Transaction failed') ||
error.message.includes('Failed to check') ||
error.message === 'Failed to check')
error instanceof CheckStatusError ||
(error.message &&
(error.message.includes('Transaction failed') ||
error.message.includes('Failed to check') ||
error.message === 'Failed to check'))
) {
throw error
}
Expand Down