@owney/sdk 0.7.28-beta.4 → 0.7.28-beta.6

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
@@ -523,23 +523,33 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
523
523
  function historyCoverage(raw) {
524
524
  const chains = /* @__PURE__ */ new Map();
525
525
  const assets = /* @__PURE__ */ new Map();
526
+ const pairs = /* @__PURE__ */ new Map();
526
527
  for (const date of Object.keys(raw.history ?? {}).sort()) {
527
528
  void date;
528
529
  const seenChains = /* @__PURE__ */ new Set();
529
530
  const seenAssets = /* @__PURE__ */ new Set();
531
+ const seenPairs = /* @__PURE__ */ new Map();
530
532
  for (const p of (raw.history ?? {})[date].positions ?? []) {
531
533
  const balance = p.balance ?? 0;
532
534
  const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
533
535
  if (balance <= 0 || typeof apy !== "number") continue;
534
536
  seenChains.add(p.chainId);
535
- if (p.tokenSymbol) seenAssets.add(p.tokenSymbol.toUpperCase());
537
+ if (p.tokenSymbol) {
538
+ const asset = p.tokenSymbol.toUpperCase();
539
+ seenAssets.add(asset);
540
+ seenPairs.set(`${p.chainId}:${asset}`, { chainId: p.chainId, asset });
541
+ }
536
542
  }
537
543
  for (const c of seenChains) chains.set(c, (chains.get(c) ?? 0) + 1);
538
544
  for (const a of seenAssets) assets.set(a, (assets.get(a) ?? 0) + 1);
545
+ for (const [key2, pair] of seenPairs) {
546
+ pairs.set(key2, { ...pair, days: (pairs.get(key2)?.days ?? 0) + 1 });
547
+ }
539
548
  }
540
549
  return {
541
550
  chains: [...chains].map(([chainId, days]) => ({ chainId, days })),
542
- assets: [...assets].map(([asset, days]) => ({ asset, days }))
551
+ assets: [...assets].map(([asset, days]) => ({ asset, days })),
552
+ pairs: [...pairs.values()]
543
553
  };
544
554
  }
545
555
  function mapApyHistory(raw, chainId, tokenSymbol) {
@@ -584,6 +594,9 @@ function mapEntries(rawEntries, chainId) {
584
594
  // "Top up wallet" and for slightly less (gas). `Deposit` already exists in
585
595
  // the public HistoryAction union, so no consumer type change is needed.
586
596
  "Deposit to protocol": "Deposit",
597
+ // Withdrawing from an async vault (Superform) is logged under its own
598
+ // label and paid out later in a separate batch (ROUT-624).
599
+ "Request async withdrawal": "Withdraw",
587
600
  Rebalance: "Rebalance"
588
601
  };
589
602
  void chainId;
@@ -626,8 +639,9 @@ function mapEntries(rawEntries, chainId) {
626
639
  ];
627
640
  }
628
641
  const mappedAction = (entry.action ? actionMap[entry.action] : void 0) ?? entry.action;
642
+ const asyncRequest = entry.action === "Request async withdrawal";
629
643
  const rawAmount = entry?.positions?.[0]?.amount;
630
- const hasAmount = mappedAction !== "Withdraw" && typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
644
+ const hasAmount = (mappedAction !== "Withdraw" || asyncRequest) && typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
631
645
  const mappedPositions = positions.filter((p) => p.protocol_name).map((p) => ({
632
646
  protocol: p.protocol_name,
633
647
  pool: p.pool ?? void 0,
@@ -641,6 +655,7 @@ function mapEntries(rawEntries, chainId) {
641
655
  newApy: newApy === null ? null : String(newApy),
642
656
  oldApy: oldApy === null ? null : String(oldApy),
643
657
  ...mappedPositions.length ? { positions: mappedPositions } : {},
658
+ ...asyncRequest ? { asyncRequest: true } : {},
644
659
  transactions: [
645
660
  {
646
661
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
@@ -1281,7 +1296,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1281
1296
  */
1282
1297
  async resolveWithdrawnAmounts(entries, chainId, recipient) {
1283
1298
  const withdrawals = entries.filter(
1284
- (e) => e.action === "Withdraw" && e.transactions[0]?.txHashes[0]
1299
+ (e) => e.action === "Withdraw" && !e.asyncRequest && e.transactions[0]?.txHashes[0]
1285
1300
  );
1286
1301
  if (withdrawals.length === 0) return;
1287
1302
  const client = this.getPublicClient(chainId);
@@ -1787,6 +1802,44 @@ var ZyfaiAgent = class _ZyfaiAgent {
1787
1802
  );
1788
1803
  }
1789
1804
  }
1805
+ /**
1806
+ * Read back what `setPoolSelection` wrote (ROUT-650). Zyfai keeps the saved
1807
+ * pools per protocol/chain and the strategy per asset; both are read for the
1808
+ * pairs and assets asked about. A pair still on autoselect, or one whose
1809
+ * lookup fails, is left out so the caller falls back to its default for it.
1810
+ */
1811
+ async getPoolSelection(params) {
1812
+ const [pairs, strategies] = await Promise.all([
1813
+ Promise.all(
1814
+ params.pairs.map(async ({ protocolId, chainId }) => {
1815
+ try {
1816
+ const current = await this.sdk.getSelectedPools(
1817
+ protocolId,
1818
+ chainId
1819
+ );
1820
+ if (current.autoselect) return null;
1821
+ return { protocolId, chainId, pools: current.pools ?? [] };
1822
+ } catch {
1823
+ return null;
1824
+ }
1825
+ })
1826
+ ),
1827
+ Promise.all(
1828
+ params.assets.map(async (asset) => {
1829
+ try {
1830
+ const current = await this.sdk.getUserDetails(asset);
1831
+ return current.strategy ? { asset, strategy: current.strategy } : null;
1832
+ } catch {
1833
+ return null;
1834
+ }
1835
+ })
1836
+ )
1837
+ ]);
1838
+ return {
1839
+ pairs: pairs.filter((value) => value !== null),
1840
+ strategies: strategies.filter((value) => value !== null)
1841
+ };
1842
+ }
1790
1843
  /**
1791
1844
  * True when `smartWallet` is the backend-managed pool wallet Zyfai assigned
1792
1845
  * to this EOA — the "new wallet" kind that is predeployed on the chains it
@@ -2884,6 +2937,33 @@ function swapChain(chainId) {
2884
2937
  return CHAINS[chainId];
2885
2938
  }
2886
2939
 
2940
+ // src/lib/swap/swap.gas-check.ts
2941
+ var CLASSIC_SWAP_GAS = 300000n;
2942
+ var APPROVE_GAS2 = 60000n;
2943
+ var isNative = (address) => address.toLowerCase().startsWith("0xeeee");
2944
+ async function swapGasShortfall(deps, quote) {
2945
+ if (quote.rail !== "classic" || isNative(quote.src.address)) return null;
2946
+ const gas = CLASSIC_SWAP_GAS + APPROVE_GAS2;
2947
+ const [balance, gasPrice] = await Promise.all([
2948
+ deps.readNativeBalance(),
2949
+ deps.getMaxFeePerGas()
2950
+ ]);
2951
+ const required = gas * gasPrice;
2952
+ return balance < required ? { chainId: quote.src.chainId, balance, required } : null;
2953
+ }
2954
+ function swapGasError(shortfall, nativeSymbol) {
2955
+ return new OwneyError(
2956
+ "SWAP_INSUFFICIENT_GAS",
2957
+ `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.`,
2958
+ {
2959
+ chainId: shortfall.chainId,
2960
+ symbol: nativeSymbol,
2961
+ balance: shortfall.balance.toString(),
2962
+ required: shortfall.required.toString()
2963
+ }
2964
+ );
2965
+ }
2966
+
2887
2967
  // src/lib/swap/swap.arrival.ts
2888
2968
  var DEFAULT_TIMEOUT_MS = 18e4;
2889
2969
  var DEFAULT_POLL_MS = 4e3;
@@ -3400,8 +3480,8 @@ async function runClassic(deps, options) {
3400
3480
  onStage?.("quoting");
3401
3481
  debugLog("owney-sdk", "swap: fetching classic calldata");
3402
3482
  let { tx } = await deps.api.swapTx(swapTxRequest);
3403
- const isNative = BigInt(tx.value ?? "0") > 0n;
3404
- if (!isNative) {
3483
+ const isNative2 = BigInt(tx.value ?? "0") > 0n;
3484
+ if (!isNative2) {
3405
3485
  const needed = amount;
3406
3486
  const current = await deps.readAllowance(tx.to);
3407
3487
  debugLog("owney-sdk", "swap: allowance", {
@@ -4467,6 +4547,15 @@ function makeSponsoredCallsCallback(deps) {
4467
4547
 
4468
4548
  // src/client.ts
4469
4549
  var WALLET_CATCHUP_TIMEOUT_MS = 3e4;
4550
+ function shortfallBalance(error, asked) {
4551
+ if (!(error instanceof OwneyError)) return void 0;
4552
+ if (error.code !== "DEPOSIT_INSUFFICIENT_BALANCE") return void 0;
4553
+ const seen = error.details?.balance;
4554
+ if (typeof seen !== "string") return void 0;
4555
+ const balance = BigInt(seen);
4556
+ if (balance <= 0n || balance >= asked) return void 0;
4557
+ return (asked - balance) * 100n <= asked ? balance : void 0;
4558
+ }
4470
4559
  function encodeMultiAgentCursor(map) {
4471
4560
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4472
4561
  }
@@ -5476,6 +5565,19 @@ var OwneySDK = class {
5476
5565
  chain: dstChain,
5477
5566
  transport: swapReadTransport(quote.dst.chainId, this.zyfaiRpcUrls)
5478
5567
  });
5568
+ const walletPublic = (0, import_viem9.createPublicClient)({
5569
+ chain: srcChain,
5570
+ transport: (0, import_viem9.custom)(provider)
5571
+ });
5572
+ const signOnSource = (payload) => signWithChainRetry(
5573
+ walletPublic,
5574
+ wallet,
5575
+ quote.src.chainId,
5576
+ () => wallet.signTypedData({
5577
+ account: state.walletAddress,
5578
+ ...payload
5579
+ })
5580
+ );
5479
5581
  return {
5480
5582
  api: this.swapApi(),
5481
5583
  // Native-aware, like readSourceBalance below. Native ETH is never a
@@ -5516,10 +5618,7 @@ var OwneySDK = class {
5516
5618
  }
5517
5619
  return hash;
5518
5620
  },
5519
- signTypedData: (typedData) => wallet.signTypedData({
5520
- account: state.walletAddress,
5521
- ...typedData
5522
- }),
5621
+ signTypedData: signOnSource,
5523
5622
  // Chain-bound like every other read here: the wallet provider's chain is
5524
5623
  // not ours to rely on mid-swap.
5525
5624
  readSourceBalance: async () => {
@@ -5576,10 +5675,7 @@ var OwneySDK = class {
5576
5675
  spender,
5577
5676
  value: amount,
5578
5677
  chainId: quote.src.chainId,
5579
- signTypedData: (payload) => wallet.signTypedData({
5580
- account: state.walletAddress,
5581
- ...payload
5582
- })
5678
+ signTypedData: signOnSource
5583
5679
  }
5584
5680
  ),
5585
5681
  /**
@@ -5673,6 +5769,38 @@ var OwneySDK = class {
5673
5769
  const balance = await this.readTokenBalance(chainId, token);
5674
5770
  return (balance < received ? balance : received).toString();
5675
5771
  }
5772
+ /**
5773
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
5774
+ *
5775
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
5776
+ * deposit's own pre-check can disagree: the approval is confirmed through
5777
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
5778
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
5779
+ * ROUT-618 failed that way with the settle step in place.
5780
+ *
5781
+ * The pre-check reports the balance it saw, and that is the authority, so
5782
+ * a shortfall is retried once at that figure. Only a small one: a node that
5783
+ * has not caught up can read near zero, and depositing that would leave the
5784
+ * rest of the swap stranded behind a "success".
5785
+ */
5786
+ async depositSwapOutput(options) {
5787
+ const attempt = (amount) => this.deposit({
5788
+ amount,
5789
+ asset: options.asset,
5790
+ ...options.agentId ? { agentId: options.agentId } : {}
5791
+ });
5792
+ try {
5793
+ return {
5794
+ deposit: await attempt(options.amount),
5795
+ deposited: options.amount
5796
+ };
5797
+ } catch (error) {
5798
+ const balance = shortfallBalance(error, BigInt(options.amount));
5799
+ if (balance === void 0) throw error;
5800
+ const deposited = balance.toString();
5801
+ return { deposit: await attempt(deposited), deposited };
5802
+ }
5803
+ }
5676
5804
  async walletSeesBalance(chainId, token, amount) {
5677
5805
  const provider = this.requireConnectedProvider();
5678
5806
  const state = this.requireState();
@@ -5698,9 +5826,39 @@ var OwneySDK = class {
5698
5826
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
5699
5827
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
5700
5828
  */
5701
- runSwap(quote, options) {
5829
+ async runSwap(quote, options) {
5830
+ await this.checkSwapGas(quote);
5702
5831
  return executeSwap(this.buildSwapDeps(quote), { quote, ...options });
5703
5832
  }
5833
+ /**
5834
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
5835
+ *
5836
+ * Only the classic rail is ever refused: it is the one where the user sends
5837
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
5838
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
5839
+ * then the funds have already left the agent (ROUT-622).
5840
+ */
5841
+ async checkSwapGas(quote) {
5842
+ const state = this.requireState();
5843
+ const chain = swapChain(quote.src.chainId);
5844
+ if (!chain) return;
5845
+ const pub = (0, import_viem9.createPublicClient)({
5846
+ chain,
5847
+ transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
5848
+ });
5849
+ const shortfall = await swapGasShortfall(
5850
+ {
5851
+ readNativeBalance: () => pub.getBalance({ address: state.walletAddress }),
5852
+ // The 1559 ceiling, as the wrap reserve reads it.
5853
+ getMaxFeePerGas: async () => {
5854
+ const fees = await pub.estimateFeesPerGas();
5855
+ return fees.maxFeePerGas ?? pub.getGasPrice();
5856
+ }
5857
+ },
5858
+ quote
5859
+ );
5860
+ if (shortfall) throw swapGasError(shortfall, chain.nativeCurrency.symbol);
5861
+ }
5704
5862
  /**
5705
5863
  * The half of the swap deps that only ever talks to the routing API.
5706
5864
  *
@@ -5772,8 +5930,15 @@ var OwneySDK = class {
5772
5930
  );
5773
5931
  }
5774
5932
  await this.ensureSwapChain(entry.chainId);
5775
- const deposit = await this.deposit({
5776
- amount: entry.received,
5933
+ const token = entry.symbol === "WETH" ? SPONSORED_WETH_BY_CHAIN[entry.chainId] : void 0;
5934
+ const amount = token ? await this.settleSwapDepositAmount(
5935
+ entry.chainId,
5936
+ token,
5937
+ entry.symbol,
5938
+ BigInt(entry.received)
5939
+ ) : entry.received;
5940
+ const { deposit } = await this.depositSwapOutput({
5941
+ amount,
5777
5942
  asset: entry.symbol,
5778
5943
  ...entry.agentId ? { agentId: entry.agentId } : {}
5779
5944
  });
@@ -6013,12 +6178,12 @@ var OwneySDK = class {
6013
6178
  options.to.symbol,
6014
6179
  BigInt(swap.received)
6015
6180
  );
6016
- const deposit = await this.deposit({
6181
+ const { deposit, deposited } = await this.depositSwapOutput({
6017
6182
  amount,
6018
6183
  asset: options.to.symbol,
6019
6184
  ...options.agentId ? { agentId: options.agentId } : {}
6020
6185
  });
6021
- return { swap, deposit, deposited: amount };
6186
+ return { swap, deposit, deposited };
6022
6187
  } catch (error) {
6023
6188
  const pending = {
6024
6189
  id: `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
@@ -6122,6 +6287,7 @@ var OwneySDK = class {
6122
6287
  walletAddress: state.walletAddress
6123
6288
  });
6124
6289
  await this.ensureSwapChain(quote.src.chainId);
6290
+ await this.checkSwapGas(quote);
6125
6291
  const swap = await executeSwap(this.buildSwapDeps(quote), {
6126
6292
  quote,
6127
6293
  walletAddress: state.walletAddress,
@@ -6632,6 +6798,25 @@ var OwneySDK = class {
6632
6798
  }
6633
6799
  await agent.setPoolSelection(params.selection);
6634
6800
  }
6801
+ /**
6802
+ * The pool selection saved on the user's account (ROUT-650), so the Pools
6803
+ * step can show what applies now rather than the defaults. `pairs` and
6804
+ * `assets` say which protocol/chain lists and strategies to read.
6805
+ */
6806
+ async getPoolSelection(params) {
6807
+ await this.ensureAgentsInitialized();
6808
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6809
+ if (!agent.getPoolSelection) {
6810
+ throw new OwneyError(
6811
+ "POOL_SELECTION_UNSUPPORTED",
6812
+ `Agent ${agent.id} does not support pool customization`
6813
+ );
6814
+ }
6815
+ return agent.getPoolSelection({
6816
+ pairs: params.pairs,
6817
+ assets: params.assets
6818
+ });
6819
+ }
6635
6820
  async getAccountApy({
6636
6821
  agentId,
6637
6822
  days,
package/dist/index.d.cts CHANGED
@@ -563,6 +563,22 @@ type UserPoolSelectionRequest = {
563
563
  }>;
564
564
  selections: UserPoolSelection[];
565
565
  };
566
+ /**
567
+ * What a user's account currently has saved (ROUT-650): each asset's strategy,
568
+ * plus the pool list of every protocol/chain pair frozen by an earlier Save.
569
+ * Pairs Zyfai still autoselects are left out, so "no entry" means "default".
570
+ */
571
+ type SavedPoolSelection = {
572
+ strategies: Array<{
573
+ asset: "USDC" | "WETH";
574
+ strategy: string;
575
+ }>;
576
+ pairs: Array<{
577
+ protocolId: string;
578
+ chainId: number;
579
+ pools: string[];
580
+ }>;
581
+ };
566
582
  /** One protocol with its pools for the deposit asset — what the Pools step
567
583
  * renders: a protocol row that opens to a list of selectable pools. */
568
584
  type PoolCatalogProtocol = {
@@ -723,6 +739,14 @@ interface ApyCoverage {
723
739
  asset: string;
724
740
  days: number;
725
741
  }[];
742
+ /** Each asset on each chain, for a menu that offers the two together
743
+ * (ROUT-647). The lists above cannot say this: USDC on Base and WETH on
744
+ * Arbitrum put both chains and both assets in them, and no WETH on Base. */
745
+ pairs: {
746
+ chainId: number;
747
+ asset: string;
748
+ days: number;
749
+ }[];
726
750
  }
727
751
  interface AccountAgentApy {
728
752
  walletAddress: string;
@@ -802,6 +826,13 @@ interface AgentHistoryEntry {
802
826
  transactions: HistoryTransaction[];
803
827
  rebalanceLog: RebalanceLog[];
804
828
  positions?: AgentHistoryPosition[];
829
+ /**
830
+ * Set on a `Withdraw` that was only requested from an async vault
831
+ * (ROUT-624). The vault pays out in batches later, so the funds have not
832
+ * reached the wallet yet and `transactions[0].amount` is the amount
833
+ * requested.
834
+ */
835
+ asyncRequest?: true;
805
836
  }
806
837
  interface OwneyAgentHistory {
807
838
  data: AgentHistoryEntry[];
@@ -914,6 +945,17 @@ interface IAgent {
914
945
  * (ROUT-569)
915
946
  */
916
947
  setPoolSelection?(selection: UserPoolSelectionRequest): Promise<void>;
948
+ /**
949
+ * Read back what `setPoolSelection` saved, for the pairs and assets given,
950
+ * so the Pools step can open on the user's own choices. Optional. (ROUT-650)
951
+ */
952
+ getPoolSelection?(params: {
953
+ pairs: Array<{
954
+ protocolId: string;
955
+ chainId: number;
956
+ }>;
957
+ assets: Array<"USDC" | "WETH">;
958
+ }): Promise<SavedPoolSelection>;
917
959
  deposit(state: ConnectionState, chainId: number, amount: string, asset: OwneySupportedTokens, depositCallback?: DepositCallback): Promise<OwneyDepositResult>;
918
960
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
919
961
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
@@ -1393,12 +1435,36 @@ declare class OwneySDK {
1393
1435
  * where the sponsor covers the transfer and Permit2 never applies.
1394
1436
  */
1395
1437
  private settleSwapDepositAmount;
1438
+ /**
1439
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
1440
+ *
1441
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
1442
+ * deposit's own pre-check can disagree: the approval is confirmed through
1443
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
1444
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
1445
+ * ROUT-618 failed that way with the settle step in place.
1446
+ *
1447
+ * The pre-check reports the balance it saw, and that is the authority, so
1448
+ * a shortfall is retried once at that figure. Only a small one: a node that
1449
+ * has not caught up can read near zero, and depositing that would leave the
1450
+ * rest of the swap stranded behind a "success".
1451
+ */
1452
+ private depositSwapOutput;
1396
1453
  private walletSeesBalance;
1397
1454
  /**
1398
1455
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
1399
1456
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
1400
1457
  */
1401
1458
  private runSwap;
1459
+ /**
1460
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
1461
+ *
1462
+ * Only the classic rail is ever refused: it is the one where the user sends
1463
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
1464
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
1465
+ * then the funds have already left the agent (ROUT-622).
1466
+ */
1467
+ checkSwapGas(quote: SwapQuote): Promise<void>;
1402
1468
  /**
1403
1469
  * The half of the swap deps that only ever talks to the routing API.
1404
1470
  *
@@ -1758,6 +1824,19 @@ declare class OwneySDK {
1758
1824
  selection: UserPoolSelectionRequest;
1759
1825
  agentId?: AgentId;
1760
1826
  }): Promise<void>;
1827
+ /**
1828
+ * The pool selection saved on the user's account (ROUT-650), so the Pools
1829
+ * step can show what applies now rather than the defaults. `pairs` and
1830
+ * `assets` say which protocol/chain lists and strategies to read.
1831
+ */
1832
+ getPoolSelection(params: {
1833
+ pairs: Array<{
1834
+ protocolId: string;
1835
+ chainId: number;
1836
+ }>;
1837
+ assets: Array<"USDC" | "WETH">;
1838
+ agentId?: AgentId;
1839
+ }): Promise<SavedPoolSelection>;
1761
1840
  getAccountApy({ agentId, days, tokenSymbol, chainId: scope, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1762
1841
  /**
1763
1842
  * Get transaction history for a specific agent, or all agents.
@@ -1900,4 +1979,4 @@ type StoredOrder = {
1900
1979
  */
1901
1980
  declare function listOrders(now?: number): StoredOrder[];
1902
1981
 
1903
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
1982
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SavedPoolSelection, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.d.ts CHANGED
@@ -563,6 +563,22 @@ type UserPoolSelectionRequest = {
563
563
  }>;
564
564
  selections: UserPoolSelection[];
565
565
  };
566
+ /**
567
+ * What a user's account currently has saved (ROUT-650): each asset's strategy,
568
+ * plus the pool list of every protocol/chain pair frozen by an earlier Save.
569
+ * Pairs Zyfai still autoselects are left out, so "no entry" means "default".
570
+ */
571
+ type SavedPoolSelection = {
572
+ strategies: Array<{
573
+ asset: "USDC" | "WETH";
574
+ strategy: string;
575
+ }>;
576
+ pairs: Array<{
577
+ protocolId: string;
578
+ chainId: number;
579
+ pools: string[];
580
+ }>;
581
+ };
566
582
  /** One protocol with its pools for the deposit asset — what the Pools step
567
583
  * renders: a protocol row that opens to a list of selectable pools. */
568
584
  type PoolCatalogProtocol = {
@@ -723,6 +739,14 @@ interface ApyCoverage {
723
739
  asset: string;
724
740
  days: number;
725
741
  }[];
742
+ /** Each asset on each chain, for a menu that offers the two together
743
+ * (ROUT-647). The lists above cannot say this: USDC on Base and WETH on
744
+ * Arbitrum put both chains and both assets in them, and no WETH on Base. */
745
+ pairs: {
746
+ chainId: number;
747
+ asset: string;
748
+ days: number;
749
+ }[];
726
750
  }
727
751
  interface AccountAgentApy {
728
752
  walletAddress: string;
@@ -802,6 +826,13 @@ interface AgentHistoryEntry {
802
826
  transactions: HistoryTransaction[];
803
827
  rebalanceLog: RebalanceLog[];
804
828
  positions?: AgentHistoryPosition[];
829
+ /**
830
+ * Set on a `Withdraw` that was only requested from an async vault
831
+ * (ROUT-624). The vault pays out in batches later, so the funds have not
832
+ * reached the wallet yet and `transactions[0].amount` is the amount
833
+ * requested.
834
+ */
835
+ asyncRequest?: true;
805
836
  }
806
837
  interface OwneyAgentHistory {
807
838
  data: AgentHistoryEntry[];
@@ -914,6 +945,17 @@ interface IAgent {
914
945
  * (ROUT-569)
915
946
  */
916
947
  setPoolSelection?(selection: UserPoolSelectionRequest): Promise<void>;
948
+ /**
949
+ * Read back what `setPoolSelection` saved, for the pairs and assets given,
950
+ * so the Pools step can open on the user's own choices. Optional. (ROUT-650)
951
+ */
952
+ getPoolSelection?(params: {
953
+ pairs: Array<{
954
+ protocolId: string;
955
+ chainId: number;
956
+ }>;
957
+ assets: Array<"USDC" | "WETH">;
958
+ }): Promise<SavedPoolSelection>;
917
959
  deposit(state: ConnectionState, chainId: number, amount: string, asset: OwneySupportedTokens, depositCallback?: DepositCallback): Promise<OwneyDepositResult>;
918
960
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
919
961
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
@@ -1393,12 +1435,36 @@ declare class OwneySDK {
1393
1435
  * where the sponsor covers the transfer and Permit2 never applies.
1394
1436
  */
1395
1437
  private settleSwapDepositAmount;
1438
+ /**
1439
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
1440
+ *
1441
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
1442
+ * deposit's own pre-check can disagree: the approval is confirmed through
1443
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
1444
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
1445
+ * ROUT-618 failed that way with the settle step in place.
1446
+ *
1447
+ * The pre-check reports the balance it saw, and that is the authority, so
1448
+ * a shortfall is retried once at that figure. Only a small one: a node that
1449
+ * has not caught up can read near zero, and depositing that would leave the
1450
+ * rest of the swap stranded behind a "success".
1451
+ */
1452
+ private depositSwapOutput;
1396
1453
  private walletSeesBalance;
1397
1454
  /**
1398
1455
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
1399
1456
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
1400
1457
  */
1401
1458
  private runSwap;
1459
+ /**
1460
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
1461
+ *
1462
+ * Only the classic rail is ever refused: it is the one where the user sends
1463
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
1464
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
1465
+ * then the funds have already left the agent (ROUT-622).
1466
+ */
1467
+ checkSwapGas(quote: SwapQuote): Promise<void>;
1402
1468
  /**
1403
1469
  * The half of the swap deps that only ever talks to the routing API.
1404
1470
  *
@@ -1758,6 +1824,19 @@ declare class OwneySDK {
1758
1824
  selection: UserPoolSelectionRequest;
1759
1825
  agentId?: AgentId;
1760
1826
  }): Promise<void>;
1827
+ /**
1828
+ * The pool selection saved on the user's account (ROUT-650), so the Pools
1829
+ * step can show what applies now rather than the defaults. `pairs` and
1830
+ * `assets` say which protocol/chain lists and strategies to read.
1831
+ */
1832
+ getPoolSelection(params: {
1833
+ pairs: Array<{
1834
+ protocolId: string;
1835
+ chainId: number;
1836
+ }>;
1837
+ assets: Array<"USDC" | "WETH">;
1838
+ agentId?: AgentId;
1839
+ }): Promise<SavedPoolSelection>;
1761
1840
  getAccountApy({ agentId, days, tokenSymbol, chainId: scope, }: AccountApyOptions): Promise<OwneyAccountApy | AccountAgentApy>;
1762
1841
  /**
1763
1842
  * Get transaction history for a specific agent, or all agents.
@@ -1900,4 +1979,4 @@ type StoredOrder = {
1900
1979
  */
1901
1980
  declare function listOrders(now?: number): StoredOrder[];
1902
1981
 
1903
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
1982
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyCoverage, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type AvailablePool, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OrgPoolPolicy, type OrgPoolSelection, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawAllLeg, type OwneyWithdrawAllResult, type OwneyWithdrawResult, type PendingDeposit, type PoolCatalogProtocol, type PoolSelectionStrategy, type RebalanceLog, type SavedPoolSelection, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type UserPoolSelection, type UserPoolSelectionRequest, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.js CHANGED
@@ -483,23 +483,33 @@ function rawPoolApyForChain(entry, chainId, tokenSymbol) {
483
483
  function historyCoverage(raw) {
484
484
  const chains = /* @__PURE__ */ new Map();
485
485
  const assets = /* @__PURE__ */ new Map();
486
+ const pairs = /* @__PURE__ */ new Map();
486
487
  for (const date of Object.keys(raw.history ?? {}).sort()) {
487
488
  void date;
488
489
  const seenChains = /* @__PURE__ */ new Set();
489
490
  const seenAssets = /* @__PURE__ */ new Set();
491
+ const seenPairs = /* @__PURE__ */ new Map();
490
492
  for (const p of (raw.history ?? {})[date].positions ?? []) {
491
493
  const balance = p.balance ?? 0;
492
494
  const apy = netApy(p.apy_withFee, p.apy, "apy_withFee");
493
495
  if (balance <= 0 || typeof apy !== "number") continue;
494
496
  seenChains.add(p.chainId);
495
- if (p.tokenSymbol) seenAssets.add(p.tokenSymbol.toUpperCase());
497
+ if (p.tokenSymbol) {
498
+ const asset = p.tokenSymbol.toUpperCase();
499
+ seenAssets.add(asset);
500
+ seenPairs.set(`${p.chainId}:${asset}`, { chainId: p.chainId, asset });
501
+ }
496
502
  }
497
503
  for (const c of seenChains) chains.set(c, (chains.get(c) ?? 0) + 1);
498
504
  for (const a of seenAssets) assets.set(a, (assets.get(a) ?? 0) + 1);
505
+ for (const [key2, pair] of seenPairs) {
506
+ pairs.set(key2, { ...pair, days: (pairs.get(key2)?.days ?? 0) + 1 });
507
+ }
499
508
  }
500
509
  return {
501
510
  chains: [...chains].map(([chainId, days]) => ({ chainId, days })),
502
- assets: [...assets].map(([asset, days]) => ({ asset, days }))
511
+ assets: [...assets].map(([asset, days]) => ({ asset, days })),
512
+ pairs: [...pairs.values()]
503
513
  };
504
514
  }
505
515
  function mapApyHistory(raw, chainId, tokenSymbol) {
@@ -544,6 +554,9 @@ function mapEntries(rawEntries, chainId) {
544
554
  // "Top up wallet" and for slightly less (gas). `Deposit` already exists in
545
555
  // the public HistoryAction union, so no consumer type change is needed.
546
556
  "Deposit to protocol": "Deposit",
557
+ // Withdrawing from an async vault (Superform) is logged under its own
558
+ // label and paid out later in a separate batch (ROUT-624).
559
+ "Request async withdrawal": "Withdraw",
547
560
  Rebalance: "Rebalance"
548
561
  };
549
562
  void chainId;
@@ -586,8 +599,9 @@ function mapEntries(rawEntries, chainId) {
586
599
  ];
587
600
  }
588
601
  const mappedAction = (entry.action ? actionMap[entry.action] : void 0) ?? entry.action;
602
+ const asyncRequest = entry.action === "Request async withdrawal";
589
603
  const rawAmount = entry?.positions?.[0]?.amount;
590
- const hasAmount = mappedAction !== "Withdraw" && typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
604
+ const hasAmount = (mappedAction !== "Withdraw" || asyncRequest) && typeof rawAmount === "string" && rawAmount !== "" && rawAmount !== "0x" && rawAmount !== "0x0" && rawAmount !== "0";
591
605
  const mappedPositions = positions.filter((p) => p.protocol_name).map((p) => ({
592
606
  protocol: p.protocol_name,
593
607
  pool: p.pool ?? void 0,
@@ -601,6 +615,7 @@ function mapEntries(rawEntries, chainId) {
601
615
  newApy: newApy === null ? null : String(newApy),
602
616
  oldApy: oldApy === null ? null : String(oldApy),
603
617
  ...mappedPositions.length ? { positions: mappedPositions } : {},
618
+ ...asyncRequest ? { asyncRequest: true } : {},
604
619
  transactions: [
605
620
  {
606
621
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
@@ -1241,7 +1256,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
1241
1256
  */
1242
1257
  async resolveWithdrawnAmounts(entries, chainId, recipient) {
1243
1258
  const withdrawals = entries.filter(
1244
- (e) => e.action === "Withdraw" && e.transactions[0]?.txHashes[0]
1259
+ (e) => e.action === "Withdraw" && !e.asyncRequest && e.transactions[0]?.txHashes[0]
1245
1260
  );
1246
1261
  if (withdrawals.length === 0) return;
1247
1262
  const client = this.getPublicClient(chainId);
@@ -1747,6 +1762,44 @@ var ZyfaiAgent = class _ZyfaiAgent {
1747
1762
  );
1748
1763
  }
1749
1764
  }
1765
+ /**
1766
+ * Read back what `setPoolSelection` wrote (ROUT-650). Zyfai keeps the saved
1767
+ * pools per protocol/chain and the strategy per asset; both are read for the
1768
+ * pairs and assets asked about. A pair still on autoselect, or one whose
1769
+ * lookup fails, is left out so the caller falls back to its default for it.
1770
+ */
1771
+ async getPoolSelection(params) {
1772
+ const [pairs, strategies] = await Promise.all([
1773
+ Promise.all(
1774
+ params.pairs.map(async ({ protocolId, chainId }) => {
1775
+ try {
1776
+ const current = await this.sdk.getSelectedPools(
1777
+ protocolId,
1778
+ chainId
1779
+ );
1780
+ if (current.autoselect) return null;
1781
+ return { protocolId, chainId, pools: current.pools ?? [] };
1782
+ } catch {
1783
+ return null;
1784
+ }
1785
+ })
1786
+ ),
1787
+ Promise.all(
1788
+ params.assets.map(async (asset) => {
1789
+ try {
1790
+ const current = await this.sdk.getUserDetails(asset);
1791
+ return current.strategy ? { asset, strategy: current.strategy } : null;
1792
+ } catch {
1793
+ return null;
1794
+ }
1795
+ })
1796
+ )
1797
+ ]);
1798
+ return {
1799
+ pairs: pairs.filter((value) => value !== null),
1800
+ strategies: strategies.filter((value) => value !== null)
1801
+ };
1802
+ }
1750
1803
  /**
1751
1804
  * True when `smartWallet` is the backend-managed pool wallet Zyfai assigned
1752
1805
  * to this EOA — the "new wallet" kind that is predeployed on the chains it
@@ -2854,6 +2907,33 @@ function swapChain(chainId) {
2854
2907
  return CHAINS[chainId];
2855
2908
  }
2856
2909
 
2910
+ // src/lib/swap/swap.gas-check.ts
2911
+ var CLASSIC_SWAP_GAS = 300000n;
2912
+ var APPROVE_GAS2 = 60000n;
2913
+ var isNative = (address) => address.toLowerCase().startsWith("0xeeee");
2914
+ async function swapGasShortfall(deps, quote) {
2915
+ if (quote.rail !== "classic" || isNative(quote.src.address)) return null;
2916
+ const gas = CLASSIC_SWAP_GAS + APPROVE_GAS2;
2917
+ const [balance, gasPrice] = await Promise.all([
2918
+ deps.readNativeBalance(),
2919
+ deps.getMaxFeePerGas()
2920
+ ]);
2921
+ const required = gas * gasPrice;
2922
+ return balance < required ? { chainId: quote.src.chainId, balance, required } : null;
2923
+ }
2924
+ function swapGasError(shortfall, nativeSymbol) {
2925
+ return new OwneyError(
2926
+ "SWAP_INSUFFICIENT_GAS",
2927
+ `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.`,
2928
+ {
2929
+ chainId: shortfall.chainId,
2930
+ symbol: nativeSymbol,
2931
+ balance: shortfall.balance.toString(),
2932
+ required: shortfall.required.toString()
2933
+ }
2934
+ );
2935
+ }
2936
+
2857
2937
  // src/lib/swap/swap.arrival.ts
2858
2938
  var DEFAULT_TIMEOUT_MS = 18e4;
2859
2939
  var DEFAULT_POLL_MS = 4e3;
@@ -3370,8 +3450,8 @@ async function runClassic(deps, options) {
3370
3450
  onStage?.("quoting");
3371
3451
  debugLog("owney-sdk", "swap: fetching classic calldata");
3372
3452
  let { tx } = await deps.api.swapTx(swapTxRequest);
3373
- const isNative = BigInt(tx.value ?? "0") > 0n;
3374
- if (!isNative) {
3453
+ const isNative2 = BigInt(tx.value ?? "0") > 0n;
3454
+ if (!isNative2) {
3375
3455
  const needed = amount;
3376
3456
  const current = await deps.readAllowance(tx.to);
3377
3457
  debugLog("owney-sdk", "swap: allowance", {
@@ -4448,6 +4528,15 @@ function makeSponsoredCallsCallback(deps) {
4448
4528
 
4449
4529
  // src/client.ts
4450
4530
  var WALLET_CATCHUP_TIMEOUT_MS = 3e4;
4531
+ function shortfallBalance(error, asked) {
4532
+ if (!(error instanceof OwneyError)) return void 0;
4533
+ if (error.code !== "DEPOSIT_INSUFFICIENT_BALANCE") return void 0;
4534
+ const seen = error.details?.balance;
4535
+ if (typeof seen !== "string") return void 0;
4536
+ const balance = BigInt(seen);
4537
+ if (balance <= 0n || balance >= asked) return void 0;
4538
+ return (asked - balance) * 100n <= asked ? balance : void 0;
4539
+ }
4451
4540
  function encodeMultiAgentCursor(map) {
4452
4541
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
4453
4542
  }
@@ -5457,6 +5546,19 @@ var OwneySDK = class {
5457
5546
  chain: dstChain,
5458
5547
  transport: swapReadTransport(quote.dst.chainId, this.zyfaiRpcUrls)
5459
5548
  });
5549
+ const walletPublic = createPublicClient2({
5550
+ chain: srcChain,
5551
+ transport: custom(provider)
5552
+ });
5553
+ const signOnSource = (payload) => signWithChainRetry(
5554
+ walletPublic,
5555
+ wallet,
5556
+ quote.src.chainId,
5557
+ () => wallet.signTypedData({
5558
+ account: state.walletAddress,
5559
+ ...payload
5560
+ })
5561
+ );
5460
5562
  return {
5461
5563
  api: this.swapApi(),
5462
5564
  // Native-aware, like readSourceBalance below. Native ETH is never a
@@ -5497,10 +5599,7 @@ var OwneySDK = class {
5497
5599
  }
5498
5600
  return hash;
5499
5601
  },
5500
- signTypedData: (typedData) => wallet.signTypedData({
5501
- account: state.walletAddress,
5502
- ...typedData
5503
- }),
5602
+ signTypedData: signOnSource,
5504
5603
  // Chain-bound like every other read here: the wallet provider's chain is
5505
5604
  // not ours to rely on mid-swap.
5506
5605
  readSourceBalance: async () => {
@@ -5557,10 +5656,7 @@ var OwneySDK = class {
5557
5656
  spender,
5558
5657
  value: amount,
5559
5658
  chainId: quote.src.chainId,
5560
- signTypedData: (payload) => wallet.signTypedData({
5561
- account: state.walletAddress,
5562
- ...payload
5563
- })
5659
+ signTypedData: signOnSource
5564
5660
  }
5565
5661
  ),
5566
5662
  /**
@@ -5654,6 +5750,38 @@ var OwneySDK = class {
5654
5750
  const balance = await this.readTokenBalance(chainId, token);
5655
5751
  return (balance < received ? balance : received).toString();
5656
5752
  }
5753
+ /**
5754
+ * Deposit a swap's output, retrying once at the balance the deposit saw.
5755
+ *
5756
+ * `settleSwapDepositAmount` approves and THEN reads, but the read and the
5757
+ * deposit's own pre-check can disagree: the approval is confirmed through
5758
+ * the wallet's node, the read goes to a pinned RPC, and gas paid in WETH
5759
+ * (MetaMask offers this to a wallet with no ETH) can land after either.
5760
+ * ROUT-618 failed that way with the settle step in place.
5761
+ *
5762
+ * The pre-check reports the balance it saw, and that is the authority, so
5763
+ * a shortfall is retried once at that figure. Only a small one: a node that
5764
+ * has not caught up can read near zero, and depositing that would leave the
5765
+ * rest of the swap stranded behind a "success".
5766
+ */
5767
+ async depositSwapOutput(options) {
5768
+ const attempt = (amount) => this.deposit({
5769
+ amount,
5770
+ asset: options.asset,
5771
+ ...options.agentId ? { agentId: options.agentId } : {}
5772
+ });
5773
+ try {
5774
+ return {
5775
+ deposit: await attempt(options.amount),
5776
+ deposited: options.amount
5777
+ };
5778
+ } catch (error) {
5779
+ const balance = shortfallBalance(error, BigInt(options.amount));
5780
+ if (balance === void 0) throw error;
5781
+ const deposited = balance.toString();
5782
+ return { deposit: await attempt(deposited), deposited };
5783
+ }
5784
+ }
5657
5785
  async walletSeesBalance(chainId, token, amount) {
5658
5786
  const provider = this.requireConnectedProvider();
5659
5787
  const state = this.requireState();
@@ -5679,9 +5807,39 @@ var OwneySDK = class {
5679
5807
  * Runs a quoted swap. A named seam rather than an inline `executeSwap` call
5680
5808
  * so the deposit half of `swapAndDeposit` can be exercised on its own.
5681
5809
  */
5682
- runSwap(quote, options) {
5810
+ async runSwap(quote, options) {
5811
+ await this.checkSwapGas(quote);
5683
5812
  return executeSwap(this.buildSwapDeps(quote), { quote, ...options });
5684
5813
  }
5814
+ /**
5815
+ * Throws SWAP_INSUFFICIENT_GAS when the wallet cannot pay for this swap.
5816
+ *
5817
+ * Only the classic rail is ever refused: it is the one where the user sends
5818
+ * the transactions. Call it on a preview quote to warn BEFORE a withdrawal
5819
+ * moves anything — `withdrawAndSwap` repeats it on its own quote, but by
5820
+ * then the funds have already left the agent (ROUT-622).
5821
+ */
5822
+ async checkSwapGas(quote) {
5823
+ const state = this.requireState();
5824
+ const chain = swapChain(quote.src.chainId);
5825
+ if (!chain) return;
5826
+ const pub = createPublicClient2({
5827
+ chain,
5828
+ transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
5829
+ });
5830
+ const shortfall = await swapGasShortfall(
5831
+ {
5832
+ readNativeBalance: () => pub.getBalance({ address: state.walletAddress }),
5833
+ // The 1559 ceiling, as the wrap reserve reads it.
5834
+ getMaxFeePerGas: async () => {
5835
+ const fees = await pub.estimateFeesPerGas();
5836
+ return fees.maxFeePerGas ?? pub.getGasPrice();
5837
+ }
5838
+ },
5839
+ quote
5840
+ );
5841
+ if (shortfall) throw swapGasError(shortfall, chain.nativeCurrency.symbol);
5842
+ }
5685
5843
  /**
5686
5844
  * The half of the swap deps that only ever talks to the routing API.
5687
5845
  *
@@ -5753,8 +5911,15 @@ var OwneySDK = class {
5753
5911
  );
5754
5912
  }
5755
5913
  await this.ensureSwapChain(entry.chainId);
5756
- const deposit = await this.deposit({
5757
- amount: entry.received,
5914
+ const token = entry.symbol === "WETH" ? SPONSORED_WETH_BY_CHAIN[entry.chainId] : void 0;
5915
+ const amount = token ? await this.settleSwapDepositAmount(
5916
+ entry.chainId,
5917
+ token,
5918
+ entry.symbol,
5919
+ BigInt(entry.received)
5920
+ ) : entry.received;
5921
+ const { deposit } = await this.depositSwapOutput({
5922
+ amount,
5758
5923
  asset: entry.symbol,
5759
5924
  ...entry.agentId ? { agentId: entry.agentId } : {}
5760
5925
  });
@@ -5994,12 +6159,12 @@ var OwneySDK = class {
5994
6159
  options.to.symbol,
5995
6160
  BigInt(swap.received)
5996
6161
  );
5997
- const deposit = await this.deposit({
6162
+ const { deposit, deposited } = await this.depositSwapOutput({
5998
6163
  amount,
5999
6164
  asset: options.to.symbol,
6000
6165
  ...options.agentId ? { agentId: options.agentId } : {}
6001
6166
  });
6002
- return { swap, deposit, deposited: amount };
6167
+ return { swap, deposit, deposited };
6003
6168
  } catch (error) {
6004
6169
  const pending = {
6005
6170
  id: `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
@@ -6103,6 +6268,7 @@ var OwneySDK = class {
6103
6268
  walletAddress: state.walletAddress
6104
6269
  });
6105
6270
  await this.ensureSwapChain(quote.src.chainId);
6271
+ await this.checkSwapGas(quote);
6106
6272
  const swap = await executeSwap(this.buildSwapDeps(quote), {
6107
6273
  quote,
6108
6274
  walletAddress: state.walletAddress,
@@ -6613,6 +6779,25 @@ var OwneySDK = class {
6613
6779
  }
6614
6780
  await agent.setPoolSelection(params.selection);
6615
6781
  }
6782
+ /**
6783
+ * The pool selection saved on the user's account (ROUT-650), so the Pools
6784
+ * step can show what applies now rather than the defaults. `pairs` and
6785
+ * `assets` say which protocol/chain lists and strategies to read.
6786
+ */
6787
+ async getPoolSelection(params) {
6788
+ await this.ensureAgentsInitialized();
6789
+ const agent = this.getAgent(params.agentId ?? "zyfai");
6790
+ if (!agent.getPoolSelection) {
6791
+ throw new OwneyError(
6792
+ "POOL_SELECTION_UNSUPPORTED",
6793
+ `Agent ${agent.id} does not support pool customization`
6794
+ );
6795
+ }
6796
+ return agent.getPoolSelection({
6797
+ pairs: params.pairs,
6798
+ assets: params.assets
6799
+ });
6800
+ }
6616
6801
  async getAccountApy({
6617
6802
  agentId,
6618
6803
  days,
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.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",