@owney/sdk 0.7.26-beta.2 → 0.7.26-beta.3

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
@@ -2366,9 +2366,10 @@ function createSwapApi(baseUrl, apiKey) {
2366
2366
  dstSymbol: params.to.symbol,
2367
2367
  amount: params.from.amount,
2368
2368
  walletAddress: params.walletAddress,
2369
- secretHashes: params.secretHashes,
2369
+ ...params.secretHashes ? { secretHashes: params.secretHashes } : {},
2370
2370
  ...params.direction ? { direction: params.direction } : {},
2371
- ...params.receiver ? { receiver: params.receiver } : {}
2371
+ ...params.receiver ? { receiver: params.receiver } : {},
2372
+ ...params.permit ? { permit: params.permit } : {}
2372
2373
  }
2373
2374
  }),
2374
2375
  submitOrder: (body) => request(baseUrl, apiKey, "/order", { method: "POST", body }),
@@ -2381,7 +2382,16 @@ function createSwapApi(baseUrl, apiKey) {
2381
2382
  method: "POST",
2382
2383
  body: { orderHash, secret }
2383
2384
  }),
2384
- orderStatus: (orderHash) => request(baseUrl, apiKey, `/order/${orderHash}`),
2385
+ /**
2386
+ * @param chainId present only for a SAME-CHAIN order. That product keys its
2387
+ * orders per chain and the Fusion+ endpoint does not know them, so asking
2388
+ * without it answers 404 — which reads as an order that vanished.
2389
+ */
2390
+ orderStatus: (orderHash, chainId) => request(
2391
+ baseUrl,
2392
+ apiKey,
2393
+ chainId === void 0 ? `/order/${orderHash}` : `/order/${orderHash}?chainId=${chainId}`
2394
+ ),
2385
2395
  readyForSecrets: (orderHash) => request(
2386
2396
  baseUrl,
2387
2397
  apiKey,
@@ -2524,7 +2534,12 @@ var SWAP_TERMINAL_STATUSES = [
2524
2534
  "executed",
2525
2535
  "expired",
2526
2536
  "cancelled",
2527
- "refunded"
2537
+ "refunded",
2538
+ "filled",
2539
+ "false-predicate",
2540
+ "not-enough-balance-or-allowance",
2541
+ "wrong-permit",
2542
+ "invalid-signature"
2528
2543
  ];
2529
2544
  var isSwapTerminal = (status) => SWAP_TERMINAL_STATUSES.includes(status);
2530
2545
 
@@ -2602,6 +2617,71 @@ async function runFusionOrder(deps, options) {
2602
2617
  }
2603
2618
  }
2604
2619
 
2620
+ // src/lib/swap/swap.same-chain-runner.ts
2621
+ var DEFAULT_POLL_MS2 = 5e3;
2622
+ var MAX_BACKOFF_MS2 = 3e4;
2623
+ var backoffFor2 = (failures, base3) => Math.min(base3 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS2);
2624
+ var DEFAULT_TIMEOUT_MS2 = 3 * 60 * 1e3;
2625
+ function explain(status) {
2626
+ switch (status) {
2627
+ case "expired":
2628
+ return "No one filled the swap in time, so nothing was exchanged. Your funds never left your wallet.";
2629
+ case "cancelled":
2630
+ return "The swap was cancelled before anyone filled it. Your funds never left your wallet.";
2631
+ case "not-enough-balance-or-allowance":
2632
+ return "The swap could not be filled because the balance or approval had changed since it was signed. Nothing was exchanged.";
2633
+ case "wrong-permit":
2634
+ return "The approval signed with this swap was not accepted. Nothing was exchanged \u2014 try again, approving in your wallet if you are asked to.";
2635
+ case "invalid-signature":
2636
+ return "The swap signature was rejected. Nothing was exchanged.";
2637
+ case "false-predicate":
2638
+ return "The swap's conditions no longer held when it was filled. Nothing was exchanged.";
2639
+ default:
2640
+ return "The swap did not complete. Nothing was exchanged.";
2641
+ }
2642
+ }
2643
+ async function runSameChainOrder(deps, options) {
2644
+ const {
2645
+ orderHash,
2646
+ onStage,
2647
+ pollIntervalMs = DEFAULT_POLL_MS2,
2648
+ timeoutMs = DEFAULT_TIMEOUT_MS2
2649
+ } = options;
2650
+ const deadline = deps.now() + timeoutMs;
2651
+ let failures = 0;
2652
+ onStage?.("swapping");
2653
+ for (; ; ) {
2654
+ if (deps.now() >= deadline) {
2655
+ throw new OwneyError(
2656
+ "SWAP_ORDER_EXPIRED",
2657
+ "No one filled the swap in time. Nothing was exchanged and your funds are still in your wallet.",
2658
+ { orderHash }
2659
+ );
2660
+ }
2661
+ let status;
2662
+ try {
2663
+ ({ status } = await deps.orderStatus(orderHash));
2664
+ failures = 0;
2665
+ } catch {
2666
+ failures += 1;
2667
+ await deps.sleep(backoffFor2(failures, pollIntervalMs));
2668
+ continue;
2669
+ }
2670
+ if (isSwapTerminal(status)) {
2671
+ if (status === "filled" || status === "executed") {
2672
+ onStage?.("swapped");
2673
+ return { status, filled: true };
2674
+ }
2675
+ throw new OwneyError(
2676
+ status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
2677
+ explain(status),
2678
+ { orderHash, status }
2679
+ );
2680
+ }
2681
+ await deps.sleep(pollIntervalMs);
2682
+ }
2683
+ }
2684
+
2605
2685
  // src/lib/swap/swap.secret-store.ts
2606
2686
  var KEY_PREFIX2 = "owney.swap.order";
2607
2687
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
@@ -2685,7 +2765,7 @@ async function executeSwap(deps, options) {
2685
2765
  });
2686
2766
  const before = await deps.readTargetBalance();
2687
2767
  debugLog("owney-sdk", "swap: target balance before", before.toString());
2688
- const result = quote.rail === "classic" ? await runClassic(deps, options) : await runFusion(deps, options, walletAddress);
2768
+ const result = quote.rail === "classic" ? await runClassic(deps, options) : quote.rail === "fusion" ? await runSameChainFusion(deps, options, walletAddress) : await runFusion(deps, options, walletAddress);
2689
2769
  const after = await deps.readTargetBalance();
2690
2770
  const received = after - before;
2691
2771
  debugLog("owney-sdk", "swap: target balance after", {
@@ -2756,25 +2836,75 @@ async function runClassic(deps, options) {
2756
2836
  onStage?.("swapped");
2757
2837
  return { txHash };
2758
2838
  }
2759
- async function runFusion(deps, options, walletAddress) {
2839
+ async function runSameChainFusion(deps, options, walletAddress) {
2760
2840
  const { quote, direction, onStage } = options;
2761
2841
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2762
2842
  const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2763
- if (quote.spender && !isNativeSource) {
2764
- const needed = amount;
2765
- const current = await deps.readAllowance(quote.spender);
2766
- debugLog("owney-sdk", "swap: fusion allowance", {
2767
- spender: quote.spender,
2768
- current: current.toString(),
2769
- needed: needed.toString()
2843
+ const permit = await authoriseSpend(deps, options, amount, isNativeSource);
2844
+ onStage?.("quoting");
2845
+ debugLog("owney-sdk", "swap: building same-chain fusion order");
2846
+ const built = await deps.api.buildOrder({
2847
+ from: {
2848
+ chainId: quote.src.chainId,
2849
+ symbol: quote.src.symbol,
2850
+ amount: amount.toString()
2851
+ },
2852
+ to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2853
+ walletAddress,
2854
+ ...direction ? { direction } : {},
2855
+ ...permit ? { permit } : {}
2856
+ });
2857
+ debugLog("owney-sdk", "swap: same-chain order built", {
2858
+ orderHash: built.orderHash
2859
+ });
2860
+ onStage?.("signing");
2861
+ await deps.ensureChain(quote.src.chainId);
2862
+ const signature = await deps.signTypedData(built.typedData);
2863
+ debugLog("owney-sdk", "swap: signed, submitting same-chain order");
2864
+ await deps.api.submitOrder({
2865
+ // Without this the routing API hands the order to the Fusion+ relayer,
2866
+ // which does not know it and answers with an error that names nothing.
2867
+ rail: "fusion",
2868
+ srcChainId: quote.src.chainId,
2869
+ order: built.order,
2870
+ signature,
2871
+ quoteId: built.quoteId,
2872
+ ...built.extension ? { extension: built.extension } : {}
2873
+ });
2874
+ await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
2875
+ orderHash: built.orderHash,
2876
+ ...onStage ? { onStage } : {}
2877
+ });
2878
+ return { orderHash: built.orderHash };
2879
+ }
2880
+ async function authoriseSpend(deps, options, amount, isNativeSource) {
2881
+ const { quote, onStage } = options;
2882
+ if (!quote.spender || isNativeSource) return void 0;
2883
+ const current = await deps.readAllowance(quote.spender);
2884
+ debugLog("owney-sdk", "swap: fusion allowance", {
2885
+ spender: quote.spender,
2886
+ current: current.toString(),
2887
+ needed: amount.toString()
2888
+ });
2889
+ if (current >= amount) return void 0;
2890
+ onStage?.("approving");
2891
+ await deps.ensureChain(quote.src.chainId);
2892
+ const permit = quote.permitSupported ? await deps.buildPermit?.(quote.spender, MAX_UINT256) ?? void 0 : void 0;
2893
+ if (permit) {
2894
+ debugLog("owney-sdk", "swap: permitting limit order protocol", {
2895
+ bytes: (permit.length - 2) / 2
2770
2896
  });
2771
- if (current < needed) {
2772
- onStage?.("approving");
2773
- await deps.ensureChain(quote.src.chainId);
2774
- await deps.approve(quote.spender, MAX_UINT256);
2775
- debugLog("owney-sdk", "swap: approved limit order protocol");
2776
- }
2897
+ return permit;
2777
2898
  }
2899
+ await deps.approve(quote.spender, MAX_UINT256);
2900
+ debugLog("owney-sdk", "swap: approved limit order protocol");
2901
+ return void 0;
2902
+ }
2903
+ async function runFusion(deps, options, walletAddress) {
2904
+ const { quote, direction, onStage } = options;
2905
+ const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2906
+ const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2907
+ const permit = await authoriseSpend(deps, options, amount, isNativeSource);
2778
2908
  const { secrets, secretHashes } = mintSecrets(quote.secretsCount ?? 1);
2779
2909
  onStage?.("quoting");
2780
2910
  debugLog("owney-sdk", "swap: building fusion order", {
@@ -2790,7 +2920,8 @@ async function runFusion(deps, options, walletAddress) {
2790
2920
  to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2791
2921
  walletAddress,
2792
2922
  secretHashes,
2793
- ...direction ? { direction } : {}
2923
+ ...direction ? { direction } : {},
2924
+ ...permit ? { permit } : {}
2794
2925
  });
2795
2926
  saveOrder({
2796
2927
  orderHash: built.orderHash,
@@ -2841,16 +2972,114 @@ async function runFusion(deps, options, walletAddress) {
2841
2972
  return { orderHash: built.orderHash };
2842
2973
  }
2843
2974
 
2975
+ // src/lib/swap/swap.permit.ts
2976
+ var import_viem5 = require("viem");
2977
+ var PERMIT_ABI = (0, import_viem5.parseAbi)([
2978
+ "function nonces(address owner) view returns (uint256)",
2979
+ "function name() view returns (string)",
2980
+ "function version() view returns (string)",
2981
+ "function DOMAIN_SEPARATOR() view returns (bytes32)"
2982
+ ]);
2983
+ var PERMIT_TYPES = {
2984
+ Permit: [
2985
+ { name: "owner", type: "address" },
2986
+ { name: "spender", type: "address" },
2987
+ { name: "value", type: "uint256" },
2988
+ { name: "nonce", type: "uint256" },
2989
+ { name: "deadline", type: "uint256" }
2990
+ ]
2991
+ };
2992
+ var VERSION_CANDIDATES = ["1", "2"];
2993
+ var PERMIT_TTL_SECONDS = 3600;
2994
+ async function tryRead(read) {
2995
+ try {
2996
+ return await read();
2997
+ } catch {
2998
+ return null;
2999
+ }
3000
+ }
3001
+ function matchDomain(name, versions, chainId, token, onChainSeparator) {
3002
+ for (const version of versions) {
3003
+ const domain = { name, version, chainId, verifyingContract: token };
3004
+ if ((0, import_viem5.domainSeparator)({ domain }) === onChainSeparator) return domain;
3005
+ }
3006
+ return null;
3007
+ }
3008
+ async function buildErc2612Permit(reads, input) {
3009
+ const { token, owner, spender, value, chainId } = input;
3010
+ const call = (functionName, args) => reads.readContract({
3011
+ address: token,
3012
+ abi: PERMIT_ABI,
3013
+ functionName,
3014
+ ...args ? { args } : {}
3015
+ });
3016
+ const nonce = await tryRead(() => call("nonces", [owner]));
3017
+ if (nonce === null) {
3018
+ debugLog("owney-sdk", "permit: token has no nonces(), using approval", {
3019
+ token
3020
+ });
3021
+ return null;
3022
+ }
3023
+ const separator = await tryRead(
3024
+ () => call("DOMAIN_SEPARATOR")
3025
+ );
3026
+ const name = await tryRead(() => call("name"));
3027
+ if (!separator || !name) {
3028
+ debugLog("owney-sdk", "permit: domain unverifiable, using approval", {
3029
+ token,
3030
+ hasSeparator: Boolean(separator),
3031
+ hasName: Boolean(name)
3032
+ });
3033
+ return null;
3034
+ }
3035
+ const declared = await tryRead(() => call("version"));
3036
+ const domain = matchDomain(
3037
+ name,
3038
+ declared ? [declared, ...VERSION_CANDIDATES] : VERSION_CANDIDATES,
3039
+ chainId,
3040
+ token,
3041
+ separator
3042
+ );
3043
+ if (!domain) {
3044
+ debugLog("owney-sdk", "permit: no domain matched, using approval", {
3045
+ token,
3046
+ declared
3047
+ });
3048
+ return null;
3049
+ }
3050
+ const deadline = BigInt(Math.floor((input.now?.() ?? Date.now()) / 1e3)) + BigInt(PERMIT_TTL_SECONDS);
3051
+ const signature = await input.signTypedData({
3052
+ domain,
3053
+ types: PERMIT_TYPES,
3054
+ primaryType: "Permit",
3055
+ message: { owner, spender, value, nonce, deadline }
3056
+ });
3057
+ const { r, s, v, yParity } = (0, import_viem5.parseSignature)(signature);
3058
+ const recoveryV = v ?? BigInt(yParity + 27);
3059
+ return (0, import_viem5.encodeAbiParameters)(
3060
+ [
3061
+ { type: "address" },
3062
+ { type: "address" },
3063
+ { type: "uint256" },
3064
+ { type: "uint256" },
3065
+ { type: "uint8" },
3066
+ { type: "bytes32" },
3067
+ { type: "bytes32" }
3068
+ ],
3069
+ [owner, spender, value, deadline, Number(recoveryV), r, s]
3070
+ );
3071
+ }
3072
+
2844
3073
  // src/lib/swap/swap.arrival.ts
2845
- var DEFAULT_TIMEOUT_MS2 = 18e4;
2846
- var DEFAULT_POLL_MS2 = 4e3;
3074
+ var DEFAULT_TIMEOUT_MS3 = 18e4;
3075
+ var DEFAULT_POLL_MS3 = 4e3;
2847
3076
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2848
3077
  async function awaitWithdrawalArrival(options) {
2849
3078
  const {
2850
3079
  readBalance,
2851
3080
  baseline,
2852
- timeoutMs = DEFAULT_TIMEOUT_MS2,
2853
- pollMs = DEFAULT_POLL_MS2
3081
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
3082
+ pollMs = DEFAULT_POLL_MS3
2854
3083
  } = options;
2855
3084
  const deadline = Date.now() + timeoutMs;
2856
3085
  debugLog("owney-sdk", "withdraw: waiting for funds to land", {
@@ -2925,7 +3154,7 @@ async function withFailureReporting(apiKey, agentType, fn, baseUrl) {
2925
3154
  }
2926
3155
 
2927
3156
  // src/lib/helpers/withdraw-helper.ts
2928
- var import_viem5 = require("viem");
3157
+ var import_viem6 = require("viem");
2929
3158
  function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decimals) {
2930
3159
  const target = asset.toUpperCase();
2931
3160
  return agents.map((agent) => {
@@ -2934,7 +3163,7 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
2934
3163
  (t) => t.chainId === chainId && t.asset.toUpperCase() === target
2935
3164
  );
2936
3165
  if (!tokenBalance) return { agent, balance: 0n };
2937
- return { agent, balance: (0, import_viem5.parseUnits)(tokenBalance.amount, decimals) };
3166
+ return { agent, balance: (0, import_viem6.parseUnits)(tokenBalance.amount, decimals) };
2938
3167
  });
2939
3168
  }
2940
3169
  function planProportionalShares(balances, requested, totalAvailable) {
@@ -3081,11 +3310,11 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
3081
3310
  }
3082
3311
 
3083
3312
  // src/client.ts
3084
- var import_viem8 = require("viem");
3313
+ var import_viem9 = require("viem");
3085
3314
  var import_chains2 = require("viem/chains");
3086
3315
 
3087
3316
  // src/lib/transfer-auth.ts
3088
- var import_viem6 = require("viem");
3317
+ var import_viem7 = require("viem");
3089
3318
  var ERC20_META_ABI = [
3090
3319
  { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] },
3091
3320
  { type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] }
@@ -3117,7 +3346,7 @@ async function readTokenMeta(publicClient, token) {
3117
3346
  function randomAuthNonce() {
3118
3347
  const bytes = new Uint8Array(32);
3119
3348
  globalThis.crypto.getRandomValues(bytes);
3120
- return (0, import_viem6.bytesToHex)(bytes);
3349
+ return (0, import_viem7.bytesToHex)(bytes);
3121
3350
  }
3122
3351
 
3123
3352
  // src/lib/sponsor-client.ts
@@ -3399,7 +3628,7 @@ function makeSponsoredWethCallback(deps) {
3399
3628
  }
3400
3629
 
3401
3630
  // src/lib/sponsored-calls-deposit.ts
3402
- var import_viem7 = require("viem");
3631
+ var import_viem8 = require("viem");
3403
3632
  var DEFAULT_POLL_INTERVAL_MS = 1500;
3404
3633
  var DEFAULT_MAX_POLLS = 30;
3405
3634
  async function paymasterSupported(provider, owner, chainId) {
@@ -3407,7 +3636,7 @@ async function paymasterSupported(provider, owner, chainId) {
3407
3636
  method: "wallet_getCapabilities",
3408
3637
  params: [owner]
3409
3638
  });
3410
- const forChain = caps?.[(0, import_viem7.toHex)(chainId)] ?? caps?.[String(chainId)];
3639
+ const forChain = caps?.[(0, import_viem8.toHex)(chainId)] ?? caps?.[String(chainId)];
3411
3640
  return Boolean(forChain?.paymasterService?.supported);
3412
3641
  }
3413
3642
  function makeSponsoredCallsCallback(deps) {
@@ -3441,8 +3670,8 @@ function makeSponsoredCallsCallback(deps) {
3441
3670
  { chainId }
3442
3671
  );
3443
3672
  }
3444
- const data = (0, import_viem7.encodeFunctionData)({
3445
- abi: import_viem7.erc20Abi,
3673
+ const data = (0, import_viem8.encodeFunctionData)({
3674
+ abi: import_viem8.erc20Abi,
3446
3675
  functionName: "transfer",
3447
3676
  args: [smartWallet, BigInt(amount)]
3448
3677
  });
@@ -3452,7 +3681,7 @@ function makeSponsoredCallsCallback(deps) {
3452
3681
  {
3453
3682
  version: "2.0.0",
3454
3683
  from: deps.ownerAddress,
3455
- chainId: (0, import_viem7.toHex)(chainId),
3684
+ chainId: (0, import_viem8.toHex)(chainId),
3456
3685
  atomicRequired: false,
3457
3686
  calls: [{ to: token, value: "0x0", data }],
3458
3687
  capabilities: {
@@ -3674,14 +3903,14 @@ var OwneySDK = class {
3674
3903
  // Casts work around viem's chain-narrowed Client vs the generic
3675
3904
  // PublicClient/WalletClient param types — structurally identical at
3676
3905
  // runtime, but the two share a name TS treats as unrelated.
3677
- getPublicClient: (cid) => (0, import_viem8.createPublicClient)({
3906
+ getPublicClient: (cid) => (0, import_viem9.createPublicClient)({
3678
3907
  chain: VIEM_CHAIN2[cid],
3679
- transport: (0, import_viem8.custom)(provider)
3908
+ transport: (0, import_viem9.custom)(provider)
3680
3909
  }),
3681
- getWalletClient: (cid) => (0, import_viem8.createWalletClient)({
3910
+ getWalletClient: (cid) => (0, import_viem9.createWalletClient)({
3682
3911
  account: owner,
3683
3912
  chain: VIEM_CHAIN2[cid],
3684
- transport: (0, import_viem8.custom)(provider)
3913
+ transport: (0, import_viem9.custom)(provider)
3685
3914
  })
3686
3915
  });
3687
3916
  if (!onApproved) this.cachedSponsoredCallback = callback;
@@ -3727,14 +3956,14 @@ var OwneySDK = class {
3727
3956
  // Casts work around viem's chain-narrowed Client vs the generic
3728
3957
  // PublicClient/WalletClient param types — structurally identical at
3729
3958
  // runtime, but the two share a name TS treats as unrelated.
3730
- getPublicClient: (cid) => (0, import_viem8.createPublicClient)({
3959
+ getPublicClient: (cid) => (0, import_viem9.createPublicClient)({
3731
3960
  chain: VIEM_CHAIN2[cid],
3732
- transport: (0, import_viem8.custom)(provider)
3961
+ transport: (0, import_viem9.custom)(provider)
3733
3962
  }),
3734
- getWalletClient: (cid) => (0, import_viem8.createWalletClient)({
3963
+ getWalletClient: (cid) => (0, import_viem9.createWalletClient)({
3735
3964
  account: owner,
3736
3965
  chain: VIEM_CHAIN2[cid],
3737
- transport: (0, import_viem8.custom)(provider)
3966
+ transport: (0, import_viem9.custom)(provider)
3738
3967
  })
3739
3968
  });
3740
3969
  if (!onApproved) this.cachedWethSponsoredCallback = callback;
@@ -4316,11 +4545,11 @@ var OwneySDK = class {
4316
4545
  );
4317
4546
  }
4318
4547
  await ensureWalletOnChain(
4319
- (0, import_viem8.createPublicClient)({ chain, transport: (0, import_viem8.custom)(provider) }),
4320
- (0, import_viem8.createWalletClient)({
4548
+ (0, import_viem9.createPublicClient)({ chain, transport: (0, import_viem9.custom)(provider) }),
4549
+ (0, import_viem9.createWalletClient)({
4321
4550
  account: state.walletAddress,
4322
4551
  chain,
4323
- transport: (0, import_viem8.custom)(provider)
4552
+ transport: (0, import_viem9.custom)(provider)
4324
4553
  }),
4325
4554
  chainId
4326
4555
  );
@@ -4336,16 +4565,16 @@ var OwneySDK = class {
4336
4565
  const provider = this.requireConnectedProvider();
4337
4566
  const srcChain = VIEM_CHAIN2[quote.src.chainId];
4338
4567
  const dstChain = VIEM_CHAIN2[quote.dst.chainId];
4339
- const wallet = (0, import_viem8.createWalletClient)({
4568
+ const wallet = (0, import_viem9.createWalletClient)({
4340
4569
  account: state.walletAddress,
4341
4570
  chain: srcChain,
4342
- transport: (0, import_viem8.custom)(provider)
4571
+ transport: (0, import_viem9.custom)(provider)
4343
4572
  });
4344
- const srcPublic = (0, import_viem8.createPublicClient)({
4573
+ const srcPublic = (0, import_viem9.createPublicClient)({
4345
4574
  chain: srcChain,
4346
4575
  transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
4347
4576
  });
4348
- const dstPublic = (0, import_viem8.createPublicClient)({
4577
+ const dstPublic = (0, import_viem9.createPublicClient)({
4349
4578
  chain: dstChain,
4350
4579
  transport: swapReadTransport(quote.dst.chainId, this.zyfaiRpcUrls)
4351
4580
  });
@@ -4362,7 +4591,7 @@ var OwneySDK = class {
4362
4591
  }
4363
4592
  return dstPublic.readContract({
4364
4593
  address: dst,
4365
- abi: import_viem8.erc20Abi,
4594
+ abi: import_viem9.erc20Abi,
4366
4595
  functionName: "balanceOf",
4367
4596
  args: [state.walletAddress]
4368
4597
  });
@@ -4430,6 +4659,41 @@ var OwneySDK = class {
4430
4659
  return hash;
4431
4660
  },
4432
4661
  ensureChain: (chainId) => this.ensureSwapChain(chainId),
4662
+ /**
4663
+ * Sign the approval instead of paying for it. Null means the token
4664
+ * cannot be permitted (WETH has no `permit`) or its domain could not be
4665
+ * verified, and the executor sends an ordinary approval instead.
4666
+ *
4667
+ * Reads go through srcPublic like every other read here — the domain is
4668
+ * chain-scoped, and reading a nonce off whatever chain the wallet
4669
+ * happens to sit on would sign a permit the token rejects.
4670
+ */
4671
+ buildPermit: (spender, amount) => buildErc2612Permit(
4672
+ {
4673
+ readContract: (args) => srcPublic.readContract(args)
4674
+ },
4675
+ {
4676
+ token: quote.src.address,
4677
+ owner: state.walletAddress,
4678
+ spender,
4679
+ value: amount,
4680
+ chainId: quote.src.chainId,
4681
+ signTypedData: (payload) => wallet.signTypedData({
4682
+ account: state.walletAddress,
4683
+ ...payload
4684
+ })
4685
+ }
4686
+ ),
4687
+ /**
4688
+ * Same-chain polling, with the chain bound in. That product keys its
4689
+ * orders per chain; asking the Fusion+ endpoint for one answers 404,
4690
+ * which would read as an order that vanished mid-swap.
4691
+ */
4692
+ sameChainRunner: {
4693
+ orderStatus: (h) => this.swapApi().orderStatus(h, quote.src.chainId),
4694
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
4695
+ now: () => Date.now()
4696
+ },
4433
4697
  runner: {
4434
4698
  readyForSecrets: (h) => this.swapApi().readyForSecrets(h),
4435
4699
  submitSecret: (h, secret) => this.swapApi().submitSecret(h, secret),
@@ -4575,13 +4839,13 @@ var OwneySDK = class {
4575
4839
  );
4576
4840
  }
4577
4841
  const srcChain = VIEM_CHAIN2[options.from.chainId];
4578
- const srcPublic = (0, import_viem8.createPublicClient)({
4842
+ const srcPublic = (0, import_viem9.createPublicClient)({
4579
4843
  chain: srcChain,
4580
4844
  transport: swapReadTransport(options.from.chainId, this.zyfaiRpcUrls)
4581
4845
  });
4582
4846
  const readWalletBalance = () => srcPublic.readContract({
4583
4847
  address: asset.address,
4584
- abi: import_viem8.erc20Abi,
4848
+ abi: import_viem9.erc20Abi,
4585
4849
  functionName: "balanceOf",
4586
4850
  args: [state.walletAddress]
4587
4851
  });
@@ -4593,7 +4857,7 @@ var OwneySDK = class {
4593
4857
  options.onSwapProgress?.("withdrawing");
4594
4858
  const withdraw = await this.withdraw({
4595
4859
  asset: options.from.symbol,
4596
- ...options.amount === void 0 ? {} : { amount: (0, import_viem8.parseUnits)(options.amount, asset.decimals).toString() },
4860
+ ...options.amount === void 0 ? {} : { amount: (0, import_viem9.parseUnits)(options.amount, asset.decimals).toString() },
4597
4861
  ...options.agentId ? { agentId: options.agentId } : {}
4598
4862
  });
4599
4863
  const arrived = await awaitWithdrawalArrival({
@@ -5229,10 +5493,10 @@ var OwneySDK = class {
5229
5493
  );
5230
5494
  }
5231
5495
  const provider = this.requireConnectedProvider();
5232
- const wallet = (0, import_viem8.createWalletClient)({
5496
+ const wallet = (0, import_viem9.createWalletClient)({
5233
5497
  account: state.walletAddress,
5234
5498
  chain: VIEM_CHAIN2[chainId],
5235
- transport: (0, import_viem8.custom)(provider)
5499
+ transport: (0, import_viem9.custom)(provider)
5236
5500
  });
5237
5501
  const hash = await wallet.writeContract({
5238
5502
  address: token,
@@ -5242,9 +5506,9 @@ var OwneySDK = class {
5242
5506
  account: state.walletAddress,
5243
5507
  chain: VIEM_CHAIN2[chainId]
5244
5508
  });
5245
- const publicClient = (0, import_viem8.createPublicClient)({
5509
+ const publicClient = (0, import_viem9.createPublicClient)({
5246
5510
  chain: VIEM_CHAIN2[chainId],
5247
- transport: (0, import_viem8.custom)(provider)
5511
+ transport: (0, import_viem9.custom)(provider)
5248
5512
  });
5249
5513
  const receipt = await publicClient.waitForTransactionReceipt({
5250
5514
  hash,
@@ -5370,7 +5634,7 @@ var OwneySDK = class {
5370
5634
  };
5371
5635
 
5372
5636
  // src/agents/zyfai/zyfai.siwx.ts
5373
- var import_viem9 = require("viem");
5637
+ var import_viem10 = require("viem");
5374
5638
  var import_siwe = require("siwe");
5375
5639
  var import_sdk2 = require("@zyfai/sdk");
5376
5640
 
@@ -5497,7 +5761,7 @@ function buildSIWXConfig(deps) {
5497
5761
  issuedAt,
5498
5762
  toString() {
5499
5763
  return new import_siwe.SiweMessage({
5500
- address: (0, import_viem9.getAddress)(accountAddress),
5764
+ address: (0, import_viem10.getAddress)(accountAddress),
5501
5765
  chainId: numericChainId(chainId),
5502
5766
  domain,
5503
5767
  uri,
package/dist/index.d.cts CHANGED
@@ -13,11 +13,16 @@ import { SIWXConfig } from '@reown/appkit-controllers';
13
13
  * `classic` — same chain. One atomic transaction: it either completes or
14
14
  * nothing moved.
15
15
  *
16
+ * `fusion` — same chain, filled by a resolver against a signed intent. No
17
+ * transaction from the user, so no native gas: that is the entire reason it
18
+ * exists. The trade is certainty — the order sits in an auction and can expire
19
+ * unfilled, where a classic swap resolves immediately either way.
20
+ *
16
21
  * `fusion-plus` — crossing chains. The user's funds sit in an escrow while a
17
22
  * resolver fills the other side, so the order has a lifecycle and can end in
18
23
  * `expired` → `refunding` → `refunded` without ever depositing.
19
24
  */
20
- type SwapRail = "classic" | "fusion-plus";
25
+ type SwapRail = "classic" | "fusion" | "fusion-plus";
21
26
  type SwapTokenInfo = {
22
27
  readonly symbol: string;
23
28
  readonly address: string;
@@ -110,6 +115,17 @@ type SwapQuote = {
110
115
  * calldata instead.
111
116
  */
112
117
  spender?: string;
118
+ /**
119
+ * Cross-chain only. True when the routing API will accept an EIP-2612 permit
120
+ * on the order, so the approval is performed by the resolver inside the fill
121
+ * and the user needs no native ETH. False or absent means send an approval
122
+ * transaction, which is what every swap did before ROUT-242.
123
+ *
124
+ * It has to arrive on the QUOTE rather than being tried and recovered from:
125
+ * the client decides whether to spend the user's gas before the order it
126
+ * would be attached to exists.
127
+ */
128
+ permitSupported?: boolean;
113
129
  /**
114
130
  * True only for a cross-chain swap FROM native ETH, which needs an on-chain
115
131
  * order creation carrying the full amount as msg.value. The user's funds
@@ -155,7 +171,7 @@ type SwapQuote = {
155
171
  * `refunding` is the window the returning-funds screen renders: the order has
156
172
  * failed and the money is on its way back, but is not back yet.
157
173
  */
158
- type SwapOrderStatus = "pending" | "executed" | "expired" | "cancelled" | "refunding" | "refunded" | "unpublished";
174
+ type SwapOrderStatus = "pending" | "executed" | "expired" | "cancelled" | "refunding" | "refunded" | "unpublished" | "filled" | "partially-filled" | "false-predicate" | "not-enough-balance-or-allowance" | "wrong-permit" | "invalid-signature";
159
175
  /**
160
176
  * Stage reported to the UI while a swap runs, in either direction.
161
177
  *
package/dist/index.d.ts CHANGED
@@ -13,11 +13,16 @@ import { SIWXConfig } from '@reown/appkit-controllers';
13
13
  * `classic` — same chain. One atomic transaction: it either completes or
14
14
  * nothing moved.
15
15
  *
16
+ * `fusion` — same chain, filled by a resolver against a signed intent. No
17
+ * transaction from the user, so no native gas: that is the entire reason it
18
+ * exists. The trade is certainty — the order sits in an auction and can expire
19
+ * unfilled, where a classic swap resolves immediately either way.
20
+ *
16
21
  * `fusion-plus` — crossing chains. The user's funds sit in an escrow while a
17
22
  * resolver fills the other side, so the order has a lifecycle and can end in
18
23
  * `expired` → `refunding` → `refunded` without ever depositing.
19
24
  */
20
- type SwapRail = "classic" | "fusion-plus";
25
+ type SwapRail = "classic" | "fusion" | "fusion-plus";
21
26
  type SwapTokenInfo = {
22
27
  readonly symbol: string;
23
28
  readonly address: string;
@@ -110,6 +115,17 @@ type SwapQuote = {
110
115
  * calldata instead.
111
116
  */
112
117
  spender?: string;
118
+ /**
119
+ * Cross-chain only. True when the routing API will accept an EIP-2612 permit
120
+ * on the order, so the approval is performed by the resolver inside the fill
121
+ * and the user needs no native ETH. False or absent means send an approval
122
+ * transaction, which is what every swap did before ROUT-242.
123
+ *
124
+ * It has to arrive on the QUOTE rather than being tried and recovered from:
125
+ * the client decides whether to spend the user's gas before the order it
126
+ * would be attached to exists.
127
+ */
128
+ permitSupported?: boolean;
113
129
  /**
114
130
  * True only for a cross-chain swap FROM native ETH, which needs an on-chain
115
131
  * order creation carrying the full amount as msg.value. The user's funds
@@ -155,7 +171,7 @@ type SwapQuote = {
155
171
  * `refunding` is the window the returning-funds screen renders: the order has
156
172
  * failed and the money is on its way back, but is not back yet.
157
173
  */
158
- type SwapOrderStatus = "pending" | "executed" | "expired" | "cancelled" | "refunding" | "refunded" | "unpublished";
174
+ type SwapOrderStatus = "pending" | "executed" | "expired" | "cancelled" | "refunding" | "refunded" | "unpublished" | "filled" | "partially-filled" | "false-predicate" | "not-enough-balance-or-allowance" | "wrong-permit" | "invalid-signature";
159
175
  /**
160
176
  * Stage reported to the UI while a swap runs, in either direction.
161
177
  *
package/dist/index.js CHANGED
@@ -2332,9 +2332,10 @@ function createSwapApi(baseUrl, apiKey) {
2332
2332
  dstSymbol: params.to.symbol,
2333
2333
  amount: params.from.amount,
2334
2334
  walletAddress: params.walletAddress,
2335
- secretHashes: params.secretHashes,
2335
+ ...params.secretHashes ? { secretHashes: params.secretHashes } : {},
2336
2336
  ...params.direction ? { direction: params.direction } : {},
2337
- ...params.receiver ? { receiver: params.receiver } : {}
2337
+ ...params.receiver ? { receiver: params.receiver } : {},
2338
+ ...params.permit ? { permit: params.permit } : {}
2338
2339
  }
2339
2340
  }),
2340
2341
  submitOrder: (body) => request(baseUrl, apiKey, "/order", { method: "POST", body }),
@@ -2347,7 +2348,16 @@ function createSwapApi(baseUrl, apiKey) {
2347
2348
  method: "POST",
2348
2349
  body: { orderHash, secret }
2349
2350
  }),
2350
- orderStatus: (orderHash) => request(baseUrl, apiKey, `/order/${orderHash}`),
2351
+ /**
2352
+ * @param chainId present only for a SAME-CHAIN order. That product keys its
2353
+ * orders per chain and the Fusion+ endpoint does not know them, so asking
2354
+ * without it answers 404 — which reads as an order that vanished.
2355
+ */
2356
+ orderStatus: (orderHash, chainId) => request(
2357
+ baseUrl,
2358
+ apiKey,
2359
+ chainId === void 0 ? `/order/${orderHash}` : `/order/${orderHash}?chainId=${chainId}`
2360
+ ),
2351
2361
  readyForSecrets: (orderHash) => request(
2352
2362
  baseUrl,
2353
2363
  apiKey,
@@ -2490,7 +2500,12 @@ var SWAP_TERMINAL_STATUSES = [
2490
2500
  "executed",
2491
2501
  "expired",
2492
2502
  "cancelled",
2493
- "refunded"
2503
+ "refunded",
2504
+ "filled",
2505
+ "false-predicate",
2506
+ "not-enough-balance-or-allowance",
2507
+ "wrong-permit",
2508
+ "invalid-signature"
2494
2509
  ];
2495
2510
  var isSwapTerminal = (status) => SWAP_TERMINAL_STATUSES.includes(status);
2496
2511
 
@@ -2568,6 +2583,71 @@ async function runFusionOrder(deps, options) {
2568
2583
  }
2569
2584
  }
2570
2585
 
2586
+ // src/lib/swap/swap.same-chain-runner.ts
2587
+ var DEFAULT_POLL_MS2 = 5e3;
2588
+ var MAX_BACKOFF_MS2 = 3e4;
2589
+ var backoffFor2 = (failures, base3) => Math.min(base3 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS2);
2590
+ var DEFAULT_TIMEOUT_MS2 = 3 * 60 * 1e3;
2591
+ function explain(status) {
2592
+ switch (status) {
2593
+ case "expired":
2594
+ return "No one filled the swap in time, so nothing was exchanged. Your funds never left your wallet.";
2595
+ case "cancelled":
2596
+ return "The swap was cancelled before anyone filled it. Your funds never left your wallet.";
2597
+ case "not-enough-balance-or-allowance":
2598
+ return "The swap could not be filled because the balance or approval had changed since it was signed. Nothing was exchanged.";
2599
+ case "wrong-permit":
2600
+ return "The approval signed with this swap was not accepted. Nothing was exchanged \u2014 try again, approving in your wallet if you are asked to.";
2601
+ case "invalid-signature":
2602
+ return "The swap signature was rejected. Nothing was exchanged.";
2603
+ case "false-predicate":
2604
+ return "The swap's conditions no longer held when it was filled. Nothing was exchanged.";
2605
+ default:
2606
+ return "The swap did not complete. Nothing was exchanged.";
2607
+ }
2608
+ }
2609
+ async function runSameChainOrder(deps, options) {
2610
+ const {
2611
+ orderHash,
2612
+ onStage,
2613
+ pollIntervalMs = DEFAULT_POLL_MS2,
2614
+ timeoutMs = DEFAULT_TIMEOUT_MS2
2615
+ } = options;
2616
+ const deadline = deps.now() + timeoutMs;
2617
+ let failures = 0;
2618
+ onStage?.("swapping");
2619
+ for (; ; ) {
2620
+ if (deps.now() >= deadline) {
2621
+ throw new OwneyError(
2622
+ "SWAP_ORDER_EXPIRED",
2623
+ "No one filled the swap in time. Nothing was exchanged and your funds are still in your wallet.",
2624
+ { orderHash }
2625
+ );
2626
+ }
2627
+ let status;
2628
+ try {
2629
+ ({ status } = await deps.orderStatus(orderHash));
2630
+ failures = 0;
2631
+ } catch {
2632
+ failures += 1;
2633
+ await deps.sleep(backoffFor2(failures, pollIntervalMs));
2634
+ continue;
2635
+ }
2636
+ if (isSwapTerminal(status)) {
2637
+ if (status === "filled" || status === "executed") {
2638
+ onStage?.("swapped");
2639
+ return { status, filled: true };
2640
+ }
2641
+ throw new OwneyError(
2642
+ status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
2643
+ explain(status),
2644
+ { orderHash, status }
2645
+ );
2646
+ }
2647
+ await deps.sleep(pollIntervalMs);
2648
+ }
2649
+ }
2650
+
2571
2651
  // src/lib/swap/swap.secret-store.ts
2572
2652
  var KEY_PREFIX2 = "owney.swap.order";
2573
2653
  var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
@@ -2651,7 +2731,7 @@ async function executeSwap(deps, options) {
2651
2731
  });
2652
2732
  const before = await deps.readTargetBalance();
2653
2733
  debugLog("owney-sdk", "swap: target balance before", before.toString());
2654
- const result = quote.rail === "classic" ? await runClassic(deps, options) : await runFusion(deps, options, walletAddress);
2734
+ const result = quote.rail === "classic" ? await runClassic(deps, options) : quote.rail === "fusion" ? await runSameChainFusion(deps, options, walletAddress) : await runFusion(deps, options, walletAddress);
2655
2735
  const after = await deps.readTargetBalance();
2656
2736
  const received = after - before;
2657
2737
  debugLog("owney-sdk", "swap: target balance after", {
@@ -2722,25 +2802,75 @@ async function runClassic(deps, options) {
2722
2802
  onStage?.("swapped");
2723
2803
  return { txHash };
2724
2804
  }
2725
- async function runFusion(deps, options, walletAddress) {
2805
+ async function runSameChainFusion(deps, options, walletAddress) {
2726
2806
  const { quote, direction, onStage } = options;
2727
2807
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2728
2808
  const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2729
- if (quote.spender && !isNativeSource) {
2730
- const needed = amount;
2731
- const current = await deps.readAllowance(quote.spender);
2732
- debugLog("owney-sdk", "swap: fusion allowance", {
2733
- spender: quote.spender,
2734
- current: current.toString(),
2735
- needed: needed.toString()
2809
+ const permit = await authoriseSpend(deps, options, amount, isNativeSource);
2810
+ onStage?.("quoting");
2811
+ debugLog("owney-sdk", "swap: building same-chain fusion order");
2812
+ const built = await deps.api.buildOrder({
2813
+ from: {
2814
+ chainId: quote.src.chainId,
2815
+ symbol: quote.src.symbol,
2816
+ amount: amount.toString()
2817
+ },
2818
+ to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2819
+ walletAddress,
2820
+ ...direction ? { direction } : {},
2821
+ ...permit ? { permit } : {}
2822
+ });
2823
+ debugLog("owney-sdk", "swap: same-chain order built", {
2824
+ orderHash: built.orderHash
2825
+ });
2826
+ onStage?.("signing");
2827
+ await deps.ensureChain(quote.src.chainId);
2828
+ const signature = await deps.signTypedData(built.typedData);
2829
+ debugLog("owney-sdk", "swap: signed, submitting same-chain order");
2830
+ await deps.api.submitOrder({
2831
+ // Without this the routing API hands the order to the Fusion+ relayer,
2832
+ // which does not know it and answers with an error that names nothing.
2833
+ rail: "fusion",
2834
+ srcChainId: quote.src.chainId,
2835
+ order: built.order,
2836
+ signature,
2837
+ quoteId: built.quoteId,
2838
+ ...built.extension ? { extension: built.extension } : {}
2839
+ });
2840
+ await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
2841
+ orderHash: built.orderHash,
2842
+ ...onStage ? { onStage } : {}
2843
+ });
2844
+ return { orderHash: built.orderHash };
2845
+ }
2846
+ async function authoriseSpend(deps, options, amount, isNativeSource) {
2847
+ const { quote, onStage } = options;
2848
+ if (!quote.spender || isNativeSource) return void 0;
2849
+ const current = await deps.readAllowance(quote.spender);
2850
+ debugLog("owney-sdk", "swap: fusion allowance", {
2851
+ spender: quote.spender,
2852
+ current: current.toString(),
2853
+ needed: amount.toString()
2854
+ });
2855
+ if (current >= amount) return void 0;
2856
+ onStage?.("approving");
2857
+ await deps.ensureChain(quote.src.chainId);
2858
+ const permit = quote.permitSupported ? await deps.buildPermit?.(quote.spender, MAX_UINT256) ?? void 0 : void 0;
2859
+ if (permit) {
2860
+ debugLog("owney-sdk", "swap: permitting limit order protocol", {
2861
+ bytes: (permit.length - 2) / 2
2736
2862
  });
2737
- if (current < needed) {
2738
- onStage?.("approving");
2739
- await deps.ensureChain(quote.src.chainId);
2740
- await deps.approve(quote.spender, MAX_UINT256);
2741
- debugLog("owney-sdk", "swap: approved limit order protocol");
2742
- }
2863
+ return permit;
2743
2864
  }
2865
+ await deps.approve(quote.spender, MAX_UINT256);
2866
+ debugLog("owney-sdk", "swap: approved limit order protocol");
2867
+ return void 0;
2868
+ }
2869
+ async function runFusion(deps, options, walletAddress) {
2870
+ const { quote, direction, onStage } = options;
2871
+ const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2872
+ const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2873
+ const permit = await authoriseSpend(deps, options, amount, isNativeSource);
2744
2874
  const { secrets, secretHashes } = mintSecrets(quote.secretsCount ?? 1);
2745
2875
  onStage?.("quoting");
2746
2876
  debugLog("owney-sdk", "swap: building fusion order", {
@@ -2756,7 +2886,8 @@ async function runFusion(deps, options, walletAddress) {
2756
2886
  to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2757
2887
  walletAddress,
2758
2888
  secretHashes,
2759
- ...direction ? { direction } : {}
2889
+ ...direction ? { direction } : {},
2890
+ ...permit ? { permit } : {}
2760
2891
  });
2761
2892
  saveOrder({
2762
2893
  orderHash: built.orderHash,
@@ -2807,16 +2938,119 @@ async function runFusion(deps, options, walletAddress) {
2807
2938
  return { orderHash: built.orderHash };
2808
2939
  }
2809
2940
 
2941
+ // src/lib/swap/swap.permit.ts
2942
+ import {
2943
+ domainSeparator,
2944
+ encodeAbiParameters,
2945
+ parseAbi as parseAbi2,
2946
+ parseSignature
2947
+ } from "viem";
2948
+ var PERMIT_ABI = parseAbi2([
2949
+ "function nonces(address owner) view returns (uint256)",
2950
+ "function name() view returns (string)",
2951
+ "function version() view returns (string)",
2952
+ "function DOMAIN_SEPARATOR() view returns (bytes32)"
2953
+ ]);
2954
+ var PERMIT_TYPES = {
2955
+ Permit: [
2956
+ { name: "owner", type: "address" },
2957
+ { name: "spender", type: "address" },
2958
+ { name: "value", type: "uint256" },
2959
+ { name: "nonce", type: "uint256" },
2960
+ { name: "deadline", type: "uint256" }
2961
+ ]
2962
+ };
2963
+ var VERSION_CANDIDATES = ["1", "2"];
2964
+ var PERMIT_TTL_SECONDS = 3600;
2965
+ async function tryRead(read) {
2966
+ try {
2967
+ return await read();
2968
+ } catch {
2969
+ return null;
2970
+ }
2971
+ }
2972
+ function matchDomain(name, versions, chainId, token, onChainSeparator) {
2973
+ for (const version of versions) {
2974
+ const domain = { name, version, chainId, verifyingContract: token };
2975
+ if (domainSeparator({ domain }) === onChainSeparator) return domain;
2976
+ }
2977
+ return null;
2978
+ }
2979
+ async function buildErc2612Permit(reads, input) {
2980
+ const { token, owner, spender, value, chainId } = input;
2981
+ const call = (functionName, args) => reads.readContract({
2982
+ address: token,
2983
+ abi: PERMIT_ABI,
2984
+ functionName,
2985
+ ...args ? { args } : {}
2986
+ });
2987
+ const nonce = await tryRead(() => call("nonces", [owner]));
2988
+ if (nonce === null) {
2989
+ debugLog("owney-sdk", "permit: token has no nonces(), using approval", {
2990
+ token
2991
+ });
2992
+ return null;
2993
+ }
2994
+ const separator = await tryRead(
2995
+ () => call("DOMAIN_SEPARATOR")
2996
+ );
2997
+ const name = await tryRead(() => call("name"));
2998
+ if (!separator || !name) {
2999
+ debugLog("owney-sdk", "permit: domain unverifiable, using approval", {
3000
+ token,
3001
+ hasSeparator: Boolean(separator),
3002
+ hasName: Boolean(name)
3003
+ });
3004
+ return null;
3005
+ }
3006
+ const declared = await tryRead(() => call("version"));
3007
+ const domain = matchDomain(
3008
+ name,
3009
+ declared ? [declared, ...VERSION_CANDIDATES] : VERSION_CANDIDATES,
3010
+ chainId,
3011
+ token,
3012
+ separator
3013
+ );
3014
+ if (!domain) {
3015
+ debugLog("owney-sdk", "permit: no domain matched, using approval", {
3016
+ token,
3017
+ declared
3018
+ });
3019
+ return null;
3020
+ }
3021
+ const deadline = BigInt(Math.floor((input.now?.() ?? Date.now()) / 1e3)) + BigInt(PERMIT_TTL_SECONDS);
3022
+ const signature = await input.signTypedData({
3023
+ domain,
3024
+ types: PERMIT_TYPES,
3025
+ primaryType: "Permit",
3026
+ message: { owner, spender, value, nonce, deadline }
3027
+ });
3028
+ const { r, s, v, yParity } = parseSignature(signature);
3029
+ const recoveryV = v ?? BigInt(yParity + 27);
3030
+ return encodeAbiParameters(
3031
+ [
3032
+ { type: "address" },
3033
+ { type: "address" },
3034
+ { type: "uint256" },
3035
+ { type: "uint256" },
3036
+ { type: "uint8" },
3037
+ { type: "bytes32" },
3038
+ { type: "bytes32" }
3039
+ ],
3040
+ [owner, spender, value, deadline, Number(recoveryV), r, s]
3041
+ );
3042
+ }
3043
+
2810
3044
  // src/lib/swap/swap.arrival.ts
2811
- var DEFAULT_TIMEOUT_MS2 = 18e4;
2812
- var DEFAULT_POLL_MS2 = 4e3;
3045
+ var DEFAULT_TIMEOUT_MS3 = 18e4;
3046
+ var DEFAULT_POLL_MS3 = 4e3;
2813
3047
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2814
3048
  async function awaitWithdrawalArrival(options) {
2815
3049
  const {
2816
3050
  readBalance,
2817
3051
  baseline,
2818
- timeoutMs = DEFAULT_TIMEOUT_MS2,
2819
- pollMs = DEFAULT_POLL_MS2
3052
+ timeoutMs = DEFAULT_TIMEOUT_MS3,
3053
+ pollMs = DEFAULT_POLL_MS3
2820
3054
  } = options;
2821
3055
  const deadline = Date.now() + timeoutMs;
2822
3056
  debugLog("owney-sdk", "withdraw: waiting for funds to land", {
@@ -4402,6 +4636,41 @@ var OwneySDK = class {
4402
4636
  return hash;
4403
4637
  },
4404
4638
  ensureChain: (chainId) => this.ensureSwapChain(chainId),
4639
+ /**
4640
+ * Sign the approval instead of paying for it. Null means the token
4641
+ * cannot be permitted (WETH has no `permit`) or its domain could not be
4642
+ * verified, and the executor sends an ordinary approval instead.
4643
+ *
4644
+ * Reads go through srcPublic like every other read here — the domain is
4645
+ * chain-scoped, and reading a nonce off whatever chain the wallet
4646
+ * happens to sit on would sign a permit the token rejects.
4647
+ */
4648
+ buildPermit: (spender, amount) => buildErc2612Permit(
4649
+ {
4650
+ readContract: (args) => srcPublic.readContract(args)
4651
+ },
4652
+ {
4653
+ token: quote.src.address,
4654
+ owner: state.walletAddress,
4655
+ spender,
4656
+ value: amount,
4657
+ chainId: quote.src.chainId,
4658
+ signTypedData: (payload) => wallet.signTypedData({
4659
+ account: state.walletAddress,
4660
+ ...payload
4661
+ })
4662
+ }
4663
+ ),
4664
+ /**
4665
+ * Same-chain polling, with the chain bound in. That product keys its
4666
+ * orders per chain; asking the Fusion+ endpoint for one answers 404,
4667
+ * which would read as an order that vanished mid-swap.
4668
+ */
4669
+ sameChainRunner: {
4670
+ orderStatus: (h) => this.swapApi().orderStatus(h, quote.src.chainId),
4671
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
4672
+ now: () => Date.now()
4673
+ },
4405
4674
  runner: {
4406
4675
  readyForSecrets: (h) => this.swapApi().readyForSecrets(h),
4407
4676
  submitSecret: (h, secret) => this.swapApi().submitSecret(h, secret),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.26-beta.2",
3
+ "version": "0.7.26-beta.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",