@ravnexchange/sdk 0.1.15 → 0.2.1

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
@@ -17,7 +17,7 @@ export declare class RavnClient {
17
17
  /** POST /v1/quote: omitting destinationAddress/refundAddress returns a preview-only quote (see QuoteDTO.executable). */
18
18
  getQuote(params: GetQuoteParams): Promise<QuoteDTO>;
19
19
  /** POST /v1/execute: branch on the returned executionType (TRANSACTION / SIGNATURE / DEPOSIT). */
20
- execute(params: ExecuteParams): Promise<ExecutionDTO>;
20
+ execute({ idempotencyKey, ...params }: ExecuteParams): Promise<ExecutionDTO>;
21
21
  /** POST /v1/submit-signature: for SIGNATURE-type executions only. Returns the ref to poll getStatus with. */
22
22
  submitSignature(params: SubmitSignatureParams): Promise<{
23
23
  statusRef: string;
@@ -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
  }
@@ -44,8 +57,16 @@ class RavnClient {
44
57
  return this.request("/quote", { method: "POST", body: JSON.stringify(params) });
45
58
  }
46
59
  /** POST /v1/execute: branch on the returned executionType (TRANSACTION / SIGNATURE / DEPOSIT). */
47
- execute(params) {
48
- return this.request("/execute", { method: "POST", body: JSON.stringify(params) });
60
+ execute({ idempotencyKey, ...params }) {
61
+ // The API's own rule: anything else fetch refuses to send (a reported NETWORK_ERROR) or RAVN answers 400.
62
+ if (idempotencyKey !== undefined && !/^[\x21-\x7e]{1,255}$/.test(idempotencyKey)) {
63
+ return Promise.reject(new types_1.RavnApiError("INVALID_REQUEST", "idempotencyKey must be 1–255 printable ASCII characters, no spaces", undefined, undefined));
64
+ }
65
+ return this.request("/execute", {
66
+ method: "POST",
67
+ body: JSON.stringify(params),
68
+ ...(idempotencyKey ? { headers: { "Idempotency-Key": idempotencyKey } } : {}),
69
+ });
49
70
  }
50
71
  /** POST /v1/submit-signature: for SIGNATURE-type executions only. Returns the ref to poll getStatus with. */
51
72
  submitSignature(params) {
@@ -67,11 +88,12 @@ class RavnClient {
67
88
  * the execution's statusRef, submitSignature's statusRef, or the origin tx hash. POST, not GET:
68
89
  * a 2-hop quoteToken in a query string can exceed header limits and fail with 431. `txHash`:
69
90
  * 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) {
91
+ * for; it's how RAVN proves the fee your share is paid from. `signal` aborts the request. */
92
+ getStatus(quoteToken, ref, txHash, options = {}) {
72
93
  return this.request("/status", {
73
94
  method: "POST",
74
95
  body: JSON.stringify({ quoteToken, ref, ...(txHash ? { txHash } : {}) }),
96
+ signal: options.signal,
75
97
  });
76
98
  }
77
99
  /** GET /v1/tokens: RAVN's listed token registry for one chain. Not a per-pair routability guarantee. */
@@ -7,27 +7,41 @@ 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
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. Also called on the swap before RAVN's fee is sent on its own
17
- * (`fee.transaction`): resolve to the receipt itself (viem's `waitForTransactionReceipt`,
18
- * ethers' `wait()`), as the fee is only sent when it says success AND is this hash's own (a
19
- * cancelled or replaced swap resolves with another transaction's). Resolving to nothing, or
20
- * anything else, sends no fee: a fee on a swap that didn't run is the user's money lost.
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.
21
33
  */
22
- waitForReceipt(hash: string, chainId?: number): Promise<unknown>;
34
+ waitForReceipt(hash: string, chainId?: number): Promise<TransactionReceiptLike | null>;
23
35
  /**
24
36
  * Optional: send several calls in ONE wallet confirmation that succeeds or fails as a whole
25
37
  * (EIP-5792 `wallet_sendCalls` with `atomicRequired: true`, a smart account) and resolve, once
26
38
  * they have landed, to the on-chain hash of the transaction that carried each call, in order
27
- * (the same hash repeated for one transaction). Not a batch id or a 4337 userOpHash: RAVN
28
- * proves its fee from that transaction. When given, a token-paid RAVN fee rides in the swap's
29
- * own confirmation; a native-coin fee is always a plain transaction of its own. Reject only when
30
- * nothing was sent: a rejection loses the swap's hash.
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.
31
45
  */
32
46
  sendCalls?(txs: TxLike[]): Promise<string[]>;
33
47
  /** Sign an EIP-712 typed-data payload. Only called for SIGNATURE executions. */
@@ -47,14 +61,42 @@ export interface ExecuteAndTrackHandlers {
47
61
  */
48
62
  hasAllowance?(approval: ApprovalDTO): Promise<boolean>;
49
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
+ }
50
74
  export interface ExecuteAndTrackOptions {
51
75
  /** Default true. Set false to get a ref back once the swap (and RAVN's fee, when one is sent) is
52
76
  * out, and poll getStatus yourself with `statusTxHash`. */
53
77
  pollUntilTerminal?: boolean;
78
+ /** Default 4000. Also where the backoff after a failed call starts (doubling, up to a minute). */
54
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;
55
95
  }
56
96
  export interface ExecuteAndTrackResult {
57
97
  execution: ExecutionDTO;
98
+ /** The `approval.reset` (approve to 0) sent before the approval, when the token needed one. */
99
+ resetTxHash?: string;
58
100
  approvalTxHash?: string;
59
101
  txHash?: string;
60
102
  /** Pass to client.getStatus(quoteToken, statusRef). Same as `execution.quoteToken`, NOT the
@@ -81,6 +123,34 @@ export interface ExecuteAndTrackResult {
81
123
  /** Set once pollUntilTerminal reaches a terminal status. Absent for DEPOSIT (see below) or when polling is turned off. */
82
124
  finalStatus?: StatusDTO;
83
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
+ }
84
154
  /** How to pay a DEPOSIT's `fee`, so it's proven against this swap. See DepositFeeDTO. Either way,
85
155
  * poll RAVN's getStatus until a terminal status even when the venue's own tracker (e.g. Rift's API)
86
156
  * already says delivered: RAVN books the fee on its own status polls. */
@@ -118,7 +188,12 @@ export declare function depositFeePayment(execution: ExecutionDTO): DepositFeePa
118
188
  * payment (if any) → status polling, using caller-supplied signing/sending functions. The SDK
119
189
  * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
120
190
  * own docs flag as the most common integration failure: sending the main tx before the approval
121
- * has confirmed.
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, sent with an Idempotency-Key, also when its
193
+ * answer was lost or is still being built, up to 4 tries; submitSignature only when rate limited),
194
+ * waiting the API's retryAfterSec. Once anything
195
+ * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
196
+ * each one first.
122
197
  */
123
198
  export declare function executeAndTrack(client: RavnClient, params: ExecuteParams, handlers: ExecuteAndTrackHandlers, options?: ExecuteAndTrackOptions): Promise<ExecuteAndTrackResult>;
124
199
  export {};
@@ -1,11 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExecuteAndTrackError = void 0;
3
4
  exports.depositFeePayment = depositFeePayment;
4
5
  exports.executeAndTrack = executeAndTrack;
6
+ const types_1 = require("./types");
5
7
  const polling_1 = require("./polling");
8
+ /**
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;
6
28
  const BITCOIN_CHAIN_ID = -1;
7
29
  const SOLANA_CHAIN_ID = -2;
8
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
+ // An execute asked again with its Idempotency-Key: ~30 s at the default interval, so a call that
39
+ // can't get through fails in about that rather than four minutes.
40
+ const EXECUTE_ATTEMPTS = 4;
9
41
  /** The fee payment a DEPOSIT execution asks for beside its deposit; undefined when it has none. */
10
42
  function depositFeePayment(execution) {
11
43
  if (execution.executionType !== "DEPOSIT" || !execution.fee)
@@ -21,145 +53,339 @@ function depositFeePayment(execution) {
21
53
  }
22
54
  // Only a receipt that says success (viem "success", ethers 1; web3.js 1n/true, so == on purpose) and
23
55
  // is `hash`'s own (viem/ethers v5 transactionHash, ethers v6 hash): viem resolves a cancelled swap
24
- // with the replacement's successful receipt.
56
+ // with the replacement's successful receipt. Typed unknown: JS callers can still hand it anything.
25
57
  const receiptSucceeded = (receipt, hash) => {
26
58
  const r = receipt;
27
59
  const own = r?.transactionHash ?? r?.hash;
28
60
  return (r?.status === "success" || r?.status == 1) && (typeof own !== "string" || own.toLowerCase() === hash.toLowerCase());
29
61
  };
62
+ // The same call can succeed later: no response at all, a 429, or a 5xx.
63
+ const transient = (err) => err instanceof types_1.RavnApiError && (err.code === "NETWORK_ERROR" || err.status === 429 || (err.status ?? 0) >= 500);
64
+ // Refused before anything was done, so safe to repeat even a call that isn't safe to repeat after
65
+ // a BUILD_FAILED (submit-signature: the venue may have taken the order).
66
+ const rateLimited = (err) => err instanceof types_1.RavnApiError && (err.code === "RATE_LIMITED" || err.code === "VENUE_RATE_LIMITED" || err.status === 429);
67
+ // An execute sent with an Idempotency-Key answers a repeat with its first answer, so one whose answer
68
+ // was lost (no response, a gateway timeout) or is still being built (UNAVAILABLE) can be asked again.
69
+ // Never a BUILD_FAILED (re-quote) or an unexpected error, which may have half-built an order.
70
+ const answerLost = (err) => rateLimited(err) ||
71
+ (err instanceof types_1.RavnApiError && (err.code === "NETWORK_ERROR" || err.code === "UNAVAILABLE" || err.status === 504));
72
+ // `promise`, or the signal's reason as soon as it aborts: a wallet or custom fetch may ignore the
73
+ // signal. `promise` is always handled, so one that rejects after the abort is never unhandled.
74
+ const orAbort = (promise, signal) => new Promise((resolve, reject) => {
75
+ const onAbort = () => reject(signal.reason);
76
+ // Promise.resolve: a JS handler may hand back the value itself rather than a promise.
77
+ Promise.resolve(promise).then((value) => {
78
+ signal.removeEventListener("abort", onAbort);
79
+ resolve(value);
80
+ }, (err) => {
81
+ signal.removeEventListener("abort", onAbort);
82
+ reject(err);
83
+ });
84
+ if (signal.aborted)
85
+ return onAbort();
86
+ signal.addEventListener("abort", onAbort, { once: true });
87
+ });
88
+ const sleep = (ms, signal) => new Promise((resolve, reject) => {
89
+ if (signal.aborted)
90
+ return reject(signal.reason);
91
+ const onAbort = () => {
92
+ clearTimeout(timer);
93
+ reject(signal.reason);
94
+ };
95
+ const timer = setTimeout(() => {
96
+ signal.removeEventListener("abort", onAbort);
97
+ resolve();
98
+ }, ms);
99
+ signal.addEventListener("abort", onAbort, { once: true });
100
+ });
30
101
  /**
31
102
  * Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → RAVN's fee
32
103
  * payment (if any) → status polling, using caller-supplied signing/sending functions. The SDK
33
104
  * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
34
105
  * own docs flag as the most common integration failure: sending the main tx before the approval
35
- * has confirmed.
106
+ * has confirmed. A failed API call is retried where repeating it is safe (status polls and
107
+ * submitTransaction on a 429, 5xx or no response; execute, sent with an Idempotency-Key, also when its
108
+ * answer was lost or is still being built, up to 4 tries; submitSignature only when rate limited),
109
+ * waiting the API's retryAfterSec. Once anything
110
+ * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
111
+ * each one first.
36
112
  */
37
113
  async function executeAndTrack(client, params, handlers, options = {}) {
38
- const { pollUntilTerminal = true, pollIntervalMs = 4000 } = options;
39
- const execution = await client.execute(params);
40
- const { quoteToken } = execution;
41
- const pollStatus = async (ref, txHash, feeSent = false) => {
42
- if (!pollUntilTerminal)
43
- return undefined;
44
- // A success answered before RAVN's node sees the fee tx proves nothing, and the row is then
45
- // left to a cron that books no fee: one more poll after it.
46
- // ponytail: one re-poll covers a block of lag; the real fix is /v1/status staying non-terminal
47
- // while a given fee tx is unproven.
48
- for (let recheck = feeSent;;) {
49
- const status = await client.getStatus(quoteToken, ref, txHash);
50
- if ((0, polling_1.isPollingTerminal)(status.status)) {
51
- if (!recheck || status.status !== "success")
52
- return status;
53
- recheck = false;
54
- }
55
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
114
+ const { pollUntilTerminal = true, pollIntervalMs = 4000, expiredPollIntervalMs = EXPIRED_POLL_INTERVAL_MS, maxWaitMs } = options;
115
+ const partial = {};
116
+ let sent = false;
117
+ const report = (stage, fields) => {
118
+ Object.assign(partial, fields);
119
+ try {
120
+ // An async hook (a database write) may reject: handled here too, never unhandled.
121
+ Promise.resolve(options.onProgress?.({ stage, partial: { ...partial } })).catch(() => { });
122
+ }
123
+ catch {
124
+ // The swap is out either way: a failing progress hook must not stop its fee or tracking.
56
125
  }
57
126
  };
58
- // Sends and confirms an approval unless the caller already knows it's covered; see
59
- // ExecuteAndTrackHandlers.hasAllowance's doc comment for why the API can't decide this itself.
60
- const maybeSendApproval = async (approval) => {
61
- if (!approval)
62
- return undefined;
63
- if (await handlers.hasAllowance?.(approval))
64
- return undefined;
65
- const hash = await handlers.sendTransaction(approval);
66
- await handlers.waitForReceipt(hash, approval.chainId);
67
- return hash;
127
+ // One signal for everything that waits: the caller's abort, or maxWaitMs once polling starts.
128
+ const stop = new AbortController();
129
+ let stopCode;
130
+ const halt = (code, reason) => {
131
+ if (stop.signal.aborted)
132
+ return;
133
+ stopCode = code;
134
+ stop.abort(reason);
68
135
  };
69
- if (execution.executionType === "TRANSACTION" && execution.submit) {
70
- // Never broadcast this one ourselves: a payment Layerswap can't match sits at Layerswap, and
71
- // RAVN's check before sending is what catches it.
72
- if (!handlers.signSolanaTransaction) {
73
- throw new Error("executeAndTrack: this execution must be submitted through RAVN and requires signSolanaTransaction, none was provided");
74
- }
75
- if (!execution.transaction.serialized) {
76
- throw new Error("executeAndTrack: TRANSACTION execution with submit is missing transaction.serialized");
136
+ const onAbort = () => halt("ABORTED", options.signal?.reason);
137
+ if (options.signal?.aborted)
138
+ onAbort();
139
+ options.signal?.addEventListener("abort", onAbort, { once: true });
140
+ let deadline;
141
+ // Before each step that asks the wallet for something: never send once stopped.
142
+ const checkpoint = () => {
143
+ if (stop.signal.aborted)
144
+ throw stop.signal.reason;
145
+ };
146
+ // Calls `call` until it succeeds, fails in a way `retryable` says repeating can't fix, or
147
+ // `attempts`: waiting the API's retryAfterSec, else an interval doubling from pollIntervalMs.
148
+ const withRetries = async (call, retryable, attempts = MAX_ATTEMPTS) => {
149
+ for (let attempt = 1;; attempt++) {
150
+ try {
151
+ return await call();
152
+ }
153
+ catch (err) {
154
+ if (attempt >= attempts || !retryable(err))
155
+ throw err;
156
+ const askedMs = (err.retryAfterSec ?? 0) * 1000;
157
+ await sleep(Math.max(askedMs, Math.min(pollIntervalMs * 2 ** (attempt - 1), MAX_BACKOFF_MS)), stop.signal);
158
+ }
77
159
  }
78
- const signedTransaction = await handlers.signSolanaTransaction(execution.transaction.serialized);
79
- const { txHash } = await client.submitTransaction({ quoteToken, signedTransaction });
80
- const statusRef = execution.statusRef ?? txHash;
81
- return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
82
- }
83
- if (execution.executionType === "TRANSACTION") {
84
- const approvalTxHash = await maybeSendApproval(execution.approval);
85
- // RAVN's fee, when the route has none of its own (Eco, Layerswap, Mayan plain MCTP). From
86
- // Solana there's nothing to send: it's a transfer inside transaction.serialized.
87
- const feeTx = execution.fee?.transaction;
88
- let txHash;
89
- let feeTxHash;
90
- let feeError;
91
- if (feeTx && BigInt(feeTx.value) === 0n && handlers.sendCalls) {
92
- // A token fee may ride in the swap's own confirmation: atomic, so it never outlives a swap
93
- // that reverted. One hash back means one transaction carried both.
94
- const [swapHash, feeHash = swapHash] = await handlers.sendCalls([execution.transaction, feeTx]);
95
- if (!swapHash)
96
- throw new Error("executeAndTrack: sendCalls returned no transaction hash");
97
- txHash = swapHash;
98
- feeTxHash = feeHash;
160
+ };
161
+ const run = async () => {
162
+ checkpoint();
163
+ // One Idempotency-Key for every try, so a repeat can't open a second order at the venue (a NEAR
164
+ // deposit address, a Houdini or Garden order). Without one (no crypto.randomUUID), only a rate
165
+ // limit is repeated. A BUILD_FAILED wants a fresh quote either way.
166
+ const key = params.idempotencyKey ?? globalThis.crypto?.randomUUID?.();
167
+ const execution = await withRetries(() => orAbort(client.execute({ ...params, ...(key ? { idempotencyKey: key } : {}) }), stop.signal), key ? answerLost : rateLimited, key ? EXECUTE_ATTEMPTS : MAX_ATTEMPTS);
168
+ const { quoteToken } = execution;
169
+ Object.assign(partial, { execution, quoteToken, statusRef: execution.statusRef });
170
+ const pollStatus = async (ref, txHash, feeSent = false) => {
171
+ if (!pollUntilTerminal)
172
+ return undefined;
173
+ if (maxWaitMs !== undefined) {
174
+ deadline = setTimeout(() => halt("TIMEOUT", new Error(`executeAndTrack: no terminal status within maxWaitMs (${maxWaitMs} ms)`)), maxWaitMs);
175
+ }
176
+ // A success answered before RAVN's node sees the fee tx proves nothing, and the row is then
177
+ // left to a cron that books no fee: one more poll after it.
178
+ // ponytail: one re-poll covers a block of lag; /v1/status also stays "processing" while a
179
+ // reported fee tx is unreadable (ravn-mvp #411).
180
+ for (let recheck = feeSent;;) {
181
+ checkpoint();
182
+ const status = await withRetries(() => orAbort(client.getStatus(quoteToken, ref, txHash, { signal: stop.signal }), stop.signal), transient);
183
+ report("status", { lastStatus: status });
184
+ if ((0, polling_1.isPollingTerminal)(status.status)) {
185
+ if (!recheck || status.status !== "success")
186
+ return status;
187
+ recheck = false;
188
+ }
189
+ await sleep(status.status === "expired" ? expiredPollIntervalMs : pollIntervalMs, stop.signal);
190
+ }
191
+ };
192
+ // A sandbox execution is never real, but a venue with no sandbox stand-in returns its live
193
+ // payload: nothing in it is signed or sent (a signed order or Solana payment is a live
194
+ // instrument even though RAVN drops it). RAVN answers its status synthetically, for any ref.
195
+ if (execution.sandbox && execution.executionType !== "DEPOSIT") {
196
+ const statusRef = execution.statusRef ?? SANDBOX_REF;
197
+ return { execution, quoteToken, statusRef, finalStatus: await pollStatus(statusRef) };
99
198
  }
100
- else {
101
- txHash = await handlers.sendTransaction(execution.transaction);
102
- if (feeTx) {
103
- // On its own, only once the swap has confirmed: a reverted, dropped or replaced one pays
104
- // no fee. Declined or failed, the swap still runs and is tracked; no fee is owed on it.
199
+ // The approval to send, if any: none when the caller already knows it's covered (see
200
+ // ExecuteAndTrackHandlers.hasAllowance).
201
+ const neededApproval = async (approval) => approval && !(handlers.hasAllowance && (await orAbort(handlers.hasAllowance(approval), stop.signal))) ? approval : undefined;
202
+ // Sends and confirms it, after its reset (approve to 0) when the token needs one first.
203
+ const maybeSendApproval = async (approval) => {
204
+ const needed = await neededApproval(approval);
205
+ if (!needed)
206
+ return undefined;
207
+ let hash = "";
208
+ for (const tx of needed.reset ? [{ ...needed.reset, chainId: needed.chainId }, needed] : [needed]) {
209
+ checkpoint();
210
+ hash = await handlers.sendTransaction(tx);
211
+ sent = true;
212
+ if (tx === needed)
213
+ report("approval_sent", { approvalTxHash: hash });
214
+ else
215
+ report("reset_sent", { resetTxHash: hash });
216
+ await orAbort(handlers.waitForReceipt(hash, needed.chainId), stop.signal);
217
+ }
218
+ return hash;
219
+ };
220
+ if (execution.executionType === "TRANSACTION" && execution.submit) {
221
+ // Never broadcast this one ourselves: a payment Layerswap can't match sits at Layerswap, and
222
+ // RAVN's check before sending is what catches it.
223
+ if (!handlers.signSolanaTransaction) {
224
+ throw new Error("executeAndTrack: this execution must be submitted through RAVN and requires signSolanaTransaction, none was provided");
225
+ }
226
+ if (!execution.transaction.serialized) {
227
+ throw new Error("executeAndTrack: TRANSACTION execution with submit is missing transaction.serialized");
228
+ }
229
+ const serialized = execution.transaction.serialized;
230
+ checkpoint();
231
+ const signedTransaction = await handlers.signSolanaTransaction(serialized);
232
+ checkpoint();
233
+ // From here RAVN may have sent it, even on a failed call. A retry with the same signed
234
+ // transaction never sends it twice, so retrying is how to find out; a failure that names
235
+ // the signature (BUILD_FAILED's details.txHash) is kept too.
236
+ sent = true;
237
+ report("swap_signed", { signedTransaction });
238
+ const submit = async () => {
105
239
  try {
106
- if (!receiptSucceeded(await handlers.waitForReceipt(txHash, execution.transaction.chainId), txHash)) {
107
- throw new Error("executeAndTrack: no successful receipt for the swap transaction itself, so RAVN's fee was not sent");
108
- }
109
- feeTxHash = await handlers.sendTransaction(feeTx);
110
- // Confirmed before polling starts: a swap that fills first would otherwise end the
111
- // polling on the one poll that had to prove the fee.
112
- await handlers.waitForReceipt(feeTxHash, feeTx.chainId);
240
+ return await client.submitTransaction({ quoteToken, signedTransaction });
113
241
  }
114
242
  catch (err) {
115
- feeError = err;
243
+ const named = err instanceof types_1.RavnApiError ? err.details?.txHash : undefined;
244
+ if (typeof named === "string")
245
+ Object.assign(partial, { txHash: named, statusRef: execution.statusRef ?? named });
246
+ throw err;
116
247
  }
117
- }
248
+ };
249
+ const { txHash } = await withRetries(submit, transient);
250
+ const statusRef = execution.statusRef ?? txHash;
251
+ report("swap_sent", { txHash, statusRef });
252
+ return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
118
253
  }
119
- const statusRef = execution.statusRef ?? txHash;
120
- // What proves the fee, from the first poll on: its own transaction, else (a statusRef: a
121
- // two-hop route) hop 1's. Only an EVM tx hash: /v1/status refuses anything else (a Solana
122
- // signature, a batch id) on every poll.
123
- const proof = feeTxHash ?? (execution.statusRef ? txHash : undefined);
124
- const statusTxHash = proof && EVM_TX_HASH.test(proof) ? proof : undefined;
125
- if (feeTxHash && !statusTxHash) {
126
- feeError ?? (feeError = new Error(`executeAndTrack: ${feeTxHash} is not an on-chain transaction hash, so RAVN can't prove the fee`));
254
+ if (execution.executionType === "TRANSACTION") {
255
+ // RAVN's fee, when the route has none of its own (Eco, Layerswap, Mayan plain MCTP). From
256
+ // Solana there's nothing to send: it's a transfer inside transaction.serialized.
257
+ const { approval, transaction } = execution;
258
+ const feeTx = execution.fee?.transaction;
259
+ // What proves the fee, from the first poll on: its own transaction, else (a statusRef: a
260
+ // two-hop route) hop 1's. Only an EVM tx hash: /v1/status refuses anything else (a Solana
261
+ // signature, a batch id) on every poll.
262
+ const proof = (swapHash, feeHash) => {
263
+ const hash = feeHash ?? (execution.statusRef ? swapHash : undefined);
264
+ return hash && EVM_TX_HASH.test(hash) ? hash : undefined;
265
+ };
266
+ let txHash;
267
+ let feeTxHash;
268
+ let feeError;
269
+ if (feeTx && BigInt(feeTx.value) === 0n && handlers.sendCalls) {
270
+ // A token fee may ride in the swap's own confirmation, and the swap's approval with it:
271
+ // atomic, so neither outlives a swap that reverted. Only here: ExecutionDTO doesn't name
272
+ // its venue, and the first-party app never batches THORChain (Bifrost ignores a router
273
+ // deposit whose tx.to is the user's own 7702 account) or Bebop (its settlement needs gas
274
+ // headroom a batch leaves to the wallet); fee-beside routes are neither.
275
+ const batchApproval = await neededApproval(approval);
276
+ const reset = batchApproval?.reset && { ...batchApproval.reset, chainId: batchApproval.chainId };
277
+ const approvals = batchApproval ? [...(reset ? [reset] : []), batchApproval] : [];
278
+ checkpoint();
279
+ const hashes = await handlers.sendCalls([...approvals, transaction, feeTx]);
280
+ // The wallet resolved, so something may be out: from here an error carries what's known.
281
+ sent = true;
282
+ // Fewer hashes back than calls: the last one carried the rest.
283
+ const carrier = (i) => hashes[Math.min(i, hashes.length - 1)];
284
+ if (!carrier(0))
285
+ throw new Error("executeAndTrack: sendCalls returned no transaction hash");
286
+ txHash = carrier(approvals.length);
287
+ feeTxHash = carrier(approvals.length + 1);
288
+ if (reset)
289
+ report("reset_sent", { resetTxHash: carrier(0) });
290
+ if (batchApproval)
291
+ report("approval_sent", { approvalTxHash: carrier(approvals.length - 1) });
292
+ report("swap_sent", { txHash, statusRef: execution.statusRef ?? txHash, statusTxHash: proof(txHash) });
293
+ report("fee_sent", { feeTxHash, statusTxHash: proof(txHash, feeTxHash) });
294
+ }
295
+ else {
296
+ await maybeSendApproval(approval);
297
+ checkpoint();
298
+ txHash = await handlers.sendTransaction(transaction);
299
+ sent = true;
300
+ report("swap_sent", { txHash, statusRef: execution.statusRef ?? txHash, statusTxHash: proof(txHash) });
301
+ if (feeTx) {
302
+ // On its own, only once the swap has confirmed: a reverted, dropped or replaced one pays
303
+ // no fee. Declined or failed (or stopped first), the swap still runs and is tracked; no
304
+ // fee is owed on it.
305
+ try {
306
+ if (!receiptSucceeded(await orAbort(handlers.waitForReceipt(txHash, transaction.chainId), stop.signal), txHash)) {
307
+ throw new Error("executeAndTrack: no successful receipt for the swap transaction itself, so RAVN's fee was not sent");
308
+ }
309
+ checkpoint();
310
+ feeTxHash = await handlers.sendTransaction(feeTx);
311
+ report("fee_sent", { feeTxHash, statusTxHash: proof(txHash, feeTxHash) });
312
+ // Confirmed before polling starts: a swap that fills first would otherwise end the
313
+ // polling on the one poll that had to prove the fee.
314
+ await orAbort(handlers.waitForReceipt(feeTxHash, feeTx.chainId), stop.signal);
315
+ }
316
+ catch (err) {
317
+ feeError = err;
318
+ }
319
+ }
320
+ }
321
+ // Stopped before or during the fee: say so, even with polling off (no fee was sent on it).
322
+ checkpoint();
323
+ const statusRef = execution.statusRef ?? txHash;
324
+ const statusTxHash = proof(txHash, feeTxHash);
325
+ if (feeTxHash && !statusTxHash) {
326
+ feeError ?? (feeError = new Error(`executeAndTrack: ${feeTxHash} is not an on-chain transaction hash, so RAVN can't prove the fee`));
327
+ }
328
+ partial.feeError = feeError;
329
+ return {
330
+ execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash: partial.approvalTxHash, txHash, statusRef, statusTxHash, feeTxHash, feeError,
331
+ finalStatus: await pollStatus(statusRef, statusTxHash, !!feeTxHash && !!statusTxHash),
332
+ };
127
333
  }
128
- return {
129
- execution, quoteToken, approvalTxHash, txHash, statusRef, statusTxHash, feeTxHash, feeError,
130
- finalStatus: await pollStatus(statusRef, statusTxHash, !!feeTxHash && !!statusTxHash),
131
- };
132
- }
133
- if (execution.executionType === "SIGNATURE") {
134
- if (!handlers.signTypedData) {
135
- throw new Error("executeAndTrack: this execution requires signTypedData, none was provided");
334
+ if (execution.executionType === "SIGNATURE") {
335
+ if (!handlers.signTypedData) {
336
+ throw new Error("executeAndTrack: this execution requires signTypedData, none was provided");
337
+ }
338
+ if (!execution.typedData) {
339
+ throw new Error("executeAndTrack: SIGNATURE execution is missing typedData");
340
+ }
341
+ // A signed order isn't allowance-free: CoW's vault relayer and Bebop/0x's Permit2 still pull
342
+ // the sell token with transferFrom, so this on-chain allowance (separate from approvalData's
343
+ // gasless per-trade signature) must be confirmed before signing, otherwise the order is
344
+ // accepted and simply never fills, with no error to explain why.
345
+ const approvalTxHash = await maybeSendApproval(execution.approval);
346
+ checkpoint();
347
+ // Called as methods: a class-instance wallet needs its `this`.
348
+ const approvalSignature = execution.approvalData ? await handlers.signTypedData(execution.approvalData) : undefined;
349
+ checkpoint();
350
+ const signature = await handlers.signTypedData(execution.typedData);
351
+ checkpoint();
352
+ // From here the venue may take the order even when the call fails (BUILD_FAILED, no answer),
353
+ // so only a rate limit, refused before anything was placed, is retried.
354
+ sent = true;
355
+ report("swap_signed", { signature });
356
+ const { statusRef } = await withRetries(() => client.submitSignature({ quoteToken, signature, approvalSignature }), rateLimited);
357
+ report("swap_sent", { statusRef });
358
+ return { execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
136
359
  }
137
- if (!execution.typedData) {
138
- throw new Error("executeAndTrack: SIGNATURE execution is missing typedData");
360
+ // DEPOSIT: nothing to sign or send from here; the user moves funds to execution.deposit.address
361
+ // out-of-band, and this helper has no way to know when that happens. Returned without polling;
362
+ // start polling yourself (getStatus / useRavnStatus) once the deposit is actually sent. Pay
363
+ // `feePayment` as it says, pass an EVM fee transaction's hash as getStatus's `txHash`, and poll
364
+ // RAVN to a terminal status even if the venue's own tracker says delivered first.
365
+ // A fee it can't describe never stops the deposit, which fills without it.
366
+ checkpoint();
367
+ try {
368
+ return { execution, quoteToken, statusRef: execution.statusRef, feePayment: depositFeePayment(execution) };
139
369
  }
140
- // A signed order isn't allowance-free: CoW's vault relayer and Bebop/0x's Permit2 still pull
141
- // the sell token with transferFrom, so this on-chain allowance (separate from approvalData's
142
- // gasless per-trade signature) must be confirmed before signing, otherwise the order is
143
- // accepted and simply never fills, with no error to explain why.
144
- const approvalTxHash = await maybeSendApproval(execution.approval);
145
- let approvalSignature;
146
- if (execution.approvalData) {
147
- approvalSignature = await handlers.signTypedData(execution.approvalData);
370
+ catch (feeError) {
371
+ return { execution, quoteToken, statusRef: execution.statusRef, feeError };
148
372
  }
149
- const signature = await handlers.signTypedData(execution.typedData);
150
- const { statusRef } = await client.submitSignature({ quoteToken, signature, approvalSignature });
151
- return { execution, quoteToken, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
152
- }
153
- // DEPOSIT: nothing to sign or send from here; the user moves funds to execution.deposit.address
154
- // out-of-band, and this helper has no way to know when that happens. Returned without polling;
155
- // start polling yourself (getStatus / useRavnStatus) once the deposit is actually sent. Pay
156
- // `feePayment` as it says, pass an EVM fee transaction's hash as getStatus's `txHash`, and poll
157
- // RAVN to a terminal status even if the venue's own tracker says delivered first.
158
- // A fee it can't describe never stops the deposit, which fills without it.
373
+ };
159
374
  try {
160
- return { execution, quoteToken, statusRef: execution.statusRef, feePayment: depositFeePayment(execution) };
375
+ return await run();
376
+ }
377
+ catch (err) {
378
+ if (stopCode) {
379
+ const why = stopCode === "ABORTED" ? "aborted" : `no terminal status within maxWaitMs (${maxWaitMs} ms)`;
380
+ throw new ExecuteAndTrackError(`executeAndTrack: ${why}`, stopCode, { ...partial }, stop.signal.reason);
381
+ }
382
+ if (!sent)
383
+ throw err;
384
+ const why = err instanceof Error ? err.message : String(err);
385
+ throw new ExecuteAndTrackError(`executeAndTrack: ${why}`, "FAILED", { ...partial }, err);
161
386
  }
162
- catch (feeError) {
163
- return { execution, quoteToken, statusRef: execution.statusRef, feeError };
387
+ finally {
388
+ clearTimeout(deadline);
389
+ options.signal?.removeEventListener("abort", onAbort);
164
390
  }
165
391
  }
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 { depositFeePayment, executeAndTrack } from "./execute-and-track";
6
- export type { ExecuteAndTrackHandlers, ExecuteAndTrackOptions, ExecuteAndTrackResult, DepositFeePayment, } 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,7 +14,7 @@ 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.depositFeePayment = 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);
@@ -23,3 +23,4 @@ Object.defineProperty(exports, "isPollingTerminal", { enumerable: true, get: fun
23
23
  var execute_and_track_1 = require("./execute-and-track");
24
24
  Object.defineProperty(exports, "depositFeePayment", { enumerable: true, get: function () { return execute_and_track_1.depositFeePayment; } });
25
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,22 @@ 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;
154
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
155
+ swapId?: string;
138
156
  /** Pass this, not the quote's own token, to getStatus. Compose: the post-bind token. */
139
157
  quoteToken: string;
140
158
  /** The venue's own tracking key (compose hop-2 handle, Garden order id, Relay request id).
@@ -172,6 +190,11 @@ export type ExecutionDTO = {
172
190
  fee?: DepositFeeDTO;
173
191
  } | {
174
192
  executionType: "SIGNATURE";
193
+ /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
194
+ * it. executeAndTrack broadcasts nothing for one. */
195
+ sandbox?: true;
196
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
197
+ swapId?: string;
175
198
  /** Pass this, not the quote's own token, to submitSignature and getStatus. */
176
199
  quoteToken: string;
177
200
  statusRef?: string;
@@ -184,6 +207,11 @@ export type ExecutionDTO = {
184
207
  };
185
208
  } | {
186
209
  executionType: "DEPOSIT";
210
+ /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
211
+ * it. executeAndTrack broadcasts nothing for one. */
212
+ sandbox?: true;
213
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
214
+ swapId?: string;
187
215
  /** Pass this, not the quote's own token, to getStatus. */
188
216
  quoteToken: string;
189
217
  /** `expiresAt` (ISO 8601), when set, is when the deposit must have ARRIVED (confirmed on
@@ -268,6 +296,11 @@ export interface GetQuoteParams {
268
296
  * carry slippage.noMinimum: true and guaranteedMin: null; slippageBps does not protect them. */
269
297
  allowNoMinimum?: boolean;
270
298
  rankingMode?: "best_output" | "fastest";
299
+ /** The race's time limit in ms, counted from its start: once it has passed and some venue has
300
+ * priced the swap, slower venues are left out (with none priced yet, the first price ends the
301
+ * wait; a quote with slippage.noMinimum counts only 2000 ms past the limit). A positive integer;
302
+ * default 6000, clamped to 1000–15000. An API that predates it ignores it. */
303
+ timeoutMs?: number;
271
304
  /** Drop specific venues from this race, e.g. a risk objection to one of them. */
272
305
  excludeVenues?: string[];
273
306
  /** Race only these venues, e.g. ["bebop"] for an RFQ-only race. Every other venue is left
@@ -307,6 +340,11 @@ export interface ExecuteParams {
307
340
  refundAddress?: string;
308
341
  /** Your end user's IANA timezone, forwarded only to Houdini. Default UTC. */
309
342
  clientTimezone?: string;
343
+ /** Sent as the Idempotency-Key header: any unique value per swap attempt (e.g. a UUID), 1–255
344
+ * printable ASCII characters, no spaces (execute throws INVALID_REQUEST otherwise). Calling execute again with the same key and request (quoteToken and any addresses) within a day
345
+ * returns the first answer instead of building again (at a deposit venue, a second deposit
346
+ * address). executeAndTrack sends one itself when you don't. */
347
+ idempotencyKey?: string;
310
348
  }
311
349
  export interface SubmitSignatureParams {
312
350
  quoteToken: string;
@@ -323,10 +361,25 @@ export interface ErrorMeta {
323
361
  requestId: string;
324
362
  version: string;
325
363
  }
326
- /** Thrown for every non-2xx response. `code` is the stable, machine-readable string from the API. */
364
+ /** Every `code` the API sends (see the API's Errors page), plus NETWORK_ERROR: no response came
365
+ * back at all (offline, DNS, CORS, a dropped connection). New codes can appear: compare against
366
+ * these, don't switch on them exhaustively. */
367
+ 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" | "UNAVAILABLE" | "ORDER_REJECTED" | "INTERNAL" | "NETWORK_ERROR";
368
+ /** Thrown for every failed call: a non-2xx response, or none at all (NETWORK_ERROR). `code` is the
369
+ * stable, machine-readable string from the API. */
327
370
  export declare class RavnApiError extends Error {
328
- readonly code: string;
371
+ readonly code: RavnErrorCode | (string & {});
329
372
  readonly details: unknown;
330
373
  readonly meta: ErrorMeta | undefined;
331
- constructor(code: string, message: string, details: unknown, meta: ErrorMeta | undefined);
374
+ /** The HTTP status; undefined when no response came back (NETWORK_ERROR). */
375
+ readonly status?: number;
376
+ /** Seconds the API asked you to wait before trying the same call again (`details.retryAfterSec`,
377
+ * sent with RATE_LIMITED, VENUE_RATE_LIMITED and UNAVAILABLE). */
378
+ readonly retryAfterSec?: number;
379
+ /** NETWORK_ERROR: the fetch error underneath. */
380
+ readonly cause?: unknown;
381
+ constructor(code: RavnErrorCode | (string & {}), message: string, details: unknown, meta: ErrorMeta | undefined, init?: {
382
+ status?: number;
383
+ cause?: unknown;
384
+ });
332
385
  }
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.15",
3
+ "version": "0.2.1",
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",