@ravnexchange/sdk 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare class RavnClient {
17
17
  /** POST /v1/quote: omitting destinationAddress/refundAddress returns a preview-only quote (see QuoteDTO.executable). */
18
18
  getQuote(params: GetQuoteParams): Promise<QuoteDTO>;
19
19
  /** POST /v1/execute: branch on the returned executionType (TRANSACTION / SIGNATURE / DEPOSIT). */
20
- execute(params: ExecuteParams): Promise<ExecutionDTO>;
20
+ execute({ idempotencyKey, ...params }: ExecuteParams): Promise<ExecutionDTO>;
21
21
  /** POST /v1/submit-signature: for SIGNATURE-type executions only. Returns the ref to poll getStatus with. */
22
22
  submitSignature(params: SubmitSignatureParams): Promise<{
23
23
  statusRef: string;
package/dist/client.js CHANGED
@@ -57,8 +57,16 @@ class RavnClient {
57
57
  return this.request("/quote", { method: "POST", body: JSON.stringify(params) });
58
58
  }
59
59
  /** POST /v1/execute: branch on the returned executionType (TRANSACTION / SIGNATURE / DEPOSIT). */
60
- execute(params) {
61
- return this.request("/execute", { method: "POST", body: JSON.stringify(params) });
60
+ execute({ idempotencyKey, ...params }) {
61
+ // The API's own rule: anything else fetch refuses to send (a reported NETWORK_ERROR) or RAVN answers 400.
62
+ if (idempotencyKey !== undefined && !/^[\x21-\x7e]{1,255}$/.test(idempotencyKey)) {
63
+ return Promise.reject(new types_1.RavnApiError("INVALID_REQUEST", "idempotencyKey must be 1–255 printable ASCII characters, no spaces", undefined, undefined));
64
+ }
65
+ return this.request("/execute", {
66
+ method: "POST",
67
+ body: JSON.stringify(params),
68
+ ...(idempotencyKey ? { headers: { "Idempotency-Key": idempotencyKey } } : {}),
69
+ });
62
70
  }
63
71
  /** POST /v1/submit-signature: for SIGNATURE-type executions only. Returns the ref to poll getStatus with. */
64
72
  submitSignature(params) {
@@ -189,8 +189,9 @@ export declare function depositFeePayment(execution: ExecutionDTO): DepositFeePa
189
189
  * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
190
190
  * own docs flag as the most common integration failure: sending the main tx before the approval
191
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
192
+ * submitTransaction on a 429, 5xx or no response; execute, sent with an Idempotency-Key, also when its
193
+ * answer was lost or is still being built, up to 4 tries; submitSignature only when rate limited),
194
+ * waiting the API's retryAfterSec. Once anything
194
195
  * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
195
196
  * each one first.
196
197
  */
@@ -35,6 +35,9 @@ const MAX_BACKOFF_MS = 60000;
35
35
  // ponytail: a fixed cap, ~4 min of backoff at the default interval; past it the call rejects with
36
36
  // every hash to resume from. Make it an option if integrators need to ride out longer outages.
37
37
  const MAX_ATTEMPTS = 8;
38
+ // An execute asked again with its Idempotency-Key: ~30 s at the default interval, so a call that
39
+ // can't get through fails in about that rather than four minutes.
40
+ const EXECUTE_ATTEMPTS = 4;
38
41
  /** The fee payment a DEPOSIT execution asks for beside its deposit; undefined when it has none. */
39
42
  function depositFeePayment(execution) {
40
43
  if (execution.executionType !== "DEPOSIT" || !execution.fee)
@@ -61,6 +64,11 @@ const transient = (err) => err instanceof types_1.RavnApiError && (err.code ===
61
64
  // Refused before anything was done, so safe to repeat even a call that isn't safe to repeat after
62
65
  // a BUILD_FAILED (submit-signature: the venue may have taken the order).
63
66
  const rateLimited = (err) => err instanceof types_1.RavnApiError && (err.code === "RATE_LIMITED" || err.code === "VENUE_RATE_LIMITED" || err.status === 429);
67
+ // An execute sent with an Idempotency-Key answers a repeat with its first answer, so one whose answer
68
+ // was lost (no response, a gateway timeout) or is still being built (UNAVAILABLE) can be asked again.
69
+ // Never a BUILD_FAILED (re-quote) or an unexpected error, which may have half-built an order.
70
+ const answerLost = (err) => rateLimited(err) ||
71
+ (err instanceof types_1.RavnApiError && (err.code === "NETWORK_ERROR" || err.code === "UNAVAILABLE" || err.status === 504));
64
72
  // `promise`, or the signal's reason as soon as it aborts: a wallet or custom fetch may ignore the
65
73
  // signal. `promise` is always handled, so one that rejects after the abort is never unhandled.
66
74
  const orAbort = (promise, signal) => new Promise((resolve, reject) => {
@@ -96,8 +104,9 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
96
104
  * stays wallet-agnostic (no RPC dependency); this just gets the ORDER right, which quickstart's
97
105
  * own docs flag as the most common integration failure: sending the main tx before the approval
98
106
  * 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
107
+ * submitTransaction on a 429, 5xx or no response; execute, sent with an Idempotency-Key, also when its
108
+ * answer was lost or is still being built, up to 4 tries; submitSignature only when rate limited),
109
+ * waiting the API's retryAfterSec. Once anything
101
110
  * is sent, it rejects only with an ExecuteAndTrackError carrying every hash; `onProgress` hears
102
111
  * each one first.
103
112
  */
@@ -135,14 +144,14 @@ async function executeAndTrack(client, params, handlers, options = {}) {
135
144
  throw stop.signal.reason;
136
145
  };
137
146
  // 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) => {
147
+ // `attempts`: waiting the API's retryAfterSec, else an interval doubling from pollIntervalMs.
148
+ const withRetries = async (call, retryable, attempts = MAX_ATTEMPTS) => {
140
149
  for (let attempt = 1;; attempt++) {
141
150
  try {
142
151
  return await call();
143
152
  }
144
153
  catch (err) {
145
- if (attempt >= MAX_ATTEMPTS || !retryable(err))
154
+ if (attempt >= attempts || !retryable(err))
146
155
  throw err;
147
156
  const askedMs = (err.retryAfterSec ?? 0) * 1000;
148
157
  await sleep(Math.max(askedMs, Math.min(pollIntervalMs * 2 ** (attempt - 1), MAX_BACKOFF_MS)), stop.signal);
@@ -151,9 +160,11 @@ async function executeAndTrack(client, params, handlers, options = {}) {
151
160
  };
152
161
  const run = async () => {
153
162
  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);
163
+ // One Idempotency-Key for every try, so a repeat can't open a second order at the venue (a NEAR
164
+ // deposit address, a Houdini or Garden order). Without one (no crypto.randomUUID), only a rate
165
+ // limit is repeated. A BUILD_FAILED wants a fresh quote either way.
166
+ const key = params.idempotencyKey ?? globalThis.crypto?.randomUUID?.();
167
+ const execution = await withRetries(() => orAbort(client.execute({ ...params, ...(key ? { idempotencyKey: key } : {}) }), stop.signal), key ? answerLost : rateLimited, key ? EXECUTE_ATTEMPTS : MAX_ATTEMPTS);
157
168
  const { quoteToken } = execution;
158
169
  Object.assign(partial, { execution, quoteToken, statusRef: execution.statusRef });
159
170
  const pollStatus = async (ref, txHash, feeSent = false) => {
package/dist/types.d.ts CHANGED
@@ -151,6 +151,8 @@ export type ExecutionDTO = {
151
151
  /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
152
152
  * it. executeAndTrack broadcasts nothing for one. */
153
153
  sandbox?: true;
154
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
155
+ swapId?: string;
154
156
  /** Pass this, not the quote's own token, to getStatus. Compose: the post-bind token. */
155
157
  quoteToken: string;
156
158
  /** The venue's own tracking key (compose hop-2 handle, Garden order id, Relay request id).
@@ -186,11 +188,21 @@ export type ExecutionDTO = {
186
188
  * stays with RAVN.
187
189
  */
188
190
  fee?: DepositFeeDTO;
191
+ /** EVM: everything to send, in order (approval.reset, approval, the swap or the deposit
192
+ * transfer, RAVN's fee). Each once the one before has succeeded (stop if one reverts), or, when
193
+ * `batch` is true, all in one atomic wallet batch (EIP-5792 `wallet_sendCalls`,
194
+ * `atomicRequired: true`): simulate it first, and send them one by one if that fails or the
195
+ * last call needs more than ~400k gas. */
196
+ calls?: CallDTO[];
197
+ /** Whether `calls` (more than one) may go in one atomic wallet batch. */
198
+ batch?: boolean;
189
199
  } | {
190
200
  executionType: "SIGNATURE";
191
201
  /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
192
202
  * it. executeAndTrack broadcasts nothing for one. */
193
203
  sandbox?: true;
204
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
205
+ swapId?: string;
194
206
  /** Pass this, not the quote's own token, to submitSignature and getStatus. */
195
207
  quoteToken: string;
196
208
  statusRef?: string;
@@ -206,6 +218,8 @@ export type ExecutionDTO = {
206
218
  /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
207
219
  * it. executeAndTrack broadcasts nothing for one. */
208
220
  sandbox?: true;
221
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
222
+ swapId?: string;
209
223
  /** Pass this, not the quote's own token, to getStatus. */
210
224
  quoteToken: string;
211
225
  /** `expiresAt` (ISO 8601), when set, is when the deposit must have ARRIVED (confirmed on
@@ -215,6 +229,13 @@ export type ExecutionDTO = {
215
229
  amount: string;
216
230
  chainId?: number;
217
231
  expiresAt?: string;
232
+ /** Bitcoin, from a native SegWit (bc1q…) userAddress: the deposit (and RAVN's fee as a second
233
+ * output) as a ready-to-sign PSBT, base64, change back to it. Absent when it can't be built. */
234
+ psbt?: string;
235
+ /** Solana, when the deposit carries RAVN's fee: deposit and fee as one unsigned transaction,
236
+ * base64. Sign and send it within a minute (its blockhash expires; a retry with the same
237
+ * Idempotency-Key returns this same one). */
238
+ transaction?: string;
218
239
  };
219
240
  /** Poll getStatus with this, not `deposit.address`. */
220
241
  statusRef: string;
@@ -223,7 +244,22 @@ export type ExecutionDTO = {
223
244
  * fee is owed to you. A fee paid on a swap the venue then refunds stays with RAVN and nothing
224
245
  * is owed on it, so send an EVM fee payment once the deposit has confirmed. */
225
246
  fee?: DepositFeeDTO;
247
+ /** EVM: everything to send, in order (approval.reset, approval, the swap or the deposit
248
+ * transfer, RAVN's fee). Each once the one before has succeeded (stop if one reverts), or, when
249
+ * `batch` is true, all in one atomic wallet batch (EIP-5792 `wallet_sendCalls`,
250
+ * `atomicRequired: true`): simulate it first, and send them one by one if that fails or the
251
+ * last call needs more than ~400k gas. */
252
+ calls?: CallDTO[];
253
+ /** Whether `calls` (more than one) may go in one atomic wallet batch. */
254
+ batch?: boolean;
226
255
  };
256
+ /** One EVM transaction to send, as given. */
257
+ export interface CallDTO {
258
+ to: string;
259
+ data: string;
260
+ value: string;
261
+ chainId: number;
262
+ }
227
263
  /** RAVN's integrator fee beside a deposit (or a transaction), in the input token, paid by your user
228
264
  * to RAVN; your share is paid out as usual. `deposit.amount` (the transaction) already leaves it out. */
229
265
  export interface DepositFeeDTO {
@@ -334,6 +370,11 @@ export interface ExecuteParams {
334
370
  refundAddress?: string;
335
371
  /** Your end user's IANA timezone, forwarded only to Houdini. Default UTC. */
336
372
  clientTimezone?: string;
373
+ /** Sent as the Idempotency-Key header: any unique value per swap attempt (e.g. a UUID), 1–255
374
+ * printable ASCII characters, no spaces (execute throws INVALID_REQUEST otherwise). Calling execute again with the same key and request (quoteToken and any addresses) within a day
375
+ * returns the first answer instead of building again (at a deposit venue, a second deposit
376
+ * address). executeAndTrack sends one itself when you don't. */
377
+ idempotencyKey?: string;
337
378
  }
338
379
  export interface SubmitSignatureParams {
339
380
  quoteToken: string;
@@ -353,7 +394,7 @@ export interface ErrorMeta {
353
394
  /** Every `code` the API sends (see the API's Errors page), plus NETWORK_ERROR: no response came
354
395
  * back at all (offline, DNS, CORS, a dropped connection). New codes can appear: compare against
355
396
  * 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";
397
+ export type RavnErrorCode = "INVALID_REQUEST" | "UNSUPPORTED_TOKEN" | "UNSUPPORTED_CHAIN" | "NO_LIQUIDITY" | "QUOTE_EXPIRED" | "QUOTE_INVALID" | "QUOTE_NOT_EXECUTABLE" | "RATE_LIMITED" | "UNAUTHORIZED" | "NOT_CONFIGURED" | "NOT_FOUND" | "BUILD_FAILED" | "VENUE_RATE_LIMITED" | "UNAVAILABLE" | "ORDER_REJECTED" | "INTERNAL" | "NETWORK_ERROR";
357
398
  /** Thrown for every failed call: a non-2xx response, or none at all (NETWORK_ERROR). `code` is the
358
399
  * stable, machine-readable string from the API. */
359
400
  export declare class RavnApiError extends Error {
@@ -363,7 +404,7 @@ export declare class RavnApiError extends Error {
363
404
  /** The HTTP status; undefined when no response came back (NETWORK_ERROR). */
364
405
  readonly status?: number;
365
406
  /** Seconds the API asked you to wait before trying the same call again (`details.retryAfterSec`,
366
- * sent with RATE_LIMITED and VENUE_RATE_LIMITED). */
407
+ * sent with RATE_LIMITED, VENUE_RATE_LIMITED and UNAVAILABLE). */
367
408
  readonly retryAfterSec?: number;
368
409
  /** NETWORK_ERROR: the fetch error underneath. */
369
410
  readonly cause?: unknown;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravnexchange/sdk",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",