Skip to content
Merged
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
15 changes: 5 additions & 10 deletions docs/content/developers/intent-gateway/placing-orders.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const walletClient = createWalletClient({
```

<Callout type="warn">
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.
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.
</Callout>

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

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.

`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.
`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.

```typescript lineNumbers title="cross-chain-order.ts" icon="typescript"
import { toHex, parseUnits } from "viem"
Expand Down
6 changes: 2 additions & 4 deletions docs/content/developers/sdk/api/intent-gateway.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,7 @@ type IntentOrderStatusUpdate =
| { status: "ORDER_PLACED"; order: Order; receipt: TransactionReceipt }
| { status: "AWAITING_BIDS"; commitment: HexString; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
| { status: "BIDS_RECEIVED"; commitment: HexString; bidCount: number; bids: FillerBid[] }
| { status: "BID_SELECTED"; commitment: HexString; selectedSolver: HexString; userOpHash: HexString; userOp: PackedUserOperation }
| { status: "USEROP_SUBMITTED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString }
| { status: "BID_SELECTED"; commitment: HexString; selectedSolver: HexString; userOpHash: HexString; userOp: PackedUserOperation; transactionHash?: HexString }
| { status: "FILLED"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
| { status: "PARTIAL_FILL"; commitment: HexString; userOpHash: HexString; selectedSolver: HexString; transactionHash?: HexString; filledAssets: TokenInfo[]; totalFilledAssets: TokenInfo[]; remainingAssets: TokenInfo[] }
| { status: "EXPIRED"; commitment: HexString; totalFilledAssets?: TokenInfo[]; remainingAssets?: TokenInfo[]; error: string }
Expand All @@ -355,8 +354,7 @@ type IntentOrderStatusUpdate =
| `ORDER_PLACED` | `order`, `receipt` | Order confirmed on-chain; `receipt` is the full viem `TransactionReceipt` of the placement transaction |
| `AWAITING_BIDS` | `commitment`, `totalFilledAssets`, `remainingAssets` | Polling the coprocessor for solver bids |
| `BIDS_RECEIVED` | `commitment`, `bidCount`, `bids` | One or more bids collected |
| `BID_SELECTED` | `commitment`, `selectedSolver`, `userOpHash`, `userOp` | Best bid selected and UserOperation submitted to the bundler |
| `USEROP_SUBMITTED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?` | UserOperation sent to the bundler |
| `BID_SELECTED` | `commitment`, `selectedSolver`, `userOpHash`, `userOp`, `transactionHash?` | Best bid selected and UserOperation submitted to the bundler |
| `FILLED` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `totalFilledAssets`, `remainingAssets` | Order fully filled on the destination chain |
| `PARTIAL_FILL` | `commitment`, `userOpHash`, `selectedSolver`, `transactionHash?`, `filledAssets`, `totalFilledAssets`, `remainingAssets` | Order partially filled; more fills may follow |
| `EXPIRED` | `commitment`, `totalFilledAssets?`, `remainingAssets?`, `error` | Order deadline reached or no new bids available — terminal |
Expand Down
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.9.0",
"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
62 changes: 56 additions & 6 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 @@ -185,7 +186,7 @@ export class IntentGateway {
* The caller must sign the transaction and pass it back via `gen.next(signedTx)`.
* 3. Yields `ORDER_PLACED` with the finalised order and transaction hash once
* the `OrderPlaced` event is confirmed.
* 4. Delegates to {@link OrderExecutor.executeIntentOrder} and forwards all
* 4. Delegates to {@link OrderExecutor.executeOrder} and forwards all
* subsequent status updates until the order is filled, exhausted, or fails.
*
* @param order - The order to place and execute. `order.fees` may be 0; it
Expand All @@ -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 @@ -252,11 +251,10 @@ export class IntentGateway {

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

for await (const status of this.orderExecutor.executeIntentOrder({
for await (const status of this.orderExecutor.executeOrder({
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.executeOrder} and
* yields the same status updates as the execution phase of {@link execute}:
* `AWAITING_BIDS`, `BIDS_RECEIVED`, `BID_SELECTED`,
* `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 *resume(
order: Order,
options?: ResumeIntentOrderOptions,
): AsyncGenerator<IntentOrderStatusUpdate, void> {
this.assertOrderCanResume(order)

for await (const status of this.orderExecutor.executeOrder({
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
Loading
Loading