@owney/sdk 0.7.28-beta.4 → 0.7.28-beta.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.
package/dist/index.cjs CHANGED
@@ -2884,6 +2884,33 @@ function swapChain(chainId) {
2884
2884
  return CHAINS[chainId];
2885
2885
  }
2886
2886
 
2887
+ // src/lib/swap/swap.gas-check.ts
2888
+ var CLASSIC_SWAP_GAS = 300000n;
2889
+ var APPROVE_GAS2 = 60000n;
2890
+ var isNative = (address) => address.toLowerCase().startsWith("0xeeee");
2891
+ async function swapGasShortfall(deps, quote) {
2892
+ if (quote.rail !== "classic" || isNative(quote.src.address)) return null;
2893
+ const gas = CLASSIC_SWAP_GAS + APPROVE_GAS2;
2894
+ const [balance, gasPrice] = await Promise.all([
2895
+ deps.readNativeBalance(),
2896
+ deps.getMaxFeePerGas()
2897
+ ]);
2898
+ const required = gas * gasPrice;
2899
+ return balance < required ? { chainId: quote.src.chainId, balance, required } : null;
2900
+ }
2901
+ function swapGasError(shortfall, nativeSymbol) {
2902
+ return new OwneyError(
2903
+ "SWAP_INSUFFICIENT_GAS",
2904
+ `This swap needs a little ${nativeSymbol} for network fees, and your wallet has too little. Add some ${nativeSymbol}, or choose a different asset to receive.`,
2905
+ {
2906
+ chainId: shortfall.chainId,
2907
+ symbol: nativeSymbol,
2908
+ balance: shortfall.balance.toString(),
2909
+ required: shortfall.required.toString()
2910
+ }
2911
+ );
2912
+ }
2913
+
2887
2914
  // src/lib/swap/swap.arrival.ts
2888
2915
  var DEFAULT_TIMEOUT_MS = 18e4;
2889
2916
  var DEFAULT_POLL_MS = 4e3;
@@ -3400,8 +3427,8 @@ async function runClassic(deps, options) {
3400
3427
  onStage?.("quoting");
3401
3428
  debugLog("owney-sdk", "swap: fetching classic calldata");
3402
3429
  let { tx } = await deps.api.swapTx(swapTxRequest);
3403
- const isNative = BigInt(tx.value ?? "0") > 0n;
3404
- if (!isNative) {
3430
+ const isNative2 = BigInt(tx.value ?? "0") > 0n;
3431
+ if (!isNative2) {
3405
3432
  const needed = amount;
3406
3433
  const current = await deps.readAllowance(tx.to);
3407
3434
  debugLog("owney-sdk", "swap: allowance", {
@@ -4467,6 +4494,15 @@ function makeSponsoredCallsCallback(deps) {
4467
4494
 
4468
4495
  // src/client.ts
4469
4496
  var WALLET_CATCHUP_TIMEOUT_MS = 3e4;
4497
+ function shortfallBalance(error, asked) {
4498
+ if (!(error instanceof OwneyError)) return void 0;
4499
+ if (error.code !== "DEPOSIT_INSUFFICIENT_BALANCE") return void 0;
4500
+ const seen = error.details?.balance;
4501
+ if (typeof seen !== "string") return void 0;
4502
+ const balance = BigInt(seen);
4503
+ if (balance <= 0n || balance >= asked) return void 0;
4504
+ return (asked - balance) * 100n <= asked ? balance : void 0;
4505
+ }
4470
4506
  function encodeMultiAgentCursor(map) {
4471
4507
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4472
4508
  }
@@ -5673,6 +5709,38 @@ var OwneySDK = class {
5673
5709
  const balance = await this.readTokenBalance(chainId, token);
5674
5710
  return (balance < received ? balance : received).toString();
5675
5711
  }
5712
+ /**
5713
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
5714
+ *
5715
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
5716
+ * deposit's own pre-check can disagree: the approval is confirmed through
5717
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
5718
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
5719
+ * ROUT-618 failed that way with the settle step in place.
5720
+ *
5721
+ * The pre-check reports the balance it saw, and that is the authority, so
5722
+ * a shortfall is retried once at that figure. Only a small one: a node that
5723
+ * has not caught up can read near zero, and depositing that would leave the
5724
+ * rest of the swap stranded behind a "success".
5725
+ */
5726
+ async depositSwapOutput(options) {
5727
+ const attempt = (amount) => this.deposit({
5728
+ amount,
5729
+ asset: options.asset,
5730
+ ...options.agentId ? { agentId: options.agentId } : {}
5731
+ });
5732
+ try {
5733
+ return {
5734
+ deposit: await attempt(options.amount),
5735
+ deposited: options.amount
5736
+ };
5737
+ } catch (error) {
5738
+ const balance = shortfallBalance(error, BigInt(options.amount));
5739
+ if (balance === void 0) throw error;
5740
+ const deposited = balance.toString();
5741
+ return { deposit: await attempt(deposited), deposited };
5742
+ }
5743
+ }
5676
5744
  async walletSeesBalance(chainId, token, amount) {
5677
5745
  const provider = this.requireConnectedProvider();
5678
5746
  const state = this.requireState();
@@ -5698,9 +5766,39 @@ var OwneySDK = class {
5698
5766
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
5699
5767
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
5700
5768
  */
5701
- runSwap(quote, options) {
5769
+ async runSwap(quote, options) {
5770
+ await this.checkSwapGas(quote);
5702
5771
  return executeSwap(this.buildSwapDeps(quote), { quote, ...options });
5703
5772
  }
5773
+ /**
5774
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
5775
+ *
5776
+ * Only the classic rail is ever refused: it is the one where the user sends
5777
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
5778
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
5779
+ * then the funds have already left the agent (ROUT-622).
5780
+ */
5781
+ async checkSwapGas(quote) {
5782
+ const state = this.requireState();
5783
+ const chain = swapChain(quote.src.chainId);
5784
+ if (!chain) return;
5785
+ const pub = (0, import_viem9.createPublicClient)({
5786
+ chain,
5787
+ transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
5788
+ });
5789
+ const shortfall = await swapGasShortfall(
5790
+ {
5791
+ readNativeBalance: () => pub.getBalance({ address: state.walletAddress }),
5792
+ // The 1559 ceiling, as the wrap reserve reads it.
5793
+ getMaxFeePerGas: async () => {
5794
+ const fees = await pub.estimateFeesPerGas();
5795
+ return fees.maxFeePerGas ?? pub.getGasPrice();
5796
+ }
5797
+ },
5798
+ quote
5799
+ );
5800
+ if (shortfall) throw swapGasError(shortfall, chain.nativeCurrency.symbol);
5801
+ }
5704
5802
  /**
5705
5803
  * The half of the swap deps that only ever talks to the routing API.
5706
5804
  *
@@ -5772,8 +5870,15 @@ var OwneySDK = class {
5772
5870
  );
5773
5871
  }
5774
5872
  await this.ensureSwapChain(entry.chainId);
5775
- const deposit = await this.deposit({
5776
- amount: entry.received,
5873
+ const token = entry.symbol === "WETH" ? SPONSORED_WETH_BY_CHAIN[entry.chainId] : void 0;
5874
+ const amount = token ? await this.settleSwapDepositAmount(
5875
+ entry.chainId,
5876
+ token,
5877
+ entry.symbol,
5878
+ BigInt(entry.received)
5879
+ ) : entry.received;
5880
+ const { deposit } = await this.depositSwapOutput({
5881
+ amount,
5777
5882
  asset: entry.symbol,
5778
5883
  ...entry.agentId ? { agentId: entry.agentId } : {}
5779
5884
  });
@@ -6013,12 +6118,12 @@ var OwneySDK = class {
6013
6118
  options.to.symbol,
6014
6119
  BigInt(swap.received)
6015
6120
  );
6016
- const deposit = await this.deposit({
6121
+ const { deposit, deposited } = await this.depositSwapOutput({
6017
6122
  amount,
6018
6123
  asset: options.to.symbol,
6019
6124
  ...options.agentId ? { agentId: options.agentId } : {}
6020
6125
  });
6021
- return { swap, deposit, deposited: amount };
6126
+ return { swap, deposit, deposited };
6022
6127
  } catch (error) {
6023
6128
  const pending = {
6024
6129
  id: `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
@@ -6122,6 +6227,7 @@ var OwneySDK = class {
6122
6227
  walletAddress: state.walletAddress
6123
6228
  });
6124
6229
  await this.ensureSwapChain(quote.src.chainId);
6230
+ await this.checkSwapGas(quote);
6125
6231
  const swap = await executeSwap(this.buildSwapDeps(quote), {
6126
6232
  quote,
6127
6233
  walletAddress: state.walletAddress,
package/dist/index.d.cts CHANGED
@@ -1393,12 +1393,36 @@ declare class OwneySDK {
1393
1393
  * where the sponsor covers the transfer and Permit2 never applies.
1394
1394
  */
1395
1395
  private settleSwapDepositAmount;
1396
+ /**
1397
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
1398
+ *
1399
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
1400
+ * deposit's own pre-check can disagree: the approval is confirmed through
1401
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
1402
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
1403
+ * ROUT-618 failed that way with the settle step in place.
1404
+ *
1405
+ * The pre-check reports the balance it saw, and that is the authority, so
1406
+ * a shortfall is retried once at that figure. Only a small one: a node that
1407
+ * has not caught up can read near zero, and depositing that would leave the
1408
+ * rest of the swap stranded behind a "success".
1409
+ */
1410
+ private depositSwapOutput;
1396
1411
  private walletSeesBalance;
1397
1412
  /**
1398
1413
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
1399
1414
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
1400
1415
  */
1401
1416
  private runSwap;
1417
+ /**
1418
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
1419
+ *
1420
+ * Only the classic rail is ever refused: it is the one where the user sends
1421
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
1422
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
1423
+ * then the funds have already left the agent (ROUT-622).
1424
+ */
1425
+ checkSwapGas(quote: SwapQuote): Promise<void>;
1402
1426
  /**
1403
1427
  * The half of the swap deps that only ever talks to the routing API.
1404
1428
  *
package/dist/index.d.ts CHANGED
@@ -1393,12 +1393,36 @@ declare class OwneySDK {
1393
1393
  * where the sponsor covers the transfer and Permit2 never applies.
1394
1394
  */
1395
1395
  private settleSwapDepositAmount;
1396
+ /**
1397
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
1398
+ *
1399
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
1400
+ * deposit's own pre-check can disagree: the approval is confirmed through
1401
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
1402
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
1403
+ * ROUT-618 failed that way with the settle step in place.
1404
+ *
1405
+ * The pre-check reports the balance it saw, and that is the authority, so
1406
+ * a shortfall is retried once at that figure. Only a small one: a node that
1407
+ * has not caught up can read near zero, and depositing that would leave the
1408
+ * rest of the swap stranded behind a "success".
1409
+ */
1410
+ private depositSwapOutput;
1396
1411
  private walletSeesBalance;
1397
1412
  /**
1398
1413
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
1399
1414
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
1400
1415
  */
1401
1416
  private runSwap;
1417
+ /**
1418
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
1419
+ *
1420
+ * Only the classic rail is ever refused: it is the one where the user sends
1421
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
1422
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
1423
+ * then the funds have already left the agent (ROUT-622).
1424
+ */
1425
+ checkSwapGas(quote: SwapQuote): Promise<void>;
1402
1426
  /**
1403
1427
  * The half of the swap deps that only ever talks to the routing API.
1404
1428
  *
package/dist/index.js CHANGED
@@ -2854,6 +2854,33 @@ function swapChain(chainId) {
2854
2854
  return CHAINS[chainId];
2855
2855
  }
2856
2856
 
2857
+ // src/lib/swap/swap.gas-check.ts
2858
+ var CLASSIC_SWAP_GAS = 300000n;
2859
+ var APPROVE_GAS2 = 60000n;
2860
+ var isNative = (address) => address.toLowerCase().startsWith("0xeeee");
2861
+ async function swapGasShortfall(deps, quote) {
2862
+ if (quote.rail !== "classic" || isNative(quote.src.address)) return null;
2863
+ const gas = CLASSIC_SWAP_GAS + APPROVE_GAS2;
2864
+ const [balance, gasPrice] = await Promise.all([
2865
+ deps.readNativeBalance(),
2866
+ deps.getMaxFeePerGas()
2867
+ ]);
2868
+ const required = gas * gasPrice;
2869
+ return balance < required ? { chainId: quote.src.chainId, balance, required } : null;
2870
+ }
2871
+ function swapGasError(shortfall, nativeSymbol) {
2872
+ return new OwneyError(
2873
+ "SWAP_INSUFFICIENT_GAS",
2874
+ `This swap needs a little ${nativeSymbol} for network fees, and your wallet has too little. Add some ${nativeSymbol}, or choose a different asset to receive.`,
2875
+ {
2876
+ chainId: shortfall.chainId,
2877
+ symbol: nativeSymbol,
2878
+ balance: shortfall.balance.toString(),
2879
+ required: shortfall.required.toString()
2880
+ }
2881
+ );
2882
+ }
2883
+
2857
2884
  // src/lib/swap/swap.arrival.ts
2858
2885
  var DEFAULT_TIMEOUT_MS = 18e4;
2859
2886
  var DEFAULT_POLL_MS = 4e3;
@@ -3370,8 +3397,8 @@ async function runClassic(deps, options) {
3370
3397
  onStage?.("quoting");
3371
3398
  debugLog("owney-sdk", "swap: fetching classic calldata");
3372
3399
  let { tx } = await deps.api.swapTx(swapTxRequest);
3373
- const isNative = BigInt(tx.value ?? "0") > 0n;
3374
- if (!isNative) {
3400
+ const isNative2 = BigInt(tx.value ?? "0") > 0n;
3401
+ if (!isNative2) {
3375
3402
  const needed = amount;
3376
3403
  const current = await deps.readAllowance(tx.to);
3377
3404
  debugLog("owney-sdk", "swap: allowance", {
@@ -4448,6 +4475,15 @@ function makeSponsoredCallsCallback(deps) {
4448
4475
 
4449
4476
  // src/client.ts
4450
4477
  var WALLET_CATCHUP_TIMEOUT_MS = 3e4;
4478
+ function shortfallBalance(error, asked) {
4479
+ if (!(error instanceof OwneyError)) return void 0;
4480
+ if (error.code !== "DEPOSIT_INSUFFICIENT_BALANCE") return void 0;
4481
+ const seen = error.details?.balance;
4482
+ if (typeof seen !== "string") return void 0;
4483
+ const balance = BigInt(seen);
4484
+ if (balance <= 0n || balance >= asked) return void 0;
4485
+ return (asked - balance) * 100n <= asked ? balance : void 0;
4486
+ }
4451
4487
  function encodeMultiAgentCursor(map) {
4452
4488
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4453
4489
  }
@@ -5654,6 +5690,38 @@ var OwneySDK = class {
5654
5690
  const balance = await this.readTokenBalance(chainId, token);
5655
5691
  return (balance < received ? balance : received).toString();
5656
5692
  }
5693
+ /**
5694
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
5695
+ *
5696
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
5697
+ * deposit's own pre-check can disagree: the approval is confirmed through
5698
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
5699
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
5700
+ * ROUT-618 failed that way with the settle step in place.
5701
+ *
5702
+ * The pre-check reports the balance it saw, and that is the authority, so
5703
+ * a shortfall is retried once at that figure. Only a small one: a node that
5704
+ * has not caught up can read near zero, and depositing that would leave the
5705
+ * rest of the swap stranded behind a "success".
5706
+ */
5707
+ async depositSwapOutput(options) {
5708
+ const attempt = (amount) => this.deposit({
5709
+ amount,
5710
+ asset: options.asset,
5711
+ ...options.agentId ? { agentId: options.agentId } : {}
5712
+ });
5713
+ try {
5714
+ return {
5715
+ deposit: await attempt(options.amount),
5716
+ deposited: options.amount
5717
+ };
5718
+ } catch (error) {
5719
+ const balance = shortfallBalance(error, BigInt(options.amount));
5720
+ if (balance === void 0) throw error;
5721
+ const deposited = balance.toString();
5722
+ return { deposit: await attempt(deposited), deposited };
5723
+ }
5724
+ }
5657
5725
  async walletSeesBalance(chainId, token, amount) {
5658
5726
  const provider = this.requireConnectedProvider();
5659
5727
  const state = this.requireState();
@@ -5679,9 +5747,39 @@ var OwneySDK = class {
5679
5747
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
5680
5748
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
5681
5749
  */
5682
- runSwap(quote, options) {
5750
+ async runSwap(quote, options) {
5751
+ await this.checkSwapGas(quote);
5683
5752
  return executeSwap(this.buildSwapDeps(quote), { quote, ...options });
5684
5753
  }
5754
+ /**
5755
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
5756
+ *
5757
+ * Only the classic rail is ever refused: it is the one where the user sends
5758
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
5759
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
5760
+ * then the funds have already left the agent (ROUT-622).
5761
+ */
5762
+ async checkSwapGas(quote) {
5763
+ const state = this.requireState();
5764
+ const chain = swapChain(quote.src.chainId);
5765
+ if (!chain) return;
5766
+ const pub = createPublicClient2({
5767
+ chain,
5768
+ transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
5769
+ });
5770
+ const shortfall = await swapGasShortfall(
5771
+ {
5772
+ readNativeBalance: () => pub.getBalance({ address: state.walletAddress }),
5773
+ // The 1559 ceiling, as the wrap reserve reads it.
5774
+ getMaxFeePerGas: async () => {
5775
+ const fees = await pub.estimateFeesPerGas();
5776
+ return fees.maxFeePerGas ?? pub.getGasPrice();
5777
+ }
5778
+ },
5779
+ quote
5780
+ );
5781
+ if (shortfall) throw swapGasError(shortfall, chain.nativeCurrency.symbol);
5782
+ }
5685
5783
  /**
5686
5784
  * The half of the swap deps that only ever talks to the routing API.
5687
5785
  *
@@ -5753,8 +5851,15 @@ var OwneySDK = class {
5753
5851
  );
5754
5852
  }
5755
5853
  await this.ensureSwapChain(entry.chainId);
5756
- const deposit = await this.deposit({
5757
- amount: entry.received,
5854
+ const token = entry.symbol === "WETH" ? SPONSORED_WETH_BY_CHAIN[entry.chainId] : void 0;
5855
+ const amount = token ? await this.settleSwapDepositAmount(
5856
+ entry.chainId,
5857
+ token,
5858
+ entry.symbol,
5859
+ BigInt(entry.received)
5860
+ ) : entry.received;
5861
+ const { deposit } = await this.depositSwapOutput({
5862
+ amount,
5758
5863
  asset: entry.symbol,
5759
5864
  ...entry.agentId ? { agentId: entry.agentId } : {}
5760
5865
  });
@@ -5994,12 +6099,12 @@ var OwneySDK = class {
5994
6099
  options.to.symbol,
5995
6100
  BigInt(swap.received)
5996
6101
  );
5997
- const deposit = await this.deposit({
6102
+ const { deposit, deposited } = await this.depositSwapOutput({
5998
6103
  amount,
5999
6104
  asset: options.to.symbol,
6000
6105
  ...options.agentId ? { agentId: options.agentId } : {}
6001
6106
  });
6002
- return { swap, deposit, deposited: amount };
6107
+ return { swap, deposit, deposited };
6003
6108
  } catch (error) {
6004
6109
  const pending = {
6005
6110
  id: `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
@@ -6103,6 +6208,7 @@ var OwneySDK = class {
6103
6208
  walletAddress: state.walletAddress
6104
6209
  });
6105
6210
  await this.ensureSwapChain(quote.src.chainId);
6211
+ await this.checkSwapGas(quote);
6106
6212
  const swap = await executeSwap(this.buildSwapDeps(quote), {
6107
6213
  quote,
6108
6214
  walletAddress: state.walletAddress,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.28-beta.4",
3
+ "version": "0.7.28-beta.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",