@owney/sdk 0.7.26-beta.5 → 0.7.26-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -26,10 +26,15 @@ __export(index_exports, {
26
26
  NotConnectedError: () => NotConnectedError,
27
27
  OwneyError: () => OwneyError,
28
28
  OwneySDK: () => OwneySDK,
29
+ clearWithdrawSwap: () => clearWithdrawSwap,
29
30
  createOwneySIWX: () => createOwneySIWX,
30
31
  listPendingDeposits: () => listPendingDeposits,
31
32
  listPendingSwaps: () => listOrders,
32
- setOwneyDebug: () => setOwneyDebug
33
+ listWithdrawSwaps: () => listWithdrawSwaps,
34
+ saveWithdrawSwap: () => saveWithdrawSwap,
35
+ setOwneyDebug: () => setOwneyDebug,
36
+ subscribeWithdrawSwaps: () => subscribeWithdrawSwaps,
37
+ updateWithdrawSwap: () => updateWithdrawSwap
33
38
  });
34
39
  module.exports = __toCommonJS(index_exports);
35
40
 
@@ -2329,7 +2334,7 @@ function createSwapApi(baseUrl, apiKey) {
2329
2334
  baseUrl,
2330
2335
  apiKey,
2331
2336
  `/tokens/search?${new URLSearchParams({
2332
- query: params.query,
2337
+ ...params.query === void 0 ? {} : { query: params.query },
2333
2338
  // Omitted, the routing API searches every supported chain — which is
2334
2339
  // what a picker wants, since its rows already span all three.
2335
2340
  ...params.chainId === void 0 ? {} : { chainId: String(params.chainId) },
@@ -3122,6 +3127,10 @@ async function runSameChainFusion(deps, options, walletAddress) {
3122
3127
  quoteId: built.quoteId,
3123
3128
  ...built.extension ? { extension: built.extension } : {}
3124
3129
  });
3130
+ try {
3131
+ options.onOrderSubmitted?.(built.orderHash);
3132
+ } catch {
3133
+ }
3125
3134
  await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
3126
3135
  orderHash: built.orderHash,
3127
3136
  ...onStage ? { onStage } : {}
@@ -3220,6 +3229,10 @@ async function runFusion(deps, options, walletAddress) {
3220
3229
  ...built.extension ? { extension: built.extension } : {}
3221
3230
  });
3222
3231
  debugLog("owney-sdk", "swap: order submitted, polling escrows");
3232
+ try {
3233
+ options.onOrderSubmitted?.(built.orderHash);
3234
+ } catch {
3235
+ }
3223
3236
  try {
3224
3237
  await runFusionOrder(deps.runner, {
3225
3238
  orderHash: built.orderHash,
@@ -3334,8 +3347,8 @@ async function buildErc2612Permit(reads, input) {
3334
3347
  );
3335
3348
  }
3336
3349
 
3337
- // src/lib/swap/swap.pending-deposit.ts
3338
- var KEY_PREFIX3 = "owney.swap.pending-deposit";
3350
+ // src/lib/swap/swap.withdraw-swap.ts
3351
+ var KEY_PREFIX3 = "owney.swap.withdraw";
3339
3352
  var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
3340
3353
  var storage3 = () => {
3341
3354
  if (typeof window === "undefined") return null;
@@ -3345,26 +3358,60 @@ var storage3 = () => {
3345
3358
  return null;
3346
3359
  }
3347
3360
  };
3348
- var keyFor2 = (id) => `${KEY_PREFIX3}.${id}`;
3349
- function savePendingDeposit(entry) {
3361
+ var keyFor2 = (orderHash) => `${KEY_PREFIX3}.${orderHash}`;
3362
+ var listeners = /* @__PURE__ */ new Set();
3363
+ function subscribeWithdrawSwaps(listener) {
3364
+ listeners.add(listener);
3365
+ return () => {
3366
+ listeners.delete(listener);
3367
+ };
3368
+ }
3369
+ var notify = () => {
3370
+ for (const listener of listeners) {
3371
+ try {
3372
+ listener();
3373
+ } catch {
3374
+ }
3375
+ }
3376
+ };
3377
+ function saveWithdrawSwap(record) {
3350
3378
  const store = storage3();
3351
3379
  if (!store) return;
3352
3380
  try {
3353
- store.setItem(keyFor2(entry.id), JSON.stringify(entry));
3381
+ store.setItem(keyFor2(record.orderHash), JSON.stringify(record));
3354
3382
  } catch {
3355
3383
  return;
3356
3384
  }
3385
+ notify();
3357
3386
  }
3358
- function clearPendingDeposit(id) {
3387
+ function updateWithdrawSwap(orderHash, patch) {
3359
3388
  const store = storage3();
3360
3389
  if (!store) return;
3361
3390
  try {
3362
- store.removeItem(keyFor2(id));
3391
+ const raw = store.getItem(keyFor2(orderHash));
3392
+ if (!raw) return;
3393
+ const current = JSON.parse(raw);
3394
+ const next = { ...current, ...patch };
3395
+ if (next.status !== "swapping" && current.status === "swapping" && next.settledAt === void 0) {
3396
+ next.settledAt = Date.now();
3397
+ }
3398
+ store.setItem(keyFor2(orderHash), JSON.stringify(next));
3363
3399
  } catch {
3364
3400
  return;
3365
3401
  }
3402
+ notify();
3366
3403
  }
3367
- function listPendingDeposits(now = Date.now()) {
3404
+ function clearWithdrawSwap(orderHash) {
3405
+ const store = storage3();
3406
+ if (!store) return;
3407
+ try {
3408
+ store.removeItem(keyFor2(orderHash));
3409
+ } catch {
3410
+ return;
3411
+ }
3412
+ notify();
3413
+ }
3414
+ function listWithdrawSwaps(now = Date.now()) {
3368
3415
  const store = storage3();
3369
3416
  if (!store) return [];
3370
3417
  const out = [];
@@ -3394,6 +3441,66 @@ function listPendingDeposits(now = Date.now()) {
3394
3441
  return out.sort((a, b) => b.createdAt - a.createdAt);
3395
3442
  }
3396
3443
 
3444
+ // src/lib/swap/swap.pending-deposit.ts
3445
+ var KEY_PREFIX4 = "owney.swap.pending-deposit";
3446
+ var MAX_AGE_MS3 = 24 * 60 * 60 * 1e3;
3447
+ var storage4 = () => {
3448
+ if (typeof window === "undefined") return null;
3449
+ try {
3450
+ return window.localStorage;
3451
+ } catch {
3452
+ return null;
3453
+ }
3454
+ };
3455
+ var keyFor3 = (id) => `${KEY_PREFIX4}.${id}`;
3456
+ function savePendingDeposit(entry) {
3457
+ const store = storage4();
3458
+ if (!store) return;
3459
+ try {
3460
+ store.setItem(keyFor3(entry.id), JSON.stringify(entry));
3461
+ } catch {
3462
+ return;
3463
+ }
3464
+ }
3465
+ function clearPendingDeposit(id) {
3466
+ const store = storage4();
3467
+ if (!store) return;
3468
+ try {
3469
+ store.removeItem(keyFor3(id));
3470
+ } catch {
3471
+ return;
3472
+ }
3473
+ }
3474
+ function listPendingDeposits(now = Date.now()) {
3475
+ const store = storage4();
3476
+ if (!store) return [];
3477
+ const out = [];
3478
+ try {
3479
+ const keys = [];
3480
+ for (let i = 0; i < store.length; i++) {
3481
+ const key2 = store.key(i);
3482
+ if (key2?.startsWith(`${KEY_PREFIX4}.`)) keys.push(key2);
3483
+ }
3484
+ for (const key2 of keys) {
3485
+ const raw = store.getItem(key2);
3486
+ if (!raw) continue;
3487
+ try {
3488
+ const parsed = JSON.parse(raw);
3489
+ if (now - parsed.createdAt > MAX_AGE_MS3) {
3490
+ store.removeItem(key2);
3491
+ continue;
3492
+ }
3493
+ out.push(parsed);
3494
+ } catch {
3495
+ store.removeItem(key2);
3496
+ }
3497
+ }
3498
+ } catch {
3499
+ return out;
3500
+ }
3501
+ return out.sort((a, b) => b.createdAt - a.createdAt);
3502
+ }
3503
+
3397
3504
  // src/lib/health-report.ts
3398
3505
  var ROUTING_API_BASE_URL2 = "https://owney-routing-api-243946518160.europe-west4.run.app";
3399
3506
  async function reportAgentFailure(apiKey, agentType, errorCode, baseUrl = ROUTING_API_BASE_URL2) {
@@ -5148,6 +5255,25 @@ var OwneySDK = class {
5148
5255
  listPendingDeposits() {
5149
5256
  return listPendingDeposits();
5150
5257
  }
5258
+ /**
5259
+ * Backgrounded withdrawal swaps and how they ended, newest first.
5260
+ *
5261
+ * Nothing to act on — unlike `listPendingDeposits`, which describes a job the
5262
+ * user can finish. This is a watch: the funds are in escrow and the only
5263
+ * open question is where they land. See `swap.withdraw-swap.ts`.
5264
+ */
5265
+ listWithdrawSwaps() {
5266
+ return listWithdrawSwaps();
5267
+ }
5268
+ /**
5269
+ * Retire a settled record. The user has read the outcome.
5270
+ *
5271
+ * Only they do this — a record that disappears on its own takes with it the
5272
+ * only thing telling someone where their money went.
5273
+ */
5274
+ dismissWithdrawSwap(orderHash) {
5275
+ clearWithdrawSwap(orderHash);
5276
+ }
5151
5277
  /**
5152
5278
  * Deposit a swap that already landed.
5153
5279
  *
@@ -5194,7 +5320,10 @@ var OwneySDK = class {
5194
5320
  */
5195
5321
  async resumePendingSwaps(options = {}) {
5196
5322
  const pending = listOrders();
5197
- if (pending.length === 0) return [];
5323
+ if (pending.length === 0) {
5324
+ await this.resumeSameChainSwaps(options);
5325
+ return [];
5326
+ }
5198
5327
  const runner = this.swapRunnerDeps();
5199
5328
  const results = [];
5200
5329
  for (const order of pending) {
@@ -5208,6 +5337,7 @@ var OwneySDK = class {
5208
5337
  } : {}
5209
5338
  });
5210
5339
  clearOrder(order.orderHash);
5340
+ updateWithdrawSwap(order.orderHash, { status: "landed" });
5211
5341
  results.push({
5212
5342
  orderHash: order.orderHash,
5213
5343
  status: "completed"
@@ -5215,6 +5345,9 @@ var OwneySDK = class {
5215
5345
  } catch (error) {
5216
5346
  if (error instanceof OwneyError && (error.code === "SWAP_ORDER_REFUNDED" || error.code === "SWAP_ORDER_CANCELLED")) {
5217
5347
  clearOrder(order.orderHash);
5348
+ updateWithdrawSwap(order.orderHash, { status: "refunded" });
5349
+ } else {
5350
+ updateWithdrawSwap(order.orderHash, { status: "unknown" });
5218
5351
  }
5219
5352
  results.push({
5220
5353
  orderHash: order.orderHash,
@@ -5223,8 +5356,56 @@ var OwneySDK = class {
5223
5356
  });
5224
5357
  }
5225
5358
  }
5359
+ await this.resumeSameChainSwaps(options);
5226
5360
  return results;
5227
5361
  }
5362
+ /**
5363
+ * Pick up SAME-CHAIN orders a previous session left in the auction.
5364
+ *
5365
+ * They are not in the secret store and never will be: that store exists to
5366
+ * keep preimages, and a same-chain fill needs none — the resolver takes the
5367
+ * maker's tokens and delivers the taker's in one transaction. So the only
5368
+ * record of one is the withdrawal record itself, which carries the order
5369
+ * hash, and that is all a resume needs here.
5370
+ *
5371
+ * Gentler than the Fusion+ case in the way that matters. Nothing is
5372
+ * escrowed, so an order nobody ever picks up leaves the wallet exactly as it
5373
+ * was — this is catching up on news, not rescuing funds.
5374
+ *
5375
+ * Same-chain is read off the record rather than stored as a rail: a
5376
+ * withdrawal whose source and destination share a chain could not have been
5377
+ * anything else.
5378
+ */
5379
+ async resumeSameChainSwaps(options = {}) {
5380
+ const running = listWithdrawSwaps().filter(
5381
+ (record) => record.status === "swapping" && record.srcChainId === record.dstChainId
5382
+ );
5383
+ for (const record of running) {
5384
+ const runner = {
5385
+ // The chain bound in. That product keys its orders per chain, and the
5386
+ // Fusion+ endpoint answers 404 for one — which would read as an order
5387
+ // that vanished rather than one still in the auction.
5388
+ orderStatus: (h) => this.swapApi().orderStatus(h, record.srcChainId),
5389
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5390
+ now: () => Date.now()
5391
+ };
5392
+ try {
5393
+ await runSameChainOrder(runner, {
5394
+ orderHash: record.orderHash,
5395
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
5396
+ ...options.onStage ? {
5397
+ onStage: (stage) => options.onStage(record.orderHash, stage)
5398
+ } : {}
5399
+ });
5400
+ updateWithdrawSwap(record.orderHash, { status: "landed" });
5401
+ } catch (error) {
5402
+ const code = error instanceof OwneyError ? error.code : void 0;
5403
+ updateWithdrawSwap(record.orderHash, {
5404
+ status: code === "SWAP_ORDER_EXPIRED" || code === "SWAP_ORDER_CANCELLED" ? "refunded" : "unknown"
5405
+ });
5406
+ }
5407
+ }
5408
+ }
5228
5409
  /**
5229
5410
  * What the wallet actually holds, across every supported chain, restricted
5230
5411
  * to tokens a swap could use.
@@ -5271,13 +5452,16 @@ var OwneySDK = class {
5271
5452
  * tokens can share a ticker, and `providers` (how many token lists carry the
5272
5453
  * address) is the only usable signal for telling them apart. Surface it.
5273
5454
  *
5274
- * Returns nothing for a blank query rather than asking for the whole list.
5455
+ * A blank query WITH a chainId lists that chain's catalogue instead of
5456
+ * searching it — what a picker wants when someone selects a chain before
5457
+ * typing anything. Blank with no chain returns nothing: that would be asking
5458
+ * for the top of every catalogue at once, which is a download, not a list.
5275
5459
  */
5276
5460
  async searchSwapTokens(params) {
5277
- const query = params.query.trim();
5278
- if (!query) return { tokens: [] };
5461
+ const query = params.query?.trim() ?? "";
5462
+ if (!query && params.chainId === void 0) return { tokens: [] };
5279
5463
  return this.swapApi().searchTokens({
5280
- query,
5464
+ ...query ? { query } : {},
5281
5465
  ...params.chainId === void 0 ? {} : { chainId: params.chainId },
5282
5466
  ...params.limit === void 0 ? {} : { limit: params.limit },
5283
5467
  ...params.direction === void 0 ? {} : { direction: params.direction }
@@ -5457,7 +5641,14 @@ var OwneySDK = class {
5457
5641
  walletAddress: state.walletAddress,
5458
5642
  direction: "withdraw",
5459
5643
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
5460
- ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {}
5644
+ ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
5645
+ ...options.onOrderSubmitted ? {
5646
+ onOrderSubmitted: (orderHash) => options.onOrderSubmitted({
5647
+ orderHash,
5648
+ withdrawn,
5649
+ expected: quote.dst.amount
5650
+ })
5651
+ } : {}
5461
5652
  });
5462
5653
  return { withdraw, withdrawn, swap };
5463
5654
  } catch (error) {
@@ -6216,8 +6407,8 @@ var import_siwe = require("siwe");
6216
6407
  var import_sdk2 = require("@zyfai/sdk");
6217
6408
 
6218
6409
  // src/agents/zyfai/zyfai.siwx-cache.ts
6219
- var KEY_PREFIX4 = "owney.siwx.session";
6220
- var storage4 = () => {
6410
+ var KEY_PREFIX5 = "owney.siwx.session";
6411
+ var storage5 = () => {
6221
6412
  if (typeof window === "undefined") return null;
6222
6413
  try {
6223
6414
  return window.localStorage;
@@ -6225,8 +6416,8 @@ var storage4 = () => {
6225
6416
  return null;
6226
6417
  }
6227
6418
  };
6228
- var buildKey2 = (address) => `${KEY_PREFIX4}:${address.toLowerCase()}`;
6229
- var legacyKeyPrefix2 = (address) => `${KEY_PREFIX4}:${address.toLowerCase()}:`;
6419
+ var buildKey2 = (address) => `${KEY_PREFIX5}:${address.toLowerCase()}`;
6420
+ var legacyKeyPrefix2 = (address) => `${KEY_PREFIX5}:${address.toLowerCase()}:`;
6230
6421
  var memorySiwxSessions = /* @__PURE__ */ new Map();
6231
6422
  var readLegacySiwxSession = (store, address) => {
6232
6423
  if (!store) return null;
@@ -6258,7 +6449,7 @@ var readLegacySiwxSession = (store, address) => {
6258
6449
  var readSiwxSession = (address, chainId) => {
6259
6450
  if (typeof window === "undefined") return null;
6260
6451
  const key2 = buildKey2(address);
6261
- const store = storage4();
6452
+ const store = storage5();
6262
6453
  let raw = null;
6263
6454
  try {
6264
6455
  raw = store?.getItem(key2) ?? null;
@@ -6288,7 +6479,7 @@ var writeSiwxSession = (address, _chainId, session) => {
6288
6479
  if (typeof window === "undefined") return;
6289
6480
  const key2 = buildKey2(address);
6290
6481
  memorySiwxSessions.set(key2, session);
6291
- const store = storage4();
6482
+ const store = storage5();
6292
6483
  try {
6293
6484
  store?.setItem(key2, JSON.stringify(session));
6294
6485
  } catch {
@@ -6297,7 +6488,7 @@ var writeSiwxSession = (address, _chainId, session) => {
6297
6488
  var clearSiwxSession = (address, _chainId) => {
6298
6489
  const key2 = buildKey2(address);
6299
6490
  memorySiwxSessions.delete(key2);
6300
- const store = storage4();
6491
+ const store = storage5();
6301
6492
  try {
6302
6493
  store?.removeItem(key2);
6303
6494
  } catch {
@@ -6430,8 +6621,13 @@ function createOwneySIWX(config) {
6430
6621
  NotConnectedError,
6431
6622
  OwneyError,
6432
6623
  OwneySDK,
6624
+ clearWithdrawSwap,
6433
6625
  createOwneySIWX,
6434
6626
  listPendingDeposits,
6435
6627
  listPendingSwaps,
6436
- setOwneyDebug
6628
+ listWithdrawSwaps,
6629
+ saveWithdrawSwap,
6630
+ setOwneyDebug,
6631
+ subscribeWithdrawSwaps,
6632
+ updateWithdrawSwap
6437
6633
  });
package/dist/index.d.cts CHANGED
@@ -785,6 +785,94 @@ declare class AgentChainIncompatibleError extends OwneyError {
785
785
  }>, connectedChainId: number);
786
786
  }
787
787
 
788
+ /**
789
+ * Withdrawal swaps the user has walked away from, and how they ended.
790
+ *
791
+ * The deposit side has `swap.pending-deposit.ts` for the same shape of
792
+ * problem, but the two are not symmetrical. A pending deposit is a JOB: the
793
+ * money is in the wallet and an ordinary deposit finishes it. This is a
794
+ * WATCH — the funds are in escrow, nothing the user does moves them along,
795
+ * and the only question is where they ended up.
796
+ *
797
+ * That is the whole reason this exists. `withdrawAndSwap` used to hold the
798
+ * screen for the entire escrow lifecycle, which on a slow mainnet fill is
799
+ * minutes of a modal the user cannot leave (ROUT-242). Backgrounding it means
800
+ * the outcome arrives after the UI that started it has gone, so the outcome
801
+ * has to be written down somewhere the next render can find it.
802
+ *
803
+ * Only cross-chain swaps are recorded. A same-chain classic swap is one
804
+ * transaction and resolves in seconds; a same-chain fusion order has no stored
805
+ * preimages, so there would be nothing for a reloaded session to resume and
806
+ * the record would sit at `swapping` for ever.
807
+ *
808
+ * Kept deliberately separate from the secret store. Secrets are retired the
809
+ * moment an order reaches a verdict — that is a safety property, not a display
810
+ * one — and the record has to outlive them so the user can still be told what
811
+ * happened after they come back.
812
+ */
813
+ /**
814
+ * `swapping` is the only non-terminal state. `unknown` is what a watch that
815
+ * gave up looks like: we stopped polling before the order spoke, so the order
816
+ * may well have filled. It is not a failure, and it must never be shown as
817
+ * one.
818
+ */
819
+ type WithdrawSwapStatus = "swapping" | "landed" | "refunded" | "unknown";
820
+ type WithdrawSwapRecord = {
821
+ /** Identity. One record per Fusion+ order. */
822
+ orderHash: string;
823
+ status: WithdrawSwapStatus;
824
+ /**
825
+ * What actually left the agent, smallest unit of the SOURCE asset.
826
+ *
827
+ * The measured figure, not the requested one — a "MAX" withdrawal has no
828
+ * requested figure at all, and this is what a refund gives back.
829
+ */
830
+ withdrawn: string;
831
+ srcSymbol: string;
832
+ srcChainId: number;
833
+ srcDecimals: number;
834
+ /**
835
+ * What the quote expected to arrive, smallest unit of the DESTINATION.
836
+ *
837
+ * `dst.amount` — the auction's start, not its floor. The bar reads "about",
838
+ * and quoting the floor understated every completed withdrawal by half a
839
+ * percent (ROUT-242, comment 16056).
840
+ */
841
+ expected: string;
842
+ dstSymbol: string;
843
+ dstChainId: number;
844
+ dstDecimals: number;
845
+ /** Measured arrival. Set only on `landed`, and shown in place of `expected`. */
846
+ received?: string;
847
+ /** Epoch ms. */
848
+ createdAt: number;
849
+ /** Epoch ms, set when the status stopped being `swapping`. */
850
+ settledAt?: number;
851
+ };
852
+ declare function subscribeWithdrawSwaps(listener: () => void): () => void;
853
+ /**
854
+ * Write the record as the order is submitted, alongside the secrets.
855
+ *
856
+ * Not after the swap resolves: the point is to survive the gap between submit
857
+ * and settle, which is exactly when the user closes the tab.
858
+ */
859
+ declare function saveWithdrawSwap(record: WithdrawSwapRecord): void;
860
+ /**
861
+ * Move a record to its outcome.
862
+ *
863
+ * A no-op for an order with no record, which is the normal case for deposit
864
+ * swaps — they share the secret store and so pass through the same resume
865
+ * loop, but they are not withdrawals and were never recorded here.
866
+ *
867
+ * `settledAt` is stamped here rather than by the caller so every writer agrees
868
+ * on what "settled" means: the moment the status stopped being `swapping`.
869
+ */
870
+ declare function updateWithdrawSwap(orderHash: string, patch: Partial<Omit<WithdrawSwapRecord, "orderHash">>): void;
871
+ /** The user has read the outcome. Only they do this. */
872
+ declare function clearWithdrawSwap(orderHash: string): void;
873
+ /** Every record, newest first, sweeping anything past MAX_AGE_MS. */
874
+ declare function listWithdrawSwaps(now?: number): WithdrawSwapRecord[];
875
+
788
876
  /**
789
877
  * Swaps that landed but were never deposited.
790
878
  *
@@ -1071,6 +1159,21 @@ declare class OwneySDK {
1071
1159
  * describe. See `completePendingDeposit`.
1072
1160
  */
1073
1161
  listPendingDeposits(): ReturnType<typeof listPendingDeposits>;
1162
+ /**
1163
+ * Backgrounded withdrawal swaps and how they ended, newest first.
1164
+ *
1165
+ * Nothing to act on — unlike `listPendingDeposits`, which describes a job the
1166
+ * user can finish. This is a watch: the funds are in escrow and the only
1167
+ * open question is where they land. See `swap.withdraw-swap.ts`.
1168
+ */
1169
+ listWithdrawSwaps(): WithdrawSwapRecord[];
1170
+ /**
1171
+ * Retire a settled record. The user has read the outcome.
1172
+ *
1173
+ * Only they do this — a record that disappears on its own takes with it the
1174
+ * only thing telling someone where their money went.
1175
+ */
1176
+ dismissWithdrawSwap(orderHash: string): void;
1074
1177
  /**
1075
1178
  * Deposit a swap that already landed.
1076
1179
  *
@@ -1106,6 +1209,24 @@ declare class OwneySDK {
1106
1209
  status: "completed" | "unfinished";
1107
1210
  error?: OwneyError;
1108
1211
  }[]>;
1212
+ /**
1213
+ * Pick up SAME-CHAIN orders a previous session left in the auction.
1214
+ *
1215
+ * They are not in the secret store and never will be: that store exists to
1216
+ * keep preimages, and a same-chain fill needs none — the resolver takes the
1217
+ * maker's tokens and delivers the taker's in one transaction. So the only
1218
+ * record of one is the withdrawal record itself, which carries the order
1219
+ * hash, and that is all a resume needs here.
1220
+ *
1221
+ * Gentler than the Fusion+ case in the way that matters. Nothing is
1222
+ * escrowed, so an order nobody ever picks up leaves the wallet exactly as it
1223
+ * was — this is catching up on news, not rescuing funds.
1224
+ *
1225
+ * Same-chain is read off the record rather than stored as a rail: a
1226
+ * withdrawal whose source and destination share a chain could not have been
1227
+ * anything else.
1228
+ */
1229
+ private resumeSameChainSwaps;
1109
1230
  /**
1110
1231
  * What the wallet actually holds, across every supported chain, restricted
1111
1232
  * to tokens a swap could use.
@@ -1137,12 +1258,16 @@ declare class OwneySDK {
1137
1258
  * tokens can share a ticker, and `providers` (how many token lists carry the
1138
1259
  * address) is the only usable signal for telling them apart. Surface it.
1139
1260
  *
1140
- * Returns nothing for a blank query rather than asking for the whole list.
1261
+ * A blank query WITH a chainId lists that chain's catalogue instead of
1262
+ * searching it — what a picker wants when someone selects a chain before
1263
+ * typing anything. Blank with no chain returns nothing: that would be asking
1264
+ * for the top of every catalogue at once, which is a download, not a list.
1141
1265
  */
1142
1266
  searchSwapTokens(params: {
1143
- /** Omit to search every supported chain. */
1267
+ /** Omit to search every supported chain. Required to list one. */
1144
1268
  chainId?: number;
1145
- query: string;
1269
+ /** Omit, with a chainId, to list the chain rather than search it. */
1270
+ query?: string;
1146
1271
  limit?: number;
1147
1272
  /**
1148
1273
  * Which picker is asking. A withdrawal ends in the user's own wallet, so
@@ -1246,6 +1371,32 @@ declare class OwneySDK {
1246
1371
  /** Percent, classic rail only. Fusion+ prices through its auction. */
1247
1372
  slippage?: number;
1248
1373
  onSwapProgress?: (stage: SwapStage) => void;
1374
+ /**
1375
+ * Fires once the order is with the relayer, before the watch.
1376
+ *
1377
+ * Both resolver-filled rails reach it — cross-chain Fusion+ and the
1378
+ * same-chain auction. Not the classic rail, which is one transaction with
1379
+ * no order behind it and resolves in seconds either way.
1380
+ *
1381
+ * The seam a backgrounded withdrawal hangs off. Up to here the flow needs
1382
+ * the user in front of it; past here it is polling that a later page load
1383
+ * can take over — from the stored preimages on Fusion+, and from the
1384
+ * order hash alone on the same-chain rail. So a caller may stop awaiting
1385
+ * this promise and let the screen go. See `swap.withdraw-swap.ts` for what
1386
+ * the caller should write down when it does.
1387
+ *
1388
+ * Carries the two figures a status bar cannot get anywhere else. The
1389
+ * withdrawal has already happened by this point and `withdrawn` is what
1390
+ * MEASURABLY arrived, which on a "MAX" withdrawal is the only figure that
1391
+ * exists — and it is also what comes back if the order never fills.
1392
+ */
1393
+ onOrderSubmitted?: (submitted: {
1394
+ orderHash: string;
1395
+ /** Smallest unit of the agent's asset. */
1396
+ withdrawn: string;
1397
+ /** Smallest unit of the destination — the auction's start, not its floor. */
1398
+ expected: string;
1399
+ }) => void;
1249
1400
  /** How long to wait for the withdrawal to land before giving up on the swap. */
1250
1401
  arrivalTimeoutMs?: number;
1251
1402
  }): Promise<{
@@ -1447,4 +1598,4 @@ type StoredOrder = {
1447
1598
  */
1448
1599
  declare function listOrders(now?: number): StoredOrder[];
1449
1600
 
1450
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, setOwneyDebug };
1601
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.d.ts CHANGED
@@ -785,6 +785,94 @@ declare class AgentChainIncompatibleError extends OwneyError {
785
785
  }>, connectedChainId: number);
786
786
  }
787
787
 
788
+ /**
789
+ * Withdrawal swaps the user has walked away from, and how they ended.
790
+ *
791
+ * The deposit side has `swap.pending-deposit.ts` for the same shape of
792
+ * problem, but the two are not symmetrical. A pending deposit is a JOB: the
793
+ * money is in the wallet and an ordinary deposit finishes it. This is a
794
+ * WATCH — the funds are in escrow, nothing the user does moves them along,
795
+ * and the only question is where they ended up.
796
+ *
797
+ * That is the whole reason this exists. `withdrawAndSwap` used to hold the
798
+ * screen for the entire escrow lifecycle, which on a slow mainnet fill is
799
+ * minutes of a modal the user cannot leave (ROUT-242). Backgrounding it means
800
+ * the outcome arrives after the UI that started it has gone, so the outcome
801
+ * has to be written down somewhere the next render can find it.
802
+ *
803
+ * Only cross-chain swaps are recorded. A same-chain classic swap is one
804
+ * transaction and resolves in seconds; a same-chain fusion order has no stored
805
+ * preimages, so there would be nothing for a reloaded session to resume and
806
+ * the record would sit at `swapping` for ever.
807
+ *
808
+ * Kept deliberately separate from the secret store. Secrets are retired the
809
+ * moment an order reaches a verdict — that is a safety property, not a display
810
+ * one — and the record has to outlive them so the user can still be told what
811
+ * happened after they come back.
812
+ */
813
+ /**
814
+ * `swapping` is the only non-terminal state. `unknown` is what a watch that
815
+ * gave up looks like: we stopped polling before the order spoke, so the order
816
+ * may well have filled. It is not a failure, and it must never be shown as
817
+ * one.
818
+ */
819
+ type WithdrawSwapStatus = "swapping" | "landed" | "refunded" | "unknown";
820
+ type WithdrawSwapRecord = {
821
+ /** Identity. One record per Fusion+ order. */
822
+ orderHash: string;
823
+ status: WithdrawSwapStatus;
824
+ /**
825
+ * What actually left the agent, smallest unit of the SOURCE asset.
826
+ *
827
+ * The measured figure, not the requested one — a "MAX" withdrawal has no
828
+ * requested figure at all, and this is what a refund gives back.
829
+ */
830
+ withdrawn: string;
831
+ srcSymbol: string;
832
+ srcChainId: number;
833
+ srcDecimals: number;
834
+ /**
835
+ * What the quote expected to arrive, smallest unit of the DESTINATION.
836
+ *
837
+ * `dst.amount` — the auction's start, not its floor. The bar reads "about",
838
+ * and quoting the floor understated every completed withdrawal by half a
839
+ * percent (ROUT-242, comment 16056).
840
+ */
841
+ expected: string;
842
+ dstSymbol: string;
843
+ dstChainId: number;
844
+ dstDecimals: number;
845
+ /** Measured arrival. Set only on `landed`, and shown in place of `expected`. */
846
+ received?: string;
847
+ /** Epoch ms. */
848
+ createdAt: number;
849
+ /** Epoch ms, set when the status stopped being `swapping`. */
850
+ settledAt?: number;
851
+ };
852
+ declare function subscribeWithdrawSwaps(listener: () => void): () => void;
853
+ /**
854
+ * Write the record as the order is submitted, alongside the secrets.
855
+ *
856
+ * Not after the swap resolves: the point is to survive the gap between submit
857
+ * and settle, which is exactly when the user closes the tab.
858
+ */
859
+ declare function saveWithdrawSwap(record: WithdrawSwapRecord): void;
860
+ /**
861
+ * Move a record to its outcome.
862
+ *
863
+ * A no-op for an order with no record, which is the normal case for deposit
864
+ * swaps — they share the secret store and so pass through the same resume
865
+ * loop, but they are not withdrawals and were never recorded here.
866
+ *
867
+ * `settledAt` is stamped here rather than by the caller so every writer agrees
868
+ * on what "settled" means: the moment the status stopped being `swapping`.
869
+ */
870
+ declare function updateWithdrawSwap(orderHash: string, patch: Partial<Omit<WithdrawSwapRecord, "orderHash">>): void;
871
+ /** The user has read the outcome. Only they do this. */
872
+ declare function clearWithdrawSwap(orderHash: string): void;
873
+ /** Every record, newest first, sweeping anything past MAX_AGE_MS. */
874
+ declare function listWithdrawSwaps(now?: number): WithdrawSwapRecord[];
875
+
788
876
  /**
789
877
  * Swaps that landed but were never deposited.
790
878
  *
@@ -1071,6 +1159,21 @@ declare class OwneySDK {
1071
1159
  * describe. See `completePendingDeposit`.
1072
1160
  */
1073
1161
  listPendingDeposits(): ReturnType<typeof listPendingDeposits>;
1162
+ /**
1163
+ * Backgrounded withdrawal swaps and how they ended, newest first.
1164
+ *
1165
+ * Nothing to act on — unlike `listPendingDeposits`, which describes a job the
1166
+ * user can finish. This is a watch: the funds are in escrow and the only
1167
+ * open question is where they land. See `swap.withdraw-swap.ts`.
1168
+ */
1169
+ listWithdrawSwaps(): WithdrawSwapRecord[];
1170
+ /**
1171
+ * Retire a settled record. The user has read the outcome.
1172
+ *
1173
+ * Only they do this — a record that disappears on its own takes with it the
1174
+ * only thing telling someone where their money went.
1175
+ */
1176
+ dismissWithdrawSwap(orderHash: string): void;
1074
1177
  /**
1075
1178
  * Deposit a swap that already landed.
1076
1179
  *
@@ -1106,6 +1209,24 @@ declare class OwneySDK {
1106
1209
  status: "completed" | "unfinished";
1107
1210
  error?: OwneyError;
1108
1211
  }[]>;
1212
+ /**
1213
+ * Pick up SAME-CHAIN orders a previous session left in the auction.
1214
+ *
1215
+ * They are not in the secret store and never will be: that store exists to
1216
+ * keep preimages, and a same-chain fill needs none — the resolver takes the
1217
+ * maker's tokens and delivers the taker's in one transaction. So the only
1218
+ * record of one is the withdrawal record itself, which carries the order
1219
+ * hash, and that is all a resume needs here.
1220
+ *
1221
+ * Gentler than the Fusion+ case in the way that matters. Nothing is
1222
+ * escrowed, so an order nobody ever picks up leaves the wallet exactly as it
1223
+ * was — this is catching up on news, not rescuing funds.
1224
+ *
1225
+ * Same-chain is read off the record rather than stored as a rail: a
1226
+ * withdrawal whose source and destination share a chain could not have been
1227
+ * anything else.
1228
+ */
1229
+ private resumeSameChainSwaps;
1109
1230
  /**
1110
1231
  * What the wallet actually holds, across every supported chain, restricted
1111
1232
  * to tokens a swap could use.
@@ -1137,12 +1258,16 @@ declare class OwneySDK {
1137
1258
  * tokens can share a ticker, and `providers` (how many token lists carry the
1138
1259
  * address) is the only usable signal for telling them apart. Surface it.
1139
1260
  *
1140
- * Returns nothing for a blank query rather than asking for the whole list.
1261
+ * A blank query WITH a chainId lists that chain's catalogue instead of
1262
+ * searching it — what a picker wants when someone selects a chain before
1263
+ * typing anything. Blank with no chain returns nothing: that would be asking
1264
+ * for the top of every catalogue at once, which is a download, not a list.
1141
1265
  */
1142
1266
  searchSwapTokens(params: {
1143
- /** Omit to search every supported chain. */
1267
+ /** Omit to search every supported chain. Required to list one. */
1144
1268
  chainId?: number;
1145
- query: string;
1269
+ /** Omit, with a chainId, to list the chain rather than search it. */
1270
+ query?: string;
1146
1271
  limit?: number;
1147
1272
  /**
1148
1273
  * Which picker is asking. A withdrawal ends in the user's own wallet, so
@@ -1246,6 +1371,32 @@ declare class OwneySDK {
1246
1371
  /** Percent, classic rail only. Fusion+ prices through its auction. */
1247
1372
  slippage?: number;
1248
1373
  onSwapProgress?: (stage: SwapStage) => void;
1374
+ /**
1375
+ * Fires once the order is with the relayer, before the watch.
1376
+ *
1377
+ * Both resolver-filled rails reach it — cross-chain Fusion+ and the
1378
+ * same-chain auction. Not the classic rail, which is one transaction with
1379
+ * no order behind it and resolves in seconds either way.
1380
+ *
1381
+ * The seam a backgrounded withdrawal hangs off. Up to here the flow needs
1382
+ * the user in front of it; past here it is polling that a later page load
1383
+ * can take over — from the stored preimages on Fusion+, and from the
1384
+ * order hash alone on the same-chain rail. So a caller may stop awaiting
1385
+ * this promise and let the screen go. See `swap.withdraw-swap.ts` for what
1386
+ * the caller should write down when it does.
1387
+ *
1388
+ * Carries the two figures a status bar cannot get anywhere else. The
1389
+ * withdrawal has already happened by this point and `withdrawn` is what
1390
+ * MEASURABLY arrived, which on a "MAX" withdrawal is the only figure that
1391
+ * exists — and it is also what comes back if the order never fills.
1392
+ */
1393
+ onOrderSubmitted?: (submitted: {
1394
+ orderHash: string;
1395
+ /** Smallest unit of the agent's asset. */
1396
+ withdrawn: string;
1397
+ /** Smallest unit of the destination — the auction's start, not its floor. */
1398
+ expected: string;
1399
+ }) => void;
1249
1400
  /** How long to wait for the withdrawal to land before giving up on the swap. */
1250
1401
  arrivalTimeoutMs?: number;
1251
1402
  }): Promise<{
@@ -1447,4 +1598,4 @@ type StoredOrder = {
1447
1598
  */
1448
1599
  declare function listOrders(now?: number): StoredOrder[];
1449
1600
 
1450
- export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, setOwneyDebug };
1601
+ export { type AccountAgentApy, type AccountApyOptions, type AccountDailyEarnings, type AgentApy, type AgentApyDetails, type AgentBalance, AgentChainIncompatibleError, type AgentEarnings, type AgentHistoryEntry, type AgentHistoryPosition, type AgentId, AgentNotFoundError, type AgentSupportedAsset, type AgentSupportedAssets, type AgentUserProfile, type AgentWithdrawResult, type AgentsApyOptions, type AllocationAgentApy, type AllocationApyOptions, type ApyByChainAndAsset, type ApyHistoryPoint, type Asset, type AssetDailyEarnings, type AvailableAgent, type AvailableAgentsOptions, type ConnectionState, type DailyApyDays, type DailyEarningsOptions, type DailyEarningsPoint, type DepositCallback, type DepositOptions, type HistoryAction, type HistoryFilters, type HistoryOptions, type HistoryTransaction, type IAgent, InvalidHistoryCursorError, type LookbackDays, NotConnectedError, type OwneyAccountApy, type OwneyAgentApy, type OwneyAgentHistory, type OwneyAllocationApy, type OwneyBalances, type OwneyDepositResult, type OwneyEarnings, OwneyError, type OwneyErrorCode, type OwneyMultiDepositResult, type OwneyPosition, OwneySDK, type OwneySDKConfig, type OwneySIWXConfig, type OwneySupportedChainId, type OwneySupportedChains, type OwneySupportedTokens, type OwneyToken, type OwneyUserProfile, type OwneyWithdrawResult, type PendingDeposit, type RebalanceLog, type SwapChainTokens, type SwapDirection, type SwapOrderStatus, type SwapQuote, type SwapQuoteParams, type SwapRail, type SwapStage, type SwapTokenInfo, type SwapTokenSearchResult, type WithdrawOptions, type WithdrawSwapRecord, type WithdrawSwapStatus, clearWithdrawSwap, createOwneySIWX, listPendingDeposits, listOrders as listPendingSwaps, listWithdrawSwaps, saveWithdrawSwap, setOwneyDebug, subscribeWithdrawSwaps, updateWithdrawSwap };
package/dist/index.js CHANGED
@@ -2294,7 +2294,7 @@ function createSwapApi(baseUrl, apiKey) {
2294
2294
  baseUrl,
2295
2295
  apiKey,
2296
2296
  `/tokens/search?${new URLSearchParams({
2297
- query: params.query,
2297
+ ...params.query === void 0 ? {} : { query: params.query },
2298
2298
  // Omitted, the routing API searches every supported chain — which is
2299
2299
  // what a picker wants, since its rows already span all three.
2300
2300
  ...params.chainId === void 0 ? {} : { chainId: String(params.chainId) },
@@ -3097,6 +3097,10 @@ async function runSameChainFusion(deps, options, walletAddress) {
3097
3097
  quoteId: built.quoteId,
3098
3098
  ...built.extension ? { extension: built.extension } : {}
3099
3099
  });
3100
+ try {
3101
+ options.onOrderSubmitted?.(built.orderHash);
3102
+ } catch {
3103
+ }
3100
3104
  await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
3101
3105
  orderHash: built.orderHash,
3102
3106
  ...onStage ? { onStage } : {}
@@ -3195,6 +3199,10 @@ async function runFusion(deps, options, walletAddress) {
3195
3199
  ...built.extension ? { extension: built.extension } : {}
3196
3200
  });
3197
3201
  debugLog("owney-sdk", "swap: order submitted, polling escrows");
3202
+ try {
3203
+ options.onOrderSubmitted?.(built.orderHash);
3204
+ } catch {
3205
+ }
3198
3206
  try {
3199
3207
  await runFusionOrder(deps.runner, {
3200
3208
  orderHash: built.orderHash,
@@ -3314,8 +3322,8 @@ async function buildErc2612Permit(reads, input) {
3314
3322
  );
3315
3323
  }
3316
3324
 
3317
- // src/lib/swap/swap.pending-deposit.ts
3318
- var KEY_PREFIX3 = "owney.swap.pending-deposit";
3325
+ // src/lib/swap/swap.withdraw-swap.ts
3326
+ var KEY_PREFIX3 = "owney.swap.withdraw";
3319
3327
  var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
3320
3328
  var storage3 = () => {
3321
3329
  if (typeof window === "undefined") return null;
@@ -3325,26 +3333,60 @@ var storage3 = () => {
3325
3333
  return null;
3326
3334
  }
3327
3335
  };
3328
- var keyFor2 = (id) => `${KEY_PREFIX3}.${id}`;
3329
- function savePendingDeposit(entry) {
3336
+ var keyFor2 = (orderHash) => `${KEY_PREFIX3}.${orderHash}`;
3337
+ var listeners = /* @__PURE__ */ new Set();
3338
+ function subscribeWithdrawSwaps(listener) {
3339
+ listeners.add(listener);
3340
+ return () => {
3341
+ listeners.delete(listener);
3342
+ };
3343
+ }
3344
+ var notify = () => {
3345
+ for (const listener of listeners) {
3346
+ try {
3347
+ listener();
3348
+ } catch {
3349
+ }
3350
+ }
3351
+ };
3352
+ function saveWithdrawSwap(record) {
3330
3353
  const store = storage3();
3331
3354
  if (!store) return;
3332
3355
  try {
3333
- store.setItem(keyFor2(entry.id), JSON.stringify(entry));
3356
+ store.setItem(keyFor2(record.orderHash), JSON.stringify(record));
3334
3357
  } catch {
3335
3358
  return;
3336
3359
  }
3360
+ notify();
3337
3361
  }
3338
- function clearPendingDeposit(id) {
3362
+ function updateWithdrawSwap(orderHash, patch) {
3339
3363
  const store = storage3();
3340
3364
  if (!store) return;
3341
3365
  try {
3342
- store.removeItem(keyFor2(id));
3366
+ const raw = store.getItem(keyFor2(orderHash));
3367
+ if (!raw) return;
3368
+ const current = JSON.parse(raw);
3369
+ const next = { ...current, ...patch };
3370
+ if (next.status !== "swapping" && current.status === "swapping" && next.settledAt === void 0) {
3371
+ next.settledAt = Date.now();
3372
+ }
3373
+ store.setItem(keyFor2(orderHash), JSON.stringify(next));
3343
3374
  } catch {
3344
3375
  return;
3345
3376
  }
3377
+ notify();
3346
3378
  }
3347
- function listPendingDeposits(now = Date.now()) {
3379
+ function clearWithdrawSwap(orderHash) {
3380
+ const store = storage3();
3381
+ if (!store) return;
3382
+ try {
3383
+ store.removeItem(keyFor2(orderHash));
3384
+ } catch {
3385
+ return;
3386
+ }
3387
+ notify();
3388
+ }
3389
+ function listWithdrawSwaps(now = Date.now()) {
3348
3390
  const store = storage3();
3349
3391
  if (!store) return [];
3350
3392
  const out = [];
@@ -3374,6 +3416,66 @@ function listPendingDeposits(now = Date.now()) {
3374
3416
  return out.sort((a, b) => b.createdAt - a.createdAt);
3375
3417
  }
3376
3418
 
3419
+ // src/lib/swap/swap.pending-deposit.ts
3420
+ var KEY_PREFIX4 = "owney.swap.pending-deposit";
3421
+ var MAX_AGE_MS3 = 24 * 60 * 60 * 1e3;
3422
+ var storage4 = () => {
3423
+ if (typeof window === "undefined") return null;
3424
+ try {
3425
+ return window.localStorage;
3426
+ } catch {
3427
+ return null;
3428
+ }
3429
+ };
3430
+ var keyFor3 = (id) => `${KEY_PREFIX4}.${id}`;
3431
+ function savePendingDeposit(entry) {
3432
+ const store = storage4();
3433
+ if (!store) return;
3434
+ try {
3435
+ store.setItem(keyFor3(entry.id), JSON.stringify(entry));
3436
+ } catch {
3437
+ return;
3438
+ }
3439
+ }
3440
+ function clearPendingDeposit(id) {
3441
+ const store = storage4();
3442
+ if (!store) return;
3443
+ try {
3444
+ store.removeItem(keyFor3(id));
3445
+ } catch {
3446
+ return;
3447
+ }
3448
+ }
3449
+ function listPendingDeposits(now = Date.now()) {
3450
+ const store = storage4();
3451
+ if (!store) return [];
3452
+ const out = [];
3453
+ try {
3454
+ const keys = [];
3455
+ for (let i = 0; i < store.length; i++) {
3456
+ const key2 = store.key(i);
3457
+ if (key2?.startsWith(`${KEY_PREFIX4}.`)) keys.push(key2);
3458
+ }
3459
+ for (const key2 of keys) {
3460
+ const raw = store.getItem(key2);
3461
+ if (!raw) continue;
3462
+ try {
3463
+ const parsed = JSON.parse(raw);
3464
+ if (now - parsed.createdAt > MAX_AGE_MS3) {
3465
+ store.removeItem(key2);
3466
+ continue;
3467
+ }
3468
+ out.push(parsed);
3469
+ } catch {
3470
+ store.removeItem(key2);
3471
+ }
3472
+ }
3473
+ } catch {
3474
+ return out;
3475
+ }
3476
+ return out.sort((a, b) => b.createdAt - a.createdAt);
3477
+ }
3478
+
3377
3479
  // src/lib/health-report.ts
3378
3480
  var ROUTING_API_BASE_URL2 = "https://owney-routing-api-243946518160.europe-west4.run.app";
3379
3481
  async function reportAgentFailure(apiKey, agentType, errorCode, baseUrl = ROUTING_API_BASE_URL2) {
@@ -5134,6 +5236,25 @@ var OwneySDK = class {
5134
5236
  listPendingDeposits() {
5135
5237
  return listPendingDeposits();
5136
5238
  }
5239
+ /**
5240
+ * Backgrounded withdrawal swaps and how they ended, newest first.
5241
+ *
5242
+ * Nothing to act on — unlike `listPendingDeposits`, which describes a job the
5243
+ * user can finish. This is a watch: the funds are in escrow and the only
5244
+ * open question is where they land. See `swap.withdraw-swap.ts`.
5245
+ */
5246
+ listWithdrawSwaps() {
5247
+ return listWithdrawSwaps();
5248
+ }
5249
+ /**
5250
+ * Retire a settled record. The user has read the outcome.
5251
+ *
5252
+ * Only they do this — a record that disappears on its own takes with it the
5253
+ * only thing telling someone where their money went.
5254
+ */
5255
+ dismissWithdrawSwap(orderHash) {
5256
+ clearWithdrawSwap(orderHash);
5257
+ }
5137
5258
  /**
5138
5259
  * Deposit a swap that already landed.
5139
5260
  *
@@ -5180,7 +5301,10 @@ var OwneySDK = class {
5180
5301
  */
5181
5302
  async resumePendingSwaps(options = {}) {
5182
5303
  const pending = listOrders();
5183
- if (pending.length === 0) return [];
5304
+ if (pending.length === 0) {
5305
+ await this.resumeSameChainSwaps(options);
5306
+ return [];
5307
+ }
5184
5308
  const runner = this.swapRunnerDeps();
5185
5309
  const results = [];
5186
5310
  for (const order of pending) {
@@ -5194,6 +5318,7 @@ var OwneySDK = class {
5194
5318
  } : {}
5195
5319
  });
5196
5320
  clearOrder(order.orderHash);
5321
+ updateWithdrawSwap(order.orderHash, { status: "landed" });
5197
5322
  results.push({
5198
5323
  orderHash: order.orderHash,
5199
5324
  status: "completed"
@@ -5201,6 +5326,9 @@ var OwneySDK = class {
5201
5326
  } catch (error) {
5202
5327
  if (error instanceof OwneyError && (error.code === "SWAP_ORDER_REFUNDED" || error.code === "SWAP_ORDER_CANCELLED")) {
5203
5328
  clearOrder(order.orderHash);
5329
+ updateWithdrawSwap(order.orderHash, { status: "refunded" });
5330
+ } else {
5331
+ updateWithdrawSwap(order.orderHash, { status: "unknown" });
5204
5332
  }
5205
5333
  results.push({
5206
5334
  orderHash: order.orderHash,
@@ -5209,8 +5337,56 @@ var OwneySDK = class {
5209
5337
  });
5210
5338
  }
5211
5339
  }
5340
+ await this.resumeSameChainSwaps(options);
5212
5341
  return results;
5213
5342
  }
5343
+ /**
5344
+ * Pick up SAME-CHAIN orders a previous session left in the auction.
5345
+ *
5346
+ * They are not in the secret store and never will be: that store exists to
5347
+ * keep preimages, and a same-chain fill needs none — the resolver takes the
5348
+ * maker's tokens and delivers the taker's in one transaction. So the only
5349
+ * record of one is the withdrawal record itself, which carries the order
5350
+ * hash, and that is all a resume needs here.
5351
+ *
5352
+ * Gentler than the Fusion+ case in the way that matters. Nothing is
5353
+ * escrowed, so an order nobody ever picks up leaves the wallet exactly as it
5354
+ * was — this is catching up on news, not rescuing funds.
5355
+ *
5356
+ * Same-chain is read off the record rather than stored as a rail: a
5357
+ * withdrawal whose source and destination share a chain could not have been
5358
+ * anything else.
5359
+ */
5360
+ async resumeSameChainSwaps(options = {}) {
5361
+ const running = listWithdrawSwaps().filter(
5362
+ (record) => record.status === "swapping" && record.srcChainId === record.dstChainId
5363
+ );
5364
+ for (const record of running) {
5365
+ const runner = {
5366
+ // The chain bound in. That product keys its orders per chain, and the
5367
+ // Fusion+ endpoint answers 404 for one — which would read as an order
5368
+ // that vanished rather than one still in the auction.
5369
+ orderStatus: (h) => this.swapApi().orderStatus(h, record.srcChainId),
5370
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
5371
+ now: () => Date.now()
5372
+ };
5373
+ try {
5374
+ await runSameChainOrder(runner, {
5375
+ orderHash: record.orderHash,
5376
+ ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
5377
+ ...options.onStage ? {
5378
+ onStage: (stage) => options.onStage(record.orderHash, stage)
5379
+ } : {}
5380
+ });
5381
+ updateWithdrawSwap(record.orderHash, { status: "landed" });
5382
+ } catch (error) {
5383
+ const code = error instanceof OwneyError ? error.code : void 0;
5384
+ updateWithdrawSwap(record.orderHash, {
5385
+ status: code === "SWAP_ORDER_EXPIRED" || code === "SWAP_ORDER_CANCELLED" ? "refunded" : "unknown"
5386
+ });
5387
+ }
5388
+ }
5389
+ }
5214
5390
  /**
5215
5391
  * What the wallet actually holds, across every supported chain, restricted
5216
5392
  * to tokens a swap could use.
@@ -5257,13 +5433,16 @@ var OwneySDK = class {
5257
5433
  * tokens can share a ticker, and `providers` (how many token lists carry the
5258
5434
  * address) is the only usable signal for telling them apart. Surface it.
5259
5435
  *
5260
- * Returns nothing for a blank query rather than asking for the whole list.
5436
+ * A blank query WITH a chainId lists that chain's catalogue instead of
5437
+ * searching it — what a picker wants when someone selects a chain before
5438
+ * typing anything. Blank with no chain returns nothing: that would be asking
5439
+ * for the top of every catalogue at once, which is a download, not a list.
5261
5440
  */
5262
5441
  async searchSwapTokens(params) {
5263
- const query = params.query.trim();
5264
- if (!query) return { tokens: [] };
5442
+ const query = params.query?.trim() ?? "";
5443
+ if (!query && params.chainId === void 0) return { tokens: [] };
5265
5444
  return this.swapApi().searchTokens({
5266
- query,
5445
+ ...query ? { query } : {},
5267
5446
  ...params.chainId === void 0 ? {} : { chainId: params.chainId },
5268
5447
  ...params.limit === void 0 ? {} : { limit: params.limit },
5269
5448
  ...params.direction === void 0 ? {} : { direction: params.direction }
@@ -5443,7 +5622,14 @@ var OwneySDK = class {
5443
5622
  walletAddress: state.walletAddress,
5444
5623
  direction: "withdraw",
5445
5624
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
5446
- ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {}
5625
+ ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
5626
+ ...options.onOrderSubmitted ? {
5627
+ onOrderSubmitted: (orderHash) => options.onOrderSubmitted({
5628
+ orderHash,
5629
+ withdrawn,
5630
+ expected: quote.dst.amount
5631
+ })
5632
+ } : {}
5447
5633
  });
5448
5634
  return { withdraw, withdrawn, swap };
5449
5635
  } catch (error) {
@@ -6202,8 +6388,8 @@ import { SiweMessage } from "siwe";
6202
6388
  import { ZyfaiSDK as ZyfaiSDK2 } from "@zyfai/sdk";
6203
6389
 
6204
6390
  // src/agents/zyfai/zyfai.siwx-cache.ts
6205
- var KEY_PREFIX4 = "owney.siwx.session";
6206
- var storage4 = () => {
6391
+ var KEY_PREFIX5 = "owney.siwx.session";
6392
+ var storage5 = () => {
6207
6393
  if (typeof window === "undefined") return null;
6208
6394
  try {
6209
6395
  return window.localStorage;
@@ -6211,8 +6397,8 @@ var storage4 = () => {
6211
6397
  return null;
6212
6398
  }
6213
6399
  };
6214
- var buildKey2 = (address) => `${KEY_PREFIX4}:${address.toLowerCase()}`;
6215
- var legacyKeyPrefix2 = (address) => `${KEY_PREFIX4}:${address.toLowerCase()}:`;
6400
+ var buildKey2 = (address) => `${KEY_PREFIX5}:${address.toLowerCase()}`;
6401
+ var legacyKeyPrefix2 = (address) => `${KEY_PREFIX5}:${address.toLowerCase()}:`;
6216
6402
  var memorySiwxSessions = /* @__PURE__ */ new Map();
6217
6403
  var readLegacySiwxSession = (store, address) => {
6218
6404
  if (!store) return null;
@@ -6244,7 +6430,7 @@ var readLegacySiwxSession = (store, address) => {
6244
6430
  var readSiwxSession = (address, chainId) => {
6245
6431
  if (typeof window === "undefined") return null;
6246
6432
  const key2 = buildKey2(address);
6247
- const store = storage4();
6433
+ const store = storage5();
6248
6434
  let raw = null;
6249
6435
  try {
6250
6436
  raw = store?.getItem(key2) ?? null;
@@ -6274,7 +6460,7 @@ var writeSiwxSession = (address, _chainId, session) => {
6274
6460
  if (typeof window === "undefined") return;
6275
6461
  const key2 = buildKey2(address);
6276
6462
  memorySiwxSessions.set(key2, session);
6277
- const store = storage4();
6463
+ const store = storage5();
6278
6464
  try {
6279
6465
  store?.setItem(key2, JSON.stringify(session));
6280
6466
  } catch {
@@ -6283,7 +6469,7 @@ var writeSiwxSession = (address, _chainId, session) => {
6283
6469
  var clearSiwxSession = (address, _chainId) => {
6284
6470
  const key2 = buildKey2(address);
6285
6471
  memorySiwxSessions.delete(key2);
6286
- const store = storage4();
6472
+ const store = storage5();
6287
6473
  try {
6288
6474
  store?.removeItem(key2);
6289
6475
  } catch {
@@ -6415,8 +6601,13 @@ export {
6415
6601
  NotConnectedError,
6416
6602
  OwneyError,
6417
6603
  OwneySDK,
6604
+ clearWithdrawSwap,
6418
6605
  createOwneySIWX,
6419
6606
  listPendingDeposits,
6420
6607
  listOrders as listPendingSwaps,
6421
- setOwneyDebug
6608
+ listWithdrawSwaps,
6609
+ saveWithdrawSwap,
6610
+ setOwneyDebug,
6611
+ subscribeWithdrawSwaps,
6612
+ updateWithdrawSwap
6422
6613
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.26-beta.5",
3
+ "version": "0.7.26-beta.6",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",