@ravnexchange/sdk 0.2.2 → 0.2.4

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.
@@ -39,7 +39,8 @@ export interface ExecuteAndTrackHandlers {
39
39
  * (the same hash repeated, or just one, for one transaction). Not a batch id or a 4337
40
40
  * userOpHash: RAVN proves its fee from that transaction. When given, a token-paid RAVN fee rides
41
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
42
+ * TRANSACTION's native-coin fee is always a plain transaction of its own (with sendDeposit, a
43
+ * DEPOSIT's calls go together only when its `batch` says so). The batch's gas limit is your
43
44
  * wallet's (EIP-5792 carries none): give it headroom, as a wallet estimating it tight reverts
44
45
  * the whole batch. Reject only when nothing was sent: a rejection loses the swap's hash.
45
46
  */
@@ -51,6 +52,20 @@ export interface ExecuteAndTrackHandlers {
51
52
  * TRANSACTION carrying `submit` (broadcastViaRavn quotes), which RAVN checks and sends itself.
52
53
  */
53
54
  signSolanaTransaction?(serialized: string): Promise<string>;
55
+ /**
56
+ * With `sendDeposit`: sign a Bitcoin DEPOSIT's ready-made PSBT (`deposit.psbt`, base64: the
57
+ * deposit, RAVN's fee as a second output, change back to the sender), finalize and broadcast it,
58
+ * and return its txid. Reject only when nothing was broadcast: a rejection loses its hash, and
59
+ * executeAndTrack called again builds a new deposit.
60
+ */
61
+ sendBitcoinPsbt?(psbt: string): Promise<string>;
62
+ /**
63
+ * With `sendDeposit`: sign a Solana DEPOSIT's ready-made transaction (`deposit.transaction`,
64
+ * base64: the deposit and RAVN's fee) and send it, returning its signature. It must land within
65
+ * about a minute of execute (its blockhash expires). Reject only when nothing was sent: a
66
+ * rejection loses its signature, and executeAndTrack called again builds a new deposit.
67
+ */
68
+ sendSolanaTransaction?(serialized: string): Promise<string>;
54
69
  /**
55
70
  * The API sends `approval` whenever the venue generically needs an allowance: it deliberately
56
71
  * never reads the chain to check whether the caller already has one (that's an RPC round-trip
@@ -89,6 +104,17 @@ export interface ExecuteAndTrackOptions {
89
104
  * sends is in the error's `partial`. A submitSignature or submitTransaction call already under
90
105
  * way is awaited first: its answer is the swap's only handle. */
91
106
  signal?: AbortSignal;
107
+ /**
108
+ * Send a DEPOSIT too, from the ready-to-sign payload RAVN returns with RAVN's fee inside it, then
109
+ * track it like any swap: EVM `calls` (sendCalls when `batch` allows, else the deposit, its
110
+ * receipt, then the fee), Bitcoin `deposit.psbt` (sendBitcoinPsbt), Solana `deposit.transaction`
111
+ * (sendSolanaTransaction). A deposit it can't send (no such payload, e.g. a Bitcoin sender that
112
+ * isn't bc1q…, or no handler for its chain) comes back unsent, as without this option, with
113
+ * `depositNotSent` saying why: send that one and `feePayment` yourself. Default false: a deposit
114
+ * is never sent from here. Turning it on, send a deposit yourself only when `depositNotSent` is
115
+ * set, or the user pays twice (a sandbox one has neither it nor `txHash`: it's never sent).
116
+ */
117
+ sendDeposit?: boolean;
92
118
  /** Called with every hash the moment it exists (approval, swap with its statusRef, fee) and with
93
119
  * every status poll. An error it throws is ignored: it never stops a swap that's out. */
94
120
  onProgress?(progress: ExecuteAndTrackProgress): void;
@@ -118,9 +144,12 @@ export interface ExecuteAndTrackResult {
118
144
  /** Why RAVN's fee wasn't sent (or its receipt not seen): the swap still runs and is tracked, but
119
145
  * no fee is owed to you on it unless `feeTxHash` is set and it landed. */
120
146
  feeError?: unknown;
121
- /** DEPOSIT only: the fee payment to make beside the deposit (see depositFeePayment). */
147
+ /** DEPOSIT only: the fee payment to make beside the deposit (see depositFeePayment). Absent once
148
+ * sendDeposit sent the deposit: its fee went with it. */
122
149
  feePayment?: DepositFeePayment;
123
- /** Set once pollUntilTerminal reaches a terminal status. Absent for DEPOSIT (see below) or when polling is turned off. */
150
+ /** With sendDeposit, a DEPOSIT it didn't send, and why: send it and `feePayment` yourself. */
151
+ depositNotSent?: string;
152
+ /** Set once pollUntilTerminal reaches a terminal status. Absent for a DEPOSIT this didn't send, or when polling is turned off. */
124
153
  finalStatus?: StatusDTO;
125
154
  }
126
155
  /** What executeAndTrack knew when it reported progress or stopped: every hash sent so far, and the
@@ -192,7 +192,7 @@ async function executeAndTrack(client, params, handlers, options = {}) {
192
192
  // A sandbox execution is never real, but a venue with no sandbox stand-in returns its live
193
193
  // payload: nothing in it is signed or sent (a signed order or Solana payment is a live
194
194
  // instrument even though RAVN drops it). RAVN answers its status synthetically, for any ref.
195
- if (execution.sandbox && execution.executionType !== "DEPOSIT") {
195
+ if (execution.sandbox && (execution.executionType !== "DEPOSIT" || options.sendDeposit)) {
196
196
  const statusRef = execution.statusRef ?? SANDBOX_REF;
197
197
  return { execution, quoteToken, statusRef, finalStatus: await pollStatus(statusRef) };
198
198
  }
@@ -217,6 +217,26 @@ async function executeAndTrack(client, params, handlers, options = {}) {
217
217
  }
218
218
  return hash;
219
219
  };
220
+ // RAVN's fee on its own, only once `hash` (the swap, or the deposit) has confirmed: a reverted,
221
+ // dropped or replaced one pays no fee. Declined or failed (or stopped first), the swap still
222
+ // runs and is tracked; no fee is owed on it. Confirmed before polling starts: a swap that fills
223
+ // first would otherwise end the polling on the one poll that had to prove the fee.
224
+ const sendFeeAfter = async (hash, chainId, fee, statusTxHash) => {
225
+ let feeTxHash;
226
+ try {
227
+ if (!receiptSucceeded(await orAbort(handlers.waitForReceipt(hash, chainId), stop.signal), hash)) {
228
+ throw new Error("executeAndTrack: no successful receipt for the swap (or deposit) transaction itself, so RAVN's fee was not sent");
229
+ }
230
+ checkpoint();
231
+ feeTxHash = await handlers.sendTransaction(fee);
232
+ report("fee_sent", { feeTxHash, statusTxHash: statusTxHash(feeTxHash) });
233
+ await orAbort(handlers.waitForReceipt(feeTxHash, fee.chainId), stop.signal);
234
+ return { feeTxHash };
235
+ }
236
+ catch (feeError) {
237
+ return { feeTxHash, feeError };
238
+ }
239
+ };
220
240
  if (execution.executionType === "TRANSACTION" && execution.submit) {
221
241
  // Never broadcast this one ourselves: a payment Layerswap can't match sits at Layerswap, and
222
242
  // RAVN's check before sending is what catches it.
@@ -298,25 +318,8 @@ async function executeAndTrack(client, params, handlers, options = {}) {
298
318
  txHash = await handlers.sendTransaction(transaction);
299
319
  sent = true;
300
320
  report("swap_sent", { txHash, statusRef: execution.statusRef ?? txHash, statusTxHash: proof(txHash) });
301
- if (feeTx) {
302
- // On its own, only once the swap has confirmed: a reverted, dropped or replaced one pays
303
- // no fee. Declined or failed (or stopped first), the swap still runs and is tracked; no
304
- // fee is owed on it.
305
- try {
306
- if (!receiptSucceeded(await orAbort(handlers.waitForReceipt(txHash, transaction.chainId), stop.signal), txHash)) {
307
- throw new Error("executeAndTrack: no successful receipt for the swap transaction itself, so RAVN's fee was not sent");
308
- }
309
- checkpoint();
310
- feeTxHash = await handlers.sendTransaction(feeTx);
311
- report("fee_sent", { feeTxHash, statusTxHash: proof(txHash, feeTxHash) });
312
- // Confirmed before polling starts: a swap that fills first would otherwise end the
313
- // polling on the one poll that had to prove the fee.
314
- await orAbort(handlers.waitForReceipt(feeTxHash, feeTx.chainId), stop.signal);
315
- }
316
- catch (err) {
317
- feeError = err;
318
- }
319
- }
321
+ if (feeTx)
322
+ ({ feeTxHash, feeError } = await sendFeeAfter(txHash, transaction.chainId, feeTx, (f) => proof(txHash, f)));
320
323
  }
321
324
  // Stopped before or during the fee: say so, even with polling off (no fee was sent on it).
322
325
  checkpoint();
@@ -357,19 +360,97 @@ async function executeAndTrack(client, params, handlers, options = {}) {
357
360
  report("swap_sent", { statusRef });
358
361
  return { execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
359
362
  }
360
- // DEPOSIT: nothing to sign or send from here; the user moves funds to execution.deposit.address
361
- // out-of-band, and this helper has no way to know when that happens. Returned without polling;
362
- // start polling yourself (getStatus / useRavnStatus) once the deposit is actually sent. Pay
363
- // `feePayment` as it says, pass an EVM fee transaction's hash as getStatus's `txHash`, and poll
364
- // RAVN to a terminal status even if the venue's own tracker says delivered first.
363
+ // DEPOSIT, unsent: the user moves funds to execution.deposit.address out-of-band, and this
364
+ // helper has no way to know when that happens. Returned without polling; start polling yourself
365
+ // (getStatus / useRavnStatus) once the deposit is actually sent. Pay `feePayment` as it says,
366
+ // pass an EVM fee transaction's hash as getStatus's `txHash`, and poll RAVN to a terminal status
367
+ // even if the venue's own tracker says delivered first.
365
368
  // A fee it can't describe never stops the deposit, which fills without it.
369
+ const { statusRef, deposit, calls, fee } = execution;
370
+ const unsent = (depositNotSent) => {
371
+ const base = { execution, quoteToken, statusRef, ...(depositNotSent ? { depositNotSent } : {}) };
372
+ try {
373
+ return { ...base, feePayment: depositFeePayment(execution) };
374
+ }
375
+ catch (feeError) {
376
+ return { ...base, feeError };
377
+ }
378
+ };
366
379
  checkpoint();
367
- try {
368
- return { execution, quoteToken, statusRef: execution.statusRef, feePayment: depositFeePayment(execution) };
380
+ if (!options.sendDeposit)
381
+ return unsent();
382
+ // Typed away, but a newer API can answer with a type this version doesn't know: never sent.
383
+ const type = execution.executionType;
384
+ if (type !== "DEPOSIT")
385
+ return unsent(`executionType ${type} isn't one this SDK version can send`);
386
+ // sendDeposit: from the payload RAVN built, RAVN's fee inside it.
387
+ if (calls?.length) {
388
+ const feeTx = fee?.transaction;
389
+ const [depositTx, ...rest] = calls;
390
+ // As RAVN builds them: deposit.amount to deposit.address (the coin, or the token's
391
+ // transfer(address,uint256) call), then the fee exactly as given.
392
+ const same = (a, b) => a.to?.toLowerCase() === b.to?.toLowerCase() && a.data?.toLowerCase() === b.data?.toLowerCase() && BigInt(a.value ?? "0") === BigInt(b.value ?? "0");
393
+ const to = deposit.address.toLowerCase();
394
+ const amount = BigInt(deposit.amount);
395
+ const value = BigInt(depositTx.value ?? "0");
396
+ const paysDeposit = (value === amount && depositTx.to?.toLowerCase() === to && (depositTx.data ?? "0x") === "0x") ||
397
+ (value === 0n && depositTx.data?.toLowerCase() === `0xa9059cbb${to.slice(2).padStart(64, "0")}${amount.toString(16).padStart(64, "0")}`);
398
+ if (!paysDeposit || (feeTx ? !(rest.length === 1 && same(rest[0], feeTx)) : rest.length !== 0)) {
399
+ return unsent("its calls aren't the deposit of deposit.amount to deposit.address, then RAVN's fee as given");
400
+ }
401
+ const evmHash = (hash) => (EVM_TX_HASH.test(hash) ? hash : undefined);
402
+ let txHash;
403
+ let feeTxHash;
404
+ let feeError;
405
+ checkpoint();
406
+ if (feeTx && execution.batch && handlers.sendCalls) {
407
+ const hashes = await handlers.sendCalls(calls);
408
+ sent = true;
409
+ // Fewer hashes back than calls: the last one carried the rest.
410
+ if (!hashes[0])
411
+ throw new Error("executeAndTrack: sendCalls returned no transaction hash");
412
+ txHash = hashes[0];
413
+ feeTxHash = hashes[Math.min(1, hashes.length - 1)];
414
+ report("swap_sent", { txHash, statusRef });
415
+ report("fee_sent", { feeTxHash, statusTxHash: evmHash(feeTxHash) });
416
+ }
417
+ else {
418
+ txHash = await handlers.sendTransaction(depositTx);
419
+ sent = true;
420
+ report("swap_sent", { txHash, statusRef });
421
+ if (feeTx)
422
+ ({ feeTxHash, feeError } = await sendFeeAfter(txHash, depositTx.chainId, feeTx, evmHash));
423
+ }
424
+ checkpoint();
425
+ const statusTxHash = feeTxHash && evmHash(feeTxHash);
426
+ if (feeTxHash && !statusTxHash) {
427
+ feeError ?? (feeError = new Error(`executeAndTrack: ${feeTxHash} is not an on-chain transaction hash, so RAVN can't prove the fee`));
428
+ }
429
+ partial.feeError = feeError;
430
+ return {
431
+ execution, quoteToken, txHash, statusRef, statusTxHash, feeTxHash, feeError,
432
+ finalStatus: await pollStatus(statusRef, statusTxHash, !!statusTxHash),
433
+ };
369
434
  }
370
- catch (feeError) {
371
- return { execution, quoteToken, statusRef: execution.statusRef, feeError };
435
+ // Bitcoin and Solana: one transaction pays the deposit and RAVN's fee; RAVN finds the fee there.
436
+ const payload = deposit.psbt ?? deposit.transaction;
437
+ if (!payload) {
438
+ const chainId = deposit.chainId ?? fee?.chainId;
439
+ return unsent(chainId === BITCOIN_CHAIN_ID
440
+ ? "RAVN returned no PSBT for it (it builds one only from a bc1q… userAddress, when it can within a few seconds)"
441
+ : chainId === SOLANA_CHAIN_ID
442
+ ? "RAVN returned no transaction for it (it builds one only for a deposit carrying RAVN's fee, when it can within a few seconds)"
443
+ : "RAVN returned nothing ready to sign for it");
372
444
  }
445
+ const handler = deposit.psbt ? "sendBitcoinPsbt" : "sendSolanaTransaction";
446
+ if (!handlers[handler])
447
+ return unsent(`no ${handler} handler was provided`);
448
+ checkpoint();
449
+ // Called as a method: a class-instance wallet needs its `this`.
450
+ const txHash = await handlers[handler](payload);
451
+ sent = true;
452
+ report("swap_sent", { txHash, statusRef });
453
+ return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
373
454
  };
374
455
  try {
375
456
  return await run();
package/dist/types.d.ts CHANGED
@@ -65,7 +65,8 @@ export interface QuoteDTO {
65
65
  * minimum shown can no longer be met). */
66
66
  expiresAt: number;
67
67
  /** Present on 2-hop (`venue.id === "compose"`) and Relay dest-call quotes. Sign hop 1; hop 2 is
68
- * a unique deposit, not a second wallet tx. */
68
+ * a unique deposit, not a second wallet tx. The stablecoin route also sweeps its leftover through
69
+ * 0x (`0x_swap`), which isn't listed here. */
69
70
  hops?: {
70
71
  id: string;
71
72
  name: string;
@@ -331,16 +332,22 @@ export interface GetQuoteParams {
331
332
  * wait; a quote with slippage.noMinimum counts only 2000 ms past the limit). A positive integer;
332
333
  * default 6000, clamped to 1000–15000. An API that predates it ignores it. */
333
334
  timeoutMs?: number;
334
- /** Drop specific venues from this race, e.g. a risk objection to one of them. */
335
+ /** Venues never to use, e.g. a risk objection to one of them. An excluded venue is used
336
+ * nowhere in the route, including the steps of RAVN's multi-step routes (compose, relay quotes
337
+ * with hops); a multi-step route that needs it isn't offered. Venues that swap for you may still
338
+ * route through it on their own (see onlyVenues). */
335
339
  excludeVenues?: string[];
336
340
  /** Race only these venues, e.g. ["bebop"] for an RFQ-only race. Every other venue is left
337
- * out, including ones RAVN adds later; excludeVenues still applies on top. Matches the quote's
338
- * venue.id, the venue you deal with, not every venue funds pass through: RAVN's multi-step
339
- * routes (compose, relay quotes with hops) and venues such as Rift, Across (swapping before or
340
- * after bridging) and Mayan (a Solana order starting in another token swaps through Jupiter
341
- * first) route through others. An empty list is
342
- * rejected (INVALID_REQUEST); if none of them can quote, NO_LIQUIDITY lists every left-out
343
- * venue in error.details.excludedVenues. */
341
+ * out, including ones RAVN adds later; excludeVenues still applies on top. It covers every
342
+ * venue RAVN routes funds through: a multi-step route is offered only when its own venue.id and
343
+ * every venue in its hops are listed, e.g. ["compose", "jupiter", "relay"] or
344
+ * ["relay", "kyberswap"]. RAVN's stablecoin route also needs "0x_swap" beside its hops: it
345
+ * sweeps the leftover. Venues that swap for you may route through other venues of their own
346
+ * choosing, which neither list can reach: Layerswap (a Solana token often goes through Jupiter
347
+ * first), Relay, Rift, Across (when it swaps before or after bridging) and Mayan (Jupiter on
348
+ * Solana, other DEXs such as Nordstern on EVM). An empty list is rejected (INVALID_REQUEST); if
349
+ * none of them can quote, NO_LIQUIDITY lists every left-out venue in
350
+ * error.details.excludedVenues. */
344
351
  onlyVenues?: string[];
345
352
  /** Settle privately: the race narrows to NEAR Intents (Confidential Intents) and Houdini Swap,
346
353
  * and NO_LIQUIDITY replaces a public fill if neither can quote. Houdini is sent the request's IP,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravnexchange/sdk",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",