@ravnexchange/sdk 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -32,8 +32,10 @@ export declare class RavnClient {
32
32
  * the execution's statusRef, submitSignature's statusRef, or the origin tx hash. POST, not GET:
33
33
  * a 2-hop quoteToken in a query string can exceed header limits and fail with 431. `txHash`:
34
34
  * a two-hop route's hop-1 transaction, or the fee payment a DEPOSIT's `fee.transaction` asked
35
- * for; it's how RAVN proves the fee your share is paid from. */
36
- getStatus(quoteToken: string, ref: string, txHash?: string): Promise<StatusDTO>;
35
+ * for; it's how RAVN proves the fee your share is paid from. `signal` aborts the request. */
36
+ getStatus(quoteToken: string, ref: string, txHash?: string, options?: {
37
+ signal?: AbortSignal;
38
+ }): Promise<StatusDTO>;
37
39
  /** GET /v1/tokens: RAVN's listed token registry for one chain. Not a per-pair routability guarantee. */
38
40
  getTokens(chainId: number): Promise<TokenDTO[]>;
39
41
  /** GET /v1/chains: every chain RAVN lists tokens for. Static; safe to cache client-side. */
package/dist/client.js CHANGED
@@ -16,26 +16,39 @@ class RavnClient {
16
16
  // Called unbound, never as this.fetchImpl(...): a browser's own fetch (the default, or one
17
17
  // passed in) throws "Illegal invocation" when `this` is the client, failing every call.
18
18
  const fetchImpl = this.fetchImpl;
19
- const res = await fetchImpl(`${this.baseUrl}${path}`, {
20
- ...init,
21
- headers: {
22
- "content-type": "application/json",
23
- ...(this.apiKey ? { "x-api-key": this.apiKey } : {}),
24
- ...init.headers,
25
- },
26
- });
19
+ let res;
20
+ try {
21
+ res = await fetchImpl(`${this.baseUrl}${path}`, {
22
+ ...init,
23
+ headers: {
24
+ "content-type": "application/json",
25
+ ...(this.apiKey ? { "x-api-key": this.apiKey } : {}),
26
+ ...init.headers,
27
+ },
28
+ });
29
+ }
30
+ catch (err) {
31
+ // The caller's own abort is theirs, as is; anything else is the one error type this client
32
+ // promises, never a raw fetch TypeError.
33
+ if (init.signal?.aborted)
34
+ throw err;
35
+ const reason = err instanceof Error ? err.message : String(err);
36
+ throw new types_1.RavnApiError("NETWORK_ERROR", `No response from ${path}: ${reason}`, undefined, undefined, { cause: err });
37
+ }
27
38
  let body;
28
39
  try {
29
40
  body = (await res.json());
30
41
  }
31
- catch {
42
+ catch (err) {
43
+ if (init.signal?.aborted)
44
+ throw err;
32
45
  // A non-JSON body (an intermediary's HTML error page, an empty 204/502/504) must still
33
46
  // surface as the one error type this client promises, never a raw SyntaxError.
34
- throw new types_1.RavnApiError("INTERNAL", `HTTP ${res.status}: response was not valid JSON`, undefined, undefined);
47
+ throw new types_1.RavnApiError("INTERNAL", `HTTP ${res.status}: response was not valid JSON`, undefined, undefined, { status: res.status });
35
48
  }
36
49
  if (!res.ok || body.error) {
37
50
  const err = body.error ?? { code: "INTERNAL", message: `HTTP ${res.status}` };
38
- throw new types_1.RavnApiError(err.code, err.message, err.details, body.meta);
51
+ throw new types_1.RavnApiError(err.code, err.message, err.details, body.meta, { status: res.status });
39
52
  }
40
53
  return body.data;
41
54
  }
@@ -67,11 +80,12 @@ class RavnClient {
67
80
  * the execution's statusRef, submitSignature's statusRef, or the origin tx hash. POST, not GET:
68
81
  * a 2-hop quoteToken in a query string can exceed header limits and fail with 431. `txHash`:
69
82
  * a two-hop route's hop-1 transaction, or the fee payment a DEPOSIT's `fee.transaction` asked
70
- * for; it's how RAVN proves the fee your share is paid from. */
71
- getStatus(quoteToken, ref, txHash) {
83
+ * for; it's how RAVN proves the fee your share is paid from. `signal` aborts the request. */
84
+ getStatus(quoteToken, ref, txHash, options = {}) {
72
85
  return this.request("/status", {
73
86
  method: "POST",
74
87
  body: JSON.stringify({ quoteToken, ref, ...(txHash ? { txHash } : {}) }),
88
+ signal: options.signal,
75
89
  });
76
90
  }
77
91
  /** GET /v1/tokens: RAVN's listed token registry for one chain. Not a per-pair routability guarantee. */
@@ -1,5 +1,5 @@
1
1
  import type { RavnClient } from "./client";
2
- import type { ApprovalDTO, ExecuteParams, ExecutionDTO, StatusDTO } from "./types";
2
+ import type { ApprovalDTO, DepositFeeDTO, ExecuteParams, ExecutionDTO, StatusDTO } from "./types";
3
3
  type TxLike = {
4
4
  to?: string;
5
5
  data?: string;
@@ -7,15 +7,43 @@ type TxLike = {
7
7
  chainId?: number;
8
8
  serialized?: string;
9
9
  };
10
+ /**
11
+ * The parts of a transaction receipt executeAndTrack reads. viem's and wagmi's
12
+ * `waitForTransactionReceipt`, ethers v5 and v6 (`tx.wait()`, `provider.waitForTransaction`) and
13
+ * web3.js receipts all fit as they are.
14
+ */
15
+ export type TransactionReceiptLike = {
16
+ status?: unknown;
17
+ transactionHash: string;
18
+ } | {
19
+ status?: unknown;
20
+ hash: string;
21
+ };
10
22
  export interface ExecuteAndTrackHandlers {
11
- /** Broadcast a transaction (an approval, or the main swap tx) and return its hash. */
23
+ /** Broadcast a transaction (an approval, the main swap tx, or RAVN's fee) and return its hash. */
12
24
  sendTransaction(tx: TxLike): Promise<string>;
13
25
  /**
14
- * Wait for a transaction hash to confirm. Called on the approval before the main tx is ever
15
- * sent: sending both back-to-back races the allowance and is the most common integration bug
16
- * this helper exists to prevent.
26
+ * Wait for a transaction hash to confirm and resolve to its receipt. Called on the approval
27
+ * before the main tx is ever sent: sending both back-to-back races the allowance and is the most
28
+ * common integration bug this helper exists to prevent. Also called on the swap before RAVN's fee
29
+ * is sent on its own (`fee.transaction`): the fee is only sent when the receipt says success AND
30
+ * is this hash's own (a cancelled or replaced swap resolves with another transaction's). A
31
+ * handler resolving to nothing doesn't compile: it would never get the fee sent. `null` (ethers
32
+ * with 0 confirmations) sends no fee: a fee on a swap that didn't run is the user's money lost.
17
33
  */
18
- waitForReceipt(hash: string, chainId?: number): Promise<void>;
34
+ waitForReceipt(hash: string, chainId?: number): Promise<TransactionReceiptLike | null>;
35
+ /**
36
+ * Optional: send several calls in ONE wallet confirmation that succeeds or fails as a whole
37
+ * (EIP-5792 `wallet_sendCalls` with `atomicRequired: true`, a smart account) and resolve, once
38
+ * they have landed, to the on-chain hash of the transaction that carried each call, in order
39
+ * (the same hash repeated, or just one, for one transaction). Not a batch id or a 4337
40
+ * userOpHash: RAVN proves its fee from that transaction. When given, a token-paid RAVN fee rides
41
+ * in the swap's own confirmation, and so does the swap's approval when it needs one; a
42
+ * native-coin fee is always a plain transaction of its own. The batch's gas limit is your
43
+ * wallet's (EIP-5792 carries none): give it headroom, as a wallet estimating it tight reverts
44
+ * the whole batch. Reject only when nothing was sent: a rejection loses the swap's hash.
45
+ */
46
+ sendCalls?(txs: TxLike[]): Promise<string[]>;
19
47
  /** Sign an EIP-712 typed-data payload. Only called for SIGNATURE executions. */
20
48
  signTypedData?(typedData: Record<string, unknown>): Promise<`0x${string}`>;
21
49
  /**
@@ -33,13 +61,42 @@ export interface ExecuteAndTrackHandlers {
33
61
  */
34
62
  hasAllowance?(approval: ApprovalDTO): Promise<boolean>;
35
63
  }
64
+ /** What just happened: a transaction's hash is known (sent, not yet confirmed), a swap was signed
65
+ * for RAVN to submit (`signedTransaction` for a Solana swap, `signature` for an order), or a status
66
+ * poll answered. */
67
+ export type ExecuteAndTrackStage = "reset_sent" | "approval_sent" | "swap_signed" | "swap_sent" | "fee_sent" | "status";
68
+ export interface ExecuteAndTrackProgress {
69
+ stage: ExecuteAndTrackStage;
70
+ /** Everything known so far. Persist it: it's all you need to keep tracking the swap if this
71
+ * call never returns (a closed tab, a crash). "swap_sent" carries `statusRef`. */
72
+ partial: ExecuteAndTrackPartial;
73
+ }
36
74
  export interface ExecuteAndTrackOptions {
37
- /** Default true. Set false to get a ref back immediately and poll getStatus yourself. */
75
+ /** Default true. Set false to get a ref back once the swap (and RAVN's fee, when one is sent) is
76
+ * out, and poll getStatus yourself with `statusTxHash`. */
38
77
  pollUntilTerminal?: boolean;
78
+ /** Default 4000. Also where the backoff after a failed call starts (doubling, up to a minute). */
39
79
  pollIntervalMs?: number;
80
+ /** How often to poll a swap that's `expired`: a refund is due and can take hours (Across returns
81
+ * it in a later settlement bundle). Default 60000. */
82
+ expiredPollIntervalMs?: number;
83
+ /** Stop polling this long after the first status poll, rejecting with an ExecuteAndTrackError
84
+ * ("TIMEOUT") that carries every hash: keep polling getStatus with them yourself. Default: no
85
+ * limit (a swap may sit `expired` for hours). */
86
+ maxWaitMs?: number;
87
+ /** Stop: nothing more goes to the wallet and polling ends, rejecting with an
88
+ * ExecuteAndTrackError ("ABORTED"). A wallet prompt already open isn't withdrawn; whatever it
89
+ * sends is in the error's `partial`. A submitSignature or submitTransaction call already under
90
+ * way is awaited first: its answer is the swap's only handle. */
91
+ signal?: AbortSignal;
92
+ /** Called with every hash the moment it exists (approval, swap with its statusRef, fee) and with
93
+ * every status poll. An error it throws is ignored: it never stops a swap that's out. */
94
+ onProgress?(progress: ExecuteAndTrackProgress): void;
40
95
  }
41
96
  export interface ExecuteAndTrackResult {
42
97
  execution: ExecutionDTO;
98
+ /** The `approval.reset` (approve to 0) sent before the approval, when the token needed one. */
99
+ resetTxHash?: string;
43
100
  approvalTxHash?: string;
44
101
  txHash?: string;
45
102
  /** Pass to client.getStatus(quoteToken, statusRef). Same as `execution.quoteToken`, NOT the
@@ -48,14 +105,94 @@ export interface ExecuteAndTrackResult {
48
105
  /** Pass to client.getStatus(quoteToken, statusRef). For TRANSACTION, the execution's own
49
106
  * statusRef when it has one (compose, Garden, Relay), else txHash: not always a tx hash. */
50
107
  statusRef?: string;
108
+ /** Pass as getStatus's third argument (`txHash`) on every poll you make yourself, from the first
109
+ * one until a terminal status and once more after a success (RAVN's node may be a block behind
110
+ * yours): the fee payment's transaction when one was sent (already confirmed by the time this
111
+ * returns), else, with a statusRef, the swap's own tx (a two-hop route's hop 1 proves its fee;
112
+ * ignored elsewhere). It's how RAVN proves the fee your share is paid from: the venue's own
113
+ * tracker saying "delivered" doesn't book it. */
114
+ statusTxHash?: string;
115
+ /** The transaction that carried RAVN's fee (`execution.fee.transaction`), once sent. Sent isn't
116
+ * paid (a batch can revert): RAVN's status polls are what prove it. */
117
+ feeTxHash?: string;
118
+ /** Why RAVN's fee wasn't sent (or its receipt not seen): the swap still runs and is tracked, but
119
+ * no fee is owed to you on it unless `feeTxHash` is set and it landed. */
120
+ feeError?: unknown;
121
+ /** DEPOSIT only: the fee payment to make beside the deposit (see depositFeePayment). */
122
+ feePayment?: DepositFeePayment;
51
123
  /** Set once pollUntilTerminal reaches a terminal status. Absent for DEPOSIT (see below) or when polling is turned off. */
52
124
  finalStatus?: StatusDTO;
53
125
  }
126
+ /** What executeAndTrack knew when it reported progress or stopped: every hash sent so far, and the
127
+ * last status it saw. `signedTransaction` (broadcastViaRavn): the signed Solana swap handed to
128
+ * submitTransaction; post it again with the same quoteToken (RAVN never sends it twice) to learn
129
+ * whether it landed, before signing anything new. `signature` (SIGNATURE): the signed order handed
130
+ * to submitSignature; "swap_signed" with no "swap_sent" after it means the call failed or got no
131
+ * answer, and the venue may still have taken the order: check before signing a new one. */
132
+ export type ExecuteAndTrackPartial = Partial<ExecuteAndTrackResult> & {
133
+ lastStatus?: StatusDTO;
134
+ signedTransaction?: string;
135
+ signature?: `0x${string}`;
136
+ };
137
+ /**
138
+ * How executeAndTrack rejects once anything has been sent or signed (an approval, the swap, a
139
+ * signature handed to RAVN), and on any abort or timeout: `partial` carries every hash, so the
140
+ * swap is never lost. Keep polling getStatus(partial.quoteToken, partial.statusRef,
141
+ * partial.statusTxHash). Before anything is sent, errors come as they are (a RavnApiError from
142
+ * execute, the wallet's own rejection).
143
+ */
144
+ export declare class ExecuteAndTrackError extends Error {
145
+ /** "ABORTED": your signal. "TIMEOUT": maxWaitMs passed. "FAILED": anything else; see `cause`. */
146
+ readonly code: "ABORTED" | "TIMEOUT" | "FAILED";
147
+ readonly partial: ExecuteAndTrackPartial;
148
+ /** What failed underneath: a RavnApiError, the wallet's error, or your signal's reason. */
149
+ readonly cause?: unknown;
150
+ constructor(message: string,
151
+ /** "ABORTED": your signal. "TIMEOUT": maxWaitMs passed. "FAILED": anything else; see `cause`. */
152
+ code: "ABORTED" | "TIMEOUT" | "FAILED", partial: ExecuteAndTrackPartial, cause?: unknown);
153
+ }
154
+ /** How to pay a DEPOSIT's `fee`, so it's proven against this swap. See DepositFeeDTO. Either way,
155
+ * poll RAVN's getStatus until a terminal status even when the venue's own tracker (e.g. Rift's API)
156
+ * already says delivered: RAVN books the fee on its own status polls. */
157
+ export type DepositFeePayment =
158
+ /** EVM: send `transaction` exactly as given once the deposit has confirmed, then pass its
159
+ * on-chain hash as `txHash` on every getStatus poll; at least one must come after it confirmed,
160
+ * or nothing is owed. A token fee (`value` "0") may instead ride in the deposit's own atomic
161
+ * wallet batch: pass the hash of the transaction that carried it (not a batch id or userOpHash).
162
+ * A native-coin fee: a plain transaction of its own. */
163
+ {
164
+ kind: "transaction";
165
+ transaction: NonNullable<DepositFeeDTO["transaction"]>;
166
+ }
167
+ /** Bitcoin: an extra output paying `amount` sats to `address` in the SAME transaction that pays
168
+ * the deposit. One deposit per transaction: a fee output proves one swap. */
169
+ | {
170
+ kind: "bitcoin_output";
171
+ address: string;
172
+ amount: string;
173
+ }
174
+ /** Solana: a transfer of `amount` to `address` in the SAME transaction that pays the deposit
175
+ * exactly `deposit.amount` (SOL as lamports, not wrapped). SOL: a System transfer of `amount`
176
+ * lamports to `address`. A token: `createAssociatedTokenAccountIdempotent` for `address`'s
177
+ * associated token account for the input mint (under the mint's token program), then
178
+ * `transferChecked` of `amount` to it. Sent in a transaction of its own, it isn't found. */
179
+ | {
180
+ kind: "solana_transfer";
181
+ address: string;
182
+ amount: string;
183
+ };
184
+ /** The fee payment a DEPOSIT execution asks for beside its deposit; undefined when it has none. */
185
+ export declare function depositFeePayment(execution: ExecutionDTO): DepositFeePayment | undefined;
54
186
  /**
55
- * Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → status
56
- * polling, using caller-supplied signing/sending functions. The SDK stays wallet-agnostic (no
57
- * RPC dependency); this just gets the ORDER right, which quickstart's own docs flag as the
58
- * most common integration failure: sending the main tx before the approval has confirmed.
187
+ * Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → RAVN's fee
188
+ * payment (if any) → status polling, using caller-supplied signing/sending functions. The SDK
189
+ * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
190
+ * own docs flag as the most common integration failure: sending the main tx before the approval
191
+ * has confirmed. A failed API call is retried where repeating it is safe (status polls and
192
+ * submitTransaction on a 429, 5xx or no response; execute and submitSignature only when rate
193
+ * limited), waiting the API's retryAfterSec. Once anything
194
+ * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
195
+ * each one first.
59
196
  */
60
197
  export declare function executeAndTrack(client: RavnClient, params: ExecuteParams, handlers: ExecuteAndTrackHandlers, options?: ExecuteAndTrackOptions): Promise<ExecuteAndTrackResult>;
61
198
  export {};
@@ -1,80 +1,380 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExecuteAndTrackError = void 0;
4
+ exports.depositFeePayment = depositFeePayment;
3
5
  exports.executeAndTrack = executeAndTrack;
6
+ const types_1 = require("./types");
4
7
  const polling_1 = require("./polling");
5
8
  /**
6
- * Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → status
7
- * polling, using caller-supplied signing/sending functions. The SDK stays wallet-agnostic (no
8
- * RPC dependency); this just gets the ORDER right, which quickstart's own docs flag as the
9
- * most common integration failure: sending the main tx before the approval has confirmed.
9
+ * How executeAndTrack rejects once anything has been sent or signed (an approval, the swap, a
10
+ * signature handed to RAVN), and on any abort or timeout: `partial` carries every hash, so the
11
+ * swap is never lost. Keep polling getStatus(partial.quoteToken, partial.statusRef,
12
+ * partial.statusTxHash). Before anything is sent, errors come as they are (a RavnApiError from
13
+ * execute, the wallet's own rejection).
14
+ */
15
+ class ExecuteAndTrackError extends Error {
16
+ constructor(message,
17
+ /** "ABORTED": your signal. "TIMEOUT": maxWaitMs passed. "FAILED": anything else; see `cause`. */
18
+ code, partial, cause) {
19
+ super(message);
20
+ this.code = code;
21
+ this.partial = partial;
22
+ this.name = "ExecuteAndTrackError";
23
+ if (cause !== undefined)
24
+ this.cause = cause;
25
+ }
26
+ }
27
+ exports.ExecuteAndTrackError = ExecuteAndTrackError;
28
+ const BITCOIN_CHAIN_ID = -1;
29
+ const SOLANA_CHAIN_ID = -2;
30
+ const EVM_TX_HASH = /^0x[0-9a-fA-F]{64}$/;
31
+ // What a sandbox status poll is sent as ref when there's no tx hash: RAVN doesn't read it.
32
+ const SANDBOX_REF = "sandbox";
33
+ const EXPIRED_POLL_INTERVAL_MS = 60000;
34
+ const MAX_BACKOFF_MS = 60000;
35
+ // ponytail: a fixed cap, ~4 min of backoff at the default interval; past it the call rejects with
36
+ // every hash to resume from. Make it an option if integrators need to ride out longer outages.
37
+ const MAX_ATTEMPTS = 8;
38
+ /** The fee payment a DEPOSIT execution asks for beside its deposit; undefined when it has none. */
39
+ function depositFeePayment(execution) {
40
+ if (execution.executionType !== "DEPOSIT" || !execution.fee)
41
+ return undefined;
42
+ const { transaction, address, amount, chainId } = execution.fee;
43
+ if (transaction)
44
+ return { kind: "transaction", transaction };
45
+ if (chainId === BITCOIN_CHAIN_ID)
46
+ return { kind: "bitcoin_output", address, amount };
47
+ if (chainId === SOLANA_CHAIN_ID)
48
+ return { kind: "solana_transfer", address, amount };
49
+ throw new Error(`depositFeePayment: no known way to pay a fee on chain ${chainId}`);
50
+ }
51
+ // Only a receipt that says success (viem "success", ethers 1; web3.js 1n/true, so == on purpose) and
52
+ // is `hash`'s own (viem/ethers v5 transactionHash, ethers v6 hash): viem resolves a cancelled swap
53
+ // with the replacement's successful receipt. Typed unknown: JS callers can still hand it anything.
54
+ const receiptSucceeded = (receipt, hash) => {
55
+ const r = receipt;
56
+ const own = r?.transactionHash ?? r?.hash;
57
+ return (r?.status === "success" || r?.status == 1) && (typeof own !== "string" || own.toLowerCase() === hash.toLowerCase());
58
+ };
59
+ // The same call can succeed later: no response at all, a 429, or a 5xx.
60
+ const transient = (err) => err instanceof types_1.RavnApiError && (err.code === "NETWORK_ERROR" || err.status === 429 || (err.status ?? 0) >= 500);
61
+ // Refused before anything was done, so safe to repeat even a call that isn't safe to repeat after
62
+ // a BUILD_FAILED (submit-signature: the venue may have taken the order).
63
+ const rateLimited = (err) => err instanceof types_1.RavnApiError && (err.code === "RATE_LIMITED" || err.code === "VENUE_RATE_LIMITED" || err.status === 429);
64
+ // `promise`, or the signal's reason as soon as it aborts: a wallet or custom fetch may ignore the
65
+ // signal. `promise` is always handled, so one that rejects after the abort is never unhandled.
66
+ const orAbort = (promise, signal) => new Promise((resolve, reject) => {
67
+ const onAbort = () => reject(signal.reason);
68
+ // Promise.resolve: a JS handler may hand back the value itself rather than a promise.
69
+ Promise.resolve(promise).then((value) => {
70
+ signal.removeEventListener("abort", onAbort);
71
+ resolve(value);
72
+ }, (err) => {
73
+ signal.removeEventListener("abort", onAbort);
74
+ reject(err);
75
+ });
76
+ if (signal.aborted)
77
+ return onAbort();
78
+ signal.addEventListener("abort", onAbort, { once: true });
79
+ });
80
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
81
+ if (signal.aborted)
82
+ return reject(signal.reason);
83
+ const onAbort = () => {
84
+ clearTimeout(timer);
85
+ reject(signal.reason);
86
+ };
87
+ const timer = setTimeout(() => {
88
+ signal.removeEventListener("abort", onAbort);
89
+ resolve();
90
+ }, ms);
91
+ signal.addEventListener("abort", onAbort, { once: true });
92
+ });
93
+ /**
94
+ * Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → RAVN's fee
95
+ * payment (if any) → status polling, using caller-supplied signing/sending functions. The SDK
96
+ * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
97
+ * own docs flag as the most common integration failure: sending the main tx before the approval
98
+ * has confirmed. A failed API call is retried where repeating it is safe (status polls and
99
+ * submitTransaction on a 429, 5xx or no response; execute and submitSignature only when rate
100
+ * limited), waiting the API's retryAfterSec. Once anything
101
+ * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
102
+ * each one first.
10
103
  */
11
104
  async function executeAndTrack(client, params, handlers, options = {}) {
12
- const { pollUntilTerminal = true, pollIntervalMs = 4000 } = options;
13
- const execution = await client.execute(params);
14
- const { quoteToken } = execution;
15
- const pollStatus = async (ref) => {
16
- if (!pollUntilTerminal)
17
- return undefined;
18
- for (;;) {
19
- const status = await client.getStatus(quoteToken, ref);
20
- if ((0, polling_1.isPollingTerminal)(status.status))
21
- return status;
22
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
105
+ const { pollUntilTerminal = true, pollIntervalMs = 4000, expiredPollIntervalMs = EXPIRED_POLL_INTERVAL_MS, maxWaitMs } = options;
106
+ const partial = {};
107
+ let sent = false;
108
+ const report = (stage, fields) => {
109
+ Object.assign(partial, fields);
110
+ try {
111
+ // An async hook (a database write) may reject: handled here too, never unhandled.
112
+ Promise.resolve(options.onProgress?.({ stage, partial: { ...partial } })).catch(() => { });
113
+ }
114
+ catch {
115
+ // The swap is out either way: a failing progress hook must not stop its fee or tracking.
23
116
  }
24
117
  };
25
- // Sends and confirms an approval unless the caller already knows it's covered; see
26
- // ExecuteAndTrackHandlers.hasAllowance's doc comment for why the API can't decide this itself.
27
- const maybeSendApproval = async (approval) => {
28
- if (!approval)
29
- return undefined;
30
- if (await handlers.hasAllowance?.(approval))
31
- return undefined;
32
- const hash = await handlers.sendTransaction(approval);
33
- await handlers.waitForReceipt(hash, approval.chainId);
34
- return hash;
118
+ // One signal for everything that waits: the caller's abort, or maxWaitMs once polling starts.
119
+ const stop = new AbortController();
120
+ let stopCode;
121
+ const halt = (code, reason) => {
122
+ if (stop.signal.aborted)
123
+ return;
124
+ stopCode = code;
125
+ stop.abort(reason);
35
126
  };
36
- if (execution.executionType === "TRANSACTION" && execution.submit) {
37
- // Never broadcast this one ourselves: a payment Layerswap can't match sits at Layerswap, and
38
- // RAVN's check before sending is what catches it.
39
- if (!handlers.signSolanaTransaction) {
40
- throw new Error("executeAndTrack: this execution must be submitted through RAVN and requires signSolanaTransaction, none was provided");
127
+ const onAbort = () => halt("ABORTED", options.signal?.reason);
128
+ if (options.signal?.aborted)
129
+ onAbort();
130
+ options.signal?.addEventListener("abort", onAbort, { once: true });
131
+ let deadline;
132
+ // Before each step that asks the wallet for something: never send once stopped.
133
+ const checkpoint = () => {
134
+ if (stop.signal.aborted)
135
+ throw stop.signal.reason;
136
+ };
137
+ // Calls `call` until it succeeds, fails in a way `retryable` says repeating can't fix, or
138
+ // MAX_ATTEMPTS: waiting the API's retryAfterSec, else an interval doubling from pollIntervalMs.
139
+ const withRetries = async (call, retryable) => {
140
+ for (let attempt = 1;; attempt++) {
141
+ try {
142
+ return await call();
143
+ }
144
+ catch (err) {
145
+ if (attempt >= MAX_ATTEMPTS || !retryable(err))
146
+ throw err;
147
+ const askedMs = (err.retryAfterSec ?? 0) * 1000;
148
+ await sleep(Math.max(askedMs, Math.min(pollIntervalMs * 2 ** (attempt - 1), MAX_BACKOFF_MS)), stop.signal);
149
+ }
41
150
  }
42
- if (!execution.transaction.serialized) {
43
- throw new Error("executeAndTrack: TRANSACTION execution with submit is missing transaction.serialized");
151
+ };
152
+ const run = async () => {
153
+ checkpoint();
154
+ // Only a rate limit: a BUILD_FAILED wants a fresh quote, and a repeat can open a second order
155
+ // at the venue (a NEAR deposit address, a Houdini or Garden order).
156
+ const execution = await withRetries(() => orAbort(client.execute(params), stop.signal), rateLimited);
157
+ const { quoteToken } = execution;
158
+ Object.assign(partial, { execution, quoteToken, statusRef: execution.statusRef });
159
+ const pollStatus = async (ref, txHash, feeSent = false) => {
160
+ if (!pollUntilTerminal)
161
+ return undefined;
162
+ if (maxWaitMs !== undefined) {
163
+ deadline = setTimeout(() => halt("TIMEOUT", new Error(`executeAndTrack: no terminal status within maxWaitMs (${maxWaitMs} ms)`)), maxWaitMs);
164
+ }
165
+ // A success answered before RAVN's node sees the fee tx proves nothing, and the row is then
166
+ // left to a cron that books no fee: one more poll after it.
167
+ // ponytail: one re-poll covers a block of lag; /v1/status also stays "processing" while a
168
+ // reported fee tx is unreadable (ravn-mvp #411).
169
+ for (let recheck = feeSent;;) {
170
+ checkpoint();
171
+ const status = await withRetries(() => orAbort(client.getStatus(quoteToken, ref, txHash, { signal: stop.signal }), stop.signal), transient);
172
+ report("status", { lastStatus: status });
173
+ if ((0, polling_1.isPollingTerminal)(status.status)) {
174
+ if (!recheck || status.status !== "success")
175
+ return status;
176
+ recheck = false;
177
+ }
178
+ await sleep(status.status === "expired" ? expiredPollIntervalMs : pollIntervalMs, stop.signal);
179
+ }
180
+ };
181
+ // A sandbox execution is never real, but a venue with no sandbox stand-in returns its live
182
+ // payload: nothing in it is signed or sent (a signed order or Solana payment is a live
183
+ // instrument even though RAVN drops it). RAVN answers its status synthetically, for any ref.
184
+ if (execution.sandbox && execution.executionType !== "DEPOSIT") {
185
+ const statusRef = execution.statusRef ?? SANDBOX_REF;
186
+ return { execution, quoteToken, statusRef, finalStatus: await pollStatus(statusRef) };
44
187
  }
45
- const signedTransaction = await handlers.signSolanaTransaction(execution.transaction.serialized);
46
- const { txHash } = await client.submitTransaction({ quoteToken, signedTransaction });
47
- const statusRef = execution.statusRef ?? txHash;
48
- return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
49
- }
50
- if (execution.executionType === "TRANSACTION") {
51
- const approvalTxHash = await maybeSendApproval(execution.approval);
52
- const txHash = await handlers.sendTransaction(execution.transaction);
53
- const statusRef = execution.statusRef ?? txHash;
54
- return { execution, quoteToken, approvalTxHash, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
55
- }
56
- if (execution.executionType === "SIGNATURE") {
57
- if (!handlers.signTypedData) {
58
- throw new Error("executeAndTrack: this execution requires signTypedData, none was provided");
188
+ // The approval to send, if any: none when the caller already knows it's covered (see
189
+ // ExecuteAndTrackHandlers.hasAllowance).
190
+ const neededApproval = async (approval) => approval && !(handlers.hasAllowance && (await orAbort(handlers.hasAllowance(approval), stop.signal))) ? approval : undefined;
191
+ // Sends and confirms it, after its reset (approve to 0) when the token needs one first.
192
+ const maybeSendApproval = async (approval) => {
193
+ const needed = await neededApproval(approval);
194
+ if (!needed)
195
+ return undefined;
196
+ let hash = "";
197
+ for (const tx of needed.reset ? [{ ...needed.reset, chainId: needed.chainId }, needed] : [needed]) {
198
+ checkpoint();
199
+ hash = await handlers.sendTransaction(tx);
200
+ sent = true;
201
+ if (tx === needed)
202
+ report("approval_sent", { approvalTxHash: hash });
203
+ else
204
+ report("reset_sent", { resetTxHash: hash });
205
+ await orAbort(handlers.waitForReceipt(hash, needed.chainId), stop.signal);
206
+ }
207
+ return hash;
208
+ };
209
+ if (execution.executionType === "TRANSACTION" && execution.submit) {
210
+ // Never broadcast this one ourselves: a payment Layerswap can't match sits at Layerswap, and
211
+ // RAVN's check before sending is what catches it.
212
+ if (!handlers.signSolanaTransaction) {
213
+ throw new Error("executeAndTrack: this execution must be submitted through RAVN and requires signSolanaTransaction, none was provided");
214
+ }
215
+ if (!execution.transaction.serialized) {
216
+ throw new Error("executeAndTrack: TRANSACTION execution with submit is missing transaction.serialized");
217
+ }
218
+ const serialized = execution.transaction.serialized;
219
+ checkpoint();
220
+ const signedTransaction = await handlers.signSolanaTransaction(serialized);
221
+ checkpoint();
222
+ // From here RAVN may have sent it, even on a failed call. A retry with the same signed
223
+ // transaction never sends it twice, so retrying is how to find out; a failure that names
224
+ // the signature (BUILD_FAILED's details.txHash) is kept too.
225
+ sent = true;
226
+ report("swap_signed", { signedTransaction });
227
+ const submit = async () => {
228
+ try {
229
+ return await client.submitTransaction({ quoteToken, signedTransaction });
230
+ }
231
+ catch (err) {
232
+ const named = err instanceof types_1.RavnApiError ? err.details?.txHash : undefined;
233
+ if (typeof named === "string")
234
+ Object.assign(partial, { txHash: named, statusRef: execution.statusRef ?? named });
235
+ throw err;
236
+ }
237
+ };
238
+ const { txHash } = await withRetries(submit, transient);
239
+ const statusRef = execution.statusRef ?? txHash;
240
+ report("swap_sent", { txHash, statusRef });
241
+ return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
242
+ }
243
+ if (execution.executionType === "TRANSACTION") {
244
+ // RAVN's fee, when the route has none of its own (Eco, Layerswap, Mayan plain MCTP). From
245
+ // Solana there's nothing to send: it's a transfer inside transaction.serialized.
246
+ const { approval, transaction } = execution;
247
+ const feeTx = execution.fee?.transaction;
248
+ // What proves the fee, from the first poll on: its own transaction, else (a statusRef: a
249
+ // two-hop route) hop 1's. Only an EVM tx hash: /v1/status refuses anything else (a Solana
250
+ // signature, a batch id) on every poll.
251
+ const proof = (swapHash, feeHash) => {
252
+ const hash = feeHash ?? (execution.statusRef ? swapHash : undefined);
253
+ return hash && EVM_TX_HASH.test(hash) ? hash : undefined;
254
+ };
255
+ let txHash;
256
+ let feeTxHash;
257
+ let feeError;
258
+ if (feeTx && BigInt(feeTx.value) === 0n && handlers.sendCalls) {
259
+ // A token fee may ride in the swap's own confirmation, and the swap's approval with it:
260
+ // atomic, so neither outlives a swap that reverted. Only here: ExecutionDTO doesn't name
261
+ // its venue, and the first-party app never batches THORChain (Bifrost ignores a router
262
+ // deposit whose tx.to is the user's own 7702 account) or Bebop (its settlement needs gas
263
+ // headroom a batch leaves to the wallet); fee-beside routes are neither.
264
+ const batchApproval = await neededApproval(approval);
265
+ const reset = batchApproval?.reset && { ...batchApproval.reset, chainId: batchApproval.chainId };
266
+ const approvals = batchApproval ? [...(reset ? [reset] : []), batchApproval] : [];
267
+ checkpoint();
268
+ const hashes = await handlers.sendCalls([...approvals, transaction, feeTx]);
269
+ // The wallet resolved, so something may be out: from here an error carries what's known.
270
+ sent = true;
271
+ // Fewer hashes back than calls: the last one carried the rest.
272
+ const carrier = (i) => hashes[Math.min(i, hashes.length - 1)];
273
+ if (!carrier(0))
274
+ throw new Error("executeAndTrack: sendCalls returned no transaction hash");
275
+ txHash = carrier(approvals.length);
276
+ feeTxHash = carrier(approvals.length + 1);
277
+ if (reset)
278
+ report("reset_sent", { resetTxHash: carrier(0) });
279
+ if (batchApproval)
280
+ report("approval_sent", { approvalTxHash: carrier(approvals.length - 1) });
281
+ report("swap_sent", { txHash, statusRef: execution.statusRef ?? txHash, statusTxHash: proof(txHash) });
282
+ report("fee_sent", { feeTxHash, statusTxHash: proof(txHash, feeTxHash) });
283
+ }
284
+ else {
285
+ await maybeSendApproval(approval);
286
+ checkpoint();
287
+ txHash = await handlers.sendTransaction(transaction);
288
+ sent = true;
289
+ report("swap_sent", { txHash, statusRef: execution.statusRef ?? txHash, statusTxHash: proof(txHash) });
290
+ if (feeTx) {
291
+ // On its own, only once the swap has confirmed: a reverted, dropped or replaced one pays
292
+ // no fee. Declined or failed (or stopped first), the swap still runs and is tracked; no
293
+ // fee is owed on it.
294
+ try {
295
+ if (!receiptSucceeded(await orAbort(handlers.waitForReceipt(txHash, transaction.chainId), stop.signal), txHash)) {
296
+ throw new Error("executeAndTrack: no successful receipt for the swap transaction itself, so RAVN's fee was not sent");
297
+ }
298
+ checkpoint();
299
+ feeTxHash = await handlers.sendTransaction(feeTx);
300
+ report("fee_sent", { feeTxHash, statusTxHash: proof(txHash, feeTxHash) });
301
+ // Confirmed before polling starts: a swap that fills first would otherwise end the
302
+ // polling on the one poll that had to prove the fee.
303
+ await orAbort(handlers.waitForReceipt(feeTxHash, feeTx.chainId), stop.signal);
304
+ }
305
+ catch (err) {
306
+ feeError = err;
307
+ }
308
+ }
309
+ }
310
+ // Stopped before or during the fee: say so, even with polling off (no fee was sent on it).
311
+ checkpoint();
312
+ const statusRef = execution.statusRef ?? txHash;
313
+ const statusTxHash = proof(txHash, feeTxHash);
314
+ if (feeTxHash && !statusTxHash) {
315
+ feeError ?? (feeError = new Error(`executeAndTrack: ${feeTxHash} is not an on-chain transaction hash, so RAVN can't prove the fee`));
316
+ }
317
+ partial.feeError = feeError;
318
+ return {
319
+ execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash: partial.approvalTxHash, txHash, statusRef, statusTxHash, feeTxHash, feeError,
320
+ finalStatus: await pollStatus(statusRef, statusTxHash, !!feeTxHash && !!statusTxHash),
321
+ };
322
+ }
323
+ if (execution.executionType === "SIGNATURE") {
324
+ if (!handlers.signTypedData) {
325
+ throw new Error("executeAndTrack: this execution requires signTypedData, none was provided");
326
+ }
327
+ if (!execution.typedData) {
328
+ throw new Error("executeAndTrack: SIGNATURE execution is missing typedData");
329
+ }
330
+ // A signed order isn't allowance-free: CoW's vault relayer and Bebop/0x's Permit2 still pull
331
+ // the sell token with transferFrom, so this on-chain allowance (separate from approvalData's
332
+ // gasless per-trade signature) must be confirmed before signing, otherwise the order is
333
+ // accepted and simply never fills, with no error to explain why.
334
+ const approvalTxHash = await maybeSendApproval(execution.approval);
335
+ checkpoint();
336
+ // Called as methods: a class-instance wallet needs its `this`.
337
+ const approvalSignature = execution.approvalData ? await handlers.signTypedData(execution.approvalData) : undefined;
338
+ checkpoint();
339
+ const signature = await handlers.signTypedData(execution.typedData);
340
+ checkpoint();
341
+ // From here the venue may take the order even when the call fails (BUILD_FAILED, no answer),
342
+ // so only a rate limit, refused before anything was placed, is retried.
343
+ sent = true;
344
+ report("swap_signed", { signature });
345
+ const { statusRef } = await withRetries(() => client.submitSignature({ quoteToken, signature, approvalSignature }), rateLimited);
346
+ report("swap_sent", { statusRef });
347
+ return { execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
59
348
  }
60
- if (!execution.typedData) {
61
- throw new Error("executeAndTrack: SIGNATURE execution is missing typedData");
349
+ // DEPOSIT: nothing to sign or send from here; the user moves funds to execution.deposit.address
350
+ // out-of-band, and this helper has no way to know when that happens. Returned without polling;
351
+ // start polling yourself (getStatus / useRavnStatus) once the deposit is actually sent. Pay
352
+ // `feePayment` as it says, pass an EVM fee transaction's hash as getStatus's `txHash`, and poll
353
+ // RAVN to a terminal status even if the venue's own tracker says delivered first.
354
+ // A fee it can't describe never stops the deposit, which fills without it.
355
+ checkpoint();
356
+ try {
357
+ return { execution, quoteToken, statusRef: execution.statusRef, feePayment: depositFeePayment(execution) };
62
358
  }
63
- // A signed order isn't allowance-free: CoW's vault relayer and Bebop/0x's Permit2 still pull
64
- // the sell token with transferFrom, so this on-chain allowance (separate from approvalData's
65
- // gasless per-trade signature) must be confirmed before signing, otherwise the order is
66
- // accepted and simply never fills, with no error to explain why.
67
- const approvalTxHash = await maybeSendApproval(execution.approval);
68
- let approvalSignature;
69
- if (execution.approvalData) {
70
- approvalSignature = await handlers.signTypedData(execution.approvalData);
359
+ catch (feeError) {
360
+ return { execution, quoteToken, statusRef: execution.statusRef, feeError };
71
361
  }
72
- const signature = await handlers.signTypedData(execution.typedData);
73
- const { statusRef } = await client.submitSignature({ quoteToken, signature, approvalSignature });
74
- return { execution, quoteToken, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
362
+ };
363
+ try {
364
+ return await run();
365
+ }
366
+ catch (err) {
367
+ if (stopCode) {
368
+ const why = stopCode === "ABORTED" ? "aborted" : `no terminal status within maxWaitMs (${maxWaitMs} ms)`;
369
+ throw new ExecuteAndTrackError(`executeAndTrack: ${why}`, stopCode, { ...partial }, stop.signal.reason);
370
+ }
371
+ if (!sent)
372
+ throw err;
373
+ const why = err instanceof Error ? err.message : String(err);
374
+ throw new ExecuteAndTrackError(`executeAndTrack: ${why}`, "FAILED", { ...partial }, err);
375
+ }
376
+ finally {
377
+ clearTimeout(deadline);
378
+ options.signal?.removeEventListener("abort", onAbort);
75
379
  }
76
- // DEPOSIT: nothing to sign or send from here; the user moves funds to execution.deposit.address
77
- // out-of-band, and this helper has no way to know when that happens. Returned without polling;
78
- // start polling yourself (getStatus / useRavnStatus) once the deposit is actually sent.
79
- return { execution, quoteToken, statusRef: execution.statusRef };
80
380
  }
package/dist/index.d.ts CHANGED
@@ -2,5 +2,5 @@ export { RavnClient } from "./client";
2
2
  export type { RavnClientConfig } from "./client";
3
3
  export * from "./types";
4
4
  export { isPollingTerminal } from "./polling";
5
- export { executeAndTrack } from "./execute-and-track";
6
- export type { ExecuteAndTrackHandlers, ExecuteAndTrackOptions, ExecuteAndTrackResult, } from "./execute-and-track";
5
+ export { depositFeePayment, executeAndTrack, ExecuteAndTrackError } from "./execute-and-track";
6
+ export type { ExecuteAndTrackHandlers, ExecuteAndTrackOptions, ExecuteAndTrackPartial, ExecuteAndTrackProgress, ExecuteAndTrackResult, ExecuteAndTrackStage, DepositFeePayment, TransactionReceiptLike, } from "./execute-and-track";
package/dist/index.js CHANGED
@@ -14,11 +14,13 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.executeAndTrack = exports.isPollingTerminal = exports.RavnClient = void 0;
17
+ exports.ExecuteAndTrackError = exports.executeAndTrack = exports.depositFeePayment = exports.isPollingTerminal = exports.RavnClient = void 0;
18
18
  var client_1 = require("./client");
19
19
  Object.defineProperty(exports, "RavnClient", { enumerable: true, get: function () { return client_1.RavnClient; } });
20
20
  __exportStar(require("./types"), exports);
21
21
  var polling_1 = require("./polling");
22
22
  Object.defineProperty(exports, "isPollingTerminal", { enumerable: true, get: function () { return polling_1.isPollingTerminal; } });
23
23
  var execute_and_track_1 = require("./execute-and-track");
24
+ Object.defineProperty(exports, "depositFeePayment", { enumerable: true, get: function () { return execute_and_track_1.depositFeePayment; } });
24
25
  Object.defineProperty(exports, "executeAndTrack", { enumerable: true, get: function () { return execute_and_track_1.executeAndTrack; } });
26
+ Object.defineProperty(exports, "ExecuteAndTrackError", { enumerable: true, get: function () { return execute_and_track_1.ExecuteAndTrackError; } });
package/dist/polling.d.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { SettlementStatus } from "./types";
2
2
  /**
3
- * Whether a status is done changing. "unknown" counts as terminal even though it isn't a real
4
- * outcome: it means the venue has no live tracker (see StatusDTO.tracking), so it will never
5
- * change and polling forever would just spin. Only "pending"/"processing" keep polling.
3
+ * Whether a status is done changing: "success", "refunded" and "failed", RAVN's own final states,
4
+ * plus "unknown", which isn't a real outcome but means the venue has no live tracker (see
5
+ * StatusDTO.tracking), so it will never change and polling forever would just spin. Everything
6
+ * else keeps polling: "expired" means the deal lapsed and a refund is due but hasn't landed
7
+ * (Across returns it in a later settlement bundle, hours on; poll it on a longer interval), and
8
+ * "not_found" that the venue has no record of this swap: brief right after sending (Relay indexes
9
+ * seconds behind the chain), otherwise permanent (the fill found isn't this quote's swap). RAVN
10
+ * keeps watching both; bound your own polling (executeAndTrack's maxWaitMs or signal).
6
11
  */
7
12
  export declare function isPollingTerminal(status: SettlementStatus): boolean;
package/dist/polling.js CHANGED
@@ -2,10 +2,15 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.isPollingTerminal = isPollingTerminal;
4
4
  /**
5
- * Whether a status is done changing. "unknown" counts as terminal even though it isn't a real
6
- * outcome: it means the venue has no live tracker (see StatusDTO.tracking), so it will never
7
- * change and polling forever would just spin. Only "pending"/"processing" keep polling.
5
+ * Whether a status is done changing: "success", "refunded" and "failed", RAVN's own final states,
6
+ * plus "unknown", which isn't a real outcome but means the venue has no live tracker (see
7
+ * StatusDTO.tracking), so it will never change and polling forever would just spin. Everything
8
+ * else keeps polling: "expired" means the deal lapsed and a refund is due but hasn't landed
9
+ * (Across returns it in a later settlement bundle, hours on; poll it on a longer interval), and
10
+ * "not_found" that the venue has no record of this swap: brief right after sending (Relay indexes
11
+ * seconds behind the chain), otherwise permanent (the fill found isn't this quote's swap). RAVN
12
+ * keeps watching both; bound your own polling (executeAndTrack's maxWaitMs or signal).
8
13
  */
9
14
  function isPollingTerminal(status) {
10
- return status !== "pending" && status !== "processing";
15
+ return status === "success" || status === "refunded" || status === "failed" || status === "unknown";
11
16
  }
package/dist/types.d.ts CHANGED
@@ -31,7 +31,12 @@ export interface QuoteDTO {
31
31
  amount: string;
32
32
  };
33
33
  /** `supported: false` means the venue has no fee mechanism at all (bps will be 0 either
34
- * way); distinct from `supported: true, bps: 0`, a real, intentional zero fee. */
34
+ * way); distinct from `supported: true, bps: 0`, a real, intentional zero fee. On NEAR Intents
35
+ * `bps`/`amount` include 1Click's own fee, taken from the input on every swap with or without
36
+ * yours (the output already reflects it): 20 bps with no fee set (1 bp when both tokens are
37
+ * USDC, USDT or DAI, or the same asset), with yours half of it plus the larger of that half and
38
+ * 20 (1) bps, and its 20 (1) bps on top of RAVN's own fee beside a private swap: NEAR never
39
+ * shows `bps: 0`. */
35
40
  fee: {
36
41
  bps: number;
37
42
  amount: string;
@@ -132,9 +137,20 @@ export interface ApprovalDTO {
132
137
  spender?: string;
133
138
  amount: string;
134
139
  unlimitedRecommended?: boolean;
140
+ /** Send this first and wait for it to be mined, then the approval: `approve(spender, 0)`. Only on
141
+ * a token whose approve reverts on a change from one non-zero allowance to another (Ethereum
142
+ * USDT) when the sender holds a partial one. executeAndTrack does this. */
143
+ reset?: {
144
+ to: string;
145
+ data: string;
146
+ value: string;
147
+ };
135
148
  }
136
149
  export type ExecutionDTO = {
137
150
  executionType: "TRANSACTION";
151
+ /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
152
+ * it. executeAndTrack broadcasts nothing for one. */
153
+ sandbox?: true;
138
154
  /** Pass this, not the quote's own token, to getStatus. Compose: the post-bind token. */
139
155
  quoteToken: string;
140
156
  /** The venue's own tracking key (compose hop-2 handle, Garden order id, Relay request id).
@@ -156,19 +172,25 @@ export type ExecutionDTO = {
156
172
  };
157
173
  /**
158
174
  * Only when your key sets a fee and the route has no fee of its own (Eco, Layerswap, Mayan's
159
- * routes paying no referrer): RAVN's fee, a transfer of the input token to RAVN;
160
- * `transaction` already leaves it out. Skip it and the swap still fills; no fee is owed to
161
- * you. With no `statusRef` here (you poll with the swap's own tx hash), a wallet that batches
162
- * atomically (EIP-5792 `wallet_sendCalls`, one transaction) may send `fee.transaction` in the
163
- * same batch as `transaction`, and the swap's tx proves it (an ERC-20 sell only: a native coin
164
- * batched leaves nothing to read). Otherwise — including a batch the wallet splits into
165
- * several transactions — send it as a transaction of its own from the user's regular wallet
166
- * once the swap has confirmed, and pass its hash as `txHash` to getStatus. A fee paid on a
167
- * swap the venue refunds stays with RAVN.
175
+ * routes paying no referrer) from an EVM chain: RAVN's fee, a transfer of the input token to
176
+ * RAVN; `transaction` already leaves it out. From Solana there is none to send: the fee is a
177
+ * transfer inside `transaction.serialized`. Skip it and the swap still fills; no fee is owed to
178
+ * you. A wallet that batches (EIP-5792 `wallet_sendCalls`) or relays (smart account, 7702,
179
+ * 4337) may send `fee.transaction` with `transaction` in one confirmation (one fee-bearing swap
180
+ * per transaction: a second fee in it isn't proven), for an ERC-20 sell
181
+ * (a native-coin fee must be a plain transaction of its own, so a contract wallet that can't
182
+ * send one — a Safe, a 4337 account — can't pay it). Pass the on-chain hash of the transaction
183
+ * that carried the fee (not an EIP-5792 batch id or a 4337 userOpHash) as `txHash` to getStatus
184
+ * — not needed with no `statusRef` here, when the fee rode in the swap's own transaction. Sent
185
+ * on its own, send it once the swap has confirmed. A fee paid on a swap the venue refunds
186
+ * stays with RAVN.
168
187
  */
169
188
  fee?: DepositFeeDTO;
170
189
  } | {
171
190
  executionType: "SIGNATURE";
191
+ /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
192
+ * it. executeAndTrack broadcasts nothing for one. */
193
+ sandbox?: true;
172
194
  /** Pass this, not the quote's own token, to submitSignature and getStatus. */
173
195
  quoteToken: string;
174
196
  statusRef?: string;
@@ -181,6 +203,9 @@ export type ExecutionDTO = {
181
203
  };
182
204
  } | {
183
205
  executionType: "DEPOSIT";
206
+ /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
207
+ * it. executeAndTrack broadcasts nothing for one. */
208
+ sandbox?: true;
184
209
  /** Pass this, not the quote's own token, to getStatus. */
185
210
  quoteToken: string;
186
211
  /** `expiresAt` (ISO 8601), when set, is when the deposit must have ARRIVED (confirmed on
@@ -194,9 +219,9 @@ export type ExecutionDTO = {
194
219
  /** Poll getStatus with this, not `deposit.address`. */
195
220
  statusRef: string;
196
221
  /** Only when your key sets a fee and the venue has no fee of its own (Rift; private swaps from
197
- * an EVM chain or Bitcoin). Skip it and the swap still fills on `deposit.amount`; no fee is
198
- * owed to you. A fee paid on a swap the venue then refunds stays with RAVN and nothing is owed
199
- * on it, so send an EVM fee payment once the deposit has confirmed. */
222
+ * an EVM chain, Bitcoin or Solana). Skip it and the swap still fills on `deposit.amount`; no
223
+ * fee is owed to you. A fee paid on a swap the venue then refunds stays with RAVN and nothing
224
+ * is owed on it, so send an EVM fee payment once the deposit has confirmed. */
200
225
  fee?: DepositFeeDTO;
201
226
  };
202
227
  /** RAVN's integrator fee beside a deposit (or a transaction), in the input token, paid by your user
@@ -207,12 +232,28 @@ export interface DepositFeeDTO {
207
232
  /** RAVN's fee address on the input chain. */
208
233
  address: string;
209
234
  chainId: number;
210
- /** EVM: send this transaction too, exactly as given and as a transaction of its own from the
211
- * user's regular wallet (it names the order, which ties the fee to this swap; bundled into a
212
- * smart-wallet batch it can't be proven, and nothing is owed on it), then pass its hash as
213
- * `txHash` to getStatus once it has confirmed (simplest: on every poll). Absent on Bitcoin: pay `amount` to `address`
214
- * as an extra output of the SAME transaction that pays the deposit; RAVN finds it there once
215
- * that transaction confirms. One deposit per Bitcoin transaction: a fee output proves one swap. */
235
+ /** EVM: send this transaction too, exactly as given (it carries a tag naming this swap): on its
236
+ * own, or in the same wallet batch or relay as the deposit or swap (EIP-5792, smart account, 7702,
237
+ * 4337) — an ERC-20 fee anywhere; a native-coin fee only beside a NEAR Intents deposit on
238
+ * Ethereum, Base or Polygon (proven from those chains' internal transfers). Any other native-coin
239
+ * fee must be a plain transaction of its own (a contract wallet that can't send one can't pay it).
240
+ * Then pass the on-chain hash of the transaction that carried it (not a batch id or a userOpHash)
241
+ * as `txHash` to getStatus once it has confirmed (simplest: on every poll). One fee-bearing swap
242
+ * per transaction: a batch carrying two fees, or a fee beside another fee-bearing swap, proves only
243
+ * one. Batch a token deposit, or a native-coin deposit to NEAR Intents (it credits coin a smart
244
+ * account moves); send any other native-coin deposit as a plain transfer, as an exchange may not
245
+ * credit coin a contract moved. Absent on Bitcoin:
246
+ * pay `amount` to `address` as an extra output of the SAME transaction that pays the deposit;
247
+ * RAVN finds it there once that transaction confirms. One deposit per Bitcoin transaction: a fee
248
+ * output proves one swap. Absent on Solana too: add the fee to the SAME transaction that pays the
249
+ * deposit exactly `deposit.amount` (SOL as lamports to the deposit wallet, not wrapped) — SOL: a
250
+ * System transfer of `amount` lamports to `address`; a token: `transferChecked` of `amount` to
251
+ * `address`'s associated token account for the input mint (under the mint's token program), after
252
+ * `createAssociatedTokenAccountIdempotent` in case RAVN has none for that token yet (its rent,
253
+ * ~0.0015 SOL once per token, is normally in the quote's network cost). RAVN finds it there once
254
+ * that transaction is finalized, among the deposit address's first and latest transactions; a fee
255
+ * sent in a transaction of its own isn't found. One deposit per Solana transaction: a fee proves
256
+ * one swap. It can't be sent after the deposit, so on a swap the venue refunds it stays with RAVN. */
216
257
  transaction?: {
217
258
  to: string;
218
259
  data: string;
@@ -249,8 +290,22 @@ export interface GetQuoteParams {
249
290
  * carry slippage.noMinimum: true and guaranteedMin: null; slippageBps does not protect them. */
250
291
  allowNoMinimum?: boolean;
251
292
  rankingMode?: "best_output" | "fastest";
293
+ /** The race's time limit in ms, counted from its start: once it has passed and some venue has
294
+ * priced the swap, slower venues are left out (with none priced yet, the first price ends the
295
+ * wait; a quote with slippage.noMinimum counts only 2000 ms past the limit). A positive integer;
296
+ * default 6000, clamped to 1000–15000. An API that predates it ignores it. */
297
+ timeoutMs?: number;
252
298
  /** Drop specific venues from this race, e.g. a risk objection to one of them. */
253
299
  excludeVenues?: string[];
300
+ /** Race only these venues, e.g. ["bebop"] for an RFQ-only race. Every other venue is left
301
+ * out, including ones RAVN adds later; excludeVenues still applies on top. Matches the quote's
302
+ * venue.id, the venue you deal with, not every venue funds pass through: RAVN's multi-step
303
+ * routes (compose, relay quotes with hops) and venues such as Rift, Across (swapping before or
304
+ * after bridging) and Mayan (a Solana order starting in another token swaps through Jupiter
305
+ * first) route through others. An empty list is
306
+ * rejected (INVALID_REQUEST); if none of them can quote, NO_LIQUIDITY lists every left-out
307
+ * venue in error.details.excludedVenues. */
308
+ onlyVenues?: string[];
254
309
  /** Settle privately: the race narrows to NEAR Intents (Confidential Intents) and Houdini Swap,
255
310
  * and NO_LIQUIDITY replaces a public fill if neither can quote. Houdini is sent the request's IP,
256
311
  * user-agent and clientTimezone for compliance screening; its terms bar US persons and
@@ -295,10 +350,25 @@ export interface ErrorMeta {
295
350
  requestId: string;
296
351
  version: string;
297
352
  }
298
- /** Thrown for every non-2xx response. `code` is the stable, machine-readable string from the API. */
353
+ /** Every `code` the API sends (see the API's Errors page), plus NETWORK_ERROR: no response came
354
+ * back at all (offline, DNS, CORS, a dropped connection). New codes can appear: compare against
355
+ * these, don't switch on them exhaustively. */
356
+ export type RavnErrorCode = "INVALID_REQUEST" | "UNSUPPORTED_TOKEN" | "UNSUPPORTED_CHAIN" | "NO_LIQUIDITY" | "QUOTE_EXPIRED" | "QUOTE_INVALID" | "QUOTE_NOT_EXECUTABLE" | "RATE_LIMITED" | "UNAUTHORIZED" | "NOT_CONFIGURED" | "NOT_FOUND" | "BUILD_FAILED" | "VENUE_RATE_LIMITED" | "ORDER_REJECTED" | "INTERNAL" | "NETWORK_ERROR";
357
+ /** Thrown for every failed call: a non-2xx response, or none at all (NETWORK_ERROR). `code` is the
358
+ * stable, machine-readable string from the API. */
299
359
  export declare class RavnApiError extends Error {
300
- readonly code: string;
360
+ readonly code: RavnErrorCode | (string & {});
301
361
  readonly details: unknown;
302
362
  readonly meta: ErrorMeta | undefined;
303
- constructor(code: string, message: string, details: unknown, meta: ErrorMeta | undefined);
363
+ /** The HTTP status; undefined when no response came back (NETWORK_ERROR). */
364
+ readonly status?: number;
365
+ /** Seconds the API asked you to wait before trying the same call again (`details.retryAfterSec`,
366
+ * sent with RATE_LIMITED and VENUE_RATE_LIMITED). */
367
+ readonly retryAfterSec?: number;
368
+ /** NETWORK_ERROR: the fetch error underneath. */
369
+ readonly cause?: unknown;
370
+ constructor(code: RavnErrorCode | (string & {}), message: string, details: unknown, meta: ErrorMeta | undefined, init?: {
371
+ status?: number;
372
+ cause?: unknown;
373
+ });
304
374
  }
package/dist/types.js CHANGED
@@ -8,14 +8,20 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.RavnApiError = void 0;
11
- /** Thrown for every non-2xx response. `code` is the stable, machine-readable string from the API. */
11
+ /** Thrown for every failed call: a non-2xx response, or none at all (NETWORK_ERROR). `code` is the
12
+ * stable, machine-readable string from the API. */
12
13
  class RavnApiError extends Error {
13
- constructor(code, message, details, meta) {
14
+ constructor(code, message, details, meta, init = {}) {
14
15
  super(message);
15
16
  this.code = code;
16
17
  this.details = details;
17
18
  this.meta = meta;
18
19
  this.name = "RavnApiError";
20
+ this.status = init.status;
21
+ const retryAfterSec = details?.retryAfterSec;
22
+ this.retryAfterSec = typeof retryAfterSec === "number" ? retryAfterSec : undefined;
23
+ if (init.cause !== undefined)
24
+ this.cause = init.cause;
19
25
  }
20
26
  }
21
27
  exports.RavnApiError = RavnApiError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravnexchange/sdk",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
4
4
  "description": "Typed client for the RAVN integrator API (/api/v1): quote, execute, submit-signature, status.",
5
5
  "keywords": ["ravn", "cross-chain", "swap", "swap-api", "dex-aggregator", "bitcoin", "native-btc", "solana", "evm", "crypto", "defi", "typescript", "sdk", "api-client"],
6
6
  "main": "./dist/index.js",