@ravnexchange/sdk 0.1.15 → 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 +4 -2
- package/dist/client.js +27 -13
- package/dist/execute-and-track.d.ts +87 -13
- package/dist/execute-and-track.js +327 -112
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/polling.d.ts +8 -3
- package/dist/polling.js +9 -4
- package/dist/types.d.ts +46 -4
- package/dist/types.js +8 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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. */
|
|
@@ -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
|
|
15
|
-
* sent: sending both back-to-back races the allowance and is the most
|
|
16
|
-
* this helper exists to prevent. Also called on the swap before RAVN's fee
|
|
17
|
-
* (`fee.transaction`):
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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<
|
|
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
|
|
28
|
-
* proves its fee from that transaction. When given, a token-paid RAVN fee rides
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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,11 @@ 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 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.
|
|
122
196
|
*/
|
|
123
197
|
export declare function executeAndTrack(client: RavnClient, params: ExecuteParams, handlers: ExecuteAndTrackHandlers, options?: ExecuteAndTrackOptions): Promise<ExecuteAndTrackResult>;
|
|
124
198
|
export {};
|
|
@@ -1,11 +1,40 @@
|
|
|
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;
|
|
9
38
|
/** The fee payment a DEPOSIT execution asks for beside its deposit; undefined when it has none. */
|
|
10
39
|
function depositFeePayment(execution) {
|
|
11
40
|
if (execution.executionType !== "DEPOSIT" || !execution.fee)
|
|
@@ -21,145 +50,331 @@ function depositFeePayment(execution) {
|
|
|
21
50
|
}
|
|
22
51
|
// Only a receipt that says success (viem "success", ethers 1; web3.js 1n/true, so == on purpose) and
|
|
23
52
|
// is `hash`'s own (viem/ethers v5 transactionHash, ethers v6 hash): viem resolves a cancelled swap
|
|
24
|
-
// with the replacement's successful receipt.
|
|
53
|
+
// with the replacement's successful receipt. Typed unknown: JS callers can still hand it anything.
|
|
25
54
|
const receiptSucceeded = (receipt, hash) => {
|
|
26
55
|
const r = receipt;
|
|
27
56
|
const own = r?.transactionHash ?? r?.hash;
|
|
28
57
|
return (r?.status === "success" || r?.status == 1) && (typeof own !== "string" || own.toLowerCase() === hash.toLowerCase());
|
|
29
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
|
+
});
|
|
30
93
|
/**
|
|
31
94
|
* Sequences execute() → approval (if any) → wait for its receipt → main tx/signature → RAVN's fee
|
|
32
95
|
* payment (if any) → status polling, using caller-supplied signing/sending functions. The SDK
|
|
33
96
|
* stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
|
|
34
97
|
* own docs flag as the most common integration failure: sending the main tx before the approval
|
|
35
|
-
* has confirmed.
|
|
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.
|
|
36
103
|
*/
|
|
37
104
|
async function executeAndTrack(client, params, handlers, options = {}) {
|
|
38
|
-
const { pollUntilTerminal = true, pollIntervalMs = 4000 } = options;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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));
|
|
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.
|
|
56
116
|
}
|
|
57
117
|
};
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
await handlers.waitForReceipt(hash, approval.chainId);
|
|
67
|
-
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);
|
|
68
126
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|
|
77
150
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// A
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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) };
|
|
99
187
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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 () => {
|
|
105
228
|
try {
|
|
106
|
-
|
|
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);
|
|
229
|
+
return await client.submitTransaction({ quoteToken, signedTransaction });
|
|
113
230
|
}
|
|
114
231
|
catch (err) {
|
|
115
|
-
|
|
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;
|
|
116
236
|
}
|
|
117
|
-
}
|
|
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) };
|
|
118
242
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
};
|
|
127
322
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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) };
|
|
136
348
|
}
|
|
137
|
-
|
|
138
|
-
|
|
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) };
|
|
139
358
|
}
|
|
140
|
-
|
|
141
|
-
|
|
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);
|
|
359
|
+
catch (feeError) {
|
|
360
|
+
return { execution, quoteToken, statusRef: execution.statusRef, feeError };
|
|
148
361
|
}
|
|
149
|
-
|
|
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.
|
|
362
|
+
};
|
|
159
363
|
try {
|
|
160
|
-
return
|
|
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);
|
|
161
375
|
}
|
|
162
|
-
|
|
163
|
-
|
|
376
|
+
finally {
|
|
377
|
+
clearTimeout(deadline);
|
|
378
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
164
379
|
}
|
|
165
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 { 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
|
|
4
|
-
* outcome
|
|
5
|
-
* change and polling forever would just spin.
|
|
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
|
|
6
|
-
* outcome
|
|
7
|
-
* change and polling forever would just spin.
|
|
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
|
|
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).
|
|
@@ -172,6 +188,9 @@ export type ExecutionDTO = {
|
|
|
172
188
|
fee?: DepositFeeDTO;
|
|
173
189
|
} | {
|
|
174
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;
|
|
175
194
|
/** Pass this, not the quote's own token, to submitSignature and getStatus. */
|
|
176
195
|
quoteToken: string;
|
|
177
196
|
statusRef?: string;
|
|
@@ -184,6 +203,9 @@ export type ExecutionDTO = {
|
|
|
184
203
|
};
|
|
185
204
|
} | {
|
|
186
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;
|
|
187
209
|
/** Pass this, not the quote's own token, to getStatus. */
|
|
188
210
|
quoteToken: string;
|
|
189
211
|
/** `expiresAt` (ISO 8601), when set, is when the deposit must have ARRIVED (confirmed on
|
|
@@ -268,6 +290,11 @@ export interface GetQuoteParams {
|
|
|
268
290
|
* carry slippage.noMinimum: true and guaranteedMin: null; slippageBps does not protect them. */
|
|
269
291
|
allowNoMinimum?: boolean;
|
|
270
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;
|
|
271
298
|
/** Drop specific venues from this race, e.g. a risk objection to one of them. */
|
|
272
299
|
excludeVenues?: string[];
|
|
273
300
|
/** Race only these venues, e.g. ["bebop"] for an RFQ-only race. Every other venue is left
|
|
@@ -323,10 +350,25 @@ export interface ErrorMeta {
|
|
|
323
350
|
requestId: string;
|
|
324
351
|
version: string;
|
|
325
352
|
}
|
|
326
|
-
/**
|
|
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. */
|
|
327
359
|
export declare class RavnApiError extends Error {
|
|
328
|
-
readonly code: string;
|
|
360
|
+
readonly code: RavnErrorCode | (string & {});
|
|
329
361
|
readonly details: unknown;
|
|
330
362
|
readonly meta: ErrorMeta | undefined;
|
|
331
|
-
|
|
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
|
+
});
|
|
332
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
|
|
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.
|
|
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",
|