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