@ravnexchange/sdk 0.2.3 → 0.2.5

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,9 +39,13 @@ 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 only when its `batch` says so (a coin sell's fee minted to RAVN as
43
+ * WETH, Robinhood Chain and Arbitrum), else a plain transaction of its own (with sendDeposit, a
44
+ * DEPOSIT's calls go together only when its `batch` says so). The batch's gas limit is your
43
45
  * 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.
46
+ * the whole batch (no fee leaves without the swap). A heavy swap needs the most: Fly's on
47
+ * Robinhood Chain measured 430–560k gas (2026-10-20). Reject only when nothing was sent: a
48
+ * rejection loses the swap's hash.
45
49
  */
46
50
  sendCalls?(txs: TxLike[]): Promise<string[]>;
47
51
  /** Sign an EIP-712 typed-data payload. Only called for SIGNATURE executions. */
@@ -51,6 +55,20 @@ export interface ExecuteAndTrackHandlers {
51
55
  * TRANSACTION carrying `submit` (broadcastViaRavn quotes), which RAVN checks and sends itself.
52
56
  */
53
57
  signSolanaTransaction?(serialized: string): Promise<string>;
58
+ /**
59
+ * With `sendDeposit`: sign a Bitcoin DEPOSIT's ready-made PSBT (`deposit.psbt`, base64: the
60
+ * deposit, RAVN's fee as a second output, change back to the sender), finalize and broadcast it,
61
+ * and return its txid. Reject only when nothing was broadcast: a rejection loses its hash, and
62
+ * executeAndTrack called again builds a new deposit.
63
+ */
64
+ sendBitcoinPsbt?(psbt: string): Promise<string>;
65
+ /**
66
+ * With `sendDeposit`: sign a Solana DEPOSIT's ready-made transaction (`deposit.transaction`,
67
+ * base64: the deposit and RAVN's fee) and send it, returning its signature. It must land within
68
+ * about a minute of execute (its blockhash expires). Reject only when nothing was sent: a
69
+ * rejection loses its signature, and executeAndTrack called again builds a new deposit.
70
+ */
71
+ sendSolanaTransaction?(serialized: string): Promise<string>;
54
72
  /**
55
73
  * The API sends `approval` whenever the venue generically needs an allowance: it deliberately
56
74
  * never reads the chain to check whether the caller already has one (that's an RPC round-trip
@@ -89,6 +107,17 @@ export interface ExecuteAndTrackOptions {
89
107
  * sends is in the error's `partial`. A submitSignature or submitTransaction call already under
90
108
  * way is awaited first: its answer is the swap's only handle. */
91
109
  signal?: AbortSignal;
110
+ /**
111
+ * Send a DEPOSIT too, from the ready-to-sign payload RAVN returns with RAVN's fee inside it, then
112
+ * track it like any swap: EVM `calls` (sendCalls when `batch` allows, else the deposit, its
113
+ * receipt, then the fee), Bitcoin `deposit.psbt` (sendBitcoinPsbt), Solana `deposit.transaction`
114
+ * (sendSolanaTransaction). A deposit it can't send (no such payload, e.g. a Bitcoin sender that
115
+ * isn't bc1q…, or no handler for its chain) comes back unsent, as without this option, with
116
+ * `depositNotSent` saying why: send that one and `feePayment` yourself. Default false: a deposit
117
+ * is never sent from here. Turning it on, send a deposit yourself only when `depositNotSent` is
118
+ * set, or the user pays twice (a sandbox one has neither it nor `txHash`: it's never sent).
119
+ */
120
+ sendDeposit?: boolean;
92
121
  /** Called with every hash the moment it exists (approval, swap with its statusRef, fee) and with
93
122
  * every status poll. An error it throws is ignored: it never stops a swap that's out. */
94
123
  onProgress?(progress: ExecuteAndTrackProgress): void;
@@ -118,9 +147,12 @@ export interface ExecuteAndTrackResult {
118
147
  /** Why RAVN's fee wasn't sent (or its receipt not seen): the swap still runs and is tracked, but
119
148
  * no fee is owed to you on it unless `feeTxHash` is set and it landed. */
120
149
  feeError?: unknown;
121
- /** DEPOSIT only: the fee payment to make beside the deposit (see depositFeePayment). */
150
+ /** DEPOSIT only: the fee payment to make beside the deposit (see depositFeePayment). Absent once
151
+ * sendDeposit sent the deposit: its fee went with it. */
122
152
  feePayment?: DepositFeePayment;
123
- /** Set once pollUntilTerminal reaches a terminal status. Absent for DEPOSIT (see below) or when polling is turned off. */
153
+ /** With sendDeposit, a DEPOSIT it didn't send, and why: send it and `feePayment` yourself. */
154
+ depositNotSent?: string;
155
+ /** Set once pollUntilTerminal reaches a terminal status. Absent for a DEPOSIT this didn't send, or when polling is turned off. */
124
156
  finalStatus?: StatusDTO;
125
157
  }
126
158
  /** 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.
@@ -266,12 +286,13 @@ async function executeAndTrack(client, params, handlers, options = {}) {
266
286
  let txHash;
267
287
  let feeTxHash;
268
288
  let feeError;
269
- if (feeTx && BigInt(feeTx.value) === 0n && handlers.sendCalls) {
270
- // A token fee may ride in the swap's own confirmation, and the swap's approval with it:
271
- // atomic, so neither outlives a swap that reverted. Only here: ExecutionDTO doesn't name
272
- // its venue, and the first-party app never batches THORChain (Bifrost ignores a router
273
- // deposit whose tx.to is the user's own 7702 account) or Bebop (its settlement needs gas
274
- // headroom a batch leaves to the wallet); fee-beside routes are neither.
289
+ // RAVN says whether the fee may share the swap's confirmation (`batch`): a token fee, or a coin
290
+ // sell's fee minted to RAVN as WETH (a coin sent from a batch is an internal transfer, which
291
+ // proves nothing on those chains), never at a venue that can't take a batch (THORChain, Bebop,
292
+ // a two-hop route). An API answering without `batch` (before calls + batch): a token fee only.
293
+ const batchFee = execution.batch ?? BigInt(feeTx?.value ?? "1") === 0n;
294
+ if (feeTx && batchFee && handlers.sendCalls) {
295
+ // Atomic, so neither the fee nor the swap's approval outlives a swap that reverted.
275
296
  const batchApproval = await neededApproval(approval);
276
297
  const reset = batchApproval?.reset && { ...batchApproval.reset, chainId: batchApproval.chainId };
277
298
  const approvals = batchApproval ? [...(reset ? [reset] : []), batchApproval] : [];
@@ -298,25 +319,8 @@ async function executeAndTrack(client, params, handlers, options = {}) {
298
319
  txHash = await handlers.sendTransaction(transaction);
299
320
  sent = true;
300
321
  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
- }
322
+ if (feeTx)
323
+ ({ feeTxHash, feeError } = await sendFeeAfter(txHash, transaction.chainId, feeTx, (f) => proof(txHash, f)));
320
324
  }
321
325
  // Stopped before or during the fee: say so, even with polling off (no fee was sent on it).
322
326
  checkpoint();
@@ -357,19 +361,97 @@ async function executeAndTrack(client, params, handlers, options = {}) {
357
361
  report("swap_sent", { statusRef });
358
362
  return { execution, quoteToken, resetTxHash: partial.resetTxHash, approvalTxHash, statusRef, finalStatus: await pollStatus(statusRef) };
359
363
  }
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.
364
+ // DEPOSIT, unsent: the user moves funds to execution.deposit.address out-of-band, and this
365
+ // helper has no way to know when that happens. Returned without polling; start polling yourself
366
+ // (getStatus / useRavnStatus) once the deposit is actually sent. Pay `feePayment` as it says,
367
+ // pass an EVM fee transaction's hash as getStatus's `txHash`, and poll RAVN to a terminal status
368
+ // even if the venue's own tracker says delivered first.
365
369
  // A fee it can't describe never stops the deposit, which fills without it.
370
+ const { statusRef, deposit, calls, fee } = execution;
371
+ const unsent = (depositNotSent) => {
372
+ const base = { execution, quoteToken, statusRef, ...(depositNotSent ? { depositNotSent } : {}) };
373
+ try {
374
+ return { ...base, feePayment: depositFeePayment(execution) };
375
+ }
376
+ catch (feeError) {
377
+ return { ...base, feeError };
378
+ }
379
+ };
366
380
  checkpoint();
367
- try {
368
- return { execution, quoteToken, statusRef: execution.statusRef, feePayment: depositFeePayment(execution) };
381
+ if (!options.sendDeposit)
382
+ return unsent();
383
+ // Typed away, but a newer API can answer with a type this version doesn't know: never sent.
384
+ const type = execution.executionType;
385
+ if (type !== "DEPOSIT")
386
+ return unsent(`executionType ${type} isn't one this SDK version can send`);
387
+ // sendDeposit: from the payload RAVN built, RAVN's fee inside it.
388
+ if (calls?.length) {
389
+ const feeTx = fee?.transaction;
390
+ const [depositTx, ...rest] = calls;
391
+ // As RAVN builds them: deposit.amount to deposit.address (the coin, or the token's
392
+ // transfer(address,uint256) call), then the fee exactly as given.
393
+ const same = (a, b) => a.to?.toLowerCase() === b.to?.toLowerCase() && a.data?.toLowerCase() === b.data?.toLowerCase() && BigInt(a.value ?? "0") === BigInt(b.value ?? "0");
394
+ const to = deposit.address.toLowerCase();
395
+ const amount = BigInt(deposit.amount);
396
+ const value = BigInt(depositTx.value ?? "0");
397
+ const paysDeposit = (value === amount && depositTx.to?.toLowerCase() === to && (depositTx.data ?? "0x") === "0x") ||
398
+ (value === 0n && depositTx.data?.toLowerCase() === `0xa9059cbb${to.slice(2).padStart(64, "0")}${amount.toString(16).padStart(64, "0")}`);
399
+ if (!paysDeposit || (feeTx ? !(rest.length === 1 && same(rest[0], feeTx)) : rest.length !== 0)) {
400
+ return unsent("its calls aren't the deposit of deposit.amount to deposit.address, then RAVN's fee as given");
401
+ }
402
+ const evmHash = (hash) => (EVM_TX_HASH.test(hash) ? hash : undefined);
403
+ let txHash;
404
+ let feeTxHash;
405
+ let feeError;
406
+ checkpoint();
407
+ if (feeTx && execution.batch && handlers.sendCalls) {
408
+ const hashes = await handlers.sendCalls(calls);
409
+ sent = true;
410
+ // Fewer hashes back than calls: the last one carried the rest.
411
+ if (!hashes[0])
412
+ throw new Error("executeAndTrack: sendCalls returned no transaction hash");
413
+ txHash = hashes[0];
414
+ feeTxHash = hashes[Math.min(1, hashes.length - 1)];
415
+ report("swap_sent", { txHash, statusRef });
416
+ report("fee_sent", { feeTxHash, statusTxHash: evmHash(feeTxHash) });
417
+ }
418
+ else {
419
+ txHash = await handlers.sendTransaction(depositTx);
420
+ sent = true;
421
+ report("swap_sent", { txHash, statusRef });
422
+ if (feeTx)
423
+ ({ feeTxHash, feeError } = await sendFeeAfter(txHash, depositTx.chainId, feeTx, evmHash));
424
+ }
425
+ checkpoint();
426
+ const statusTxHash = feeTxHash && evmHash(feeTxHash);
427
+ if (feeTxHash && !statusTxHash) {
428
+ feeError ?? (feeError = new Error(`executeAndTrack: ${feeTxHash} is not an on-chain transaction hash, so RAVN can't prove the fee`));
429
+ }
430
+ partial.feeError = feeError;
431
+ return {
432
+ execution, quoteToken, txHash, statusRef, statusTxHash, feeTxHash, feeError,
433
+ finalStatus: await pollStatus(statusRef, statusTxHash, !!statusTxHash),
434
+ };
369
435
  }
370
- catch (feeError) {
371
- return { execution, quoteToken, statusRef: execution.statusRef, feeError };
436
+ // Bitcoin and Solana: one transaction pays the deposit and RAVN's fee; RAVN finds the fee there.
437
+ const payload = deposit.psbt ?? deposit.transaction;
438
+ if (!payload) {
439
+ const chainId = deposit.chainId ?? fee?.chainId;
440
+ return unsent(chainId === BITCOIN_CHAIN_ID
441
+ ? "RAVN returned no PSBT for it (it builds one only from a bc1q… userAddress, when it can within a few seconds)"
442
+ : chainId === SOLANA_CHAIN_ID
443
+ ? "RAVN returned no transaction for it (it builds one only for a deposit carrying RAVN's fee, when it can within a few seconds)"
444
+ : "RAVN returned nothing ready to sign for it");
372
445
  }
446
+ const handler = deposit.psbt ? "sendBitcoinPsbt" : "sendSolanaTransaction";
447
+ if (!handlers[handler])
448
+ return unsent(`no ${handler} handler was provided`);
449
+ checkpoint();
450
+ // Called as a method: a class-instance wallet needs its `this`.
451
+ const txHash = await handlers[handler](payload);
452
+ sent = true;
453
+ report("swap_sent", { txHash, statusRef });
454
+ return { execution, quoteToken, txHash, statusRef, finalStatus: await pollStatus(statusRef) };
373
455
  };
374
456
  try {
375
457
  return await run();
package/dist/types.d.ts CHANGED
@@ -175,25 +175,28 @@ export type ExecutionDTO = {
175
175
  };
176
176
  /**
177
177
  * Only when your key sets a fee and the route has no fee of its own (Eco, Layerswap, Mayan's
178
- * routes paying no referrer) from an EVM chain: RAVN's fee, a transfer of the input token to
179
- * RAVN; `transaction` already leaves it out. From Solana there is none to send: the fee is a
180
- * transfer inside `transaction.serialized`. Skip it and the swap still fills; no fee is owed to
181
- * you. A wallet that batches (EIP-5792 `wallet_sendCalls`) or relays (smart account, 7702,
182
- * 4337) may send `fee.transaction` with `transaction` in one confirmation (one fee-bearing swap
183
- * per transaction: a second fee in it isn't proven), for an ERC-20 sell
184
- * (a native-coin fee must be a plain transaction of its own, so a contract wallet that can't
185
- * send one — a Safe, a 4337 account — can't pay it). Pass the on-chain hash of the transaction
186
- * that carried the fee (not an EIP-5792 batch id or a 4337 userOpHash) as `txHash` to getStatus
187
- * — not needed with no `statusRef` here, when the fee rode in the swap's own transaction. Sent
188
- * on its own, send it once the swap has confirmed. A fee paid on a swap the venue refunds
189
- * stays with RAVN.
178
+ * routes paying no referrer, Fly Trade's coin sells where its own fee can't be proven:
179
+ * Robinhood Chain, Arbitrum and others) from an EVM chain: RAVN's fee, a transfer of the input
180
+ * token to RAVN, or on Robinhood Chain and Arbitrum a coin sell's fee as WETH minted to RAVN
181
+ * (the coin sent to WETH's `depositTo`; `fee.token` in the quote is WETH); `transaction`
182
+ * already leaves it out. From Solana there is none to send: the fee is a transfer inside
183
+ * `transaction.serialized`. Skip it and the swap still fills; no fee is owed to you. A wallet
184
+ * that batches (EIP-5792 `wallet_sendCalls`) or relays (smart account, 7702, 4337) may send
185
+ * `fee.transaction` with `transaction` in one confirmation (one fee-bearing swap per
186
+ * transaction: a second fee in it isn't proven), for an ERC-20 sell, or a coin sell whose fee
187
+ * is WETH minted to RAVN (`batch` true; a native-coin fee must otherwise be a plain transaction
188
+ * of its own, so a contract wallet that can't send one — a Safe, a 4337 account — can't pay
189
+ * it). Pass the on-chain hash of the transaction that carried the fee (not an EIP-5792 batch id
190
+ * or a 4337 userOpHash) as `txHash` to getStatus — not needed with no `statusRef` here, when
191
+ * the fee rode in the swap's own transaction. Sent on its own, send it once the swap has
192
+ * confirmed. A fee paid on a swap the venue refunds stays with RAVN.
190
193
  */
191
194
  fee?: DepositFeeDTO;
192
195
  /** EVM: everything to send, in order (approval.reset, approval, the swap or the deposit
193
196
  * transfer, RAVN's fee). Each once the one before has succeeded (stop if one reverts), or, when
194
197
  * `batch` is true, all in one atomic wallet batch (EIP-5792 `wallet_sendCalls`,
195
198
  * `atomicRequired: true`): simulate it first, and send them one by one if that fails or the
196
- * last call needs more than ~400k gas. */
199
+ * swap's call needs more than ~400k gas. */
197
200
  calls?: CallDTO[];
198
201
  /** Whether `calls` (more than one) may go in one atomic wallet batch. */
199
202
  batch?: boolean;
@@ -249,7 +252,7 @@ export type ExecutionDTO = {
249
252
  * transfer, RAVN's fee). Each once the one before has succeeded (stop if one reverts), or, when
250
253
  * `batch` is true, all in one atomic wallet batch (EIP-5792 `wallet_sendCalls`,
251
254
  * `atomicRequired: true`): simulate it first, and send them one by one if that fails or the
252
- * last call needs more than ~400k gas. */
255
+ * swap's call needs more than ~400k gas. */
253
256
  calls?: CallDTO[];
254
257
  /** Whether `calls` (more than one) may go in one atomic wallet batch. */
255
258
  batch?: boolean;
@@ -264,14 +267,17 @@ export interface CallDTO {
264
267
  /** RAVN's integrator fee beside a deposit (or a transaction), in the input token, paid by your user
265
268
  * to RAVN; your share is paid out as usual. `deposit.amount` (the transaction) already leaves it out. */
266
269
  export interface DepositFeeDTO {
267
- /** Base units of the input token. */
270
+ /** Base units of the input token (the same in WETH, 18 decimals, where a coin sell's fee is
271
+ * minted as WETH). */
268
272
  amount: string;
269
273
  /** RAVN's fee address on the input chain. */
270
274
  address: string;
271
275
  chainId: number;
272
276
  /** EVM: send this transaction too, exactly as given (it carries a tag naming this swap): on its
273
277
  * own, or in the same wallet batch or relay as the deposit or swap (EIP-5792, smart account, 7702,
274
- * 4337) — an ERC-20 fee anywhere; a native-coin fee only beside a NEAR Intents deposit on
278
+ * 4337) — an ERC-20 fee anywhere, and a coin sell's fee minted to RAVN as WETH (Robinhood Chain,
279
+ * Arbitrum: the coin sent to WETH's `depositTo`, booked and paid out in WETH, proven by WETH's
280
+ * Transfer log wherever it rides); a native-coin fee only beside a NEAR Intents deposit on
275
281
  * Ethereum, Base or Polygon (proven from those chains' internal transfers). Any other native-coin
276
282
  * fee must be a plain transaction of its own (a contract wallet that can't send one can't pay it).
277
283
  * Then pass the on-chain hash of the transaction that carried it (not a batch id or a userOpHash)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ravnexchange/sdk",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",