@owney/sdk 0.7.24-beta.0 → 0.7.25-beta.0

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
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  OwneyError: () => OwneyError,
28
28
  OwneySDK: () => OwneySDK,
29
29
  createOwneySIWX: () => createOwneySIWX,
30
+ listPendingSwaps: () => listOrders,
30
31
  setOwneyDebug: () => setOwneyDebug
31
32
  });
32
33
  module.exports = __toCommonJS(index_exports);
@@ -2198,6 +2199,653 @@ async function fetchAgentKeys(apiKey, baseUrl = ROUTING_API_BASE_URL) {
2198
2199
  return json.data;
2199
2200
  }
2200
2201
 
2202
+ // src/lib/chain-guard.ts
2203
+ var CHAIN_NAMES = {
2204
+ 1: "Ethereum",
2205
+ 8453: "Base",
2206
+ 42161: "Arbitrum"
2207
+ };
2208
+ function chainName(chainId) {
2209
+ return CHAIN_NAMES[chainId] ?? `chain ${chainId}`;
2210
+ }
2211
+ async function ensureWalletOnChain(pub, wallet, expected) {
2212
+ const actual = await pub.getChainId();
2213
+ if (actual === expected) return;
2214
+ try {
2215
+ await wallet.switchChain({ id: expected });
2216
+ } catch (error) {
2217
+ throw new OwneyError(
2218
+ "CHAIN_MISMATCH",
2219
+ `Your wallet is on ${chainName(actual)}. Switch it to ${chainName(expected)} and try again.`,
2220
+ {
2221
+ expectedChainId: expected,
2222
+ actualChainId: actual,
2223
+ cause: error instanceof Error ? error.message : String(error)
2224
+ }
2225
+ );
2226
+ }
2227
+ const after = await pub.getChainId();
2228
+ if (after !== expected) {
2229
+ throw new OwneyError(
2230
+ "CHAIN_MISMATCH",
2231
+ `Your wallet is still on ${chainName(after)}. Switch it to ${chainName(expected)} and try again.`,
2232
+ { expectedChainId: expected, actualChainId: after }
2233
+ );
2234
+ }
2235
+ }
2236
+
2237
+ // src/lib/swap/swap-api.ts
2238
+ async function request(baseUrl, apiKey, path, init) {
2239
+ const url = `${baseUrl}/api/v1/swap${path}`;
2240
+ const res = await fetch(url, {
2241
+ method: init?.method ?? "GET",
2242
+ headers: {
2243
+ "Content-Type": "application/json",
2244
+ "x-owney-api-key": apiKey
2245
+ },
2246
+ ...init ? { body: JSON.stringify(init.body) } : {}
2247
+ });
2248
+ if (!res.ok) {
2249
+ const text = await res.text().catch(() => "");
2250
+ if (res.status === 429) {
2251
+ throw new OwneyError(
2252
+ "SWAP_RATE_LIMITED",
2253
+ "Swap provider is rate limiting, retry shortly",
2254
+ { statusCode: res.status }
2255
+ );
2256
+ }
2257
+ if (res.status === 403) {
2258
+ throw new OwneyError(
2259
+ "SWAP_DISABLED",
2260
+ "Swap is not enabled for this organization",
2261
+ { statusCode: res.status }
2262
+ );
2263
+ }
2264
+ throw new OwneyError(
2265
+ "SWAP_REQUEST_FAILED",
2266
+ `Swap API error ${res.status}: ${text}`,
2267
+ { statusCode: res.status, responseBody: text }
2268
+ );
2269
+ }
2270
+ const json = await res.json();
2271
+ if (!json.success) {
2272
+ throw new OwneyError(
2273
+ "SWAP_REQUEST_FAILED",
2274
+ `Swap API request failed: ${json.message ?? "unknown error"}`,
2275
+ { message: json.message }
2276
+ );
2277
+ }
2278
+ return json.data;
2279
+ }
2280
+ function createSwapApi(baseUrl, apiKey) {
2281
+ return {
2282
+ /** Source assets the user may pay with, and each chain's deposit targets. */
2283
+ listTokens: () => request(baseUrl, apiKey, "/tokens"),
2284
+ /**
2285
+ * `walletAddress` is required even though the routing API could not infer
2286
+ * it: the Fusion+ quoter binds a quote to whoever will sign the order and
2287
+ * rejects the request without it.
2288
+ */
2289
+ quote: (params) => request(baseUrl, apiKey, "/quote", {
2290
+ method: "POST",
2291
+ body: {
2292
+ srcChainId: params.from.chainId,
2293
+ srcSymbol: params.from.symbol,
2294
+ dstChainId: params.to.chainId,
2295
+ dstSymbol: params.to.symbol,
2296
+ amount: params.from.amount,
2297
+ walletAddress: params.walletAddress,
2298
+ ...params.direction ? { direction: params.direction } : {}
2299
+ }
2300
+ }),
2301
+ /** Ready-to-send calldata for a same-chain swap. */
2302
+ swapTx: (params) => request(baseUrl, apiKey, "/tx", {
2303
+ method: "POST",
2304
+ body: {
2305
+ srcChainId: params.from.chainId,
2306
+ srcSymbol: params.from.symbol,
2307
+ dstChainId: params.to.chainId,
2308
+ dstSymbol: params.to.symbol,
2309
+ amount: params.from.amount,
2310
+ walletAddress: params.walletAddress,
2311
+ slippage: params.slippage,
2312
+ ...params.direction ? { direction: params.direction } : {}
2313
+ }
2314
+ }),
2315
+ /**
2316
+ * Builds a Fusion+ order server-side and returns EIP-712 typed data.
2317
+ *
2318
+ * Only HASHES go over the wire. The preimages never leave the browser —
2319
+ * see swap.secrets.
2320
+ */
2321
+ buildOrder: (params) => request(baseUrl, apiKey, "/order/build", {
2322
+ method: "POST",
2323
+ body: {
2324
+ srcChainId: params.from.chainId,
2325
+ srcSymbol: params.from.symbol,
2326
+ dstChainId: params.to.chainId,
2327
+ dstSymbol: params.to.symbol,
2328
+ amount: params.from.amount,
2329
+ walletAddress: params.walletAddress,
2330
+ secretHashes: params.secretHashes,
2331
+ ...params.direction ? { direction: params.direction } : {},
2332
+ ...params.receiver ? { receiver: params.receiver } : {}
2333
+ }
2334
+ }),
2335
+ submitOrder: (body) => request(baseUrl, apiKey, "/order", { method: "POST", body }),
2336
+ /**
2337
+ * Only call once `readyForSecrets` reports the escrow deployed. Publishing
2338
+ * earlier hands a resolver the preimage while the user's funds are locked
2339
+ * and nothing has been posted on the destination chain.
2340
+ */
2341
+ submitSecret: (orderHash, secret) => request(baseUrl, apiKey, "/order/secret", {
2342
+ method: "POST",
2343
+ body: { orderHash, secret }
2344
+ }),
2345
+ orderStatus: (orderHash) => request(baseUrl, apiKey, `/order/${orderHash}`),
2346
+ readyForSecrets: (orderHash) => request(
2347
+ baseUrl,
2348
+ apiKey,
2349
+ `/order/${orderHash}/ready-for-secrets`
2350
+ )
2351
+ };
2352
+ }
2353
+
2354
+ // src/lib/swap/swap.rpc.ts
2355
+ var import_viem2 = require("viem");
2356
+ var DEFAULT_RPC_URLS = {
2357
+ 1: ["https://cloudflare-eth.com", "https://ethereum-rpc.publicnode.com"],
2358
+ 8453: ["https://mainnet.base.org", "https://base-rpc.publicnode.com"],
2359
+ 42161: [
2360
+ "https://arb1.arbitrum.io/rpc",
2361
+ "https://arbitrum-one-rpc.publicnode.com"
2362
+ ]
2363
+ };
2364
+ function swapReadTransport(chainId, overrides) {
2365
+ const override = overrides?.[chainId];
2366
+ if (override) return (0, import_viem2.http)(override);
2367
+ const urls = DEFAULT_RPC_URLS[chainId];
2368
+ if (!urls || urls.length === 0) return (0, import_viem2.http)();
2369
+ return (0, import_viem2.fallback)(urls.map((url) => (0, import_viem2.http)(url)));
2370
+ }
2371
+ function receiptTimeoutMs(chainId) {
2372
+ return chainId === 1 ? 6e5 : 18e4;
2373
+ }
2374
+
2375
+ // src/lib/permit2.ts
2376
+ var import_viem3 = require("viem");
2377
+ var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2378
+ var MAX_UINT256 = 2n ** 256n - 1n;
2379
+ var ERC20_ALLOWANCE_ABI = [
2380
+ {
2381
+ type: "function",
2382
+ name: "allowance",
2383
+ stateMutability: "view",
2384
+ inputs: [
2385
+ { name: "owner", type: "address" },
2386
+ { name: "spender", type: "address" }
2387
+ ],
2388
+ outputs: [{ name: "", type: "uint256" }]
2389
+ },
2390
+ {
2391
+ type: "function",
2392
+ name: "approve",
2393
+ stateMutability: "nonpayable",
2394
+ inputs: [
2395
+ { name: "spender", type: "address" },
2396
+ { name: "amount", type: "uint256" }
2397
+ ],
2398
+ outputs: [{ name: "", type: "bool" }]
2399
+ },
2400
+ {
2401
+ type: "function",
2402
+ name: "balanceOf",
2403
+ stateMutability: "view",
2404
+ inputs: [{ name: "account", type: "address" }],
2405
+ outputs: [{ name: "", type: "uint256" }]
2406
+ }
2407
+ ];
2408
+ function buildPermitTransferFromTypedData(input) {
2409
+ return {
2410
+ domain: {
2411
+ name: "Permit2",
2412
+ chainId: input.chainId,
2413
+ verifyingContract: PERMIT2_ADDRESS
2414
+ },
2415
+ types: {
2416
+ PermitTransferFrom: [
2417
+ { name: "permitted", type: "TokenPermissions" },
2418
+ { name: "spender", type: "address" },
2419
+ { name: "nonce", type: "uint256" },
2420
+ { name: "deadline", type: "uint256" }
2421
+ ],
2422
+ TokenPermissions: [
2423
+ { name: "token", type: "address" },
2424
+ { name: "amount", type: "uint256" }
2425
+ ]
2426
+ },
2427
+ primaryType: "PermitTransferFrom",
2428
+ message: input.message
2429
+ };
2430
+ }
2431
+ function randomPermit2Nonce() {
2432
+ const bytes = new Uint8Array(32);
2433
+ globalThis.crypto.getRandomValues(bytes);
2434
+ return BigInt((0, import_viem3.bytesToHex)(bytes));
2435
+ }
2436
+ async function readPermit2Allowance(publicClient, token, owner) {
2437
+ return publicClient.readContract({
2438
+ address: token,
2439
+ abi: ERC20_ALLOWANCE_ABI,
2440
+ functionName: "allowance",
2441
+ args: [owner, PERMIT2_ADDRESS]
2442
+ });
2443
+ }
2444
+ async function readErc20Balance(publicClient, token, owner) {
2445
+ return publicClient.readContract({
2446
+ address: token,
2447
+ abi: ERC20_ALLOWANCE_ABI,
2448
+ functionName: "balanceOf",
2449
+ args: [owner]
2450
+ });
2451
+ }
2452
+
2453
+ // src/lib/swap/swap.secrets.ts
2454
+ var import_viem4 = require("viem");
2455
+ var SECRET_BYTES = 32;
2456
+ function randomBytes(length) {
2457
+ const bytes = new Uint8Array(length);
2458
+ const cryptoObj = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
2459
+ if (!cryptoObj?.getRandomValues) {
2460
+ throw new Error(
2461
+ "[owney-sdk] Secure randomness is unavailable, so a swap secret cannot be generated safely."
2462
+ );
2463
+ }
2464
+ cryptoObj.getRandomValues(bytes);
2465
+ return bytes;
2466
+ }
2467
+ function mintSecrets(count) {
2468
+ if (!Number.isInteger(count) || count < 1) {
2469
+ throw new Error(
2470
+ `[owney-sdk] A swap needs at least one secret, got ${String(count)}.`
2471
+ );
2472
+ }
2473
+ const secrets = [];
2474
+ const secretHashes = [];
2475
+ for (let i = 0; i < count; i++) {
2476
+ const secret = (0, import_viem4.toHex)(randomBytes(SECRET_BYTES));
2477
+ secrets.push(secret);
2478
+ secretHashes.push((0, import_viem4.keccak256)(secret));
2479
+ }
2480
+ return { secrets, secretHashes };
2481
+ }
2482
+
2483
+ // src/lib/swap/swap.types.ts
2484
+ var SWAP_TERMINAL_STATUSES = [
2485
+ "executed",
2486
+ "expired",
2487
+ "cancelled",
2488
+ "refunded"
2489
+ ];
2490
+ var isSwapTerminal = (status) => SWAP_TERMINAL_STATUSES.includes(status);
2491
+
2492
+ // src/lib/swap/swap.order-runner.ts
2493
+ var DEFAULT_POLL_MS = 5e3;
2494
+ var MAX_BACKOFF_MS = 3e4;
2495
+ var backoffFor = (failures, base3) => Math.min(base3 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS);
2496
+ var DEFAULT_TIMEOUT_MS = 15 * 60 * 1e3;
2497
+ async function runFusionOrder(deps, options) {
2498
+ const {
2499
+ orderHash,
2500
+ secrets,
2501
+ onStage,
2502
+ pollIntervalMs = DEFAULT_POLL_MS,
2503
+ timeoutMs = DEFAULT_TIMEOUT_MS
2504
+ } = options;
2505
+ const deadline = deps.now() + timeoutMs;
2506
+ let failures = 0;
2507
+ const published = /* @__PURE__ */ new Set();
2508
+ onStage?.("swapping");
2509
+ for (; ; ) {
2510
+ if (deps.now() >= deadline) {
2511
+ throw new OwneyError(
2512
+ "SWAP_REQUEST_FAILED",
2513
+ "Timed out waiting for the swap to settle. It may still complete \u2014 check the order status before retrying.",
2514
+ { orderHash }
2515
+ );
2516
+ }
2517
+ let ready;
2518
+ try {
2519
+ ready = await deps.readyForSecrets(orderHash);
2520
+ } catch {
2521
+ ready = {};
2522
+ }
2523
+ for (const fill of ready.fills ?? []) {
2524
+ if (published.has(fill.idx)) continue;
2525
+ const secret = secrets[fill.idx];
2526
+ if (secret === void 0) {
2527
+ throw new OwneyError(
2528
+ "SWAP_REQUEST_FAILED",
2529
+ `Swap needs a secret for fill ${fill.idx} that this session does not have. The order will refund once its timelock expires.`,
2530
+ { orderHash, fillIndex: fill.idx }
2531
+ );
2532
+ }
2533
+ try {
2534
+ await deps.submitSecret(orderHash, secret);
2535
+ published.add(fill.idx);
2536
+ } catch {
2537
+ failures += 1;
2538
+ }
2539
+ }
2540
+ let status;
2541
+ try {
2542
+ ({ status } = await deps.orderStatus(orderHash));
2543
+ failures = 0;
2544
+ } catch {
2545
+ failures += 1;
2546
+ await deps.sleep(backoffFor(failures, pollIntervalMs));
2547
+ continue;
2548
+ }
2549
+ if (status === "refunding") onStage?.("refunding");
2550
+ if (isSwapTerminal(status)) {
2551
+ if (status === "executed") {
2552
+ onStage?.("swapped");
2553
+ return { status, filled: true };
2554
+ }
2555
+ if (status === "refunded") onStage?.("refunded");
2556
+ throw new OwneyError(
2557
+ status === "refunded" ? "SWAP_ORDER_REFUNDED" : status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
2558
+ status === "refunded" ? "The swap did not complete and your funds have been returned." : "The swap did not complete in time. Your funds will be returned once the timelock expires.",
2559
+ { orderHash, status }
2560
+ );
2561
+ }
2562
+ await deps.sleep(pollIntervalMs);
2563
+ }
2564
+ }
2565
+
2566
+ // src/lib/swap/swap.secret-store.ts
2567
+ var KEY_PREFIX2 = "owney.swap.order";
2568
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
2569
+ var storage2 = () => {
2570
+ if (typeof window === "undefined") return null;
2571
+ try {
2572
+ return window.localStorage;
2573
+ } catch {
2574
+ return null;
2575
+ }
2576
+ };
2577
+ var keyFor = (orderHash) => `${KEY_PREFIX2}.${orderHash}`;
2578
+ function saveOrder(order) {
2579
+ const store = storage2();
2580
+ if (!store) return;
2581
+ try {
2582
+ store.setItem(keyFor(order.orderHash), JSON.stringify(order));
2583
+ } catch {
2584
+ }
2585
+ }
2586
+ function clearOrder(orderHash) {
2587
+ const store = storage2();
2588
+ if (!store) return;
2589
+ try {
2590
+ store.removeItem(keyFor(orderHash));
2591
+ } catch {
2592
+ }
2593
+ }
2594
+ function listOrders(now = Date.now()) {
2595
+ const store = storage2();
2596
+ if (!store) return [];
2597
+ const out = [];
2598
+ try {
2599
+ const keys = [];
2600
+ for (let i = 0; i < store.length; i++) {
2601
+ const key2 = store.key(i);
2602
+ if (key2?.startsWith(`${KEY_PREFIX2}.`)) keys.push(key2);
2603
+ }
2604
+ for (const key2 of keys) {
2605
+ const raw = store.getItem(key2);
2606
+ if (!raw) continue;
2607
+ try {
2608
+ const parsed = JSON.parse(raw);
2609
+ if (now - parsed.createdAt > MAX_AGE_MS) {
2610
+ store.removeItem(key2);
2611
+ continue;
2612
+ }
2613
+ if (Array.isArray(parsed.secrets) && parsed.secrets.length > 0) {
2614
+ out.push(parsed);
2615
+ }
2616
+ } catch {
2617
+ store.removeItem(key2);
2618
+ }
2619
+ }
2620
+ } catch {
2621
+ return out;
2622
+ }
2623
+ return out.sort((a, b) => b.createdAt - a.createdAt);
2624
+ }
2625
+
2626
+ // src/lib/swap/swap.executor.ts
2627
+ var DEFAULT_SLIPPAGE = 1;
2628
+ async function affordableAmount(deps, quoted) {
2629
+ const balance = await deps.readSourceBalance();
2630
+ if (balance >= quoted) return quoted;
2631
+ debugLog("owney-sdk", "swap: trimming to the current source balance", {
2632
+ quoted: quoted.toString(),
2633
+ balance: balance.toString(),
2634
+ short: (quoted - balance).toString()
2635
+ });
2636
+ return balance;
2637
+ }
2638
+ async function executeSwap(deps, options) {
2639
+ const { quote, walletAddress, onStage } = options;
2640
+ debugLog("owney-sdk", "swap: start", {
2641
+ rail: quote.rail,
2642
+ from: `${quote.src.amount} ${quote.src.symbol} on ${quote.src.chainId}`,
2643
+ to: `${quote.dst.symbol} on ${quote.dst.chainId}`,
2644
+ expected: quote.dst.amount,
2645
+ floor: quote.dstAmountMin
2646
+ });
2647
+ const before = await deps.readTargetBalance();
2648
+ debugLog("owney-sdk", "swap: target balance before", before.toString());
2649
+ const result = quote.rail === "classic" ? await runClassic(deps, options) : await runFusion(deps, options, walletAddress);
2650
+ const after = await deps.readTargetBalance();
2651
+ const received = after - before;
2652
+ debugLog("owney-sdk", "swap: target balance after", {
2653
+ after: after.toString(),
2654
+ received: received.toString()
2655
+ });
2656
+ if (received <= 0n) {
2657
+ throw new OwneyError(
2658
+ "SWAP_REQUEST_FAILED",
2659
+ "The swap completed but no funds arrived in the wallet. Check the transaction before retrying.",
2660
+ { rail: quote.rail, ...result }
2661
+ );
2662
+ }
2663
+ return { received: received.toString(), ...result };
2664
+ }
2665
+ async function runClassic(deps, options) {
2666
+ const {
2667
+ quote,
2668
+ walletAddress,
2669
+ slippage = DEFAULT_SLIPPAGE,
2670
+ direction,
2671
+ onStage
2672
+ } = options;
2673
+ const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2674
+ const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2675
+ onStage?.("quoting");
2676
+ debugLog("owney-sdk", "swap: fetching classic calldata");
2677
+ const { tx } = await deps.api.swapTx({
2678
+ from: {
2679
+ chainId: quote.src.chainId,
2680
+ symbol: quote.src.symbol,
2681
+ amount: amount.toString()
2682
+ },
2683
+ to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2684
+ walletAddress,
2685
+ slippage,
2686
+ ...direction ? { direction } : {}
2687
+ });
2688
+ const isNative = BigInt(tx.value ?? "0") > 0n;
2689
+ if (!isNative) {
2690
+ const needed = amount;
2691
+ const current = await deps.readAllowance(tx.to);
2692
+ debugLog("owney-sdk", "swap: allowance", {
2693
+ spender: tx.to,
2694
+ current: current.toString(),
2695
+ needed: needed.toString()
2696
+ });
2697
+ if (current < needed) {
2698
+ onStage?.("approving");
2699
+ await deps.ensureChain(quote.src.chainId);
2700
+ await deps.approve(tx.to, MAX_UINT256);
2701
+ }
2702
+ }
2703
+ onStage?.("signing");
2704
+ await deps.ensureChain(quote.src.chainId);
2705
+ debugLog("owney-sdk", "swap: sending classic swap tx", { to: tx.to });
2706
+ const txHash = await deps.sendTransaction({
2707
+ to: tx.to,
2708
+ data: tx.data,
2709
+ value: tx.value ?? "0"
2710
+ });
2711
+ onStage?.("swapped");
2712
+ return { txHash };
2713
+ }
2714
+ async function runFusion(deps, options, walletAddress) {
2715
+ const { quote, direction, onStage } = options;
2716
+ const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
2717
+ const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
2718
+ if (quote.spender && !isNativeSource) {
2719
+ const needed = amount;
2720
+ const current = await deps.readAllowance(quote.spender);
2721
+ debugLog("owney-sdk", "swap: fusion allowance", {
2722
+ spender: quote.spender,
2723
+ current: current.toString(),
2724
+ needed: needed.toString()
2725
+ });
2726
+ if (current < needed) {
2727
+ onStage?.("approving");
2728
+ await deps.ensureChain(quote.src.chainId);
2729
+ await deps.approve(quote.spender, MAX_UINT256);
2730
+ debugLog("owney-sdk", "swap: approved limit order protocol");
2731
+ }
2732
+ }
2733
+ const { secrets, secretHashes } = mintSecrets(quote.secretsCount ?? 1);
2734
+ onStage?.("quoting");
2735
+ debugLog("owney-sdk", "swap: building fusion order", {
2736
+ secrets: secretHashes.length
2737
+ });
2738
+ const built = await deps.api.buildOrder({
2739
+ from: {
2740
+ chainId: quote.src.chainId,
2741
+ symbol: quote.src.symbol,
2742
+ // The trimmed amount — the order is re-quoted at this size server-side.
2743
+ amount: amount.toString()
2744
+ },
2745
+ to: { chainId: quote.dst.chainId, symbol: quote.dst.symbol },
2746
+ walletAddress,
2747
+ secretHashes,
2748
+ ...direction ? { direction } : {}
2749
+ });
2750
+ saveOrder({
2751
+ orderHash: built.orderHash,
2752
+ secrets,
2753
+ srcChainId: quote.src.chainId,
2754
+ srcSymbol: quote.src.symbol,
2755
+ dstChainId: quote.dst.chainId,
2756
+ dstSymbol: quote.dst.symbol,
2757
+ amount: amount.toString(),
2758
+ createdAt: Date.now()
2759
+ });
2760
+ debugLog("owney-sdk", "swap: order built", { orderHash: built.orderHash });
2761
+ onStage?.("signing");
2762
+ await deps.ensureChain(quote.src.chainId);
2763
+ debugLog("owney-sdk", "swap: awaiting signature in wallet", {
2764
+ signingOnChain: quote.src.chainId
2765
+ });
2766
+ const signature = await deps.signTypedData(built.typedData);
2767
+ debugLog("owney-sdk", "swap: signed, submitting to relayer");
2768
+ await deps.api.submitOrder({
2769
+ srcChainId: quote.src.chainId,
2770
+ // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
2771
+ // the envelope here gets a bare 500 from the relayer.
2772
+ order: built.order,
2773
+ signature,
2774
+ quoteId: built.quoteId,
2775
+ // Single-fill orders must NOT carry secretHashes — the relayer rejects
2776
+ // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
2777
+ // order's hashlock, so repeating it here is redundant, and only a
2778
+ // multi-fill order (a Merkle tree of hashes) needs them listed.
2779
+ ...secretHashes.length > 1 ? { secretHashes } : {},
2780
+ ...built.extension ? { extension: built.extension } : {}
2781
+ });
2782
+ debugLog("owney-sdk", "swap: order submitted, polling escrows");
2783
+ try {
2784
+ await runFusionOrder(deps.runner, {
2785
+ orderHash: built.orderHash,
2786
+ secrets,
2787
+ ...onStage ? { onStage } : {}
2788
+ });
2789
+ } catch (error) {
2790
+ if (error instanceof OwneyError && (error.code === "SWAP_ORDER_REFUNDED" || error.code === "SWAP_ORDER_CANCELLED")) {
2791
+ clearOrder(built.orderHash);
2792
+ }
2793
+ throw error;
2794
+ }
2795
+ clearOrder(built.orderHash);
2796
+ return { orderHash: built.orderHash };
2797
+ }
2798
+
2799
+ // src/lib/swap/swap.arrival.ts
2800
+ var DEFAULT_TIMEOUT_MS2 = 18e4;
2801
+ var DEFAULT_POLL_MS2 = 4e3;
2802
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
2803
+ async function awaitWithdrawalArrival(options) {
2804
+ const {
2805
+ readBalance,
2806
+ baseline,
2807
+ timeoutMs = DEFAULT_TIMEOUT_MS2,
2808
+ pollMs = DEFAULT_POLL_MS2
2809
+ } = options;
2810
+ const deadline = Date.now() + timeoutMs;
2811
+ debugLog("owney-sdk", "withdraw: waiting for funds to land", {
2812
+ baseline: baseline.toString(),
2813
+ timeoutMs
2814
+ });
2815
+ let lastError;
2816
+ for (; ; ) {
2817
+ try {
2818
+ const balance = await readBalance();
2819
+ if (balance > baseline) {
2820
+ const arrived = balance - baseline;
2821
+ debugLog("owney-sdk", "withdraw: funds landed", {
2822
+ arrived: arrived.toString()
2823
+ });
2824
+ return arrived;
2825
+ }
2826
+ } catch (error) {
2827
+ lastError = error;
2828
+ debugLog("owney-sdk", "withdraw: balance read failed, retrying", {
2829
+ message: error instanceof Error ? error.message : String(error)
2830
+ });
2831
+ }
2832
+ if (Date.now() >= deadline) {
2833
+ throw new OwneyError(
2834
+ "WITHDRAW_ARRIVAL_TIMEOUT",
2835
+ "The withdrawal was accepted but the funds had not arrived in time to swap them. They are on their way to your wallet in the original asset.",
2836
+ {
2837
+ baseline: baseline.toString(),
2838
+ waitedMs: timeoutMs,
2839
+ ...lastError ? {
2840
+ lastReadError: lastError instanceof Error ? lastError.message : String(lastError)
2841
+ } : {}
2842
+ }
2843
+ );
2844
+ }
2845
+ await sleep(pollMs);
2846
+ }
2847
+ }
2848
+
2201
2849
  // src/lib/health-report.ts
2202
2850
  var ROUTING_API_BASE_URL2 = "https://owney-routing-api-243946518160.europe-west4.run.app";
2203
2851
  async function reportAgentFailure(apiKey, agentType, errorCode, baseUrl = ROUTING_API_BASE_URL2) {
@@ -2232,7 +2880,7 @@ async function withFailureReporting(apiKey, agentType, fn, baseUrl) {
2232
2880
  }
2233
2881
 
2234
2882
  // src/lib/helpers/withdraw-helper.ts
2235
- var import_viem2 = require("viem");
2883
+ var import_viem5 = require("viem");
2236
2884
  function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decimals) {
2237
2885
  const target = asset.toUpperCase();
2238
2886
  return agents.map((agent) => {
@@ -2241,7 +2889,7 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
2241
2889
  (t) => t.chainId === chainId && t.asset.toUpperCase() === target
2242
2890
  );
2243
2891
  if (!tokenBalance) return { agent, balance: 0n };
2244
- return { agent, balance: (0, import_viem2.parseUnits)(tokenBalance.amount, decimals) };
2892
+ return { agent, balance: (0, import_viem5.parseUnits)(tokenBalance.amount, decimals) };
2245
2893
  });
2246
2894
  }
2247
2895
  function planProportionalShares(balances, requested, totalAvailable) {
@@ -2388,11 +3036,11 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
2388
3036
  }
2389
3037
 
2390
3038
  // src/client.ts
2391
- var import_viem6 = require("viem");
3039
+ var import_viem8 = require("viem");
2392
3040
  var import_chains2 = require("viem/chains");
2393
3041
 
2394
3042
  // src/lib/transfer-auth.ts
2395
- var import_viem3 = require("viem");
3043
+ var import_viem6 = require("viem");
2396
3044
  var ERC20_META_ABI = [
2397
3045
  { type: "function", name: "name", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] },
2398
3046
  { type: "function", name: "version", stateMutability: "view", inputs: [], outputs: [{ name: "", type: "string" }] }
@@ -2424,7 +3072,7 @@ async function readTokenMeta(publicClient, token) {
2424
3072
  function randomAuthNonce() {
2425
3073
  const bytes = new Uint8Array(32);
2426
3074
  globalThis.crypto.getRandomValues(bytes);
2427
- return (0, import_viem3.bytesToHex)(bytes);
3075
+ return (0, import_viem6.bytesToHex)(bytes);
2428
3076
  }
2429
3077
 
2430
3078
  // src/lib/sponsor-client.ts
@@ -2544,119 +3192,6 @@ async function getSponsorRelayerAddress(input) {
2544
3192
  return parsed.data.relayer;
2545
3193
  }
2546
3194
 
2547
- // src/lib/permit2.ts
2548
- var import_viem4 = require("viem");
2549
- var PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
2550
- var MAX_UINT256 = 2n ** 256n - 1n;
2551
- var ERC20_ALLOWANCE_ABI = [
2552
- {
2553
- type: "function",
2554
- name: "allowance",
2555
- stateMutability: "view",
2556
- inputs: [
2557
- { name: "owner", type: "address" },
2558
- { name: "spender", type: "address" }
2559
- ],
2560
- outputs: [{ name: "", type: "uint256" }]
2561
- },
2562
- {
2563
- type: "function",
2564
- name: "approve",
2565
- stateMutability: "nonpayable",
2566
- inputs: [
2567
- { name: "spender", type: "address" },
2568
- { name: "amount", type: "uint256" }
2569
- ],
2570
- outputs: [{ name: "", type: "bool" }]
2571
- },
2572
- {
2573
- type: "function",
2574
- name: "balanceOf",
2575
- stateMutability: "view",
2576
- inputs: [{ name: "account", type: "address" }],
2577
- outputs: [{ name: "", type: "uint256" }]
2578
- }
2579
- ];
2580
- function buildPermitTransferFromTypedData(input) {
2581
- return {
2582
- domain: {
2583
- name: "Permit2",
2584
- chainId: input.chainId,
2585
- verifyingContract: PERMIT2_ADDRESS
2586
- },
2587
- types: {
2588
- PermitTransferFrom: [
2589
- { name: "permitted", type: "TokenPermissions" },
2590
- { name: "spender", type: "address" },
2591
- { name: "nonce", type: "uint256" },
2592
- { name: "deadline", type: "uint256" }
2593
- ],
2594
- TokenPermissions: [
2595
- { name: "token", type: "address" },
2596
- { name: "amount", type: "uint256" }
2597
- ]
2598
- },
2599
- primaryType: "PermitTransferFrom",
2600
- message: input.message
2601
- };
2602
- }
2603
- function randomPermit2Nonce() {
2604
- const bytes = new Uint8Array(32);
2605
- globalThis.crypto.getRandomValues(bytes);
2606
- return BigInt((0, import_viem4.bytesToHex)(bytes));
2607
- }
2608
- async function readPermit2Allowance(publicClient, token, owner) {
2609
- return publicClient.readContract({
2610
- address: token,
2611
- abi: ERC20_ALLOWANCE_ABI,
2612
- functionName: "allowance",
2613
- args: [owner, PERMIT2_ADDRESS]
2614
- });
2615
- }
2616
- async function readErc20Balance(publicClient, token, owner) {
2617
- return publicClient.readContract({
2618
- address: token,
2619
- abi: ERC20_ALLOWANCE_ABI,
2620
- functionName: "balanceOf",
2621
- args: [owner]
2622
- });
2623
- }
2624
-
2625
- // src/lib/chain-guard.ts
2626
- var CHAIN_NAMES = {
2627
- 1: "Ethereum",
2628
- 8453: "Base",
2629
- 42161: "Arbitrum"
2630
- };
2631
- function chainName(chainId) {
2632
- return CHAIN_NAMES[chainId] ?? `chain ${chainId}`;
2633
- }
2634
- async function ensureWalletOnChain(pub, wallet, expected) {
2635
- const actual = await pub.getChainId();
2636
- if (actual === expected) return;
2637
- try {
2638
- await wallet.switchChain({ id: expected });
2639
- } catch (error) {
2640
- throw new OwneyError(
2641
- "CHAIN_MISMATCH",
2642
- `Your wallet is on ${chainName(actual)}. Switch it to ${chainName(expected)} and try again.`,
2643
- {
2644
- expectedChainId: expected,
2645
- actualChainId: actual,
2646
- cause: error instanceof Error ? error.message : String(error)
2647
- }
2648
- );
2649
- }
2650
- const after = await pub.getChainId();
2651
- if (after !== expected) {
2652
- throw new OwneyError(
2653
- "CHAIN_MISMATCH",
2654
- `Your wallet is still on ${chainName(after)}. Switch it to ${chainName(expected)} and try again.`,
2655
- { expectedChainId: expected, actualChainId: after }
2656
- );
2657
- }
2658
- }
2659
-
2660
3195
  // src/lib/sponsored-deposit.ts
2661
3196
  var AUTH_WINDOW_SECONDS = 15 * 60;
2662
3197
  function makeSponsoredDepositCallback(deps) {
@@ -2819,7 +3354,7 @@ function makeSponsoredWethCallback(deps) {
2819
3354
  }
2820
3355
 
2821
3356
  // src/lib/sponsored-calls-deposit.ts
2822
- var import_viem5 = require("viem");
3357
+ var import_viem7 = require("viem");
2823
3358
  var DEFAULT_POLL_INTERVAL_MS = 1500;
2824
3359
  var DEFAULT_MAX_POLLS = 30;
2825
3360
  async function paymasterSupported(provider, owner, chainId) {
@@ -2827,7 +3362,7 @@ async function paymasterSupported(provider, owner, chainId) {
2827
3362
  method: "wallet_getCapabilities",
2828
3363
  params: [owner]
2829
3364
  });
2830
- const forChain = caps?.[(0, import_viem5.toHex)(chainId)] ?? caps?.[String(chainId)];
3365
+ const forChain = caps?.[(0, import_viem7.toHex)(chainId)] ?? caps?.[String(chainId)];
2831
3366
  return Boolean(forChain?.paymasterService?.supported);
2832
3367
  }
2833
3368
  function makeSponsoredCallsCallback(deps) {
@@ -2861,8 +3396,8 @@ function makeSponsoredCallsCallback(deps) {
2861
3396
  { chainId }
2862
3397
  );
2863
3398
  }
2864
- const data = (0, import_viem5.encodeFunctionData)({
2865
- abi: import_viem5.erc20Abi,
3399
+ const data = (0, import_viem7.encodeFunctionData)({
3400
+ abi: import_viem7.erc20Abi,
2866
3401
  functionName: "transfer",
2867
3402
  args: [smartWallet, BigInt(amount)]
2868
3403
  });
@@ -2872,7 +3407,7 @@ function makeSponsoredCallsCallback(deps) {
2872
3407
  {
2873
3408
  version: "2.0.0",
2874
3409
  from: deps.ownerAddress,
2875
- chainId: (0, import_viem5.toHex)(chainId),
3410
+ chainId: (0, import_viem7.toHex)(chainId),
2876
3411
  atomicRequired: false,
2877
3412
  calls: [{ to: token, value: "0x0", data }],
2878
3413
  capabilities: {
@@ -3094,14 +3629,14 @@ var OwneySDK = class {
3094
3629
  // Casts work around viem's chain-narrowed Client vs the generic
3095
3630
  // PublicClient/WalletClient param types — structurally identical at
3096
3631
  // runtime, but the two share a name TS treats as unrelated.
3097
- getPublicClient: (cid) => (0, import_viem6.createPublicClient)({
3632
+ getPublicClient: (cid) => (0, import_viem8.createPublicClient)({
3098
3633
  chain: VIEM_CHAIN2[cid],
3099
- transport: (0, import_viem6.custom)(provider)
3634
+ transport: (0, import_viem8.custom)(provider)
3100
3635
  }),
3101
- getWalletClient: (cid) => (0, import_viem6.createWalletClient)({
3636
+ getWalletClient: (cid) => (0, import_viem8.createWalletClient)({
3102
3637
  account: owner,
3103
3638
  chain: VIEM_CHAIN2[cid],
3104
- transport: (0, import_viem6.custom)(provider)
3639
+ transport: (0, import_viem8.custom)(provider)
3105
3640
  })
3106
3641
  });
3107
3642
  if (!onApproved) this.cachedSponsoredCallback = callback;
@@ -3147,14 +3682,14 @@ var OwneySDK = class {
3147
3682
  // Casts work around viem's chain-narrowed Client vs the generic
3148
3683
  // PublicClient/WalletClient param types — structurally identical at
3149
3684
  // runtime, but the two share a name TS treats as unrelated.
3150
- getPublicClient: (cid) => (0, import_viem6.createPublicClient)({
3685
+ getPublicClient: (cid) => (0, import_viem8.createPublicClient)({
3151
3686
  chain: VIEM_CHAIN2[cid],
3152
- transport: (0, import_viem6.custom)(provider)
3687
+ transport: (0, import_viem8.custom)(provider)
3153
3688
  }),
3154
- getWalletClient: (cid) => (0, import_viem6.createWalletClient)({
3689
+ getWalletClient: (cid) => (0, import_viem8.createWalletClient)({
3155
3690
  account: owner,
3156
3691
  chain: VIEM_CHAIN2[cid],
3157
- transport: (0, import_viem6.custom)(provider)
3692
+ transport: (0, import_viem8.custom)(provider)
3158
3693
  })
3159
3694
  });
3160
3695
  if (!onApproved) this.cachedWethSponsoredCallback = callback;
@@ -3185,12 +3720,10 @@ var OwneySDK = class {
3185
3720
  this.orgAgentConfigPromise = fetchOrgAgentConfig(
3186
3721
  this.apiKey,
3187
3722
  this.routingApiBaseUrl
3188
- ).then(
3189
- (config) => {
3190
- this.orgAgentConfig = config;
3191
- return config;
3192
- }
3193
- );
3723
+ ).then((config) => {
3724
+ this.orgAgentConfig = config;
3725
+ return config;
3726
+ });
3194
3727
  }
3195
3728
  return this.orgAgentConfigPromise;
3196
3729
  }
@@ -3709,6 +4242,330 @@ var OwneySDK = class {
3709
4242
  return eligible;
3710
4243
  }
3711
4244
  // --- Fund operations ---
4245
+ // --- Swap to yield (ROUT-242) ---
4246
+ /** Lazily built so an app that never swaps pays nothing for it. */
4247
+ swapApiClient;
4248
+ swapApi() {
4249
+ this.swapApiClient ??= createSwapApi(
4250
+ this.routingApiBaseUrl ?? ROUTING_API_BASE_URL,
4251
+ this.apiKey
4252
+ );
4253
+ return this.swapApiClient;
4254
+ }
4255
+ /**
4256
+ * Put the wallet on `chainId`, or fail with something actionable.
4257
+ *
4258
+ * Reuses the same guard the deposit rail uses, which re-reads the chain after
4259
+ * switching — some wallets resolve wallet_switchEthereumChain before the
4260
+ * network has actually changed.
4261
+ */
4262
+ async ensureSwapChain(chainId) {
4263
+ const provider = this.requireConnectedProvider();
4264
+ const state = this.requireState();
4265
+ const chain = VIEM_CHAIN2[chainId];
4266
+ if (!chain) {
4267
+ throw new OwneyError(
4268
+ "CHAIN_UNSUPPORTED",
4269
+ `Chain ${chainId} is not supported`,
4270
+ { chainId }
4271
+ );
4272
+ }
4273
+ await ensureWalletOnChain(
4274
+ (0, import_viem8.createPublicClient)({ chain, transport: (0, import_viem8.custom)(provider) }),
4275
+ (0, import_viem8.createWalletClient)({
4276
+ account: state.walletAddress,
4277
+ chain,
4278
+ transport: (0, import_viem8.custom)(provider)
4279
+ }),
4280
+ chainId
4281
+ );
4282
+ }
4283
+ /**
4284
+ * Binds the executor's abstract deps to this client's wallet.
4285
+ *
4286
+ * Kept as a builder rather than baked into the executor so the whole swap
4287
+ * flow stays testable without a provider — the executor never imports viem.
4288
+ */
4289
+ buildSwapDeps(quote) {
4290
+ const state = this.requireState();
4291
+ const provider = this.requireConnectedProvider();
4292
+ const srcChain = VIEM_CHAIN2[quote.src.chainId];
4293
+ const dstChain = VIEM_CHAIN2[quote.dst.chainId];
4294
+ const wallet = (0, import_viem8.createWalletClient)({
4295
+ account: state.walletAddress,
4296
+ chain: srcChain,
4297
+ transport: (0, import_viem8.custom)(provider)
4298
+ });
4299
+ const srcPublic = (0, import_viem8.createPublicClient)({
4300
+ chain: srcChain,
4301
+ transport: swapReadTransport(quote.src.chainId, this.zyfaiRpcUrls)
4302
+ });
4303
+ const dstPublic = (0, import_viem8.createPublicClient)({
4304
+ chain: dstChain,
4305
+ transport: swapReadTransport(quote.dst.chainId, this.zyfaiRpcUrls)
4306
+ });
4307
+ return {
4308
+ api: this.swapApi(),
4309
+ // Native-aware, like readSourceBalance below. Native ETH is never a
4310
+ // DEPOSIT target, so this only ever mattered once withdrawal shipped —
4311
+ // and there it is the headline case. balanceOf() on the 0xEeee sentinel
4312
+ // reverts, which would have read as "the swap landed nothing".
4313
+ readTargetBalance: async () => {
4314
+ const dst = quote.dst.address;
4315
+ if (dst.toLowerCase().startsWith("0xeeee")) {
4316
+ return dstPublic.getBalance({ address: state.walletAddress });
4317
+ }
4318
+ return dstPublic.readContract({
4319
+ address: dst,
4320
+ abi: import_viem8.erc20Abi,
4321
+ functionName: "balanceOf",
4322
+ args: [state.walletAddress]
4323
+ });
4324
+ },
4325
+ sendTransaction: async (tx) => {
4326
+ const hash = await wallet.sendTransaction({
4327
+ to: tx.to,
4328
+ data: tx.data,
4329
+ value: BigInt(tx.value || "0"),
4330
+ account: state.walletAddress,
4331
+ chain: srcChain
4332
+ });
4333
+ const receipt = await srcPublic.waitForTransactionReceipt({
4334
+ timeout: receiptTimeoutMs(quote.src.chainId),
4335
+ hash,
4336
+ confirmations: 1
4337
+ });
4338
+ if (receipt.status !== "success") {
4339
+ throw new OwneyError(
4340
+ "SWAP_REQUEST_FAILED",
4341
+ `Swap transaction reverted (tx ${hash})`,
4342
+ { hash }
4343
+ );
4344
+ }
4345
+ return hash;
4346
+ },
4347
+ signTypedData: (typedData) => wallet.signTypedData({
4348
+ account: state.walletAddress,
4349
+ ...typedData
4350
+ }),
4351
+ // Chain-bound like every other read here: the wallet provider's chain is
4352
+ // not ours to rely on mid-swap.
4353
+ readSourceBalance: async () => {
4354
+ const src = quote.src.address;
4355
+ if (src.toLowerCase().startsWith("0xeeee")) {
4356
+ return srcPublic.getBalance({ address: state.walletAddress });
4357
+ }
4358
+ return srcPublic.readContract({
4359
+ address: src,
4360
+ abi: ERC20_ALLOWANCE_ABI,
4361
+ functionName: "balanceOf",
4362
+ args: [state.walletAddress]
4363
+ });
4364
+ },
4365
+ readAllowance: (spender) => srcPublic.readContract({
4366
+ address: quote.src.address,
4367
+ abi: ERC20_ALLOWANCE_ABI,
4368
+ functionName: "allowance",
4369
+ args: [state.walletAddress, spender]
4370
+ }),
4371
+ approve: async (spender, amount) => {
4372
+ const hash = await wallet.writeContract({
4373
+ address: quote.src.address,
4374
+ abi: ERC20_ALLOWANCE_ABI,
4375
+ functionName: "approve",
4376
+ args: [spender, amount],
4377
+ account: state.walletAddress,
4378
+ chain: srcChain
4379
+ });
4380
+ await srcPublic.waitForTransactionReceipt({
4381
+ hash,
4382
+ confirmations: 1,
4383
+ timeout: receiptTimeoutMs(quote.src.chainId)
4384
+ });
4385
+ return hash;
4386
+ },
4387
+ ensureChain: (chainId) => this.ensureSwapChain(chainId),
4388
+ runner: {
4389
+ readyForSecrets: (h) => this.swapApi().readyForSecrets(h),
4390
+ submitSecret: (h, secret) => this.swapApi().submitSecret(h, secret),
4391
+ orderStatus: (h) => this.swapApi().orderStatus(h),
4392
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
4393
+ now: () => Date.now()
4394
+ }
4395
+ };
4396
+ }
4397
+ /**
4398
+ * Assets the user may pay with, and what each chain deposits into.
4399
+ *
4400
+ * The source list is deliberately wider than the deposit list: it includes
4401
+ * native ETH and USDT, which Owney never holds but users often do.
4402
+ */
4403
+ async getSwapTokens() {
4404
+ return this.swapApi().listTokens();
4405
+ }
4406
+ /**
4407
+ * Price a swap without committing to it.
4408
+ *
4409
+ * `dstAmountMin` is the number to validate against a deposit minimum —
4410
+ * `dst.amount` is an estimate that a decaying auction or slippage can undercut,
4411
+ * and a swap landing below the floor leaves the user swapped but not
4412
+ * deposited.
4413
+ */
4414
+ async getSwapQuote(params) {
4415
+ const state = this.requireState();
4416
+ return this.swapApi().quote({
4417
+ ...params,
4418
+ walletAddress: state.walletAddress
4419
+ });
4420
+ }
4421
+ /**
4422
+ * Swap an asset the user holds into a deposit asset, then deposit it.
4423
+ *
4424
+ * Kept separate from `deposit()` rather than bolted on as an option: the
4425
+ * return shape differs, the staging callback is meaningless on the plain
4426
+ * path, and integrators who never swap should not have to reason about any
4427
+ * of it.
4428
+ *
4429
+ * The deposit runs on the MEASURED arrival, not the quote. A quote is an
4430
+ * estimate, so depositing the quoted figure would either strand dust or try
4431
+ * to move funds that never came.
4432
+ *
4433
+ * Failure modes differ in a way callers must respect. A same-chain swap is
4434
+ * atomic — if it fails, nothing moved. A cross-chain swap escrows the user's
4435
+ * funds first, so SWAP_ORDER_EXPIRED / REFUNDED / CANCELLED all mean the
4436
+ * money left the wallet. Only the former can honestly say "nothing has left
4437
+ * your wallet".
4438
+ */
4439
+ async swapAndDeposit(options) {
4440
+ const state = this.requireState();
4441
+ const api = this.swapApi();
4442
+ const quote = await api.quote({
4443
+ from: options.from,
4444
+ to: options.to,
4445
+ walletAddress: state.walletAddress
4446
+ });
4447
+ await this.ensureSwapChain(quote.src.chainId);
4448
+ const swap = await executeSwap(this.buildSwapDeps(quote), {
4449
+ quote,
4450
+ walletAddress: state.walletAddress,
4451
+ ...options.slippage === void 0 ? {} : { slippage: options.slippage },
4452
+ ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {}
4453
+ });
4454
+ options.onSwapProgress?.("depositing");
4455
+ await this.ensureSwapChain(quote.dst.chainId);
4456
+ const deposit = await this.deposit({
4457
+ amount: swap.received,
4458
+ asset: options.to.symbol,
4459
+ ...options.agentId ? { agentId: options.agentId } : {}
4460
+ });
4461
+ return { swap, deposit };
4462
+ }
4463
+ /**
4464
+ * Withdraw from an agent and swap the proceeds into whatever the user wants
4465
+ * to hold, delivered to their own wallet.
4466
+ *
4467
+ * The mirror of `swapAndDeposit()`, with one structural difference that
4468
+ * drives the whole implementation: a deposit swap starts from funds already
4469
+ * sitting in the wallet, but a withdrawal has to wait for them. The agent's
4470
+ * provider acknowledges a withdrawal and *then* queues the on-chain transfer
4471
+ * to the EOA, so `withdraw()` resolving means "accepted", not "arrived".
4472
+ * Quoting before the tokens land would size the swap against a balance that
4473
+ * is not there yet.
4474
+ *
4475
+ * The swap is therefore sized from the MEASURED arrival, exactly as the
4476
+ * deposit path sizes its deposit from the measured swap output. On a full
4477
+ * withdrawal there is no other number available — "MAX" has no figure until
4478
+ * the agent picks one.
4479
+ *
4480
+ * **Failure here is not symmetrical with the deposit path.** A failed
4481
+ * deposit-swap leaves the user holding what they started with. A failed
4482
+ * withdrawal-swap leaves them holding the AGENT'S asset in their own wallet:
4483
+ * the money is out, safe, and in the wrong denomination. Both
4484
+ * `WITHDRAW_ARRIVAL_TIMEOUT` and `WITHDRAW_SWAP_FAILED` carry `withdrawn` for
4485
+ * that reason — the UI has to tell the user where their money actually is,
4486
+ * and must never present either as a lost withdrawal.
4487
+ */
4488
+ async withdrawAndSwap(options) {
4489
+ const state = this.requireState();
4490
+ const activeChainId = this.requireChainId();
4491
+ if (options.from.chainId !== activeChainId) {
4492
+ throw new OwneyError(
4493
+ "CHAIN_MISMATCH",
4494
+ `Cannot withdraw from chain ${options.from.chainId} while the active chain is ${activeChainId}. Activate on that chain first.`,
4495
+ { requested: options.from.chainId, active: activeChainId }
4496
+ );
4497
+ }
4498
+ const asset = SupportedAssets.find(
4499
+ (a) => a.chainId === options.from.chainId && a.symbol === options.from.symbol.toUpperCase()
4500
+ );
4501
+ if (!asset) {
4502
+ throw new OwneyError(
4503
+ "WITHDRAW_NO_PERMITTED_TOKENS",
4504
+ `${options.from.symbol} on chain ${options.from.chainId} is not an asset Owney holds`,
4505
+ { ...options.from }
4506
+ );
4507
+ }
4508
+ const srcChain = VIEM_CHAIN2[options.from.chainId];
4509
+ const srcPublic = (0, import_viem8.createPublicClient)({
4510
+ chain: srcChain,
4511
+ transport: swapReadTransport(options.from.chainId, this.zyfaiRpcUrls)
4512
+ });
4513
+ const readWalletBalance = () => srcPublic.readContract({
4514
+ address: asset.address,
4515
+ abi: import_viem8.erc20Abi,
4516
+ functionName: "balanceOf",
4517
+ args: [state.walletAddress]
4518
+ });
4519
+ const baseline = await readWalletBalance();
4520
+ debugLog("owney-sdk", "withdrawAndSwap: baseline", {
4521
+ asset: `${asset.symbol}@${asset.chainId}`,
4522
+ baseline: baseline.toString()
4523
+ });
4524
+ options.onSwapProgress?.("withdrawing");
4525
+ const withdraw = await this.withdraw({
4526
+ asset: options.from.symbol,
4527
+ ...options.amount === void 0 ? {} : { amount: options.amount },
4528
+ ...options.agentId ? { agentId: options.agentId } : {}
4529
+ });
4530
+ const arrived = await awaitWithdrawalArrival({
4531
+ readBalance: readWalletBalance,
4532
+ baseline,
4533
+ ...options.arrivalTimeoutMs === void 0 ? {} : { timeoutMs: options.arrivalTimeoutMs }
4534
+ });
4535
+ const withdrawn = arrived.toString();
4536
+ options.onSwapProgress?.("withdrawn");
4537
+ try {
4538
+ const quote = await this.swapApi().quote({
4539
+ from: { ...options.from, amount: withdrawn },
4540
+ to: options.to,
4541
+ direction: "withdraw",
4542
+ walletAddress: state.walletAddress
4543
+ });
4544
+ await this.ensureSwapChain(quote.src.chainId);
4545
+ const swap = await executeSwap(this.buildSwapDeps(quote), {
4546
+ quote,
4547
+ walletAddress: state.walletAddress,
4548
+ direction: "withdraw",
4549
+ ...options.slippage === void 0 ? {} : { slippage: options.slippage },
4550
+ ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {}
4551
+ });
4552
+ return { withdraw, withdrawn, swap };
4553
+ } catch (error) {
4554
+ throw new OwneyError(
4555
+ "WITHDRAW_SWAP_FAILED",
4556
+ `Withdrew ${withdrawn} ${asset.symbol} to your wallet, but the swap to ${options.to.symbol} did not complete. The funds are in your wallet as ${asset.symbol}.`,
4557
+ {
4558
+ withdrawn,
4559
+ asset: asset.symbol,
4560
+ chainId: asset.chainId,
4561
+ intendedSymbol: options.to.symbol,
4562
+ intendedChainId: options.to.chainId,
4563
+ cause: error instanceof Error ? error.message : String(error),
4564
+ ...error instanceof OwneyError ? { causeCode: error.code } : {}
4565
+ }
4566
+ );
4567
+ }
4568
+ }
3712
4569
  /**
3713
4570
  * Withdraw funds from a specific agent, or all agents that support the active chain+asset if agentId is omitted.
3714
4571
  * Validates that the asset is supported by the target agent(s) on the active chain.
@@ -3879,7 +4736,11 @@ var OwneySDK = class {
3879
4736
  const chainId = this.requireChainId();
3880
4737
  if (agentId) {
3881
4738
  const agent = this.getAgent(agentId);
3882
- const result = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
4739
+ const result = await this.readAgent(
4740
+ agent,
4741
+ "balances",
4742
+ () => agent.getBalances(state, chainId)
4743
+ );
3883
4744
  return result;
3884
4745
  }
3885
4746
  let totalBalance = 0;
@@ -3887,7 +4748,11 @@ var OwneySDK = class {
3887
4748
  const entries = [...this.getActiveAgents().entries()];
3888
4749
  const balanceResults = await Promise.allSettled(
3889
4750
  entries.map(async ([id, agent]) => {
3890
- const b = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
4751
+ const b = await this.readAgent(
4752
+ agent,
4753
+ "balances",
4754
+ () => agent.getBalances(state, chainId)
4755
+ );
3891
4756
  return [id, b];
3892
4757
  })
3893
4758
  );
@@ -3908,7 +4773,8 @@ var OwneySDK = class {
3908
4773
  const reason = settledResult.reason;
3909
4774
  agentFailures.push(reason);
3910
4775
  const retryDelay = rateLimitDelay(reason);
3911
- if (retryDelay !== void 0) agentRetryAt[agentId2] = Date.now() + retryDelay;
4776
+ if (retryDelay !== void 0)
4777
+ agentRetryAt[agentId2] = Date.now() + retryDelay;
3912
4778
  agentErrors[agentId2] = reason instanceof Error ? reason.message : String(reason);
3913
4779
  }
3914
4780
  if (successCount === 0) {
@@ -3935,14 +4801,22 @@ var OwneySDK = class {
3935
4801
  const chainId = this.requireChainId();
3936
4802
  if (agentId) {
3937
4803
  const agent = this.getAgent(agentId);
3938
- return this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId));
4804
+ return this.readAgent(
4805
+ agent,
4806
+ "earnings",
4807
+ () => agent.getEarnings(state, chainId)
4808
+ );
3939
4809
  }
3940
4810
  let totalEarnings = 0;
3941
4811
  const results = {};
3942
4812
  const entries = [...this.getActiveAgents().entries()];
3943
4813
  const earningsResults = await Promise.all(
3944
4814
  entries.map(async ([id, agent]) => {
3945
- const e = await this.readAgent(agent, "earnings", () => agent.getEarnings(state, chainId));
4815
+ const e = await this.readAgent(
4816
+ agent,
4817
+ "earnings",
4818
+ () => agent.getEarnings(state, chainId)
4819
+ );
3946
4820
  return [id, e];
3947
4821
  })
3948
4822
  );
@@ -4087,7 +4961,11 @@ var OwneySDK = class {
4087
4961
  ),
4088
4962
  Promise.all(
4089
4963
  entries.map(async ([id, agent]) => {
4090
- const b = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
4964
+ const b = await this.readAgent(
4965
+ agent,
4966
+ "balances",
4967
+ () => agent.getBalances(state, chainId)
4968
+ );
4091
4969
  return [id, balanceForApyScope(b, chainId, tokenSymbol)];
4092
4970
  })
4093
4971
  )
@@ -4147,7 +5025,12 @@ var OwneySDK = class {
4147
5025
  const { agentId, filters } = options ?? {};
4148
5026
  if (agentId) {
4149
5027
  const agent = this.getAgent(agentId);
4150
- return this.readAgent(agent, "history", () => agent.getHistory(state, chainId, filters), filters);
5028
+ return this.readAgent(
5029
+ agent,
5030
+ "history",
5031
+ () => agent.getHistory(state, chainId, filters),
5032
+ filters
5033
+ );
4151
5034
  }
4152
5035
  const activeAgents = [...this.getActiveAgents().values()];
4153
5036
  const cursorMap = filters?.cursor ? decodeMultiAgentCursor(filters.cursor) : {};
@@ -4204,13 +5087,21 @@ var OwneySDK = class {
4204
5087
  const chainId = this.requireChainId();
4205
5088
  if (agentId) {
4206
5089
  const agent = this.getAgent(agentId);
4207
- return this.readAgent(agent, "profile", () => agent.getUserProfile(state, chainId));
5090
+ return this.readAgent(
5091
+ agent,
5092
+ "profile",
5093
+ () => agent.getUserProfile(state, chainId)
5094
+ );
4208
5095
  }
4209
5096
  const results = {};
4210
5097
  const entries = [...this.getActiveAgents().entries()];
4211
5098
  const profileResults = await Promise.all(
4212
5099
  entries.map(async ([id, agent]) => {
4213
- const p = await this.readAgent(agent, "profile", () => agent.getUserProfile(state, chainId));
5100
+ const p = await this.readAgent(
5101
+ agent,
5102
+ "profile",
5103
+ () => agent.getUserProfile(state, chainId)
5104
+ );
4214
5105
  return [id, p];
4215
5106
  })
4216
5107
  );
@@ -4269,10 +5160,10 @@ var OwneySDK = class {
4269
5160
  );
4270
5161
  }
4271
5162
  const provider = this.requireConnectedProvider();
4272
- const wallet = (0, import_viem6.createWalletClient)({
5163
+ const wallet = (0, import_viem8.createWalletClient)({
4273
5164
  account: state.walletAddress,
4274
5165
  chain: VIEM_CHAIN2[chainId],
4275
- transport: (0, import_viem6.custom)(provider)
5166
+ transport: (0, import_viem8.custom)(provider)
4276
5167
  });
4277
5168
  const hash = await wallet.writeContract({
4278
5169
  address: token,
@@ -4282,9 +5173,9 @@ var OwneySDK = class {
4282
5173
  account: state.walletAddress,
4283
5174
  chain: VIEM_CHAIN2[chainId]
4284
5175
  });
4285
- const publicClient = (0, import_viem6.createPublicClient)({
5176
+ const publicClient = (0, import_viem8.createPublicClient)({
4286
5177
  chain: VIEM_CHAIN2[chainId],
4287
- transport: (0, import_viem6.custom)(provider)
5178
+ transport: (0, import_viem8.custom)(provider)
4288
5179
  });
4289
5180
  const receipt = await publicClient.waitForTransactionReceipt({
4290
5181
  hash,
@@ -4318,13 +5209,23 @@ var OwneySDK = class {
4318
5209
  const agentOptions = { tokenSymbol, chainId };
4319
5210
  if (agentId) {
4320
5211
  const agent = this.getAgent(agentId);
4321
- return this.readAgent(agent, "agentApy", () => agent.getAgentApy(days, agentOptions), { days, ...agentOptions });
5212
+ return this.readAgent(
5213
+ agent,
5214
+ "agentApy",
5215
+ () => agent.getAgentApy(days, agentOptions),
5216
+ { days, ...agentOptions }
5217
+ );
4322
5218
  }
4323
5219
  const results = {};
4324
5220
  const agentEntries = [...this.agents.entries()];
4325
5221
  const apyResults = await Promise.all(
4326
5222
  agentEntries.map(async ([id, agent]) => {
4327
- const apy = await this.readAgent(agent, "agentApy", () => agent.getAgentApy(days, agentOptions), { days, ...agentOptions });
5223
+ const apy = await this.readAgent(
5224
+ agent,
5225
+ "agentApy",
5226
+ () => agent.getAgentApy(days, agentOptions),
5227
+ { days, ...agentOptions }
5228
+ );
4328
5229
  return [id, apy];
4329
5230
  })
4330
5231
  );
@@ -4351,7 +5252,11 @@ var OwneySDK = class {
4351
5252
  const entries = [...activeAgents.entries()];
4352
5253
  const balanceResults = await Promise.allSettled(
4353
5254
  entries.map(async ([id, agent]) => {
4354
- const b = await this.readAgent(agent, "balances", () => agent.getBalances(state, chainId));
5255
+ const b = await this.readAgent(
5256
+ agent,
5257
+ "balances",
5258
+ () => agent.getBalances(state, chainId)
5259
+ );
4355
5260
  return [id, b.positions ?? []];
4356
5261
  })
4357
5262
  );
@@ -4396,13 +5301,13 @@ var OwneySDK = class {
4396
5301
  };
4397
5302
 
4398
5303
  // src/agents/zyfai/zyfai.siwx.ts
4399
- var import_viem7 = require("viem");
5304
+ var import_viem9 = require("viem");
4400
5305
  var import_siwe = require("siwe");
4401
5306
  var import_sdk2 = require("@zyfai/sdk");
4402
5307
 
4403
5308
  // src/agents/zyfai/zyfai.siwx-cache.ts
4404
- var KEY_PREFIX2 = "owney.siwx.session";
4405
- var storage2 = () => {
5309
+ var KEY_PREFIX3 = "owney.siwx.session";
5310
+ var storage3 = () => {
4406
5311
  if (typeof window === "undefined") return null;
4407
5312
  try {
4408
5313
  return window.localStorage;
@@ -4410,8 +5315,8 @@ var storage2 = () => {
4410
5315
  return null;
4411
5316
  }
4412
5317
  };
4413
- var buildKey2 = (address) => `${KEY_PREFIX2}:${address.toLowerCase()}`;
4414
- var legacyKeyPrefix2 = (address) => `${KEY_PREFIX2}:${address.toLowerCase()}:`;
5318
+ var buildKey2 = (address) => `${KEY_PREFIX3}:${address.toLowerCase()}`;
5319
+ var legacyKeyPrefix2 = (address) => `${KEY_PREFIX3}:${address.toLowerCase()}:`;
4415
5320
  var memorySiwxSessions = /* @__PURE__ */ new Map();
4416
5321
  var readLegacySiwxSession = (store, address) => {
4417
5322
  if (!store) return null;
@@ -4443,7 +5348,7 @@ var readLegacySiwxSession = (store, address) => {
4443
5348
  var readSiwxSession = (address, chainId) => {
4444
5349
  if (typeof window === "undefined") return null;
4445
5350
  const key2 = buildKey2(address);
4446
- const store = storage2();
5351
+ const store = storage3();
4447
5352
  let raw = null;
4448
5353
  try {
4449
5354
  raw = store?.getItem(key2) ?? null;
@@ -4473,7 +5378,7 @@ var writeSiwxSession = (address, _chainId, session) => {
4473
5378
  if (typeof window === "undefined") return;
4474
5379
  const key2 = buildKey2(address);
4475
5380
  memorySiwxSessions.set(key2, session);
4476
- const store = storage2();
5381
+ const store = storage3();
4477
5382
  try {
4478
5383
  store?.setItem(key2, JSON.stringify(session));
4479
5384
  } catch {
@@ -4482,7 +5387,7 @@ var writeSiwxSession = (address, _chainId, session) => {
4482
5387
  var clearSiwxSession = (address, _chainId) => {
4483
5388
  const key2 = buildKey2(address);
4484
5389
  memorySiwxSessions.delete(key2);
4485
- const store = storage2();
5390
+ const store = storage3();
4486
5391
  try {
4487
5392
  store?.removeItem(key2);
4488
5393
  } catch {
@@ -4523,7 +5428,7 @@ function buildSIWXConfig(deps) {
4523
5428
  issuedAt,
4524
5429
  toString() {
4525
5430
  return new import_siwe.SiweMessage({
4526
- address: (0, import_viem7.getAddress)(accountAddress),
5431
+ address: (0, import_viem9.getAddress)(accountAddress),
4527
5432
  chainId: numericChainId(chainId),
4528
5433
  domain,
4529
5434
  uri,
@@ -4601,9 +5506,9 @@ function buildSIWXConfig(deps) {
4601
5506
  }
4602
5507
  function createOwneySIWX(config) {
4603
5508
  const zyfai = new import_sdk2.ZyfaiSDK({ apiKey: config.apiKey });
4604
- const http2 = zyfai.httpClient;
5509
+ const http4 = zyfai.httpClient;
4605
5510
  return buildSIWXConfig({
4606
- post: (url, data) => http2.post(url, data),
5511
+ post: (url, data) => http4.post(url, data),
4607
5512
  referralSource: config.referralSource
4608
5513
  });
4609
5514
  }
@@ -4616,5 +5521,6 @@ function createOwneySIWX(config) {
4616
5521
  OwneyError,
4617
5522
  OwneySDK,
4618
5523
  createOwneySIWX,
5524
+ listPendingSwaps,
4619
5525
  setOwneyDebug
4620
5526
  });