Skip to content

Commit dd05b69

Browse files
authored
[sdk]: add retry logic to execute gen and improve interface (#744)
1 parent f12acf9 commit dd05b69

File tree

6 files changed

+50
-48
lines changed

6 files changed

+50
-48
lines changed

docs/content/developers/intent-gateway/placing-orders.mdx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ The generator is bidirectional — it pauses at two points to hand control back
223223

224224
**Second yield**`ORDER_PLACED`. After the transaction is confirmed, the generator yields `{ status: "ORDER_PLACED", order, receipt }` where `order` is the finalized `Order` (with `nonce`, `id`, and `inputs` populated from the `OrderPlaced` event) and `receipt` is the full viem `TransactionReceipt` of the placement transaction.
225225

226-
**Subsequent yields** — status updates. The generator then yields `IntentOrderStatusUpdate` objects continuously until the order reaches a terminal state (`FILLED`, `FAILED`, `PARTIAL_FILL_EXHAUSTED`). Call `gen.next()` with no arguments to advance after each update.
226+
**Subsequent yields** — status updates. The generator then yields `IntentOrderStatusUpdate` objects continuously until the order reaches a terminal state (`FILLED`, `EXPIRED`). `FAILED` events are informational — the executor automatically retries by looping back to bid polling. Call `gen.next()` with no arguments to advance after each update.
227227

228228
```typescript lineNumbers title="execute.ts" icon="typescript"
229229
import { IntentOrderStatus, DEFAULT_GRAFFITI } from "@hyperbridge/sdk"
@@ -282,19 +282,20 @@ async function runOrder(order: Order) {
282282
console.log("Partial fill:", filledAssets, "| total filled:", totalFilledAssets, "| remaining:", remainingAssets)
283283
break
284284
}
285-
case IntentOrderStatus.PARTIAL_FILL_EXHAUSTED:
286-
// Order deadline reached, or no new bids arrived for the remaining amount
287-
// The order is still live on-chain — cancel it or wait for a solver to fill the rest manually
288-
console.warn("Could not fully fill order after max attempts. Remaining:", update.remainingAssets)
289-
return
290285
case IntentOrderStatus.FILLED:
291286
// Order fully filled; escrow released to the solver
292287
console.log("Order fully filled! tx:", update.transactionHash)
293288
return
294-
case IntentOrderStatus.FAILED:
295-
// Unrecoverable error (e.g. bundler rejection, RPC failure)
296-
console.error("Order failed:", update.error)
289+
case IntentOrderStatus.EXPIRED:
290+
// Order deadline reached, or no new bids arrived for the remaining amount
291+
// The order is still live on-chain — cancel it or wait for a solver to fill the rest manually
292+
console.warn("Order expired:", update.error, "Remaining:", update.remainingAssets)
297293
return
294+
case IntentOrderStatus.FAILED:
295+
// Retryable error (e.g. bundler rejection, simulation failure)
296+
// The executor automatically retries — this is informational only
297+
console.warn("Retryable error, retrying:", update.error)
298+
break
298299
}
299300
}
300301
}
@@ -312,9 +313,9 @@ async function runOrder(order: Order) {
312313
| `BID_SELECTED` | Best bid chosen | `commitment`, `selectedSolver`, `userOpHash` |
313314
| `USEROP_SUBMITTED` | UserOp sent to bundler | `commitment`, `userOpHash`, `transactionHash` |
314315
| `PARTIAL_FILL` | Same-chain partial fill confirmed; loop restarts | `commitment`, `filledAssets`, `totalFilledAssets`, `remainingAssets` |
315-
| `PARTIAL_FILL_EXHAUSTED` | Deadline reached or no new bids for remainder | `commitment`, `totalFilledAssets`, `remainingAssets`, `error` |
316316
| `FILLED` | Order fully filled | `commitment`, `userOpHash`, `transactionHash` |
317-
| `FAILED` | Unrecoverable error | `error` |
317+
| `EXPIRED` | Deadline reached or no new bids available — terminal | `commitment`, `totalFilledAssets`, `remainingAssets`, `error` |
318+
| `FAILED` | Retryable error (e.g. bundler rejection) — executor retries automatically | `commitment`, `totalFilledAssets`, `remainingAssets`, `error` |
318319

319320
### `execute()` options
320321

@@ -324,9 +325,13 @@ async function runOrder(order: Order) {
324325
| `bidTimeoutMs` | `number` | `60000` | Max time (ms) to wait for bids before proceeding with whatever has arrived |
325326
| `pollIntervalMs` | `number` | SDK default | How frequently (ms) to poll the coprocessor for new bids |
326327

327-
### `PARTIAL_FILL_EXHAUSTED`
328+
### `EXPIRED`
329+
330+
When `EXPIRED` fires, the order deadline has been reached or no new bids are available. The user can cancel and reclaim the remaining escrow. See [Cancelling Orders](./cancelling-orders) for cancellation options.
331+
332+
### `FAILED` (retryable)
328333

329-
When `PARTIAL_FILL_EXHAUSTED` fires, the user can cancel and reclaim the remaining escrow. See [Cancelling Orders](./cancelling-orders) for cancellation options.
334+
`FAILED` events are informational — the executor automatically retries by looping back to bid polling. Common causes include bundler gas price rejections or bid simulation failures. The retry continues until the order reaches `FILLED` or `EXPIRED`.
330335

331336
---
332337

docs/content/developers/sdk/api/intent-gateway.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ type IntentOrderStatusUpdate =
345345
| { status: "USEROP_SUBMITTED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString }
346346
| { status: "FILLED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
347347
| { status: "PARTIAL_FILL"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; filledAssets: TokenInfo[]; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
348-
| { status: "PARTIAL_FILL_EXHAUSTED"; commitment: HexString; totalFilledAssets?: TokenInfo[]; remainingAssets?: TokenInfo[]; error: string }
348+
| { status: "EXPIRED"; commitment: HexString; totalFilledAssets?: TokenInfo[]; remainingAssets?: TokenInfo[]; error: string }
349349
| { status: "FAILED"; commitment?: HexString; totalFilledAssets?: TokenInfo[]; remainingAssets?: TokenInfo[]; error: string }
350350
```
351351
@@ -359,8 +359,8 @@ type IntentOrderStatusUpdate =
359359
| `USEROP_SUBMITTED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?` | UserOperation sent to the bundler |
360360
| `FILLED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `totalFilledAssets`, `remainingAssets` | Order fully filled on the destination chain |
361361
| `PARTIAL_FILL` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `filledAssets`, `totalFilledAssets`, `remainingAssets` | Order partially filled; more fills may follow |
362-
| `PARTIAL_FILL_EXHAUSTED` | `commitment`, `totalFilledAssets?`, `remainingAssets?`, `error` | No further fills expected for a partially filled order |
363-
| `FAILED` | `commitment?`, `totalFilledAssets?`, `remainingAssets?`, `error` | Order execution failed unrecoverably |
362+
| `EXPIRED` | `commitment`, `totalFilledAssets?`, `remainingAssets?`, `error` | Order deadline reached or no new bids available — terminal |
363+
| `FAILED` | `commitment?`, `totalFilledAssets?`, `remainingAssets?`, `error` | Retryable error (e.g. bundler rejection, simulation failure) — executor retries automatically |
364364
365365
---
366366

sdk/packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hyperbridge/sdk",
3-
"version": "1.8.7",
3+
"version": "1.8.8",
44
"description": "The hyperclient SDK provides utilities for querying proofs and statuses for cross-chain requests from HyperBridge.",
55
"type": "module",
66
"types": "./dist/node/index.d.ts",

sdk/packages/sdk/src/protocols/intents/OrderExecutor.ts

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ export class OrderExecutor {
5050
*
5151
* **Status progression (same-chain orders):**
5252
* `AWAITING_BIDS` → `BIDS_RECEIVED` → `BID_SELECTED` → `USEROP_SUBMITTED`
53-
* → (`FILLED` | `PARTIAL_FILL`)* → (`FILLED` | `PARTIAL_FILL_EXHAUSTED`)
53+
* → (`FILLED` | `PARTIAL_FILL`)* → (`FILLED` | `EXPIRED`)
5454
*
55-
* **Error statuses:** `FAILED` (fatal, no fills) or `PARTIAL_FILL_EXHAUSTED`
56-
* (deadline reached or no new bids after at least one partial fill).
55+
* **Error statuses:** `FAILED` (retryable error during bid selection/submission,
56+
* triggers automatic retry) or `EXPIRED` (deadline reached or no new bids —
57+
* terminal, no further retries).
5758
*
5859
* @param options - Execution parameters including the placed order, its
5960
* session private key, bid collection settings, and poll interval.
@@ -121,18 +122,13 @@ export class OrderExecutor {
121122
while (true) {
122123
const currentBlock = await this.ctx.dest.client.getBlockNumber()
123124
if (currentBlock >= order.deadline) {
124-
const isPartiallyFilled = totalFilledAssets.some((a) => a.amount > 0n)
125125
const deadlineError = `Order deadline reached (block ${currentBlock} >= ${order.deadline})`
126-
if (isPartiallyFilled) {
127-
yield {
128-
status: "PARTIAL_FILL_EXHAUSTED",
129-
commitment,
130-
totalFilledAssets,
131-
remainingAssets,
132-
error: deadlineError,
133-
}
134-
} else {
135-
yield { status: "FAILED", commitment, error: deadlineError }
126+
yield {
127+
status: "EXPIRED",
128+
commitment,
129+
totalFilledAssets,
130+
remainingAssets,
131+
error: deadlineError,
136132
}
137133
return
138134
}
@@ -175,22 +171,18 @@ export class OrderExecutor {
175171
})
176172

177173
if (freshBids.length === 0) {
178-
const isPartiallyFilled = totalFilledAssets.some((a) => a.amount > 0n)
179174
const solverClause = solver && !solverLockExpired ? ` for requested solver ${solver.address}` : ""
175+
const isPartiallyFilled = totalFilledAssets.some((a) => a.amount > 0n)
180176
const noBidsError = isPartiallyFilled
181177
? `No new bids${solverClause} after partial fill`
182178
: `No new bids${solverClause} available within ${bidTimeoutMs}ms timeout`
183179

184-
if (isPartiallyFilled) {
185-
yield {
186-
status: "PARTIAL_FILL_EXHAUSTED",
187-
commitment,
188-
totalFilledAssets,
189-
remainingAssets,
190-
error: noBidsError,
191-
}
192-
} else {
193-
yield { status: "FAILED", commitment, error: noBidsError }
180+
yield {
181+
status: "EXPIRED",
182+
commitment,
183+
totalFilledAssets,
184+
remainingAssets,
185+
error: noBidsError,
194186
}
195187
return
196188
}
@@ -208,7 +200,9 @@ export class OrderExecutor {
208200
remainingAssets,
209201
error: `Failed to select bid and submit: ${err instanceof Error ? err.message : String(err)}`,
210202
}
211-
return
203+
// Back off before retrying
204+
await sleep(pollIntervalMs)
205+
continue
212206
}
213207

214208
const usedKey = userOpHashKey(result.userOp)
@@ -265,7 +259,10 @@ export class OrderExecutor {
265259
remainingAssets = targetAssets.map((target) => {
266260
const filled = totalFilledAssets.find((a) => a.token === target.token)
267261
const filledAmt = filled?.amount ?? 0n
268-
return { token: target.token, amount: filledAmt >= target.amount ? 0n : target.amount - filledAmt }
262+
return {
263+
token: target.token,
264+
amount: filledAmt >= target.amount ? 0n : target.amount - filledAmt,
265+
}
269266
})
270267

271268
const fullyFilled = remainingAssets.every((a) => a.amount === 0n)

sdk/packages/sdk/src/types/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1321,7 +1321,7 @@ export const IntentOrderStatus = Object.freeze({
13211321
USEROP_SUBMITTED: "USEROP_SUBMITTED",
13221322
FILLED: "FILLED",
13231323
PARTIAL_FILL: "PARTIAL_FILL",
1324-
PARTIAL_FILL_EXHAUSTED: "PARTIAL_FILL_EXHAUSTED",
1324+
EXPIRED: "EXPIRED",
13251325
FAILED: "FAILED",
13261326
})
13271327

@@ -1368,7 +1368,7 @@ export type IntentOrderStatusUpdate =
13681368
remainingAssets: TokenInfo[]
13691369
}
13701370
| {
1371-
status: "PARTIAL_FILL_EXHAUSTED"
1371+
status: "EXPIRED"
13721372
commitment: HexString
13731373
totalFilledAssets?: TokenInfo[]
13741374
remainingAssets?: TokenInfo[]

sdk/packages/simplex/src/tests/strategies/fx.mainnet.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,8 +534,8 @@ describe.skip("Filler V2 FX - Base mainnet same-chain swap", () => {
534534
txHash: status.transactionHash,
535535
})
536536
break
537-
case "PARTIAL_FILL_EXHAUSTED":
538-
tlog("PARTIAL_FILL_EXHAUSTED", { commitment: status.commitment, error: status.error })
537+
case "EXPIRED":
538+
tlog("EXPIRED", { commitment: status.commitment, error: status.error })
539539
break
540540
case "FAILED":
541541
tlog("FAILED", status)

0 commit comments

Comments
 (0)