@ravnexchange/sdk 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -17,7 +17,7 @@ export declare class RavnClient {
17
17
  /** POST /v1/quote: omitting destinationAddress/refundAddress returns a preview-only quote (see QuoteDTO.executable). */
18
18
  getQuote(params: GetQuoteParams): Promise<QuoteDTO>;
19
19
  /** POST /v1/execute: branch on the returned executionType (TRANSACTION / SIGNATURE / DEPOSIT). */
20
- execute(params: ExecuteParams): Promise<ExecutionDTO>;
20
+ execute({ idempotencyKey, ...params }: ExecuteParams): Promise<ExecutionDTO>;
21
21
  /** POST /v1/submit-signature: for SIGNATURE-type executions only. Returns the ref to poll getStatus with. */
22
22
  submitSignature(params: SubmitSignatureParams): Promise<{
23
23
  statusRef: string;
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).
@@ -191,6 +193,8 @@ export type ExecutionDTO = {
191
193
  /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
192
194
  * it. executeAndTrack broadcasts nothing for one. */
193
195
  sandbox?: true;
196
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
197
+ swapId?: string;
194
198
  /** Pass this, not the quote's own token, to submitSignature and getStatus. */
195
199
  quoteToken: string;
196
200
  statusRef?: string;
@@ -206,6 +210,8 @@ export type ExecutionDTO = {
206
210
  /** True for a sandbox quote: nothing here is real (see GetQuoteParams.sandbox); never send
207
211
  * it. executeAndTrack broadcasts nothing for one. */
208
212
  sandbox?: true;
213
+ /** This swap's id in RAVN's ledger: settlement webhooks name it (`swapId`). Absent on a sandbox quote. */
214
+ swapId?: string;
209
215
  /** Pass this, not the quote's own token, to getStatus. */
210
216
  quoteToken: string;
211
217
  /** `expiresAt` (ISO 8601), when set, is when the deposit must have ARRIVED (confirmed on
@@ -334,6 +340,11 @@ export interface ExecuteParams {
334
340
  refundAddress?: string;
335
341
  /** Your end user's IANA timezone, forwarded only to Houdini. Default UTC. */
336
342
  clientTimezone?: string;
343
+ /** Sent as the Idempotency-Key header: any unique value per swap attempt (e.g. a UUID), 1–255
344
+ * printable ASCII characters, no spaces (execute throws INVALID_REQUEST otherwise). Calling execute again with the same key and request (quoteToken and any addresses) within a day
345
+ * returns the first answer instead of building again (at a deposit venue, a second deposit
346
+ * address). executeAndTrack sends one itself when you don't. */
347
+ idempotencyKey?: string;
337
348
  }
338
349
  export interface SubmitSignatureParams {
339
350
  quoteToken: string;
@@ -353,7 +364,7 @@ export interface ErrorMeta {
353
364
  /** Every `code` the API sends (see the API's Errors page), plus NETWORK_ERROR: no response came
354
365
  * back at all (offline, DNS, CORS, a dropped connection). New codes can appear: compare against
355
366
  * 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";
367
+ export type RavnErrorCode = "INVALID_REQUEST" | "UNSUPPORTED_TOKEN" | "UNSUPPORTED_CHAIN" | "NO_LIQUIDITY" | "QUOTE_EXPIRED" | "QUOTE_INVALID" | "QUOTE_NOT_EXECUTABLE" | "RATE_LIMITED" | "UNAUTHORIZED" | "NOT_CONFIGURED" | "NOT_FOUND" | "BUILD_FAILED" | "VENUE_RATE_LIMITED" | "UNAVAILABLE" | "ORDER_REJECTED" | "INTERNAL" | "NETWORK_ERROR";
357
368
  /** Thrown for every failed call: a non-2xx response, or none at all (NETWORK_ERROR). `code` is the
358
369
  * stable, machine-readable string from the API. */
359
370
  export declare class RavnApiError extends Error {
@@ -363,7 +374,7 @@ export declare class RavnApiError extends Error {
363
374
  /** The HTTP status; undefined when no response came back (NETWORK_ERROR). */
364
375
  readonly status?: number;
365
376
  /** Seconds the API asked you to wait before trying the same call again (`details.retryAfterSec`,
366
- * sent with RATE_LIMITED and VENUE_RATE_LIMITED). */
377
+ * sent with RATE_LIMITED, VENUE_RATE_LIMITED and UNAVAILABLE). */
367
378
  readonly retryAfterSec?: number;
368
379
  /** NETWORK_ERROR: the fetch error underneath. */
369
380
  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.1",
4
4
  "description": "Typed client for the RAVN integrator API (/api/v1): quote, execute, submit-signature, status.",
5
5
  "keywords": ["ravn", "cross-chain", "swap", "swap-api", "dex-aggregator", "bitcoin", "native-btc", "solana", "evm", "crypto", "defi", "typescript", "sdk", "api-client"],
6
6
  "main": "./dist/index.js",