Skip to content

Commit 0d14143

Browse files
[sdk]: improve order executor interface and add support for resumption (#746)
Co-authored-by: Seun Lanlege <seun@polytope.technology>
1 parent dd05b69 commit 0d14143

File tree

8 files changed

+406
-267
lines changed

8 files changed

+406
-267
lines changed

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const walletClient = createWalletClient({
7272
```
7373

7474
<Callout type="warn">
75-
The `bundlerUrl` on the destination chain is **required** for cross-chain orders. `IntentGateway` uses it to submit the solver's `UserOperation` to the ERC-4337 bundler on the destination chain. Without it, solver selection will fail at the `BID_SELECTED` stage.
75+
The `bundlerUrl` on the destination chain is **required** for cross-chain orders. `IntentGateway` uses it to submit the solver's `UserOperation` to the ERC-4337 bundler on the destination chain. Without it, bid submission will fail at the `BID_SELECTED` stage.
7676
</Callout>
7777

7878
### Same-Chain Orders
@@ -266,14 +266,10 @@ async function runOrder(order: Order) {
266266
console.log(`${update.bidCount} bids received`)
267267
break
268268
case IntentOrderStatus.BID_SELECTED:
269-
// Best bid chosen; SDK is encoding the SelectSolver UserOperation
270-
console.log("Best bid selected, solver:", update.selectedSolver)
271-
break
272-
case IntentOrderStatus.USEROP_SUBMITTED:
273-
// UserOp submitted to the ERC-4337 bundler
269+
// Best bid chosen and UserOp submitted to the ERC-4337 bundler
274270
// Cross-chain: execute() exits here — Hyperbridge finalisation is async
275271
// Same-chain: SDK waits for the fill tx, then moves to FILLED or PARTIAL_FILL
276-
console.log("UserOp submitted to bundler:", update.userOpHash)
272+
console.log("Bid selected, solver:", update.selectedSolver, "tx:", update.transactionHash)
277273
break
278274
case IntentOrderStatus.PARTIAL_FILL: {
279275
// Same-chain only: a solver partially filled the output
@@ -310,8 +306,7 @@ async function runOrder(order: Order) {
310306
| `ORDER_PLACED` | `placeOrder` tx confirmed on-chain | `order` (finalized `Order`), `receipt` |
311307
| `AWAITING_BIDS` | Polling coprocessor for bids | `commitment`, `totalFilledAssets`, `remainingAssets` |
312308
| `BIDS_RECEIVED` | `minBids` collected or `bidTimeoutMs` elapsed | `commitment`, `bidCount`, `bids` |
313-
| `BID_SELECTED` | Best bid chosen | `commitment`, `selectedSolver`, `userOpHash` |
314-
| `USEROP_SUBMITTED` | UserOp sent to bundler | `commitment`, `userOpHash`, `transactionHash` |
309+
| `BID_SELECTED` | Best bid chosen and UserOp sent to bundler | `commitment`, `selectedSolver`, `userOpHash`, `userOp`, `transactionHash` |
315310
| `PARTIAL_FILL` | Same-chain partial fill confirmed; loop restarts | `commitment`, `filledAssets`, `totalFilledAssets`, `remainingAssets` |
316311
| `FILLED` | Order fully filled | `commitment`, `userOpHash`, `transactionHash` |
317312
| `EXPIRED` | Deadline reached or no new bids available — terminal | `commitment`, `totalFilledAssets`, `remainingAssets`, `error` |
@@ -387,7 +382,7 @@ Set `destination` to the target chain's state machine ID. The contract verifies
387382

388383
Cross-chain fills are **all-or-nothing** — the solver must provide the full required amount for every output asset in a single transaction. Partial fills are not supported for cross-chain orders. On fill, the contract dispatches a `RedeemEscrow` POST request via Hyperbridge back to the source chain, which releases escrowed tokens to the solver on receipt.
389384

390-
`execute()` exits after `USEROP_SUBMITTED` for cross-chain orders — it does not wait for Hyperbridge finalisation. To track cross-chain settlement, monitor the `EscrowReleased` event on the source chain or use the indexer.
385+
`execute()` exits after `BID_SELECTED` for cross-chain orders — it does not wait for Hyperbridge finalisation. To track cross-chain settlement, monitor the `EscrowReleased` event on the source chain or use the indexer.
391386

392387
```typescript lineNumbers title="cross-chain-order.ts" icon="typescript"
393388
import { toHex, parseUnits } from "viem"

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,7 @@ type IntentOrderStatusUpdate =
341341
| { status: "ORDER_PLACED"; order: Order; receipt: TransactionReceipt }
342342
| { status: "AWAITING_BIDS"; commitment: HexString; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
343343
| { status: "BIDS_RECEIVED"; commitment: HexString; bidCount: number; bids: FillerBid[] }
344-
| { status: "BID_SELECTED"; commitment: HexString; selectedSolver: HexString; userOpHash: HexString; userOp: PackedUserOperation }
345-
| { status: "USEROP_SUBMITTED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString }
344+
| { status: "BID_SELECTED"; commitment: HexString; selectedSolver: HexString; userOpHash: HexString; userOp: PackedUserOperation; transactionHash?: HexString }
346345
| { status: "FILLED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
347346
| { status: "PARTIAL_FILL"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; filledAssets: TokenInfo[]; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
348347
| { status: "EXPIRED"; commitment: HexString; totalFilledAssets?: TokenInfo[]; remainingAssets?: TokenInfo[]; error: string }
@@ -355,8 +354,7 @@ type IntentOrderStatusUpdate =
355354
| `ORDER_PLACED` | `order`, `receipt` | Order confirmed on-chain; `receipt` is the full viem `TransactionReceipt` of the placement transaction |
356355
| `AWAITING_BIDS` | `commitment`, `totalFilledAssets`, `remainingAssets` | Polling the coprocessor for solver bids |
357356
| `BIDS_RECEIVED` | `commitment`, `bidCount`, `bids` | One or more bids collected |
358-
| `BID_SELECTED` | `commitment`, `selectedSolver`, `userOpHash`, `userOp` | Best bid selected and UserOperation submitted to the bundler |
359-
| `USEROP_SUBMITTED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?` | UserOperation sent to the bundler |
357+
| `BID_SELECTED` | `commitment`, `selectedSolver`, `userOpHash`, `userOp`, `transactionHash?` | Best bid selected and UserOperation submitted to the bundler |
360358
| `FILLED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `totalFilledAssets`, `remainingAssets` | Order fully filled on the destination chain |
361359
| `PARTIAL_FILL` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `filledAssets`, `totalFilledAssets`, `remainingAssets` | Order partially filled; more fills may follow |
362360
| `EXPIRED` | `commitment`, `totalFilledAssets?`, `remainingAssets?`, `error` | Order deadline reached or no new bids available — terminal |

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.8",
3+
"version": "1.9.0",
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/IntentGateway.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
SelectBidResult,
1212
FillerBid,
1313
} from "@/types"
14+
import type { ResumeIntentOrderOptions } from "@/types"
1415
import type { IEvmChain } from "@/chain"
1516
import type { IntentsCoprocessor } from "@/chains/intentsCoprocessor"
1617
import type { IndexerClient } from "@/client"
@@ -24,7 +25,7 @@ import { BidManager } from "./BidManager"
2425
import { GasEstimator } from "./GasEstimator"
2526
import { OrderStatusChecker } from "./OrderStatusChecker"
2627
import type { ERC7821Call } from "@/types"
27-
import { DEFAULT_GRAFFITI } from "@/utils"
28+
import { DEFAULT_GRAFFITI, ADDRESS_ZERO } from "@/utils"
2829

2930
/**
3031
* High-level facade for the IntentGatewayV2 protocol.
@@ -185,7 +186,7 @@ export class IntentGateway {
185186
* The caller must sign the transaction and pass it back via `gen.next(signedTx)`.
186187
* 3. Yields `ORDER_PLACED` with the finalised order and transaction hash once
187188
* the `OrderPlaced` event is confirmed.
188-
* 4. Delegates to {@link OrderExecutor.executeIntentOrder} and forwards all
189+
* 4. Delegates to {@link OrderExecutor.executeOrder} and forwards all
189190
* subsequent status updates until the order is filled, exhausted, or fails.
190191
*
191192
* @param order - The order to place and execute. `order.fees` may be 0; it
@@ -196,7 +197,6 @@ export class IntentGateway {
196197
* - `maxPriorityFeePerGasBumpPercent` — bump % for the priority fee estimate (default 8).
197198
* - `maxFeePerGasBumpPercent` — bump % for the max fee estimate (default 10).
198199
* - `minBids` — minimum bids to collect before selecting (default 1).
199-
* - `bidTimeoutMs` — how long to poll for bids before giving up (default 60 000 ms).
200200
* - `pollIntervalMs` — interval between bid-polling attempts.
201201
* @yields {@link IntentOrderStatusUpdate} at each lifecycle stage.
202202
* @throws If the `placeOrder` generator behaves unexpectedly, or if gas
@@ -209,7 +209,6 @@ export class IntentGateway {
209209
maxPriorityFeePerGasBumpPercent?: number
210210
maxFeePerGasBumpPercent?: number
211211
minBids?: number
212-
bidTimeoutMs?: number
213212
pollIntervalMs?: number
214213
solver?: { address: HexString; timeoutMs: number }
215214
},
@@ -252,11 +251,10 @@ export class IntentGateway {
252251

253252
yield { status: "ORDER_PLACED", order: finalizedOrder, receipt: placementReceipt }
254253

255-
for await (const status of this.orderExecutor.executeIntentOrder({
254+
for await (const status of this.orderExecutor.executeOrder({
256255
order: finalizedOrder,
257256
sessionPrivateKey,
258257
minBids: options?.minBids,
259-
bidTimeoutMs: options?.bidTimeoutMs,
260258
pollIntervalMs: options?.pollIntervalMs,
261259
solver: options?.solver,
262260
})) {
@@ -266,6 +264,58 @@ export class IntentGateway {
266264
return
267265
}
268266

267+
/**
268+
* Validates that an order has the minimum fields required for post-placement
269+
* resume (i.e. it was previously placed and has an on-chain identity).
270+
*
271+
* @throws If `order.id` or `order.session` is missing or zero-valued.
272+
*/
273+
private assertOrderCanResume(order: Order): void {
274+
if (!order.id) {
275+
throw new Error("Cannot resume execution without order.id")
276+
}
277+
if (!order.session || order.session === ADDRESS_ZERO) {
278+
throw new Error("Cannot resume execution without order.session")
279+
}
280+
}
281+
282+
/**
283+
* Resumes execution of a previously placed order.
284+
*
285+
* Use this method after an app restart or crash to pick up where
286+
* {@link execute} left off. The order must already be placed on-chain
287+
* (i.e. it must have a valid `id` and `session`).
288+
*
289+
* Internally delegates to {@link OrderExecutor.executeOrder} and
290+
* yields the same status updates as the execution phase of {@link execute}:
291+
* `AWAITING_BIDS`, `BIDS_RECEIVED`, `BID_SELECTED`,
292+
* `FILLED`, `PARTIAL_FILL`, `EXPIRED`, or `FAILED`.
293+
*
294+
* Callers may check {@link isOrderFilled} or {@link isOrderRefunded} before
295+
* calling this method to avoid resuming an already-terminal order.
296+
*
297+
* @param order - A previously placed order with a valid `id` and `session`.
298+
* @param options - Optional tuning parameters for bid collection and execution.
299+
* @yields {@link IntentOrderStatusUpdate} at each execution stage.
300+
* @throws If the order is missing required fields for resumption.
301+
*/
302+
async *resume(
303+
order: Order,
304+
options?: ResumeIntentOrderOptions,
305+
): AsyncGenerator<IntentOrderStatusUpdate, void> {
306+
this.assertOrderCanResume(order)
307+
308+
for await (const status of this.orderExecutor.executeOrder({
309+
order,
310+
sessionPrivateKey: options?.sessionPrivateKey,
311+
minBids: options?.minBids,
312+
pollIntervalMs: options?.pollIntervalMs,
313+
solver: options?.solver,
314+
})) {
315+
yield status
316+
}
317+
}
318+
269319
/**
270320
* Returns both the native token cost and the relayer fee for cancelling an
271321
* order. Use `relayerFee` to approve the ERC-20 spend before submitting.

0 commit comments

Comments
 (0)