Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion sdk/packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperbridge/sdk",
"version": "1.8.8",
"version": "1.8.9",
"description": "The hyperclient SDK provides utilities for querying proofs and statuses for cross-chain requests from HyperBridge.",
"type": "module",
"types": "./dist/node/index.d.ts",
Expand Down
58 changes: 54 additions & 4 deletions sdk/packages/sdk/src/protocols/intents/IntentGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
SelectBidResult,
FillerBid,
} from "@/types"
import type { ResumeIntentOrderOptions } from "@/types"
import type { IEvmChain } from "@/chain"
import type { IntentsCoprocessor } from "@/chains/intentsCoprocessor"
import type { IndexerClient } from "@/client"
Expand All @@ -24,7 +25,7 @@ import { BidManager } from "./BidManager"
import { GasEstimator } from "./GasEstimator"
import { OrderStatusChecker } from "./OrderStatusChecker"
import type { ERC7821Call } from "@/types"
import { DEFAULT_GRAFFITI } from "@/utils"
import { DEFAULT_GRAFFITI, ADDRESS_ZERO } from "@/utils"

/**
* High-level facade for the IntentGatewayV2 protocol.
Expand Down Expand Up @@ -196,7 +197,6 @@ export class IntentGateway {
* - `maxPriorityFeePerGasBumpPercent` — bump % for the priority fee estimate (default 8).
* - `maxFeePerGasBumpPercent` — bump % for the max fee estimate (default 10).
* - `minBids` — minimum bids to collect before selecting (default 1).
* - `bidTimeoutMs` — how long to poll for bids before giving up (default 60 000 ms).
* - `pollIntervalMs` — interval between bid-polling attempts.
* @yields {@link IntentOrderStatusUpdate} at each lifecycle stage.
* @throws If the `placeOrder` generator behaves unexpectedly, or if gas
Expand All @@ -209,7 +209,6 @@ export class IntentGateway {
maxPriorityFeePerGasBumpPercent?: number
maxFeePerGasBumpPercent?: number
minBids?: number
bidTimeoutMs?: number
pollIntervalMs?: number
solver?: { address: HexString; timeoutMs: number }
},
Expand Down Expand Up @@ -256,7 +255,6 @@ export class IntentGateway {
order: finalizedOrder,
sessionPrivateKey,
minBids: options?.minBids,
bidTimeoutMs: options?.bidTimeoutMs,
pollIntervalMs: options?.pollIntervalMs,
solver: options?.solver,
})) {
Expand All @@ -266,6 +264,58 @@ export class IntentGateway {
return
}

/**
* Validates that an order has the minimum fields required for post-placement
* resume (i.e. it was previously placed and has an on-chain identity).
*
* @throws If `order.id` or `order.session` is missing or zero-valued.
*/
private assertOrderCanResume(order: Order): void {
if (!order.id) {
throw new Error("Cannot resume execution without order.id")
}
if (!order.session || order.session === ADDRESS_ZERO) {
throw new Error("Cannot resume execution without order.session")
}
}

/**
* Resumes execution of a previously placed order.
*
* Use this method after an app restart or crash to pick up where
* {@link execute} left off. The order must already be placed on-chain
* (i.e. it must have a valid `id` and `session`).
*
* Internally delegates to {@link OrderExecutor.executeIntentOrder} and
* yields the same status updates as the execution phase of {@link execute}:
* `AWAITING_BIDS`, `BIDS_RECEIVED`, `BID_SELECTED`, `USEROP_SUBMITTED`,
* `FILLED`, `PARTIAL_FILL`, `EXPIRED`, or `FAILED`.
*
* Callers may check {@link isOrderFilled} or {@link isOrderRefunded} before
* calling this method to avoid resuming an already-terminal order.
*
* @param order - A previously placed order with a valid `id` and `session`.
* @param options - Optional tuning parameters for bid collection and execution.
* @yields {@link IntentOrderStatusUpdate} at each execution stage.
* @throws If the order is missing required fields for resumption.
*/
async *resumeExecute(
order: Order,
options?: ResumeIntentOrderOptions,
): AsyncGenerator<IntentOrderStatusUpdate, void> {
this.assertOrderCanResume(order)

for await (const status of this.orderExecutor.executeIntentOrder({
order,
sessionPrivateKey: options?.sessionPrivateKey,
minBids: options?.minBids,
pollIntervalMs: options?.pollIntervalMs,
solver: options?.solver,
})) {
yield status
}
}

/**
* Returns both the native token cost and the relayer fee for cancelling an
* order. Use `relayerFee` to approve the ERC-20 spend before submitting.
Expand Down
29 changes: 17 additions & 12 deletions sdk/packages/sdk/src/protocols/intents/OrderExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ const USED_USEROPS_STORAGE_KEY = (commitment: HexString) => `used-userops:${comm
* After an order is placed on the source chain, `OrderExecutor` polls the
* Hyperbridge coprocessor for solver bids, selects and validates the best
* bid, submits the corresponding ERC-4337 UserOperation via the bundler, and
* tracks partial fills until the order is fully satisfied or exhausted.
* tracks partial fills until the order is fully satisfied or its on-chain
* block deadline is reached. Polling is bounded solely by the order's
* deadline — there is no separate wall-clock timeout.
*
* Deduplication of UserOperations is persisted across restarts using
* `usedUserOpsStorage` so that the executor can resume safely after a crash.
Expand All @@ -42,7 +44,12 @@ export class OrderExecutor {
/**
* Async generator that executes an intent order by polling for bids and
* submitting UserOperations until the order is filled, partially exhausted,
* or an unrecoverable error occurs.
* or the on-chain deadline is reached.
*
* Bid polling is bounded by the order's block deadline (`order.deadline`),
* which is checked before every poll iteration. There is no separate
* wall-clock timeout — the order's on-chain lifetime is the single
* source of truth for expiry.
*
* **Status progression (cross-chain orders):**
* `AWAITING_BIDS` → `BIDS_RECEIVED` → `BID_SELECTED` → `USEROP_SUBMITTED`
Expand All @@ -62,14 +69,7 @@ export class OrderExecutor {
* @throws Never throws directly; all errors are reported as `FAILED` yields.
*/
async *executeIntentOrder(options: ExecuteIntentOrderOptions): AsyncGenerator<IntentOrderStatusUpdate, void> {
const {
order,
sessionPrivateKey,
minBids = 1,
bidTimeoutMs = 60_000,
pollIntervalMs = DEFAULT_POLL_INTERVAL,
solver,
} = options
const { order, sessionPrivateKey, minBids = 1, pollIntervalMs = DEFAULT_POLL_INTERVAL, solver } = options

const commitment = order.id as HexString
const isSameChain = order.source === order.destination
Expand Down Expand Up @@ -139,7 +139,12 @@ export class OrderExecutor {
let bids: FillerBid[] = []
let solverLockExpired = false

while (Date.now() - startTime < bidTimeoutMs) {
while (true) {
const pollBlock = await this.ctx.dest.client.getBlockNumber()
if (pollBlock >= order.deadline) {
break
}

try {
const fetchedBids = await this.ctx.intentsCoprocessor!.getBidsForOrder(commitment)

Expand Down Expand Up @@ -175,7 +180,7 @@ export class OrderExecutor {
const isPartiallyFilled = totalFilledAssets.some((a) => a.amount > 0n)
const noBidsError = isPartiallyFilled
? `No new bids${solverClause} after partial fill`
: `No new bids${solverClause} available within ${bidTimeoutMs}ms timeout`
: `No new bids${solverClause} for order`

yield {
status: "EXPIRED",
Expand Down
12 changes: 11 additions & 1 deletion sdk/packages/sdk/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,6 @@ export interface ExecuteIntentOrderOptions {
order: Order
sessionPrivateKey?: HexString
minBids?: number
bidTimeoutMs?: number
pollIntervalMs?: number
/**
* If set, bids are restricted to the given solver until `timeoutMs` elapses,
Expand All @@ -1413,6 +1412,17 @@ export interface ExecuteIntentOrderOptions {
}
}

/** Options for resuming execution of a previously placed intent order */
export interface ResumeIntentOrderOptions {
sessionPrivateKey?: HexString
minBids?: number
pollIntervalMs?: number
solver?: {
address: HexString
timeoutMs: number
}
}

/** Type for ERC-7821 Call struct */
export type ERC7821Call = {
target: `0x${string}`
Expand Down
Loading