@owney/sdk 0.7.32-beta.2 → 0.7.32-beta.4

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.js CHANGED
@@ -8,10 +8,13 @@ import {
8
8
  PERMIT2_BATCH_ABI,
9
9
  batchArgs,
10
10
  batchTypedData,
11
+ discardHashlessTokenBatch,
12
+ dropExpiredHashlessTokenBatch,
11
13
  erc2612TypedData,
12
14
  finishTokenBatchFamily,
13
15
  loadTokenBatchFamily,
14
16
  saveTokenBatchAttempt,
17
+ savedTokenBatchBody,
15
18
  supportsSponsoredErc2612,
16
19
  tokenBatchClear,
17
20
  tokenBatchKey,
@@ -19,7 +22,7 @@ import {
19
22
  tokenBatchStoredBody,
20
23
  tokenBatchWrite,
21
24
  validateTokenBatchAttempt
22
- } from "./chunk-YBMCUL5D.js";
25
+ } from "./chunk-CW55W2WO.js";
23
26
 
24
27
  // src/lib/funding-evidence.ts
25
28
  import { decodeEventLog, decodeFunctionData, erc20Abi, parseAbi } from "viem";
@@ -356,25 +359,41 @@ async function observeTokenBatchFamily(input) {
356
359
  }
357
360
 
358
361
  // src/lib/swap/swap.observation.ts
359
- var active = /* @__PURE__ */ new Set();
360
- var isOrderObserved = (key2) => active.has(key2);
361
- async function observeOrder(key2, run) {
362
+ var claims = /* @__PURE__ */ new Map();
363
+ var watches = /* @__PURE__ */ new Map();
364
+ var isOrderObserved = (key2) => claims.has(key2) || watches.has(key2);
365
+ function claimOrder(key2) {
366
+ claims.set(key2, (claims.get(key2) ?? 0) + 1);
367
+ let released = false;
368
+ return () => {
369
+ if (released) return;
370
+ released = true;
371
+ const left = (claims.get(key2) ?? 1) - 1;
372
+ if (left > 0) claims.set(key2, left);
373
+ else claims.delete(key2);
374
+ };
375
+ }
376
+ async function observeOrder(key2, run, options = {}) {
362
377
  const busy = () => new OwneyError(
363
378
  "SWAP_REQUEST_FAILED",
364
379
  "This order is already being observed.",
365
380
  { orderHash: key2, watchActive: true, settlementUnconfirmed: true }
366
381
  );
367
- if (active.has(key2)) throw busy();
368
- active.add(key2);
382
+ if (options.wait) {
383
+ for (let other = watches.get(key2); other; other = watches.get(key2))
384
+ await other.catch(() => void 0);
385
+ } else if (watches.has(key2)) throw busy();
386
+ const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
387
+ const name = `owney.swap.observe:${key2}`;
388
+ const watch = !locks ? run() : options.wait ? locks.request(name, () => run()) : locks.request(name, { ifAvailable: true }, (lock) => {
389
+ if (!lock) throw busy();
390
+ return run();
391
+ });
392
+ watches.set(key2, watch);
369
393
  try {
370
- if (typeof navigator !== "undefined" && navigator.locks)
371
- return await navigator.locks.request(`owney.swap.observe:${key2}`, { ifAvailable: true }, (lock) => {
372
- if (!lock) throw busy();
373
- return run();
374
- });
375
- return await run();
394
+ return await watch;
376
395
  } finally {
377
- active.delete(key2);
396
+ if (watches.get(key2) === watch) watches.delete(key2);
378
397
  }
379
398
  }
380
399
 
@@ -529,7 +548,7 @@ function parseLeg(item) {
529
548
  const r = item;
530
549
  if (typeof r.legKey !== "string" || !r.legKey) return null;
531
550
  if (typeof r.status !== "string" || !r.status) return null;
532
- return {
551
+ const leg = {
533
552
  legKey: r.legKey,
534
553
  agentType: strOrNull(r.agentType),
535
554
  status: r.status,
@@ -539,8 +558,11 @@ function parseLeg(item) {
539
558
  confirmedAt: strOrNull(r.confirmedAt),
540
559
  agentWallet: strOrNull(r.agentWallet),
541
560
  providerIndexedAt: strOrNull(r.providerIndexedAt),
542
- providerBookedAt: strOrNull(r.providerBookedAt)
561
+ providerBookedAt: strOrNull(r.providerBookedAt),
562
+ providerRef: strOrNull(r.providerRef)
543
563
  };
564
+ if (r.closed === true) leg.closed = true;
565
+ return leg;
544
566
  }
545
567
  function parseRow(item) {
546
568
  if (!item || typeof item !== "object") return null;
@@ -960,7 +982,7 @@ var SnapshotHistory = class {
960
982
  };
961
983
 
962
984
  // src/lib/funding-ledger.ts
963
- var SDK_VERSION = "0.7.32-beta.2";
985
+ var SDK_VERSION = "0.7.32-beta.4";
964
986
  var SCOPE2 = "funding-ledger";
965
987
  var REQUEST_TIMEOUT_MS2 = 5e3;
966
988
  var RETRY_DELAYS_MS = [1e3, 4e3, 16e3];
@@ -1210,13 +1232,213 @@ function buildReporter(config) {
1210
1232
  } catch (err) {
1211
1233
  debugLog(SCOPE2, `event failed: ${describeError2(err)}`);
1212
1234
  }
1235
+ },
1236
+ settle(timeoutMs) {
1237
+ return new Promise((resolve) => {
1238
+ let deadline;
1239
+ const done = () => {
1240
+ if (deadline !== void 0) clearTimeout(deadline);
1241
+ resolve();
1242
+ };
1243
+ try {
1244
+ flush();
1245
+ deadline = setTimeout(done, Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 0);
1246
+ unrefTimer(deadline);
1247
+ chain2.then(done, done);
1248
+ } catch (err) {
1249
+ debugLog(SCOPE2, `settle failed: ${describeError2(err)}`);
1250
+ done();
1251
+ }
1252
+ });
1213
1253
  }
1214
1254
  };
1215
1255
  }
1216
1256
  }
1217
1257
 
1258
+ // src/lib/zyfai-server-tasks.ts
1259
+ var SCOPE3 = "zyfai-server-tasks";
1260
+ var ZYFAI_SERVER_TASKS = 1;
1261
+ var ZYFAI_SERVER_TASKS_OFF_CODE = "ZYFAI_SERVER_TASKS_OFF";
1262
+ function zyfaiServerTasksEnabled(option) {
1263
+ if (typeof option !== "function") return option === true;
1264
+ try {
1265
+ return option() === true;
1266
+ } catch {
1267
+ return false;
1268
+ }
1269
+ }
1270
+ var ZYFAI_HANDOFF_FEATURE_OFF_CODES = /* @__PURE__ */ new Set([
1271
+ "FUNDING_TASK_KIND_DISABLED",
1272
+ "ZYFAI_NOT_CONFIGURED",
1273
+ "PROBE_ONLY",
1274
+ "HTTP_404",
1275
+ "HTTP_401",
1276
+ "HTTP_403",
1277
+ ZYFAI_SERVER_TASKS_OFF_CODE
1278
+ ]);
1279
+ var ZYFAI_HANDOFF_NO_TASK_CODES = /* @__PURE__ */ new Set([
1280
+ ...ZYFAI_HANDOFF_FEATURE_OFF_CODES,
1281
+ "ZYFAI_SESSION_MISMATCH",
1282
+ "NEEDS_USER_SESSION"
1283
+ ]);
1284
+ var UNCHECKED_NO_TASK_CODES = /* @__PURE__ */ new Set([
1285
+ "HTTP_401",
1286
+ "HTTP_403",
1287
+ "HTTP_404",
1288
+ ZYFAI_SERVER_TASKS_OFF_CODE
1289
+ ]);
1290
+ function provesNoServerTask(code, afterLostAnswer) {
1291
+ return ZYFAI_HANDOFF_NO_TASK_CODES.has(code) && !(afterLostAnswer && UNCHECKED_NO_TASK_CODES.has(code));
1292
+ }
1293
+ var ZYFAI_WITHDRAWAL_NOT_SENT_CODES = /* @__PURE__ */ new Set([
1294
+ "PREREQUISITE_FAILED",
1295
+ "PREREQUISITE_TIMEOUT",
1296
+ "SUPERSEDED",
1297
+ "ZYFAI_SESSION_MISMATCH",
1298
+ "ZYFAI_REDEMPTION_IN_FLIGHT",
1299
+ "NEEDS_USER_SESSION",
1300
+ "NEEDS_USER_SESSION_KEY",
1301
+ "ZYFAI_FORBIDDEN",
1302
+ "WITHDRAW_INSUFFICIENT_BALANCE",
1303
+ "WITHDRAWAL_IN_PROGRESS",
1304
+ // The task's own agent:zyfai leg was already failed or confirmed (P4-8).
1305
+ "LEG_CLOSED",
1306
+ "MAX_ATTEMPTS",
1307
+ "SECRET_UNREADABLE"
1308
+ ]);
1309
+ var ZYFAI_REGISTRATION_NOT_SENT_CODES = /* @__PURE__ */ new Set([
1310
+ "TRANSFER_NOT_SEEN",
1311
+ "DEPOSIT_NOT_SENT",
1312
+ "DEPOSIT_REVERTED",
1313
+ "TRANSFER_MISMATCH",
1314
+ "NEEDS_USER_SESSION",
1315
+ "ZYFAI_REGISTRATION_REJECTED",
1316
+ "MAX_ATTEMPTS",
1317
+ "SECRET_UNREADABLE"
1318
+ ]);
1319
+ var ZYFAI_TASK_LIFETIME_MS = {
1320
+ zyfai_withdrawal: 72e5,
1321
+ zyfai_deposit_registration: 36e5
1322
+ };
1323
+ var SESSION_MARGIN_MS = 3e5;
1324
+ var SESSION_SLACK_MS = 6e4;
1325
+ function sessionExp(session) {
1326
+ try {
1327
+ const parts = session.split(".");
1328
+ if (parts.length !== 3) return void 0;
1329
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
1330
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
1331
+ const decoded = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
1332
+ const claims2 = JSON.parse(decoded);
1333
+ const exp = isRecord(claims2) ? claims2.exp : void 0;
1334
+ return typeof exp === "number" && Number.isFinite(exp) ? exp : void 0;
1335
+ } catch {
1336
+ return void 0;
1337
+ }
1338
+ }
1339
+ function zyfaiSessionOutlivesTask(session, kind, nowMs = Date.now()) {
1340
+ const lifetimeMs = Object.prototype.hasOwnProperty.call(ZYFAI_TASK_LIFETIME_MS, kind) ? ZYFAI_TASK_LIFETIME_MS[kind] : void 0;
1341
+ if (typeof session !== "string" || lifetimeMs === void 0) return false;
1342
+ const exp = sessionExp(session);
1343
+ return exp !== void 0 && exp * 1e3 > nowMs + lifetimeMs + SESSION_MARGIN_MS + SESSION_SLACK_MS;
1344
+ }
1345
+ var DEFAULT_TIMEOUT_MS = 25e3;
1346
+ var DECLINED_STATUSES = /* @__PURE__ */ new Set([400, 401, 403, 404, 409, 429]);
1347
+ var ID = /^.{1,128}$/s;
1348
+ var TX_HASH = /^0x[0-9a-fA-F]{64}$/;
1349
+ var unknown = () => ({ type: "unknown" });
1350
+ var isRecord = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1351
+ function requestBody(body, session) {
1352
+ const out = {
1353
+ kind: body.kind,
1354
+ operationId: body.operationId,
1355
+ owner: body.owner,
1356
+ chainId: body.chainId,
1357
+ asset: body.asset,
1358
+ amount: body.amount,
1359
+ agentWallet: body.agentWallet,
1360
+ zyfaiSession: session
1361
+ };
1362
+ if (body.after !== void 0) out.after = body.after;
1363
+ if (body.runNow !== void 0) out.runNow = body.runNow;
1364
+ return out;
1365
+ }
1366
+ function taskResult(value) {
1367
+ const result = {};
1368
+ if (!isRecord(value)) return result;
1369
+ if (typeof value.providerWithdrawalId === "string" && ID.test(value.providerWithdrawalId))
1370
+ result.providerWithdrawalId = value.providerWithdrawalId;
1371
+ if (typeof value.providerDepositId === "string" && ID.test(value.providerDepositId))
1372
+ result.providerDepositId = value.providerDepositId;
1373
+ if (typeof value.txHash === "string" && TX_HASH.test(value.txHash)) result.txHash = value.txHash;
1374
+ if (value.asyncPending === true) result.asyncPending = true;
1375
+ if (value.handedOff === "reconciler") result.handedOff = "reconciler";
1376
+ if (value.lifecycle === "failed") result.lifecycle = "failed";
1377
+ return result;
1378
+ }
1379
+ function answerOf(payload) {
1380
+ if (!isRecord(payload) || payload.success !== true || !isRecord(payload.data)) return unknown();
1381
+ const data = payload.data;
1382
+ if (data.accepted === false)
1383
+ return typeof data.code === "string" && data.code ? { type: "declined", code: data.code } : unknown();
1384
+ if (data.accepted !== true || typeof data.taskId !== "string" || !data.taskId || typeof data.state !== "string" || !data.state) return unknown();
1385
+ return {
1386
+ type: "accepted",
1387
+ taskId: data.taskId,
1388
+ state: data.state,
1389
+ ...typeof data.code === "string" && data.code ? { code: data.code } : {},
1390
+ result: taskResult(data.result)
1391
+ };
1392
+ }
1393
+ async function handOffZyfaiTask(cfg, body, session) {
1394
+ if (cfg?.zyfaiServerTasks !== true) return { type: "declined", code: ZYFAI_SERVER_TASKS_OFF_CODE };
1395
+ let timer;
1396
+ try {
1397
+ const controller = new AbortController();
1398
+ const timeoutMs = typeof cfg.timeoutMs === "number" && Number.isFinite(cfg.timeoutMs) && cfg.timeoutMs > 0 ? cfg.timeoutMs : DEFAULT_TIMEOUT_MS;
1399
+ const url = `${getRoutingApiBaseUrl(cfg.baseUrl).replace(/\/$/, "")}/api/v1/funding/zyfai-tasks`;
1400
+ const timedOut = new Promise((resolve) => {
1401
+ timer = setTimeout(() => {
1402
+ controller.abort();
1403
+ resolve("timeout");
1404
+ }, timeoutMs);
1405
+ timer.unref?.();
1406
+ });
1407
+ const exchange = (async () => {
1408
+ const res = await fetch(url, {
1409
+ method: "POST",
1410
+ headers: { "content-type": "application/json", "x-owney-api-key": cfg.apiKey },
1411
+ body: JSON.stringify(requestBody(body, session)),
1412
+ signal: controller.signal,
1413
+ // The body carries the session: never follow a redirect to another host.
1414
+ redirect: "error"
1415
+ });
1416
+ if (DECLINED_STATUSES.has(res.status)) return { type: "declined", code: `HTTP_${res.status}` };
1417
+ if (res.status !== 200) {
1418
+ debugLog(SCOPE3, `handoff answered ${res.status}`);
1419
+ return unknown();
1420
+ }
1421
+ return answerOf(await res.json());
1422
+ })();
1423
+ exchange.catch(() => {
1424
+ });
1425
+ const answer = await Promise.race([exchange, timedOut]);
1426
+ if (answer === "timeout") {
1427
+ debugLog(SCOPE3, "handoff timed out");
1428
+ return unknown();
1429
+ }
1430
+ debugLog(SCOPE3, `handoff ${answer.type}`);
1431
+ return answer;
1432
+ } catch {
1433
+ debugLog(SCOPE3, "handoff failed");
1434
+ return unknown();
1435
+ } finally {
1436
+ if (timer !== void 0) clearTimeout(timer);
1437
+ }
1438
+ }
1439
+
1218
1440
  // src/lib/withdrawal-reservation.ts
1219
- var SCOPE3 = "withdrawal-reservation";
1441
+ var SCOPE4 = "withdrawal-reservation";
1220
1442
  var CLAIM_TIMEOUT_MS = 4e3;
1221
1443
  var HEARTBEAT_INTERVAL_MS = 6e4;
1222
1444
  var HOLDER_TIMEOUT_MS = 5e3;
@@ -1314,7 +1536,7 @@ function compact2(obj) {
1314
1536
  async function claimWithdrawalReservation(cfg, input) {
1315
1537
  const result = await claimOnce(cfg, input);
1316
1538
  if (result.status === "unavailable" && result.httpStatus === 403 && input.supersedes) {
1317
- debugLog(SCOPE3, "claim 403 with supersedes: retrying without it");
1539
+ debugLog(SCOPE4, "claim 403 with supersedes: retrying without it");
1318
1540
  const { supersedes: _dropped, ...rest } = input;
1319
1541
  const retried = await claimOnce(cfg, rest);
1320
1542
  return retried.status === "claimed" ? { ...retried, supersedesDropped: true } : retried;
@@ -1341,7 +1563,7 @@ async function claimOnce(cfg, input) {
1341
1563
  res = await post(cfg, endpoint(cfg), body, CLAIM_TIMEOUT_MS);
1342
1564
  } catch (err) {
1343
1565
  const reason = err instanceof RequestTimeout ? "timeout" : "network";
1344
- debugLog(SCOPE3, `claim ${reason}: ${describeError3(err)}`);
1566
+ debugLog(SCOPE4, `claim ${reason}: ${describeError3(err)}`);
1345
1567
  return { status: "unavailable", reason };
1346
1568
  }
1347
1569
  if (!res) return { status: "unavailable", reason: "network" };
@@ -1357,7 +1579,7 @@ async function claimOnce(cfg, input) {
1357
1579
  }
1358
1580
  }
1359
1581
  if (!res.ok) {
1360
- debugLog(SCOPE3, `claim ${res.status}`);
1582
+ debugLog(SCOPE4, `claim ${res.status}`);
1361
1583
  return { status: "unavailable", reason: "http", httpStatus: res.status };
1362
1584
  }
1363
1585
  const data = unwrap2(res.body);
@@ -1392,12 +1614,12 @@ async function holderRequest(cfg, operationId, action, body, retryDelays = []) {
1392
1614
  if (res.ok) return "ok";
1393
1615
  if (res.status === 404) return "gone";
1394
1616
  if (res.status < 500 && res.status !== 429) {
1395
- debugLog(SCOPE3, `${action} rejected with ${res.status}`);
1617
+ debugLog(SCOPE4, `${action} rejected with ${res.status}`);
1396
1618
  return "rejected";
1397
1619
  }
1398
- debugLog(SCOPE3, `${action} ${res.status}`);
1620
+ debugLog(SCOPE4, `${action} ${res.status}`);
1399
1621
  } catch (err) {
1400
- debugLog(SCOPE3, `${action} failed: ${describeError3(err)}`);
1622
+ debugLog(SCOPE4, `${action} failed: ${describeError3(err)}`);
1401
1623
  }
1402
1624
  if (attempt < retryDelays.length) {
1403
1625
  await new Promise((resolve) => unrefTimer2(setTimeout(resolve, retryDelays[attempt])));
@@ -1570,7 +1792,7 @@ function ledgerLegFromEvidence(e, kind) {
1570
1792
  if (e.callsId && e.callsId.length <= 200) leg.callsId = e.callsId;
1571
1793
  if (isUint(e.confirmedAmount)) leg.confirmedAmountRaw = e.confirmedAmount;
1572
1794
  if (e.blockNumber && /^\d{1,19}$/.test(e.blockNumber)) leg.blockNumber = e.blockNumber;
1573
- const providerRef = e.withdrawalRequest?.providerWithdrawalId;
1795
+ const providerRef = e.withdrawalRequest?.providerWithdrawalId || e.providerDepositId;
1574
1796
  if (typeof providerRef === "string" && providerRef.length >= 1 && providerRef.length <= 128) leg.providerRef = providerRef;
1575
1797
  const failure = e.withdrawalRequest?.failure;
1576
1798
  if (failure) leg.errorCode = failure.toUpperCase().replace(/-/g, "_");
@@ -1599,6 +1821,12 @@ function withdrawalLedgerRail(e) {
1599
1821
  return e.userOpHash ? "yieldseeker_userop" : e.callsId ? "yieldseeker_wallet_calls" : e.txHash ? "yieldseeker_paid" : void 0;
1600
1822
  return void 0;
1601
1823
  }
1824
+ function depositLedgerRail(e) {
1825
+ if (e.callsId) return "wallet_calls";
1826
+ if (e.depositAuthorization || e.serverBroadcast) return "permit2_batch";
1827
+ return void 0;
1828
+ }
1829
+ var FUNDING_DEPOSIT_RECOVERY_REPORT = 1;
1602
1830
  var SWAP_LEDGER_RAILS = {
1603
1831
  classic: "swap_classic",
1604
1832
  fusion: "swap_fusion",
@@ -1666,6 +1894,12 @@ var TRANSFER_EVENT = getAbiItem({
1666
1894
  abi: parseAbi2(["event Transfer(address indexed from, address indexed to, uint256 value)"]),
1667
1895
  name: "Transfer"
1668
1896
  });
1897
+ var acceptedChunk = /* @__PURE__ */ new Map();
1898
+ var acceptedChunkByClient = /* @__PURE__ */ new WeakMap();
1899
+ function providerKey(client) {
1900
+ const { chain: chain2, transport } = client;
1901
+ return typeof chain2?.id === "number" ? `${chain2.id}:${typeof transport?.url === "string" ? transport.url : ""}` : void 0;
1902
+ }
1669
1903
  function parseBlock(value) {
1670
1904
  if (value === void 0 || value === null) return void 0;
1671
1905
  return typeof value === "string" && /^\d{1,19}$/.test(value) ? BigInt(value) : null;
@@ -1690,11 +1924,12 @@ async function scanWithdrawalPayouts(client, input) {
1690
1924
  const candidates = [];
1691
1925
  if (start > end) {
1692
1926
  const kept = input.cursor ?? (start > 0n ? start - 1n : 0n);
1693
- return { candidates, scannedTo: kept.toString() };
1927
+ return { candidates, scannedTo: kept.toString(), caughtUp: true };
1694
1928
  }
1695
1929
  const tokenAddress = input.token.toLowerCase();
1696
1930
  const owner = input.owner.toLowerCase();
1697
- let size = PAYOUT_SCAN_CHUNK_BLOCKS;
1931
+ const key2 = providerKey(client);
1932
+ let size = (key2 !== void 0 ? acceptedChunk.get(key2) : acceptedChunkByClient.get(client)) ?? PAYOUT_SCAN_CHUNK_BLOCKS;
1698
1933
  let from = start;
1699
1934
  let scannedTo = start - 1n;
1700
1935
  for (let requests = 0; from <= end && requests < MAX_CHUNKS_PER_SCAN; ) {
@@ -1709,12 +1944,21 @@ async function scanWithdrawalPayouts(client, input) {
1709
1944
  toBlock: to
1710
1945
  });
1711
1946
  } catch (error) {
1712
- if (size > 1n && !isRateLimited(error) && isRangeError(error)) {
1713
- size /= 2n;
1714
- continue;
1947
+ if (!isRateLimited(error) && isRangeError(error)) {
1948
+ const requested = to - from + 1n;
1949
+ const allowed = allowedRange(error);
1950
+ const next = allowed !== void 0 && allowed < requested ? allowed : requested / 2n;
1951
+ if (next >= 1n && next < requested) {
1952
+ size = next;
1953
+ continue;
1954
+ }
1715
1955
  }
1716
1956
  throw error;
1717
1957
  }
1958
+ if (size < PAYOUT_SCAN_CHUNK_BLOCKS) {
1959
+ if (key2 !== void 0) acceptedChunk.set(key2, size);
1960
+ else acceptedChunkByClient.set(client, size);
1961
+ }
1718
1962
  check();
1719
1963
  requests++;
1720
1964
  for (const log of logs) {
@@ -1732,7 +1976,8 @@ async function scanWithdrawalPayouts(client, input) {
1732
1976
  scannedTo = to;
1733
1977
  from = to + 1n;
1734
1978
  }
1735
- return { candidates, scannedTo: (scannedTo < 0n ? 0n : scannedTo).toString() };
1979
+ const stalled = input.cursor !== void 0 && scannedTo <= input.cursor;
1980
+ return { candidates, scannedTo: (scannedTo < 0n ? 0n : scannedTo).toString(), caughtUp: from > end || stalled };
1736
1981
  }
1737
1982
  async function estimateBlockAt(client, latest, targetMs) {
1738
1983
  const head = await client.getBlock({ blockNumber: latest });
@@ -1757,6 +2002,7 @@ function causeChain(error) {
1757
2002
  chain2.push({
1758
2003
  code: value.code,
1759
2004
  status: value.status,
2005
+ data: value.data,
1760
2006
  text: [value.message, value.details, value.shortMessage].filter((part) => typeof part === "string").join(" ")
1761
2007
  });
1762
2008
  current = value.cause;
@@ -1766,6 +2012,14 @@ function causeChain(error) {
1766
2012
  function isRateLimited(error) {
1767
2013
  return causeChain(error).some((entry) => entry.status === 429 || entry.code === 429 || /rate|too many requests/i.test(entry.text));
1768
2014
  }
2015
+ function allowedRange(error) {
2016
+ for (const entry of causeChain(error)) {
2017
+ const hinted = entry.data && typeof entry.data === "object" ? entry.data.maxBlockRange : void 0;
2018
+ const value = typeof hinted === "number" ? hinted : Number(/"maxBlockRange"\s*:\s*(\d+)/.exec(entry.text)?.[1]);
2019
+ if (Number.isSafeInteger(value) && value >= 1) return BigInt(value);
2020
+ }
2021
+ return void 0;
2022
+ }
1769
2023
  function isRangeError(error) {
1770
2024
  return causeChain(error).some((entry) => entry.code === -32005 || /block range|range (is )?too (large|wide|big)|range limit|exceed(s|ed)? .*(range|limit|results)|too many (results|logs|blocks)|response size|query returned more than|max(imum)? .*blocks/i.test(entry.text));
1771
2025
  }
@@ -1871,6 +2125,7 @@ async function getSponsorRelayerAddress(input) {
1871
2125
  return parsed.data.relayer;
1872
2126
  }
1873
2127
  async function postSponsorBatchTransfer(input) {
2128
+ const flaggedPrepare = input.body.serverBroadcast === true && !input.body.serializedTransaction;
1874
2129
  let res;
1875
2130
  try {
1876
2131
  res = await fetch(
@@ -1889,7 +2144,7 @@ async function postSponsorBatchTransfer(input) {
1889
2144
  throw new OwneyError(
1890
2145
  "SPONSOR_REQUEST_FAILED",
1891
2146
  "Deposit status is unknown. Retry the same amount to check it.",
1892
- { safeToFallback: false }
2147
+ { safeToFallback: false, ...flaggedPrepare ? { outcomeUnknown: true } : {} }
1893
2148
  );
1894
2149
  }
1895
2150
  const parsed = await res.json().catch(() => null);
@@ -1900,7 +2155,13 @@ async function postSponsorBatchTransfer(input) {
1900
2155
  {
1901
2156
  statusCode: res.status,
1902
2157
  safeToFallback: false,
1903
- notSubmitted: parsed?.notSubmitted === true || parsed?.error?.notSubmitted === true || parsed?.error?.details?.notSubmitted === true
2158
+ notSubmitted: parsed?.notSubmitted === true || parsed?.error?.notSubmitted === true || parsed?.error?.details?.notSubmitted === true,
2159
+ // Another authorization of this operation is being sent by the
2160
+ // server (409, nothing signed).
2161
+ ...parsed?.code === "DEPOSIT_ALREADY_SENDING" ? { code: "DEPOSIT_ALREADY_SENDING" } : {},
2162
+ // No definitive answer (5xx, an unreadable or malformed body): the
2163
+ // server may have accepted it. A 4xx with a body is a refusal.
2164
+ ...flaggedPrepare && (res.status >= 500 || parsed === null || res.ok) ? { outcomeUnknown: true } : {}
1904
2165
  }
1905
2166
  );
1906
2167
  }
@@ -1917,19 +2178,19 @@ async function postSponsorBatchRecovery(input) {
1917
2178
  input.signal?.throwIfAborted();
1918
2179
  if (!response.ok || payload?.success !== true) throw failure();
1919
2180
  const value = payload.data;
1920
- const { isAddress: isAddress5, keccak256: keccak2564, parseTransaction: parseTransaction3, recoverTransactionAddress } = await import("viem");
1921
- const { validateTokenBatchAttempt: validateTokenBatchAttempt2 } = await import("./token-batch-family-EKOLMSTD.js");
2181
+ const { isAddress: isAddress6, keccak256: keccak2564, parseTransaction: parseTransaction3, recoverTransactionAddress } = await import("viem");
2182
+ const { validateTokenBatchAttempt: validateTokenBatchAttempt2 } = await import("./token-batch-family-TUGZWETV.js");
1922
2183
  const phase = input.body.phase;
1923
2184
  const original = phase === "approval" ? input.body.serializedPermitTransaction : input.body.serializedTransaction;
1924
2185
  if (!original) throw failure();
1925
2186
  const hash2 = keccak2564(original), relayer = await recoverTransactionAddress({ serializedTransaction: original });
1926
- const validHash = (v) => typeof v === "string" && /^0x[\da-f]{64}$/i.test(v);
1927
- if (!value || value.protocol !== 1 || value.phase !== phase || !validHash(value.txHash) || !isAddress5(value.relayer) || value.relayer.toLowerCase() !== relayer.toLowerCase()) throw failure();
2187
+ const validHash2 = (v) => typeof v === "string" && /^0x[\da-f]{64}$/i.test(v);
2188
+ if (!value || value.protocol !== 1 || value.phase !== phase || !validHash2(value.txHash) || !isAddress6(value.relayer) || value.relayer.toLowerCase() !== relayer.toLowerCase()) throw failure();
1928
2189
  if (value.status !== "prepared" && value.txHash.toLowerCase() !== hash2.toLowerCase()) throw failure();
1929
2190
  if (value.status === "unresolved") {
1930
2191
  if (!["nonce-not-consumed", "authorization-used", "authorization-expired", "state-unavailable", "preparation-rejected", "relayer-changed"].includes(value.reason)) throw failure();
1931
2192
  } else {
1932
- if (!("blockNumber" in value) || typeof value.blockNumber !== "string" || !/^\d{1,78}$/.test(value.blockNumber) || BigInt(value.blockNumber) >= 1n << 256n || !validHash(value.blockHash)) throw failure();
2193
+ if (!("blockNumber" in value) || typeof value.blockNumber !== "string" || !/^\d{1,78}$/.test(value.blockNumber) || BigInt(value.blockNumber) >= 1n << 256n || !validHash2(value.blockHash)) throw failure();
1933
2194
  if (value.status === "prepared") {
1934
2195
  if (value.predecessorHash?.toLowerCase() !== hash2.toLowerCase()) throw failure();
1935
2196
  const tx = parseTransaction3(value.serializedTransaction), old = parseTransaction3(original);
@@ -2227,11 +2488,19 @@ var AgentReads = class {
2227
2488
  }
2228
2489
  };
2229
2490
 
2491
+ // src/agents/zyfai/zyfai.session-errors.ts
2492
+ var SESSION_REJECTED = /(?:^|: )Unauthorized: (?:(?:Invalid or expired user session|Missing user session)\b.*|Invalid API key)$/;
2493
+ function isZyfaiSessionRejected(message) {
2494
+ return SESSION_REJECTED.test(message);
2495
+ }
2496
+
2230
2497
  // src/agents/zyfai/zyfai.withdraw-submission.ts
2231
2498
  var BELOW_MINIMUM = /^Idle balance (\d+(?:\.\d+)?) (USDC|WETH|ETH|EURC) is below the minimum withdrawal of (\d+(?:\.\d+)?) \2\.?$/;
2232
2499
  var listenersByClient = /* @__PURE__ */ new WeakMap();
2500
+ var ZYFAI_WITHDRAW_DISPATCH_CHECKPOINT = 1;
2233
2501
  var MAX_WITHDRAWAL_ID_LENGTH = 128;
2234
- function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2502
+ var WITHDRAWAL_PATHS = { get: "/users/withdraw/v2", post: "/users/partial-withdraw/v2" };
2503
+ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent, beforeDispatch) {
2235
2504
  const client = sdk.httpClient;
2236
2505
  if (!client || typeof client.get !== "function" || typeof client.post !== "function")
2237
2506
  return {
@@ -2251,7 +2520,7 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2251
2520
  const original = client[method];
2252
2521
  client[method] = async function(...args) {
2253
2522
  const [url, options] = args;
2254
- const withdrawal = method === "get" && url === "/users/withdraw" || method === "post" && url === "/users/partial-withdraw";
2523
+ const withdrawal = url === WITHDRAWAL_PATHS[method];
2255
2524
  const payload = method === "get" ? options?.params : options;
2256
2525
  const matching = withdrawal ? [...observers].filter((observer) => observer.chainId === payload?.chainId && observer.token === payload?.tokenSymbol) : [];
2257
2526
  for (const observer of matching) observer.assertCurrent?.();
@@ -2259,9 +2528,12 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2259
2528
  const request3 = {
2260
2529
  attempted: true,
2261
2530
  method: method === "get" ? "GET" : "POST",
2262
- path: method === "get" ? "/users/withdraw" : "/users/partial-withdraw"
2531
+ path: WITHDRAWAL_PATHS[method]
2263
2532
  };
2264
- if (withdrawal) request3.path = `${request3.path}/v2`;
2533
+ if (withdrawal) {
2534
+ for (const observer of matching) await observer.beforeDispatch?.({ ...request3 });
2535
+ for (const observer of matching) observer.assertCurrent?.();
2536
+ }
2265
2537
  let belowMinimum2;
2266
2538
  const notify2 = (outcome) => {
2267
2539
  if (outcome === "rejected" && matching.length !== 1) return;
@@ -2294,23 +2566,11 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2294
2566
  captureStatus
2295
2567
  ] };
2296
2568
  }
2297
- const send = (route) => original.apply(this, [route, ...callArgs.slice(1)]);
2298
2569
  try {
2299
- let result;
2300
- if (!withdrawal) result = await send(url);
2301
- else {
2302
- try {
2303
- result = await send(`${url}/v2`);
2304
- } catch (error) {
2305
- const status = request3.httpStatus ?? error?.response?.status;
2306
- if (status !== 404) throw error;
2307
- for (const observer of matching) observer.assertCurrent?.();
2308
- request3.path = url;
2309
- delete request3.httpStatus;
2310
- result = await send(url);
2311
- }
2570
+ const result = await original.apply(this, callArgs);
2571
+ if (withdrawal) {
2312
2572
  const id = result?.id;
2313
- if (request3.path?.endsWith("/v2") && typeof id === "string" && id.length >= 1 && id.length <= MAX_WITHDRAWAL_ID_LENGTH)
2573
+ if (typeof id === "string" && id.length >= 1 && id.length <= MAX_WITHDRAWAL_ID_LENGTH)
2314
2574
  request3.providerWithdrawalId = id;
2315
2575
  }
2316
2576
  return result;
@@ -2321,7 +2581,7 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2321
2581
  const status = error?.response?.status;
2322
2582
  if (request3.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
2323
2583
  request3.httpStatus = status;
2324
- request3.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : request3.httpStatus === 400 && message === "No balance to withdraw" ? "insufficient-balance" : minimum ? "insufficient-balance" : /^User doesn't have any active session key\.?$/.test(message) ? "session-key-missing" : method === "post" && request3.httpStatus === 400 && message === "Partial withdrawal already in progress" ? "withdrawal-in-progress" : message === "Network error: Unable to reach the server" ? "network" : "other";
2584
+ request3.failure = isZyfaiSessionRejected(message) && (request3.httpStatus === void 0 || request3.httpStatus === 401) ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : request3.httpStatus === 400 && message === "No balance to withdraw" ? "insufficient-balance" : minimum ? "insufficient-balance" : /^User doesn't have any active session key\.?$/.test(message) ? "session-key-missing" : method === "post" && request3.httpStatus === 400 && message === "Partial withdrawal already in progress" ? "withdrawal-in-progress" : message === "Network error: Unable to reach the server" ? "network" : "other";
2325
2585
  if (["unauthorized", "forbidden", "rate-limited", "insufficient-balance", "session-key-missing", "withdrawal-in-progress"].includes(request3.failure) && (request3.httpStatus === void 0 || request3.httpStatus >= 400 && request3.httpStatus < 500)) notify2("rejected");
2326
2586
  throw error;
2327
2587
  } finally {
@@ -2335,7 +2595,7 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
2335
2595
  let rejected = false;
2336
2596
  let request2 = { attempted: false };
2337
2597
  let belowMinimum;
2338
- const listener = { chainId, token, assertCurrent, notify: (outcome, evidence, detail) => {
2598
+ const listener = { chainId, token, assertCurrent, beforeDispatch, notify: (outcome, evidence, detail) => {
2339
2599
  request2 = evidence;
2340
2600
  if (outcome === "attempted") submitted = true;
2341
2601
  else if (outcome === "rejected") {
@@ -2476,7 +2736,7 @@ async function submitZyfaiDeposit({ sdk, state, chain: chain2, amount, asset, sm
2476
2736
  if (!token || !isAddress(owner) || !isAddress(smartWallet) || !/^\d+$/.test(amount) || BigInt(amount) <= 0n)
2477
2737
  throw new OwneyError("AGENT_INVALID_RESPONSE", "Invalid Zyfai deposit transfer.", { chainId: chain2.id, asset }, "zyfai");
2478
2738
  assertCurrent();
2479
- const prepared = await sdk.prepareEnterPosition({ userAddress: owner, chainId: chain2.id, amount, asset, strategy: "aggressive" });
2739
+ const prepared = await sdk.prepareDeposit({ userAddress: owner, chainId: chain2.id, amount, asset });
2480
2740
  assertCurrent();
2481
2741
  const expectedData = encodeFunctionData({ abi: erc20Abi2, functionName: "transfer", args: [smartWallet, BigInt(amount)] });
2482
2742
  const transfer = prepared?.transfer;
@@ -2558,6 +2818,13 @@ function tokenDecimals(symbol2, explicit) {
2558
2818
  return explicit;
2559
2819
  return DECIMALS_BY_SYMBOL[(symbol2 ?? "").toLowerCase()] ?? 6;
2560
2820
  }
2821
+ function decodeDeltaAmount(raw2, decimals) {
2822
+ if (typeof raw2 !== "string") return void 0;
2823
+ if (/^0x[0-9a-f]+$/i.test(raw2)) return String(hexToDecimal(raw2, decimals));
2824
+ if (/^\d+$/.test(raw2))
2825
+ return String(hexToDecimal(BigInt(raw2).toString(16), decimals));
2826
+ return void 0;
2827
+ }
2561
2828
  function mapWithdraw(raw2) {
2562
2829
  return {
2563
2830
  txHash: raw2.txHash,
@@ -2607,6 +2874,40 @@ function mapPendingAllocations(raw2) {
2607
2874
  }
2608
2875
  return pending.length > 0 ? pending : void 0;
2609
2876
  }
2877
+ function mapPendingWithdrawals(raw2) {
2878
+ if (!Array.isArray(raw2)) return void 0;
2879
+ const pending = [];
2880
+ for (const entry of raw2) {
2881
+ if (typeof entry !== "object" || entry === null) continue;
2882
+ if (typeof entry.id !== "string" || entry.id === "") continue;
2883
+ if (entry.status !== "REQUESTED" && entry.status !== "CLAIMABLE") continue;
2884
+ if (typeof entry.chainId !== "number" || !Number.isFinite(entry.chainId)) continue;
2885
+ const asset = entry.token?.symbol;
2886
+ if (typeof asset !== "string" || asset === "" || typeof entry.amount !== "string") continue;
2887
+ let amountRaw;
2888
+ try {
2889
+ const amount = BigInt(/^0x/i.test(entry.amount) ? entry.amount : `0x${entry.amount}`);
2890
+ if (amount <= 0n) continue;
2891
+ amountRaw = amount.toString();
2892
+ } catch {
2893
+ continue;
2894
+ }
2895
+ pending.push({ id: entry.id, status: entry.status, chainId: entry.chainId, asset, amountRaw });
2896
+ }
2897
+ return pending.length > 0 ? pending : void 0;
2898
+ }
2899
+ function mapRecentAsyncWithdrawals(raw2) {
2900
+ if (!Array.isArray(raw2)) return void 0;
2901
+ const recent = [];
2902
+ for (const entry of raw2) {
2903
+ if (typeof entry !== "object" || entry === null) return void 0;
2904
+ if (entry.status === "FAILED") continue;
2905
+ const asset = entry.token?.symbol;
2906
+ if (typeof entry.id !== "string" || entry.id === "" || typeof entry.status !== "string" || entry.status === "" || typeof entry.chainId !== "number" || !Number.isFinite(entry.chainId) || typeof asset !== "string" || asset === "") return void 0;
2907
+ recent.push({ id: entry.id, status: entry.status, chainId: entry.chainId, asset });
2908
+ }
2909
+ return recent;
2910
+ }
2610
2911
  function mapBalances(raw2, _chainId, smartWallet) {
2611
2912
  const portfolio = raw2.portfolio;
2612
2913
  if (!portfolio?.portfolioByChain || typeof portfolio.portfolioByChain !== "object")
@@ -2665,6 +2966,9 @@ function mapBalances(raw2, _chainId, smartWallet) {
2665
2966
  const pendingAllocations = mapPendingAllocations(
2666
2967
  portfolio.staleBalances
2667
2968
  );
2969
+ const asyncWithdrawals = portfolio.pendingAsyncWithdrawals;
2970
+ const pendingWithdrawals = mapPendingWithdrawals(asyncWithdrawals);
2971
+ const recentAsyncWithdrawals = mapRecentAsyncWithdrawals(asyncWithdrawals);
2668
2972
  return {
2669
2973
  smartWallet,
2670
2974
  totalBalance: String(totalBalance),
@@ -2680,7 +2984,49 @@ function mapBalances(raw2, _chainId, smartWallet) {
2680
2984
  blockNumber: provenance.blockNumber === void 0 ? void 0 : String(provenance.blockNumber),
2681
2985
  positionsStatus: Array.isArray(portfolio.positions) ? "available" : "unavailable"
2682
2986
  })),
2683
- ...pendingAllocations ? { pendingAllocations } : {}
2987
+ ...pendingAllocations ? { pendingAllocations } : {},
2988
+ ...pendingWithdrawals ? { pendingWithdrawals } : {},
2989
+ ...recentAsyncWithdrawals ? { recentAsyncWithdrawals } : {}
2990
+ };
2991
+ }
2992
+ var TX_HASH2 = /^0x[0-9a-fA-F]{64}$/;
2993
+ var validHash = (value) => typeof value === "string" && TX_HASH2.test(value) ? value : void 0;
2994
+ var validDate = (value) => typeof value === "string" && Number.isFinite(Date.parse(value)) ? value : void 0;
2995
+ function mapWithdrawalLifecycle(raw2) {
2996
+ const body = raw2;
2997
+ if (!body || typeof body !== "object" || typeof body.status !== "string" || typeof body.syncSettled !== "boolean")
2998
+ return null;
2999
+ const syncTxHash = validHash(body.syncTxHash);
3000
+ const estimatedClaimAt = validDate(body.estimatedClaimAt);
3001
+ const asyncLegs = [];
3002
+ for (const leg of Array.isArray(body.asyncLegs) ? body.asyncLegs : []) {
3003
+ if (!leg || typeof leg !== "object" || typeof leg.id !== "string" || leg.id === "" || typeof leg.status !== "string") continue;
3004
+ const legClaimAt = validDate(leg.estimatedClaimAt);
3005
+ const claimTxHash = validHash(leg.claimTxHash);
3006
+ asyncLegs.push({
3007
+ id: leg.id,
3008
+ status: leg.status,
3009
+ ...legClaimAt ? { estimatedClaimAt: legClaimAt } : {},
3010
+ ...claimTxHash ? { claimTxHash } : {}
3011
+ });
3012
+ }
3013
+ const failedAsyncIds = Array.isArray(body.failedAsyncIds) ? body.failedAsyncIds.filter((id) => typeof id === "string" && id !== "") : [];
3014
+ return {
3015
+ status: body.status,
3016
+ syncSettled: body.syncSettled,
3017
+ ...syncTxHash ? { syncTxHash } : {},
3018
+ asyncLegs,
3019
+ failedAsyncIds,
3020
+ ...estimatedClaimAt ? { estimatedClaimAt } : {},
3021
+ hasError: typeof body.lastError === "string" && body.lastError !== ""
3022
+ };
3023
+ }
3024
+ function mapDepositPlacement(raw2) {
3025
+ const body = raw2;
3026
+ if (!body || typeof body !== "object" || typeof body.status !== "string") return null;
3027
+ return {
3028
+ status: body.status,
3029
+ ...typeof body.positionOutcome === "string" ? { positionOutcome: body.positionOutcome } : {}
2684
3030
  };
2685
3031
  }
2686
3032
  function sumTokenValues(tokens) {
@@ -2844,12 +3190,24 @@ function mapEntries(rawEntries, chainId) {
2844
3190
  const mappedAction = (entry.action ? actionMap[entry.action] : void 0) ?? entry.action;
2845
3191
  const rawAmount2 = entry?.positions?.[0]?.amount;
2846
3192
  const hasAmount = mappedAction !== "Withdraw" && typeof rawAmount2 === "string" && rawAmount2 !== "" && rawAmount2 !== "0x" && rawAmount2 !== "0x0" && rawAmount2 !== "0";
3193
+ const rawDelta = mappedAction === "Deposit" ? decodeDeltaAmount(
3194
+ entry?.positions?.[0]?.deltaAmount,
3195
+ tokenDecimals(entry?.positions?.[0]?.token_symbol)
3196
+ ) : void 0;
2847
3197
  const mappedPositions = positions.filter((p) => p.protocol_name).map((p) => ({
2848
3198
  protocol: p.protocol_name,
2849
3199
  pool: p.pool ?? void 0,
2850
3200
  tokenSymbol: p.token_symbol ?? void 0,
2851
- amount: mappedAction !== "Withdraw" && typeof p.amount === "string" && p.amount !== "" && p.amount !== "0x" ? String(hexToDecimal(p.amount, tokenDecimals(p.token_symbol))) : void 0
3201
+ amount: mappedAction !== "Withdraw" && typeof p.amount === "string" && p.amount !== "" && p.amount !== "0x" ? String(hexToDecimal(p.amount, tokenDecimals(p.token_symbol))) : void 0,
3202
+ deltaAmount: mappedAction !== "Withdraw" ? decodeDeltaAmount(p.deltaAmount, tokenDecimals(p.token_symbol)) : void 0
2852
3203
  }));
3204
+ const reportedAmount = hasAmount ? String(
3205
+ hexToDecimal(
3206
+ rawAmount2,
3207
+ tokenDecimals(entry?.positions?.[0]?.token_symbol)
3208
+ )
3209
+ ) : void 0;
3210
+ const transactionAmount = rawDelta === void 0 ? reportedAmount : Number(rawDelta) > 0 ? rawDelta : void 0;
2853
3211
  return {
2854
3212
  agent: "zyfai",
2855
3213
  action: mappedAction,
@@ -2862,12 +3220,7 @@ function mapEntries(rawEntries, chainId) {
2862
3220
  txHashes: entry.transactionHash ? [entry.transactionHash] : [],
2863
3221
  chainId: entry.chainId,
2864
3222
  tokenSymbol: entry?.positions?.[0]?.token_symbol,
2865
- amount: hasAmount ? String(
2866
- hexToDecimal(
2867
- rawAmount2,
2868
- tokenDecimals(entry?.positions?.[0]?.token_symbol)
2869
- )
2870
- ) : void 0
3223
+ amount: transactionAmount
2871
3224
  }
2872
3225
  ],
2873
3226
  rebalanceLog
@@ -3066,9 +3419,9 @@ var isJwtExpired = (token) => {
3066
3419
  const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
3067
3420
  const padded = payload + "=".repeat((4 - payload.length % 4) % 4);
3068
3421
  const decoded = typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8");
3069
- const claims = JSON.parse(decoded);
3070
- if (typeof claims.exp !== "number") return false;
3071
- return Date.now() / 1e3 >= claims.exp - 30;
3422
+ const claims2 = JSON.parse(decoded);
3423
+ if (typeof claims2.exp !== "number") return false;
3424
+ return Date.now() / 1e3 >= claims2.exp - 30;
3072
3425
  } catch {
3073
3426
  return false;
3074
3427
  }
@@ -3309,7 +3662,7 @@ function protocolsPolicyNeedsUpdate(current, desiredProtocols, desiredAutoSelect
3309
3662
  }
3310
3663
 
3311
3664
  // src/lib/agent-accounts.ts
3312
- var SCOPE4 = "agent-accounts";
3665
+ var SCOPE5 = "agent-accounts";
3313
3666
  var REQUEST_TIMEOUT_MS3 = 5e3;
3314
3667
  function compact3(obj) {
3315
3668
  const out = {};
@@ -3379,7 +3732,7 @@ async function fetchAgentAccounts(input) {
3379
3732
  signal
3380
3733
  });
3381
3734
  if (!res.ok) {
3382
- debugLog(SCOPE4, `fetch rejected with ${res.status}`);
3735
+ debugLog(SCOPE5, `fetch rejected with ${res.status}`);
3383
3736
  return null;
3384
3737
  }
3385
3738
  const data = unwrap3(await res.json());
@@ -3394,7 +3747,7 @@ async function fetchAgentAccounts(input) {
3394
3747
  cancel();
3395
3748
  }
3396
3749
  } catch (err) {
3397
- debugLog(SCOPE4, `fetch failed: ${describeError4(err)}`);
3750
+ debugLog(SCOPE5, `fetch failed: ${describeError4(err)}`);
3398
3751
  return null;
3399
3752
  }
3400
3753
  }
@@ -3425,7 +3778,7 @@ async function reportAgentAccount(input) {
3425
3778
  signal
3426
3779
  });
3427
3780
  if (!res.ok) {
3428
- debugLog(SCOPE4, `report rejected with ${res.status}`);
3781
+ debugLog(SCOPE5, `report rejected with ${res.status}`);
3429
3782
  return null;
3430
3783
  }
3431
3784
  const data = unwrap3(await res.json());
@@ -3437,7 +3790,7 @@ async function reportAgentAccount(input) {
3437
3790
  cancel();
3438
3791
  }
3439
3792
  } catch (err) {
3440
- debugLog(SCOPE4, `report failed: ${describeError4(err)}`);
3793
+ debugLog(SCOPE5, `report failed: ${describeError4(err)}`);
3441
3794
  return null;
3442
3795
  }
3443
3796
  }
@@ -3598,6 +3951,17 @@ var ZYFAI_LEDGER_IDLE_CACHE_MS = 6e4;
3598
3951
  var ZYFAI_LEDGER_REUSE_MS = 6e4;
3599
3952
  var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
3600
3953
  var PAYOUT_SEARCH_HEAD_LIMIT_MS = 2e3;
3954
+ var WITHDRAW_HANDOFF_TIMEOUT_MS = 25e3;
3955
+ var PLANNED_HANDOFF_TIMEOUT_MS = 8e3;
3956
+ var PLANNED_HANDOFFS_LIMIT = 32;
3957
+ var REGISTRATION_HANDOFF_TIMEOUT_MS = 8e3;
3958
+ var REGISTRATION_NUDGE_TIMEOUT_MS = 25e3;
3959
+ var REGISTRATION_AMOUNT = /^[1-9][0-9]{0,77}$/;
3960
+ var TASK_CODE = /^[A-Z0-9_]{1,64}$/;
3961
+ var SERVER_LEG_CLEARED_CODE = "ZYFAI_LEG_CLEARED";
3962
+ var SESSION_ENDING_CODE = "ZYFAI_SESSION_ENDING";
3963
+ var SERVER_LEG_STATES_LIMIT = 2e3;
3964
+ var ADDRESS_PATTERN = /^0x[0-9a-fA-F]{40}$/;
3601
3965
  function settleWithin(value, ms) {
3602
3966
  let timer;
3603
3967
  const limit = new Promise((resolve) => {
@@ -3607,6 +3971,28 @@ function settleWithin(value, ms) {
3607
3971
  if (timer) clearTimeout(timer);
3608
3972
  });
3609
3973
  }
3974
+ function depositIdFromLog(logged) {
3975
+ const id = logged?.deposit?.id;
3976
+ return typeof id === "string" && id.length >= 1 && id.length <= 128 ? id : void 0;
3977
+ }
3978
+ function registrationOutcome(answer, knownTask) {
3979
+ if (answer.type === "unknown") return { kind: "unknown" };
3980
+ if (answer.type === "declined") {
3981
+ return !knownTask && provesNoServerTask(answer.code, true) ? { kind: "local" } : { kind: "unknown", code: answer.code };
3982
+ }
3983
+ if (answer.result.providerDepositId) return { kind: "confirmed", providerDepositId: answer.result.providerDepositId };
3984
+ if (answer.state === "failed" && answer.code && ZYFAI_REGISTRATION_NOT_SENT_CODES.has(answer.code)) return { kind: "local" };
3985
+ if (answer.state === "queued" || answer.state === "running") return { kind: "pending" };
3986
+ return { kind: "unknown", code: answer.code ?? answer.state.toUpperCase(), accepted: true };
3987
+ }
3988
+ function registrationOutcomeUnknown(code) {
3989
+ const error = new OwneyError(
3990
+ "AGENT_API_ERROR",
3991
+ "Zyfai registration by the server has no known outcome, so this browser does not register the deposit again.",
3992
+ { status: 409, ...code && TASK_CODE.test(code) ? { reason: code } : {} }
3993
+ );
3994
+ return Object.assign(error, { status: 409 });
3995
+ }
3610
3996
  function belowMinimumCeiling(rejection, token, decimals) {
3611
3997
  if ((rejection.symbol === "ETH" ? "WETH" : rejection.symbol) !== token) return void 0;
3612
3998
  const ceiling = (display) => {
@@ -3630,6 +4016,8 @@ var ZyfaiAgent = class _ZyfaiAgent {
3630
4016
  referralSource;
3631
4017
  accountReporting;
3632
4018
  id = "zyfai";
4019
+ /** This adapter can hand its withdrawals to our server (part 4). */
4020
+ supportsServerTasks = true;
3633
4021
  supportedChainIds = [8453, 42161, 1];
3634
4022
  supportedAssets = [
3635
4023
  {
@@ -3706,6 +4094,18 @@ var ZyfaiAgent = class _ZyfaiAgent {
3706
4094
  /** A ledger read slower than this counts as "no ledger information" for that read. */
3707
4095
  ledgerBudgetMs = LEDGER_READ_BUDGET_MS;
3708
4096
  balanceSnapshots = /* @__PURE__ */ new WeakMap();
4097
+ /** operationId -> this session's plan-time handoff of that withdrawal (part 4),
4098
+ * read once by its nudge: the `after` legs and the planned share the task
4099
+ * was handed off with ("MAX" or digits). Who owns the leg is
4100
+ * `serverLegStates`, never this record. */
4101
+ plannedHandoffs = /* @__PURE__ */ new Map();
4102
+ /** operationId -> the server ownership of its Zyfai withdrawal leg (T9-I5).
4103
+ * The one source for "may this browser send it" (`mayWithdrawLocally`). */
4104
+ serverLegStates = /* @__PURE__ */ new Map();
4105
+ /** Deposit operations whose registration create was sent in this session:
4106
+ * a retry of one is always handed off again, whatever its session's
4107
+ * lifetime (F-I1(a)), so the server answers the task it may hold. */
4108
+ requestedRegistrations = /* @__PURE__ */ new Set();
3709
4109
  /** A replaced wallet session gets its own vendor client. In-flight calls
3710
4110
  * keep their original client; authentication must never change owners under
3711
4111
  * an operation that is waiting for provider reads or an HTTP response. */
@@ -3916,10 +4316,11 @@ var ZyfaiAgent = class _ZyfaiAgent {
3916
4316
  * Wraps `sdk.connectAccount` with a one-shot retry on auth errors. If the
3917
4317
  * cached JWT is rejected by the backend (revoked, signing-key rotated, etc.)
3918
4318
  * we drop the cache and try again — the second attempt has no cache entry,
3919
- * so it falls through to a real SIWE prompt. Zyfai labels every HTTP 401 with
3920
- * an `"Unauthorized: Invalid API key"` error so we can't distinguish revoked
3921
- * JWT from a misconfigured API key; we retry once either way, and if the
3922
- * second attempt still fails the original error propagates.
4319
+ * so it falls through to a real SIWE prompt. @zyfai/sdk 0.10.0 labels every
4320
+ * HTTP 401 as an invalid/expired or missing user session ("Unauthorized: …"),
4321
+ * which does not separate a revoked JWT from a misconfigured API key; we
4322
+ * retry once either way, and if the second attempt still fails the original
4323
+ * error propagates.
3923
4324
  */
3924
4325
  async connectAccountWithRetry(provider, chainId, executionSdk = this.sdk) {
3925
4326
  try {
@@ -3956,9 +4357,14 @@ var ZyfaiAgent = class _ZyfaiAgent {
3956
4357
  }
3957
4358
  }
3958
4359
  /** A rejected execution session must not be reused by the next foreground attempt. */
3959
- invalidateRejectedWithdrawalSession(error, state, chainId, authToken) {
4360
+ invalidateRejectedWithdrawalSession(error, state, chainId, authToken, httpStatus) {
3960
4361
  const message = error instanceof Error ? error.message : "";
3961
- if (!/(?:^|: )Unauthorized: Invalid API key$/.test(message)) return;
4362
+ if (!isZyfaiSessionRejected(message) || httpStatus !== void 0 && httpStatus !== 401) return;
4363
+ this.dropWithdrawalSession(state, chainId, authToken);
4364
+ }
4365
+ /** Drops the execution session a withdrawal used (Zyfai rejected it), unless
4366
+ * it has already changed. The next foreground connection signs in again. */
4367
+ dropWithdrawalSession(state, chainId, authToken) {
3962
4368
  const internals = this.sdk;
3963
4369
  if (typeof authToken !== "string" || internals.httpClient.authToken !== authToken || this.connectedAddress?.toLowerCase() !== state.walletAddress.toLowerCase() || this.connectedChainId !== chainId) return;
3964
4370
  clearSession(state.walletAddress, chainId);
@@ -4566,7 +4972,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
4566
4972
  await this.ensureSessionKey(ownerAddress, chainId, redeployed.address);
4567
4973
  return redeployed.address;
4568
4974
  }
4569
- async deposit(state, chainId, amount, asset, depositCallback, onProgress, onBeforeSubmission) {
4975
+ async deposit(state, chainId, amount, asset, depositCallback, onProgress, onBeforeSubmission, context) {
4570
4976
  const validChainId = isValidChainId(chainId);
4571
4977
  const owner = state.walletAddress;
4572
4978
  const provider = state.provider;
@@ -4581,6 +4987,12 @@ var ZyfaiAgent = class _ZyfaiAgent {
4581
4987
  assertCurrent();
4582
4988
  const smartWallet = await this.ensureWalletDeployed(owner, validChainId);
4583
4989
  assertCurrent();
4990
+ let server;
4991
+ if (this.accountReporting.owneyApiKey && typeof context?.operationId === "string" && context.operationId && this.serverTasksOn(this.requestedRegistrations.has(context.operationId))) {
4992
+ server = await this.handOffDepositRegistration(owner, validChainId, asset, amount, smartWallet, context.operationId);
4993
+ assertCurrent();
4994
+ }
4995
+ const mark = server ?? {};
4584
4996
  notifyFunding(onProgress, {
4585
4997
  agentId: this.id,
4586
4998
  chainId,
@@ -4590,7 +5002,8 @@ var ZyfaiAgent = class _ZyfaiAgent {
4590
5002
  intendedAmount: amount,
4591
5003
  transfer: "pending",
4592
5004
  registration: "pending",
4593
- delivery: "not-applicable"
5005
+ delivery: "not-applicable",
5006
+ ...mark
4594
5007
  });
4595
5008
  const txHash = depositCallback ? await depositCallback(smartWallet, validChainId, amount) : await submitZyfaiDeposit({
4596
5009
  sdk: executionSdk,
@@ -4609,7 +5022,8 @@ var ZyfaiAgent = class _ZyfaiAgent {
4609
5022
  intendedAmount: amount,
4610
5023
  transfer: "pending",
4611
5024
  registration: "pending",
4612
- delivery: "not-applicable"
5025
+ delivery: "not-applicable",
5026
+ ...mark
4613
5027
  }) : void 0
4614
5028
  });
4615
5029
  let evidence = {
@@ -4622,7 +5036,8 @@ var ZyfaiAgent = class _ZyfaiAgent {
4622
5036
  txHash,
4623
5037
  transfer: "pending",
4624
5038
  registration: "pending",
4625
- delivery: "not-applicable"
5039
+ delivery: "not-applicable",
5040
+ ...mark
4626
5041
  };
4627
5042
  notifyFunding(onProgress, evidence);
4628
5043
  try {
@@ -4642,11 +5057,32 @@ var ZyfaiAgent = class _ZyfaiAgent {
4642
5057
  throw new OwneyError("AGENT_TRANSACTION_REVERTED", "Zyfai deposit transaction reverted.", { txHash }, this.id);
4643
5058
  if (evidence.transfer !== "confirmed") return { txHash, smartWallet, amount, evidence };
4644
5059
  this.ledgerReads.clear();
5060
+ if (server) {
5061
+ let outcome;
5062
+ try {
5063
+ assertCurrent();
5064
+ outcome = await this.nudgeDepositRegistration(evidence, this.executionSession());
5065
+ } catch {
5066
+ outcome = { kind: "unknown" };
5067
+ }
5068
+ if (outcome.kind !== "local") {
5069
+ evidence = outcome.kind === "confirmed" ? { ...evidence, registration: "confirmed", providerDepositId: outcome.providerDepositId, serverTask: true } : {
5070
+ ...evidence,
5071
+ registration: outcome.kind,
5072
+ ...outcome.kind === "pending" || outcome.accepted ? { serverTask: true } : {}
5073
+ };
5074
+ if (outcome.kind !== "confirmed") this.notePendingRegistration(owner, validChainId, txHash, Date.now(), asset);
5075
+ notifyFunding(onProgress, evidence);
5076
+ return { txHash, smartWallet, amount, evidence };
5077
+ }
5078
+ evidence = { ...evidence, serverTask: false };
5079
+ }
4645
5080
  try {
4646
5081
  assertCurrent();
4647
5082
  const tokenAddress = asset === "WETH" ? WETH_ADDRESS_BY_CHAIN[validChainId] : void 0;
4648
- await (tokenAddress ? executionSdk.logDeposit(validChainId, txHash, amount, tokenAddress) : executionSdk.logDeposit(validChainId, txHash, amount));
4649
- evidence = { ...evidence, registration: "confirmed" };
5083
+ const logged = await (tokenAddress ? executionSdk.logDeposit(validChainId, txHash, amount, tokenAddress) : executionSdk.logDeposit(validChainId, txHash, amount));
5084
+ const providerDepositId = depositIdFromLog(logged);
5085
+ evidence = { ...evidence, registration: "confirmed", ...providerDepositId ? { providerDepositId } : {} };
4650
5086
  } catch {
4651
5087
  evidence = { ...evidence, registration: "unknown" };
4652
5088
  this.notePendingRegistration(owner, validChainId, txHash, Date.now(), asset);
@@ -4766,10 +5202,69 @@ var ZyfaiAgent = class _ZyfaiAgent {
4766
5202
  throw unavailable();
4767
5203
  }
4768
5204
  }
4769
- async withdraw(state, chainId, token, amount, onProgress, _gasMode, _onBeforeSubmission, assertOperation) {
5205
+ /**
5206
+ * Settles Zyfai's own wallet prompts before another agent's owner signature:
5207
+ * the SIWE login and, for a legacy wallet the login reported without a
5208
+ * session key, that key (attempted once per session, like activation). Reads
5209
+ * only the login's cached flags; never withdraws. A rejected prompt throws;
5210
+ * any other session-key failure is left to `withdraw`'s own recovery.
5211
+ */
5212
+ async prepareWithdrawal(state, chainId, _token) {
5213
+ const validChainId = isValidChainId(chainId);
5214
+ await this.ensureConnected(state, validChainId);
5215
+ await this.ensureWithdrawalSessionKey(state.walletAddress, validChainId);
5216
+ }
5217
+ /** The session key a legacy wallet's login reported missing, created once per
5218
+ * session (one prompt). A rejected prompt throws; any other failure is left
5219
+ * to the withdrawal itself. */
5220
+ async ensureWithdrawalSessionKey(owner, validChainId) {
5221
+ const internals = this.sdk;
5222
+ const key2 = `${owner.toLowerCase()}:${validChainId}`;
5223
+ if (internals.isPredeployed !== false || internals.hasActiveSessionKey !== false || this.sessionKeyAttempts.has(key2)) return;
5224
+ this.sessionKeyAttempts.add(key2);
5225
+ try {
5226
+ await this.sdk.createSessionKey(owner, validChainId);
5227
+ } catch (error) {
5228
+ const code = error?.code;
5229
+ if (code === 4001 || code === "ACTION_REJECTED" || error instanceof Error && /user (rejected|denied)|request rejected|action rejected/i.test(error.message)) {
5230
+ this.sessionKeyAttempts.delete(key2);
5231
+ throw error;
5232
+ }
5233
+ console.warn(
5234
+ "[zyfai] session key before withdrawal skipped (non-fatal):",
5235
+ error instanceof Error ? error.message : String(error)
5236
+ );
5237
+ }
5238
+ }
5239
+ async withdraw(state, chainId, token, amount, onProgress, _gasMode, onBeforeSubmission, assertOperation, context) {
4770
5240
  if (amount !== void 0 && (typeof amount !== "string" || !/^[0-9]+$/.test(amount) || BigInt(amount) <= 0n))
4771
5241
  throw new OwneyError("WITHDRAW_INVALID_AMOUNT", "Withdrawal amount must be a positive integer in token base units.", void 0, this.id);
4772
5242
  const validChainId = isValidChainId(chainId);
5243
+ if (this.accountReporting.owneyApiKey && typeof context?.operationId === "string" && context.operationId && // T10-I1: a leg whose mark was cleared is never handed off again.
5244
+ this.serverLegStates.get(context.operationId) !== "cleared" && // F-I2(iii): the opt-in, read now, unless the leg was already handed
5245
+ // off (planned, requested or accepted).
5246
+ this.serverTasksOn(context.planned === true || this.serverLegStates.has(context.operationId))) {
5247
+ const served = await this.withdrawThroughServer(
5248
+ state,
5249
+ validChainId,
5250
+ token,
5251
+ amount,
5252
+ onProgress,
5253
+ onBeforeSubmission,
5254
+ assertOperation,
5255
+ { operationId: context.operationId, planned: context.planned === true }
5256
+ );
5257
+ if (served) return served;
5258
+ }
5259
+ if (typeof context?.operationId === "string" && context.operationId && !this.mayWithdrawLocally(context.operationId))
5260
+ this.serverLegUnresolved(onProgress, {
5261
+ agentId: this.id,
5262
+ chainId,
5263
+ token,
5264
+ walletAddress: state.walletAddress,
5265
+ intendedAmount: amount ?? "MAX",
5266
+ registration: "not-applicable"
5267
+ });
4773
5268
  try {
4774
5269
  await this.ensureConnected(state, validChainId);
4775
5270
  } catch (error) {
@@ -4806,15 +5301,35 @@ var ZyfaiAgent = class _ZyfaiAgent {
4806
5301
  assertCurrent();
4807
5302
  assertOperation?.();
4808
5303
  };
4809
- let observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission);
5304
+ let headRead = Promise.resolve(void 0);
5305
+ let settledHead;
5306
+ const searchHead = () => settledHead ??= settleWithin(headRead, PAYOUT_SEARCH_HEAD_LIMIT_MS);
5307
+ const checkpoint2 = onBeforeSubmission ? async (request2) => {
5308
+ const head = await searchHead();
5309
+ const agentWallet = this.smartWalletCache.get(`${owner.toLowerCase()}_${validChainId}`);
5310
+ await onBeforeSubmission({
5311
+ agentId: this.id,
5312
+ chainId,
5313
+ token,
5314
+ walletAddress: state.walletAddress,
5315
+ intendedAmount: amount ?? "MAX",
5316
+ transfer: "pending",
5317
+ registration: "not-applicable",
5318
+ delivery: "pending",
5319
+ withdrawalRequest: request2,
5320
+ ...agentWallet ? { agentWallet } : {},
5321
+ ...head !== void 0 ? { payoutSearch: { fromBlock: head.toString() } } : {}
5322
+ });
5323
+ } : void 0;
5324
+ let observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission, checkpoint2);
4810
5325
  const authToken = internals.httpClient?.authToken;
4811
5326
  let raw2;
4812
- let headRead = Promise.resolve(void 0);
4813
5327
  try {
4814
5328
  for (let attempt = 0; attempt < 2; attempt++) {
4815
5329
  try {
4816
5330
  assertCurrent();
4817
5331
  headRead = this.readPayoutSearchHead(validChainId);
5332
+ settledHead = void 0;
4818
5333
  raw2 = await executionSdk.withdrawFunds(owner, validChainId, amount, token);
4819
5334
  break;
4820
5335
  } catch (error) {
@@ -4843,7 +5358,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
4843
5358
  assertSubmission();
4844
5359
  await this.recoverWithdrawalSession(state, validChainId, token, owner);
4845
5360
  observation.stop();
4846
- observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission);
5361
+ observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission, checkpoint2);
4847
5362
  }
4848
5363
  }
4849
5364
  if (!raw2) throw new OwneyError(
@@ -4854,7 +5369,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
4854
5369
  );
4855
5370
  } catch (error) {
4856
5371
  if (staleSessionError && observation.attempted() === false) error = staleSessionError;
4857
- this.invalidateRejectedWithdrawalSession(error, state, validChainId, authToken);
5372
+ this.invalidateRejectedWithdrawalSession(error, state, validChainId, authToken, observation.request()?.httpStatus);
4858
5373
  if (observation.attempted() !== void 0) {
4859
5374
  const failed = observation.attempted() === false || observation.rejected();
4860
5375
  notifyFunding(onProgress, {
@@ -4908,7 +5423,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
4908
5423
  if (!raw2.txHash) {
4909
5424
  const acceptedAt = Date.now();
4910
5425
  const agentWallet = this.smartWalletCache.get(`${owner.toLowerCase()}_${validChainId}`);
4911
- const head = await settleWithin(headRead, PAYOUT_SEARCH_HEAD_LIMIT_MS);
5426
+ const head = await searchHead();
4912
5427
  evidence = {
4913
5428
  ...evidence,
4914
5429
  ...agentWallet ? { agentWallet } : {},
@@ -4920,7 +5435,456 @@ var ZyfaiAgent = class _ZyfaiAgent {
4920
5435
  this.ledgerReads.clear();
4921
5436
  return { ...mapWithdraw(raw2), evidence };
4922
5437
  }
4923
- /** Registration recovery never signs, transfers, activates, or refreshes authentication. */
5438
+ /**
5439
+ * Hands a planned withdrawal leg to our server at plan time (part 4), before
5440
+ * the earlier wallet-approval legs (`after`) are signed. The server sends it
5441
+ * once they are accepted; the browser nudges it at that point
5442
+ * (`withdraw(…, { operationId, planned: true })`). Uses the current execution
5443
+ * client's session, after `ensureConnected`. Never throws, never prompts
5444
+ * beyond that connection, and never withdraws locally.
5445
+ */
5446
+ async handOffPlannedWithdrawal(state, chainId, token, amount, ctx) {
5447
+ const { owneyApiKey, routingApiBaseUrl } = this.accountReporting;
5448
+ if (!this.serverTasksOn(this.serverLegStates.has(ctx.operationId)))
5449
+ return { type: "declined", code: ZYFAI_SERVER_TASKS_OFF_CODE };
5450
+ if (this.serverLegStates.get(ctx.operationId) === "cleared") return { type: "declined", code: SERVER_LEG_CLEARED_CODE };
5451
+ const noSession = { type: "declined", code: "NO_SESSION" };
5452
+ if (!owneyApiKey) return noSession;
5453
+ let validChainId;
5454
+ let session;
5455
+ let agentWallet;
5456
+ try {
5457
+ validChainId = isValidChainId(chainId);
5458
+ await this.ensureConnected(state, validChainId);
5459
+ session = this.executionSession();
5460
+ if (!session) return noSession;
5461
+ if (this.serverLegStates.get(ctx.operationId) === void 0 && !zyfaiSessionOutlivesTask(session, "zyfai_withdrawal")) return { type: "declined", code: SESSION_ENDING_CODE };
5462
+ agentWallet = await this.handoffAgentWallet(state.walletAddress, validChainId);
5463
+ } catch {
5464
+ return noSession;
5465
+ }
5466
+ if (!agentWallet) return { type: "declined", code: "NO_AGENT_WALLET" };
5467
+ try {
5468
+ ctx.beforeRequest?.();
5469
+ } catch {
5470
+ }
5471
+ const afterLostAnswer = this.serverLegStates.get(ctx.operationId) === "requested";
5472
+ this.setServerLegState(ctx.operationId, "requested");
5473
+ let answer = await handOffZyfaiTask(
5474
+ { apiKey: owneyApiKey, baseUrl: routingApiBaseUrl, timeoutMs: PLANNED_HANDOFF_TIMEOUT_MS, zyfaiServerTasks: true },
5475
+ {
5476
+ kind: "zyfai_withdrawal",
5477
+ operationId: ctx.operationId,
5478
+ owner: state.walletAddress,
5479
+ chainId: validChainId,
5480
+ asset: token,
5481
+ amount,
5482
+ agentWallet,
5483
+ after: ctx.after
5484
+ },
5485
+ session
5486
+ );
5487
+ if (answer.type === "declined" && ZYFAI_HANDOFF_NO_TASK_CODES.has(answer.code) && !provesNoServerTask(answer.code, afterLostAnswer)) answer = { type: "unknown" };
5488
+ if (answer.type === "accepted" || answer.type === "declined" && answer.code === "LEG_ALREADY_SENT")
5489
+ this.setServerLegState(ctx.operationId, "accepted");
5490
+ if (answer.type === "declined") {
5491
+ if (answer.code !== "LEG_ALREADY_SENT" && this.serverLegStates.get(ctx.operationId) === "accepted")
5492
+ return { type: "unknown" };
5493
+ if (provesNoServerTask(answer.code, afterLostAnswer)) this.clearServerLeg(ctx.operationId, "no-task-decline");
5494
+ return answer;
5495
+ }
5496
+ this.plannedHandoffs.delete(ctx.operationId);
5497
+ this.plannedHandoffs.set(ctx.operationId, { after: [...ctx.after], amount });
5498
+ while (this.plannedHandoffs.size > PLANNED_HANDOFFS_LIMIT)
5499
+ this.plannedHandoffs.delete(this.plannedHandoffs.keys().next().value);
5500
+ return answer;
5501
+ }
5502
+ /**
5503
+ * F-I2(iii): whether the server path may run. The caller's opt-in is read
5504
+ * now (a function is called) as an operation's first handoff would start.
5505
+ * An operation this session already handed off (`handedOff`: a planned leg,
5506
+ * a leg requested or accepted, a registration already sent) keeps going
5507
+ * through the server even after the opt-in turns off: its nudges, re-runs
5508
+ * and retries, so the server answers the task it may hold and the browser
5509
+ * never sends it too. Without any opt-in (P4-0) nothing was handed off.
5510
+ */
5511
+ serverTasksOn(handedOff = false) {
5512
+ const option = this.accountReporting.zyfaiServerTasks;
5513
+ if (handedOff) return option === true || typeof option === "function";
5514
+ return zyfaiServerTasksEnabled(option);
5515
+ }
5516
+ /** True when this browser may send the operation's Zyfai withdrawal leg
5517
+ * itself: it never handed it off, or the server proved it has no task. */
5518
+ mayWithdrawLocally(operationId) {
5519
+ const current = this.serverLegStates.get(operationId);
5520
+ return current === void 0 || current === "cleared";
5521
+ }
5522
+ /** Moves the leg's server ownership forward: absent -> requested ->
5523
+ * accepted. "accepted" is sticky, and "cleared" is final (such a leg is
5524
+ * never handed off again). Clearing goes through `clearServerLeg`. */
5525
+ setServerLegState(operationId, next) {
5526
+ const current = this.serverLegStates.get(operationId);
5527
+ if (current === "accepted" || current === "cleared" || current === next) return;
5528
+ this.serverLegStates.set(operationId, next);
5529
+ while (this.serverLegStates.size > SERVER_LEG_STATES_LIMIT)
5530
+ this.serverLegStates.delete(this.serverLegStates.keys().next().value);
5531
+ }
5532
+ /** Clears the leg on a proof that the server has no task: a no-task decline
5533
+ * after a request, or nothing ever requested (T8-R4b). Never from accepted.
5534
+ * Returns whether the leg is cleared; the caller then emits
5535
+ * `serverTask: false` and may send locally (T10-I1, T9-I5). */
5536
+ clearServerLeg(operationId, proof) {
5537
+ const current = this.serverLegStates.get(operationId);
5538
+ if (current === "cleared") return true;
5539
+ if (proof === "no-task-decline" ? current !== "requested" : current !== void 0) return false;
5540
+ this.serverLegStates.set(operationId, "cleared");
5541
+ while (this.serverLegStates.size > SERVER_LEG_STATES_LIMIT)
5542
+ this.serverLegStates.delete(this.serverLegStates.keys().next().value);
5543
+ return true;
5544
+ }
5545
+ /** T9-I5: the server may hold this leg's task, so this browser never sends
5546
+ * it. The leg is reported failed with `serverTask: "unknown"` (never
5547
+ * cleared; a re-run hands it off again), and `error`, or an unresolved-leg
5548
+ * error, is thrown. */
5549
+ serverLegUnresolved(onProgress, base7, error) {
5550
+ notifyFunding(onProgress, {
5551
+ ...base7,
5552
+ transfer: "failed",
5553
+ delivery: "failed",
5554
+ withdrawalRequest: { attempted: false },
5555
+ serverTask: "unknown"
5556
+ });
5557
+ throw error ?? new OwneyError(
5558
+ "WITHDRAW_FAILED",
5559
+ "The server may still send this Zyfai withdrawal, so this browser does not send it.",
5560
+ { reason: "ZYFAI_SERVER_LEG_UNRESOLVED", serverTask: true, rejectedBeforeSubmission: true },
5561
+ this.id
5562
+ );
5563
+ }
5564
+ /** The current execution client's session. Never logged or returned. */
5565
+ executionSession() {
5566
+ const token = this.sdk.httpClient?.authToken;
5567
+ return typeof token === "string" && token ? token : void 0;
5568
+ }
5569
+ /** The owner's Zyfai wallet for a handoff (read-only), or undefined. */
5570
+ async handoffAgentWallet(owner, chainId) {
5571
+ if (!ADDRESS_PATTERN.test(owner)) return void 0;
5572
+ try {
5573
+ const wallet = await this.readSmartWallet(owner, chainId);
5574
+ return typeof wallet === "string" && ADDRESS_PATTERN.test(wallet) ? wallet : void 0;
5575
+ } catch {
5576
+ return void 0;
5577
+ }
5578
+ }
5579
+ /**
5580
+ * Part 4: hands this withdrawal to our server, or nudges the task a plan-time
5581
+ * handoff created (`planned`). The server is the one sender from then on:
5582
+ * this never calls Zyfai's withdraw route, and never sends again after an
5583
+ * ambiguous answer. Returns undefined only when nothing was handed off, or
5584
+ * when the server proved no task exists (a decline in
5585
+ * `ZYFAI_HANDOFF_NO_TASK_CODES`, rulings P4-11, T7-R1, T8-C1 and F-I1(b);
5586
+ * after a lost answer only the server's own declines, F-I2(i)); today's
5587
+ * local path then runs, after the handoff's mark was cleared (T8-C2).
5588
+ */
5589
+ async withdrawThroughServer(state, chainId, token, requestedAmount, onProgress, onBeforeSubmission, assertOperation, context) {
5590
+ const owner = state.walletAddress;
5591
+ const operationId = context.operationId;
5592
+ const plan = context.planned ? this.plannedHandoffs.get(operationId) : void 0;
5593
+ if (context.planned) this.plannedHandoffs.delete(operationId);
5594
+ if (context.planned && this.serverLegStates.get(operationId) === void 0)
5595
+ this.setServerLegState(operationId, "accepted");
5596
+ const planLost = context.planned && this.serverLegStates.get(operationId) === "requested";
5597
+ const serverOwned = () => context.planned || this.serverLegStates.get(operationId) === "accepted";
5598
+ const amount = plan?.amount === "MAX" ? void 0 : plan && /^[1-9][0-9]*$/.test(plan.amount) ? plan.amount : requestedAmount;
5599
+ const route = amount ? { attempted: true, method: "POST", path: "/users/partial-withdraw/v2" } : { attempted: true, method: "GET", path: "/users/withdraw/v2" };
5600
+ const base7 = {
5601
+ agentId: this.id,
5602
+ chainId,
5603
+ token,
5604
+ walletAddress: owner,
5605
+ intendedAmount: amount ?? "MAX",
5606
+ registration: "not-applicable"
5607
+ };
5608
+ let head;
5609
+ const searchHead = () => head ??= settleWithin(this.readPayoutSearchHead(chainId), PAYOUT_SEARCH_HEAD_LIMIT_MS);
5610
+ const accept = async (serverTask, agentWallet2, txHash, providerWithdrawalId) => {
5611
+ const withdrawalRequest = { ...route, ...providerWithdrawalId ? { providerWithdrawalId } : {} };
5612
+ let evidence;
5613
+ if (txHash) {
5614
+ evidence = { ...base7, txHash, transfer: "pending", delivery: "pending", withdrawalRequest, serverTask };
5615
+ } else {
5616
+ const acceptedAt = Date.now();
5617
+ const fromBlock = await searchHead();
5618
+ evidence = {
5619
+ ...base7,
5620
+ transfer: "pending",
5621
+ delivery: "pending",
5622
+ ...agentWallet2 ? { agentWallet: agentWallet2 } : {},
5623
+ withdrawalRequest: { ...withdrawalRequest, acceptedAt },
5624
+ ...fromBlock !== void 0 ? { payoutSearch: { fromBlock: fromBlock.toString() } } : {},
5625
+ serverTask
5626
+ };
5627
+ }
5628
+ notifyFunding(onProgress, evidence);
5629
+ this.ledgerReads.clear();
5630
+ return { ...mapWithdraw({
5631
+ success: true,
5632
+ message: "Withdrawal handed to the server",
5633
+ txHash,
5634
+ type: amount ? "partial" : "full",
5635
+ amount: amount || "all"
5636
+ }), evidence };
5637
+ };
5638
+ try {
5639
+ await this.ensureConnected(state, chainId);
5640
+ } catch (error) {
5641
+ if (serverOwned()) return accept("unknown", void 0);
5642
+ if (!this.mayWithdrawLocally(operationId)) this.serverLegUnresolved(onProgress, base7, error);
5643
+ notifyFunding(onProgress, { ...base7, transfer: "failed", delivery: "failed" });
5644
+ throw error;
5645
+ }
5646
+ const generation = this.connectionGeneration;
5647
+ const internals = this.sdk;
5648
+ const session = this.executionSession();
5649
+ if (session && !context.planned && this.serverLegStates.get(operationId) === void 0 && !zyfaiSessionOutlivesTask(session, "zyfai_withdrawal")) return void 0;
5650
+ const agentWallet = session ? await this.handoffAgentWallet(owner, chainId) : void 0;
5651
+ if (!session || !agentWallet) {
5652
+ if (serverOwned()) return accept("unknown", agentWallet);
5653
+ if (!this.mayWithdrawLocally(operationId)) this.serverLegUnresolved(onProgress, base7);
5654
+ return void 0;
5655
+ }
5656
+ const assertSubmission = () => {
5657
+ const account = internals.walletClient?.account?.address;
5658
+ if (generation !== this.connectionGeneration || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== chainId || state.chainId !== null && state.chainId !== chainId || account && account.toLowerCase() !== owner.toLowerCase())
5659
+ throw new OwneyError(
5660
+ "NOT_CONNECTED",
5661
+ "Wallet changed before Zyfai withdrawal submission.",
5662
+ { phase: "prepare", rejectedBeforeSubmission: true },
5663
+ this.id
5664
+ );
5665
+ assertOperation?.();
5666
+ };
5667
+ try {
5668
+ if (!context.planned) await this.ensureWithdrawalSessionKey(owner, chainId);
5669
+ assertSubmission();
5670
+ const fromBlock = await searchHead();
5671
+ await onBeforeSubmission?.({
5672
+ ...base7,
5673
+ transfer: "pending",
5674
+ delivery: "pending",
5675
+ withdrawalRequest: { ...route },
5676
+ agentWallet,
5677
+ ...fromBlock !== void 0 ? { payoutSearch: { fromBlock: fromBlock.toString() } } : {},
5678
+ serverTask: true
5679
+ });
5680
+ assertSubmission();
5681
+ } catch (error) {
5682
+ if (serverOwned()) return accept("unknown", agentWallet);
5683
+ if (!this.clearServerLeg(operationId, "never-requested")) this.serverLegUnresolved(onProgress, base7, error);
5684
+ notifyFunding(onProgress, {
5685
+ ...base7,
5686
+ transfer: "failed",
5687
+ delivery: "failed",
5688
+ withdrawalRequest: { attempted: false },
5689
+ serverTask: false
5690
+ });
5691
+ throw error;
5692
+ }
5693
+ const cfg = {
5694
+ apiKey: this.accountReporting.owneyApiKey,
5695
+ baseUrl: this.accountReporting.routingApiBaseUrl,
5696
+ timeoutMs: WITHDRAW_HANDOFF_TIMEOUT_MS,
5697
+ zyfaiServerTasks: true
5698
+ };
5699
+ const body = {
5700
+ kind: "zyfai_withdrawal",
5701
+ operationId: context.operationId,
5702
+ owner,
5703
+ chainId,
5704
+ asset: token,
5705
+ amount: amount ?? "MAX",
5706
+ agentWallet,
5707
+ ...plan?.after.length ? { after: plan.after } : {},
5708
+ ...context.planned ? { runNow: true } : {}
5709
+ };
5710
+ const requestedBefore = this.serverLegStates.get(operationId) === "requested";
5711
+ let afterUnknown = planLost;
5712
+ this.setServerLegState(operationId, "requested");
5713
+ let answer = await handOffZyfaiTask(cfg, body, session);
5714
+ if (answer.type === "unknown" && !context.planned) {
5715
+ afterUnknown = true;
5716
+ answer = await handOffZyfaiTask(cfg, body, session);
5717
+ }
5718
+ if (answer.type === "declined" && ZYFAI_HANDOFF_NO_TASK_CODES.has(answer.code) && !provesNoServerTask(answer.code, requestedBefore || afterUnknown)) answer = { type: "unknown" };
5719
+ if (answer.type === "accepted" || answer.type === "declined" && answer.code === "LEG_ALREADY_SENT")
5720
+ this.setServerLegState(operationId, "accepted");
5721
+ if (answer.type === "declined") {
5722
+ if (ZYFAI_HANDOFF_NO_TASK_CODES.has(answer.code) && this.clearServerLeg(operationId, "no-task-decline")) {
5723
+ const cleared = { ...base7, transfer: "pending", delivery: "pending", serverTask: false };
5724
+ notifyFunding(onProgress, cleared);
5725
+ try {
5726
+ await onBeforeSubmission?.({ ...cleared, withdrawalRequest: { attempted: false } });
5727
+ } catch (error) {
5728
+ notifyFunding(onProgress, { ...cleared, transfer: "failed", delivery: "failed", withdrawalRequest: { attempted: false } });
5729
+ throw error;
5730
+ }
5731
+ return void 0;
5732
+ }
5733
+ if (this.serverLegStates.get(operationId) === "accepted" || afterUnknown) return accept("unknown", agentWallet);
5734
+ return this.serverWithdrawalNotSent(answer.code, state, chainId, token, session, onProgress, base7, route, "unknown");
5735
+ }
5736
+ if (answer.type === "unknown") return accept("unknown", agentWallet);
5737
+ if (answer.state === "failed" && answer.code && ZYFAI_WITHDRAWAL_NOT_SENT_CODES.has(answer.code))
5738
+ return this.serverWithdrawalNotSent(answer.code, state, chainId, token, session, onProgress, base7, route, true);
5739
+ if (answer.state === "failed" || answer.state === "expired") return accept("unknown", agentWallet);
5740
+ return accept(true, agentWallet, answer.result.txHash, answer.result.providerWithdrawalId);
5741
+ }
5742
+ /** A server answer proving the withdrawal was never sent: today's matching
5743
+ * error, with evidence transfer and delivery `failed`. "Already in progress"
5744
+ * keeps the hold: failure `withdrawal-in-progress`, never rejectedBeforeSubmission.
5745
+ * `serverTask`: true for a task that ended unsent (the server owns the leg's
5746
+ * close), "unknown" for a declined handoff whose code does not prove no task
5747
+ * (never false: such a leg is never cleared, ruling T9-I3). */
5748
+ serverWithdrawalNotSent(code, state, chainId, token, session, onProgress, base7, route, serverTask) {
5749
+ let failure;
5750
+ let error;
5751
+ const sessionUnavailable = () => new OwneyError(
5752
+ "AGENT_AUTH_FAILED",
5753
+ "Zyfai has no active withdrawal session. Its withdrawal was not submitted.",
5754
+ { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token, serverTask: true, rejectedBeforeSubmission: true },
5755
+ this.id
5756
+ );
5757
+ switch (code) {
5758
+ case "NEEDS_USER_SESSION":
5759
+ failure = "unauthorized";
5760
+ error = sessionUnavailable();
5761
+ this.dropWithdrawalSession(state, chainId, session);
5762
+ break;
5763
+ case "NEEDS_USER_SESSION_KEY":
5764
+ failure = "session-key-missing";
5765
+ error = sessionUnavailable();
5766
+ this.rearmWithdrawalSessionKey(state.walletAddress, chainId);
5767
+ break;
5768
+ case "WITHDRAW_INSUFFICIENT_BALANCE":
5769
+ failure = "insufficient-balance";
5770
+ error = new OwneyError(
5771
+ "WITHDRAW_INSUFFICIENT_BALANCE",
5772
+ "No funds remain in Zyfai for this asset.",
5773
+ { available: "0", chainId, token, rejectedBeforeSubmission: true },
5774
+ this.id
5775
+ );
5776
+ break;
5777
+ case "WITHDRAWAL_IN_PROGRESS":
5778
+ case "ZYFAI_REDEMPTION_IN_FLIGHT":
5779
+ failure = "withdrawal-in-progress";
5780
+ error = new OwneyError(
5781
+ "WITHDRAW_IN_PROGRESS",
5782
+ "A Zyfai withdrawal is already in progress.",
5783
+ { reason: code, chainId, token, serverTask: true },
5784
+ this.id
5785
+ );
5786
+ break;
5787
+ default:
5788
+ failure = code === "ZYFAI_FORBIDDEN" ? "forbidden" : "other";
5789
+ error = new OwneyError(
5790
+ "WITHDRAW_FAILED",
5791
+ "The server did not send the Zyfai withdrawal.",
5792
+ { reason: code, serverTask: true, rejectedBeforeSubmission: true },
5793
+ this.id
5794
+ );
5795
+ }
5796
+ notifyFunding(onProgress, {
5797
+ ...base7,
5798
+ transfer: "failed",
5799
+ delivery: "failed",
5800
+ serverTask,
5801
+ withdrawalRequest: { ...route, failure }
5802
+ });
5803
+ throw error;
5804
+ }
5805
+ /** Zyfai has no session key for this wallet: for a legacy (not pre-deployed)
5806
+ * wallet, forget the login's "active" flag and this session's attempt, so the
5807
+ * next `ensureWithdrawalSessionKey` prompts once. Sends and prompts nothing. */
5808
+ rearmWithdrawalSessionKey(owner, chainId) {
5809
+ const internals = this.sdk;
5810
+ if (internals.isPredeployed !== false || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== chainId) return;
5811
+ internals.hasActiveSessionKey = false;
5812
+ this.sessionKeyAttempts.delete(`${owner.toLowerCase()}:${chainId}`);
5813
+ }
5814
+ /** The routing API's handoff config. Only called with the opt-in and the key. */
5815
+ registrationHandoffConfig(timeoutMs) {
5816
+ return {
5817
+ apiKey: this.accountReporting.owneyApiKey,
5818
+ baseUrl: this.accountReporting.routingApiBaseUrl,
5819
+ timeoutMs,
5820
+ zyfaiServerTasks: true
5821
+ };
5822
+ }
5823
+ /**
5824
+ * Part 4: hands a deposit's registration to our server before its transfer.
5825
+ * Undefined when nothing was handed off, or when the server proved it has no
5826
+ * task (a decline in `ZYFAI_HANDOFF_NO_TASK_CODES`, rulings P4-11, T7-R1 and
5827
+ * T8-F2; for a retried create only the server's own declines, F-I2(i)):
5828
+ * today's local registration then runs. Any other answer means the
5829
+ * server holds the registration, or may hold it (an unknown answer, or
5830
+ * another decline), so the browser never registers it except by the
5831
+ * not-sent fallback. Never throws; never logs or returns the session.
5832
+ */
5833
+ async handOffDepositRegistration(owner, chainId, asset, amount, agentWallet, operationId) {
5834
+ const session = this.executionSession();
5835
+ if (!session || !ADDRESS_PATTERN.test(owner) || !ADDRESS_PATTERN.test(agentWallet) || !REGISTRATION_AMOUNT.test(amount) || asset !== "USDC" && asset !== "WETH") return void 0;
5836
+ const retried = this.requestedRegistrations.has(operationId);
5837
+ if (!retried) {
5838
+ if (!zyfaiSessionOutlivesTask(session, "zyfai_deposit_registration")) return void 0;
5839
+ this.requestedRegistrations.add(operationId);
5840
+ while (this.requestedRegistrations.size > SERVER_LEG_STATES_LIMIT)
5841
+ this.requestedRegistrations.delete(this.requestedRegistrations.values().next().value);
5842
+ }
5843
+ const answer = await handOffZyfaiTask(
5844
+ this.registrationHandoffConfig(REGISTRATION_HANDOFF_TIMEOUT_MS),
5845
+ { kind: "zyfai_deposit_registration", operationId, owner, chainId, asset, amount, agentWallet },
5846
+ session
5847
+ );
5848
+ if (answer.type === "declined" && provesNoServerTask(answer.code, retried)) return void 0;
5849
+ if (answer.type === "accepted") return { serverTask: true, serverTaskOperationId: operationId };
5850
+ const lost = answer.type === "unknown" || ZYFAI_HANDOFF_NO_TASK_CODES.has(answer.code);
5851
+ return { serverTask: "unknown", serverTaskOperationId: operationId, serverTaskCreate: lost ? "lost" : "declined" };
5852
+ }
5853
+ /**
5854
+ * Nudges the server's registration task of this deposit evidence (with
5855
+ * `serverTask`) and reads what its answer proves. Without the opt-in (read
5856
+ * now, unless this session sent the registration's create, F-I2(iii)), the
5857
+ * key, a session or the task's identity no nudge is sent and the outcome is
5858
+ * unknown. Never throws; never registers locally.
5859
+ */
5860
+ async nudgeDepositRegistration(evidence, session) {
5861
+ const { owneyApiKey } = this.accountReporting;
5862
+ const operationId = evidence.serverTaskOperationId;
5863
+ const agentWallet = evidence.agentWallet;
5864
+ const chainId = evidence.chainId;
5865
+ if (typeof operationId !== "string" || !operationId || !this.serverTasksOn(this.requestedRegistrations.has(operationId)) || !owneyApiKey || !session || typeof agentWallet !== "string" || !ADDRESS_PATTERN.test(agentWallet) || !ADDRESS_PATTERN.test(evidence.walletAddress) || !REGISTRATION_AMOUNT.test(evidence.intendedAmount) || evidence.token !== "USDC" && evidence.token !== "WETH" || chainId !== 1 && chainId !== 8453 && chainId !== 42161)
5866
+ return { kind: "unknown" };
5867
+ const answer = await handOffZyfaiTask(
5868
+ this.registrationHandoffConfig(REGISTRATION_NUDGE_TIMEOUT_MS),
5869
+ {
5870
+ kind: "zyfai_deposit_registration",
5871
+ operationId,
5872
+ owner: evidence.walletAddress,
5873
+ chainId,
5874
+ asset: evidence.token,
5875
+ amount: evidence.intendedAmount,
5876
+ agentWallet,
5877
+ runNow: true
5878
+ },
5879
+ session
5880
+ );
5881
+ return registrationOutcome(answer, evidence.serverTask === true || evidence.serverTaskCreate !== "lost");
5882
+ }
5883
+ /** Registration recovery never signs, transfers, activates, or refreshes authentication.
5884
+ * For a deposit the server registers (`serverTask`), it nudges the server's
5885
+ * task instead: still pending gives a transient error, and an unknown or
5886
+ * expired outcome the status-409 error. It registers locally only when the
5887
+ * server proved it never sent the registration (ruling A4). */
4924
5888
  async retryDepositRegistration(state, evidence) {
4925
5889
  const executionSdk = this.sdk;
4926
5890
  const internals = executionSdk;
@@ -4931,11 +5895,37 @@ var ZyfaiAgent = class _ZyfaiAgent {
4931
5895
  throw new OwneyError("AGENT_API_ERROR", "Deposit transfer must be verified before registration recovery.");
4932
5896
  }
4933
5897
  const cid = isValidChainId(evidence.chainId);
5898
+ let cleared = {};
5899
+ if (evidence.serverTask === true || evidence.serverTask === "unknown") {
5900
+ const outcome = await this.nudgeDepositRegistration(evidence, internals.httpClient.authToken ?? void 0);
5901
+ if (outcome.kind === "confirmed") {
5902
+ this.pendingRegistrations.get(`${evidence.walletAddress.toLowerCase()}:${cid}`)?.delete(evidence.txHash.toLowerCase());
5903
+ this.ledgerReads.clear();
5904
+ return { ...evidence, registration: "confirmed", providerDepositId: outcome.providerDepositId, serverTask: true };
5905
+ }
5906
+ if (outcome.kind === "pending")
5907
+ throw new OwneyError("AGENT_API_ERROR", "Zyfai registration by the server is temporarily unavailable.");
5908
+ if (outcome.kind === "unknown") throw registrationOutcomeUnknown(outcome.code);
5909
+ cleared = { serverTask: false };
5910
+ }
4934
5911
  const tokenAddress = evidence.token === "WETH" ? WETH_ADDRESS_BY_CHAIN[cid] : void 0;
4935
- await executionSdk.logDeposit(cid, evidence.txHash, evidence.intendedAmount, tokenAddress);
5912
+ const logged = await executionSdk.logDeposit(cid, evidence.txHash, evidence.intendedAmount, tokenAddress);
5913
+ const providerDepositId = depositIdFromLog(logged);
4936
5914
  this.pendingRegistrations.get(`${evidence.walletAddress.toLowerCase()}:${cid}`)?.delete(evidence.txHash.toLowerCase());
4937
5915
  this.ledgerReads.clear();
4938
- return { ...evidence, registration: "confirmed" };
5916
+ return { ...evidence, ...cleared, registration: "confirmed", ...providerDepositId ? { providerDepositId } : {} };
5917
+ }
5918
+ /** Reads a deposit's placement status by its id. Never signs, prompts or authenticates:
5919
+ * it uses only a session the vendor client already holds, and any failure is `null`. */
5920
+ async getDepositPlacement(state, depositId) {
5921
+ const internals = this.sdk;
5922
+ const client = internals.httpClient;
5923
+ if (!client?.authToken || typeof client.get !== "function" || this.connectedAddress?.toLowerCase() !== state.walletAddress.toLowerCase()) return null;
5924
+ try {
5925
+ return mapDepositPlacement(await client.get(`/users/deposits/${encodeURIComponent(depositId)}/v2`));
5926
+ } catch {
5927
+ return null;
5928
+ }
4939
5929
  }
4940
5930
  /** Reads a withdrawal's lifecycle by its id. Never signs, prompts or authenticates:
4941
5931
  * it uses only a session the vendor client already holds, and any failure is `null`. */
@@ -4944,11 +5934,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
4944
5934
  const client = internals.httpClient;
4945
5935
  if (!client?.authToken || typeof client.get !== "function" || this.connectedAddress?.toLowerCase() !== state.walletAddress.toLowerCase()) return null;
4946
5936
  try {
4947
- const body = await client.get(`/users/withdrawals/${encodeURIComponent(withdrawalId)}`);
4948
- if (!body || typeof body !== "object" || typeof body.status !== "string" || typeof body.syncSettled !== "boolean")
4949
- return null;
4950
- const hash2 = typeof body.syncTxHash === "string" && /^0x[0-9a-fA-F]{64}$/.test(body.syncTxHash) ? body.syncTxHash : void 0;
4951
- return { status: body.status, syncSettled: body.syncSettled, ...hash2 ? { syncTxHash: hash2 } : {} };
5937
+ return mapWithdrawalLifecycle(await client.get(`/users/withdrawals/${encodeURIComponent(withdrawalId)}`));
4952
5938
  } catch {
4953
5939
  return null;
4954
5940
  }
@@ -5181,16 +6167,16 @@ var ZyfaiAgent = class _ZyfaiAgent {
5181
6167
  }
5182
6168
  const now = Date.now();
5183
6169
  const pending = [...this.pendingRegistrations.get(scope)?.values() ?? []].map((entry) => entry.at);
5184
- const active2 = zyfaiHoldActive({
6170
+ const active = zyfaiHoldActive({
5185
6171
  movements,
5186
6172
  pendingRegistrations: pending,
5187
6173
  lastCheckTimestamp: raw2?.data?.lastCheckTimestamp,
5188
6174
  lastLogDate: raw2?.data?.lastLogDate?.[String(chainId)],
5189
6175
  now
5190
6176
  });
5191
- if (active2) this.holdActiveScopes.add(scope);
6177
+ if (active) this.holdActiveScopes.add(scope);
5192
6178
  else this.holdActiveScopes.delete(scope);
5193
- if (!active2) {
6179
+ if (!active) {
5194
6180
  this.lastGoodEarnings.set(key2, raw2);
5195
6181
  return { value: raw2, active: false };
5196
6182
  }
@@ -6173,7 +7159,7 @@ function actionEntry(action) {
6173
7159
  rebalanceLog: []
6174
7160
  };
6175
7161
  }
6176
- var TX_HASH = /^0x[a-fA-F0-9]{64}$/;
7162
+ var TX_HASH3 = /^0x[a-fA-F0-9]{64}$/;
6177
7163
  function allocationLegs(value, context, requireAmount) {
6178
7164
  if (!Array.isArray(value)) return void 0;
6179
7165
  const legs = [];
@@ -6202,7 +7188,7 @@ function allocationEntry(action, context) {
6202
7188
  if (action.actionType.toLowerCase() !== "autoseek") return void 0;
6203
7189
  const hash2 = action.details?.depositTransactionHash;
6204
7190
  const withdrawHash = action.details?.withdrawTransactionHash;
6205
- if (typeof hash2 !== "string" || !TX_HASH.test(hash2)) return null;
7191
+ if (typeof hash2 !== "string" || !TX_HASH3.test(hash2)) return null;
6206
7192
  const deposits = allocationLegs(action.details?.deposits, context, true);
6207
7193
  const withdrawals = action.details?.withdrawals == null ? [] : allocationLegs(action.details.withdrawals, context, false);
6208
7194
  if (!deposits?.length || !withdrawals) return null;
@@ -6215,7 +7201,7 @@ function allocationEntry(action, context) {
6215
7201
  }));
6216
7202
  const amountRaw = deposits.reduce((total, leg) => total + leg.rawAmount, 0n);
6217
7203
  const rebalance = withdrawals.length > 0;
6218
- const hashes = rebalance && typeof withdrawHash === "string" && TX_HASH.test(withdrawHash) && withdrawHash.toLowerCase() !== hash2.toLowerCase() ? [hash2, withdrawHash] : [hash2];
7204
+ const hashes = rebalance && typeof withdrawHash === "string" && TX_HASH3.test(withdrawHash) && withdrawHash.toLowerCase() !== hash2.toLowerCase() ? [hash2, withdrawHash] : [hash2];
6219
7205
  const source = withdrawals.length === 1 ? withdrawals[0] : {
6220
7206
  protocol: [...new Set(withdrawals.map((leg) => leg.protocol))].join(" + "),
6221
7207
  pool: withdrawals.map((leg) => leg.pool).filter(Boolean).join(" + ") || void 0
@@ -6607,6 +7593,7 @@ function operationHash(operation) {
6607
7593
  }
6608
7594
  });
6609
7595
  }
7596
+ var YIELDSEEKER_WITHDRAW_SERVER_SUBMIT = 1;
6610
7597
  function sponsorError(message, safeToFallback, details = {}) {
6611
7598
  return new OwneyError(
6612
7599
  "SPONSOR_REQUEST_FAILED",
@@ -6629,6 +7616,22 @@ function assertIntentFresh(expiresAt) {
6629
7616
  { failureCode: "SPONSOR_INTENT_EXPIRED" }
6630
7617
  );
6631
7618
  }
7619
+ var PAYMASTER_EXPIRY_MARGIN_SECONDS = 30;
7620
+ function paymasterValidUntil(paymasterAndData) {
7621
+ if (paymasterAndData.length < 2 + 64) return void 0;
7622
+ const validUntil = Number.parseInt(paymasterAndData.slice(42, 54), 16);
7623
+ const now = Date.now() / 1e3;
7624
+ return validUntil > now - 86400 && validUntil < now + 86400 ? validUntil : void 0;
7625
+ }
7626
+ function assertPaymasterFresh(paymasterAndData) {
7627
+ const validUntil = paymasterValidUntil(paymasterAndData);
7628
+ if (validUntil !== void 0 && Date.now() / 1e3 + PAYMASTER_EXPIRY_MARGIN_SECONDS >= validUntil)
7629
+ throw sponsorError(
7630
+ "The withdrawal sponsorship expired while waiting for the signature. Please try again.",
7631
+ true,
7632
+ { failureCode: "SPONSOR_INTENT_EXPIRED" }
7633
+ );
7634
+ }
6632
7635
  function providerReason(value) {
6633
7636
  if (typeof value !== "string") return void 0;
6634
7637
  return value.replace(/https?:\/\/\S+/gi, "[url]").replace(/0x[0-9a-fA-F]{40,}/g, "[hex]").replace(/[A-Za-z0-9_+=\/-]{48,}/g, "[token]").slice(0, 300);
@@ -6710,6 +7713,12 @@ async function checkpoint(callback, pending) {
6710
7713
  );
6711
7714
  }
6712
7715
  }
7716
+ function reportAccepted(onAccepted, accepted) {
7717
+ try {
7718
+ onAccepted?.(accepted);
7719
+ } catch {
7720
+ }
7721
+ }
6713
7722
  function finalPaymasterFields(value) {
6714
7723
  const result = {};
6715
7724
  for (const key2 of [
@@ -6788,15 +7797,57 @@ async function requestIntent(input) {
6788
7797
  url.searchParams.set("intent", intent.intent);
6789
7798
  return { intent, rpcUrl: url.toString() };
6790
7799
  }
7800
+ var RPC_TIMEOUT_MS = 15e3;
7801
+ var SERVER_SUBMIT_SEND_TIMEOUT_MS = 25e3;
7802
+ function postRpc(url, method, params, timeoutMs) {
7803
+ return fetch(url, {
7804
+ method: "POST",
7805
+ headers: { "content-type": "application/json" },
7806
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
7807
+ signal: AbortSignal.timeout(timeoutMs)
7808
+ });
7809
+ }
7810
+ function rpcRejection(status, payload, method, phase) {
7811
+ const rejection = payload?.error;
7812
+ const diagnostics = rpcDiagnostics(rejection?.data);
7813
+ const executionReverted = rejection?.code === -32521 || diagnostics.failureCode === "SPONSOR_EXECUTION_REVERTED";
7814
+ const details = {
7815
+ statusCode: status,
7816
+ rpcCode: rejection?.code,
7817
+ rpcMethod: method,
7818
+ phase,
7819
+ providerReason: providerReason(rejection?.data?.providerReason) ?? providerReason(rejection?.message),
7820
+ failureCode: "RPC_REJECTED",
7821
+ ...diagnostics
7822
+ };
7823
+ if (phase === "prepare" && executionReverted)
7824
+ return new OwneyError(
7825
+ "WITHDRAW_FAILED",
7826
+ "Yieldseeker withdrawal simulation reverted before submission.",
7827
+ {
7828
+ ...details,
7829
+ failureCode: "SPONSOR_EXECUTION_REVERTED",
7830
+ safeToFallback: false,
7831
+ rejectedBeforeSubmission: true
7832
+ },
7833
+ "yieldseeker"
7834
+ );
7835
+ const definitiveRejection = phase === "send" && !executionReverted && status < 500 && !!rejection && (rejection.data?.rejectedBeforeSubmission === true || [-32501, -32502, -32503, -32504, -32505, -32507].includes(rejection.code ?? 0) || // AA3x: the paymaster failed validation (e.g. AA32 expired), so the
7836
+ // bundler never accepted it. AA2x stays ambiguous (AA25 can mean it ran).
7837
+ /\bAA3\d\b/.test(rejection.message ?? "") || /maxFeePerGas.*(?:too low|lower than|less than|must be at least)/i.test(rejection.message ?? ""));
7838
+ return sponsorError(
7839
+ phase !== "prepare" && !definitiveRejection ? "Sponsored withdrawal submission status is unknown." : "Withdrawal sponsorship was declined before submission.",
7840
+ phase === "prepare" || definitiveRejection,
7841
+ {
7842
+ ...details,
7843
+ ...phase === "prepare" || definitiveRejection ? { rejectedBeforeSubmission: true } : {}
7844
+ }
7845
+ );
7846
+ }
6791
7847
  async function rpc(url, method, params, phase) {
6792
7848
  let response;
6793
7849
  try {
6794
- response = await fetch(url, {
6795
- method: "POST",
6796
- headers: { "content-type": "application/json" },
6797
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
6798
- signal: AbortSignal.timeout(15e3)
6799
- });
7850
+ response = await postRpc(url, method, params, RPC_TIMEOUT_MS);
6800
7851
  } catch {
6801
7852
  throw sponsorError(
6802
7853
  "Withdrawal sponsorship request status is unknown.",
@@ -6810,43 +7861,37 @@ async function rpc(url, method, params, phase) {
6810
7861
  );
6811
7862
  }
6812
7863
  const payload = await response.json().catch(() => null);
6813
- if (!response.ok || payload?.error || payload?.result === void 0) {
6814
- const rejection = payload?.error;
6815
- const diagnostics = rpcDiagnostics(rejection?.data);
6816
- const executionReverted = rejection?.code === -32521 || diagnostics.failureCode === "SPONSOR_EXECUTION_REVERTED";
6817
- const details = {
6818
- statusCode: response.status,
6819
- rpcCode: rejection?.code,
6820
- rpcMethod: method,
6821
- phase,
6822
- providerReason: providerReason(rejection?.data?.providerReason) ?? providerReason(rejection?.message),
6823
- failureCode: "RPC_REJECTED",
6824
- ...diagnostics
6825
- };
6826
- if (phase === "prepare" && executionReverted)
6827
- throw new OwneyError(
6828
- "WITHDRAW_FAILED",
6829
- "Yieldseeker withdrawal simulation reverted before submission.",
6830
- {
6831
- ...details,
6832
- failureCode: "SPONSOR_EXECUTION_REVERTED",
6833
- safeToFallback: false,
6834
- rejectedBeforeSubmission: true
6835
- },
6836
- "yieldseeker"
6837
- );
6838
- const definitiveRejection = phase === "send" && !executionReverted && response.status < 500 && !!rejection && (rejection.data?.rejectedBeforeSubmission === true || [-32501, -32502, -32503, -32504, -32505, -32507].includes(rejection.code ?? 0) || /maxFeePerGas.*(?:too low|lower than|less than|must be at least)/i.test(rejection.message ?? ""));
6839
- throw sponsorError(
6840
- phase !== "prepare" && !definitiveRejection ? "Sponsored withdrawal submission status is unknown." : "Withdrawal sponsorship was declined before submission.",
6841
- phase === "prepare" || definitiveRejection,
6842
- {
6843
- ...details,
6844
- ...phase === "prepare" || definitiveRejection ? { rejectedBeforeSubmission: true } : {}
6845
- }
6846
- );
6847
- }
7864
+ if (!response.ok || payload?.error || payload?.result === void 0)
7865
+ throw rpcRejection(response.status, payload, method, phase);
6848
7866
  return payload.result;
6849
7867
  }
7868
+ async function sendSignedUserOperation(rpcUrl, operation, operationId) {
7869
+ const method = "eth_sendUserOperation";
7870
+ const params = [operation, ENTRY_POINT_V06];
7871
+ if (!operationId) return { type: "accepted", hash: await rpc(rpcUrl, method, params, "send"), serverSubmit: false };
7872
+ const url = new URL(rpcUrl);
7873
+ url.searchParams.set("operationId", operationId);
7874
+ url.searchParams.set("serverSubmit", "1");
7875
+ let response;
7876
+ try {
7877
+ response = await postRpc(url.toString(), method, params, SERVER_SUBMIT_SEND_TIMEOUT_MS);
7878
+ } catch {
7879
+ return { type: "pending", serverSubmit: "unknown" };
7880
+ }
7881
+ const payload = await response.json().catch(() => null);
7882
+ if (!payload) return { type: "pending", serverSubmit: "unknown" };
7883
+ if (response.ok && !payload.error && payload.result !== void 0)
7884
+ return {
7885
+ type: "accepted",
7886
+ hash: payload.result,
7887
+ serverSubmit: payload.owney?.serverSubmit === true && payload.owney.state === "accepted"
7888
+ };
7889
+ const rejection = rpcRejection(response.status, payload, method, "send");
7890
+ if (rejection.details?.rejectedBeforeSubmission === true) throw rejection;
7891
+ if (payload.error?.data?.serverSubmit === true) return { type: "pending", serverSubmit: true };
7892
+ if (response.status >= 500) return { type: "pending", serverSubmit: "unknown" };
7893
+ throw rejection;
7894
+ }
6850
7895
  async function waitForTransaction(client, txHash) {
6851
7896
  const receipt = await client.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
6852
7897
  if (receipt.status !== "success")
@@ -6928,6 +7973,7 @@ async function submitEoa(input) {
6928
7973
  operation = { ...operation, signature };
6929
7974
  await assertOwnerSession(input.state, input.owner);
6930
7975
  assertIntentFresh(input.expiresAt);
7976
+ assertPaymasterFresh(operation.paymasterAndData);
6931
7977
  const userOperation = {
6932
7978
  sender: operation.sender,
6933
7979
  nonce: operation.nonce,
@@ -6944,18 +7990,19 @@ async function submitEoa(input) {
6944
7990
  input.assertCurrent?.();
6945
7991
  await checkpoint(input.onBeforeSubmission, { userOpHash, userOperation });
6946
7992
  input.assertCurrent?.();
6947
- const submittedHash = await rpc(
6948
- input.rpcUrl,
6949
- "eth_sendUserOperation",
6950
- [operation, ENTRY_POINT_V06],
6951
- "send"
6952
- );
6953
- if (!isHash(submittedHash) || submittedHash.toLowerCase() !== userOpHash.toLowerCase())
6954
- throw sponsorError(
6955
- "Bundler returned a different UserOperation hash.",
6956
- false,
6957
- { userOpHash, submittedHash, phase: "send", failureCode: "BUNDLER_HASH_MISMATCH" }
6958
- );
7993
+ const sent = await sendSignedUserOperation(input.rpcUrl, operation, input.operationId);
7994
+ const serverSubmit = sent.type === "pending" ? sent.serverSubmit : sent.serverSubmit ? true : void 0;
7995
+ if (serverSubmit) savePending(input.onPending, { userOpHash, serverSubmit });
7996
+ if (sent.type === "accepted") {
7997
+ const submittedHash = sent.hash;
7998
+ if (!isHash(submittedHash) || submittedHash.toLowerCase() !== userOpHash.toLowerCase())
7999
+ throw sponsorError(
8000
+ "Bundler returned a different UserOperation hash.",
8001
+ false,
8002
+ { userOpHash, submittedHash, phase: "send", failureCode: "BUNDLER_HASH_MISMATCH" }
8003
+ );
8004
+ reportAccepted(input.onAccepted, { userOpHash });
8005
+ }
6959
8006
  const deadline = Date.now() + OBSERVATION_WINDOW_MS;
6960
8007
  let lastReadError;
6961
8008
  for (let poll = 0; poll < 40 && Date.now() < deadline; poll++) {
@@ -6988,6 +8035,7 @@ async function submitEoa(input) {
6988
8035
  userOpHash,
6989
8036
  phase: "observe",
6990
8037
  failureCode: "RECEIPT_UNRESOLVED",
8038
+ ...serverSubmit ? { serverSubmit } : {},
6991
8039
  ...lastReadError instanceof OwneyError ? { lastObservationError: lastReadError.details } : {}
6992
8040
  }
6993
8041
  );
@@ -7048,6 +8096,7 @@ async function submitSmart(input) {
7048
8096
  { callsId: requestedId, phase: "send", failureCode: "WALLET_SEND_UNRESOLVED" }
7049
8097
  );
7050
8098
  savePending(input.onPending, { callsId });
8099
+ reportAccepted(input.onAccepted, { callsId });
7051
8100
  const deadline = Date.now() + OBSERVATION_WINDOW_MS;
7052
8101
  for (let poll = 0; poll < 40 && Date.now() < deadline; poll++) {
7053
8102
  let status;
@@ -7341,6 +8390,9 @@ var YieldseekerAgent = class {
7341
8390
  endpointReadFailures = /* @__PURE__ */ new Map();
7342
8391
  reconcileUntil = /* @__PURE__ */ new Map();
7343
8392
  allocationRefresh = /* @__PURE__ */ new Set();
8393
+ /** Contexts whose next permitted snapshot read is forced once: a funding
8394
+ * invalidation with a tx hash (receipt recovery, a re-armed refresh). */
8395
+ forceNextSnapshot = /* @__PURE__ */ new Set();
7344
8396
  activityRefreshUntil = /* @__PURE__ */ new Map();
7345
8397
  // null means a confirmed movement has no reliable baseline. Do not guess
7346
8398
  // settlement; keep slow reconciliation after the fast window expires.
@@ -7747,6 +8799,7 @@ var YieldseekerAgent = class {
7747
8799
  this.endpointReadFailures.clear();
7748
8800
  this.reconcileUntil.clear();
7749
8801
  this.allocationRefresh.clear();
8802
+ this.forceNextSnapshot.clear();
7750
8803
  this.activityRefreshUntil.clear();
7751
8804
  this.snapshotMovements.clear();
7752
8805
  this.movementEpochs.clear();
@@ -7860,7 +8913,7 @@ var YieldseekerAgent = class {
7860
8913
  notifyFunding(onProgress, evidence);
7861
8914
  return { txHash, smartWallet: context.wallet.walletAddress, amount, evidence };
7862
8915
  }
7863
- async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto", onBeforeSubmission, assertOperation) {
8916
+ async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto", onBeforeSubmission, assertOperation, submission) {
7864
8917
  let ownerTransferAttempted = false;
7865
8918
  let evidence = {
7866
8919
  agentId: this.id,
@@ -7873,12 +8926,23 @@ var YieldseekerAgent = class {
7873
8926
  delivery: "pending"
7874
8927
  };
7875
8928
  try {
7876
- return await this.withdrawInternal(state, chainId, asset, amount, (next) => {
7877
- evidence = { ...evidence, ...next };
7878
- notifyFunding(onProgress, evidence);
7879
- }, () => {
7880
- ownerTransferAttempted = true;
7881
- }, gasMode, onBeforeSubmission, assertOperation);
8929
+ return await this.withdrawInternal(
8930
+ state,
8931
+ chainId,
8932
+ asset,
8933
+ amount,
8934
+ (next) => {
8935
+ evidence = { ...evidence, ...next };
8936
+ notifyFunding(onProgress, evidence);
8937
+ },
8938
+ () => {
8939
+ ownerTransferAttempted = true;
8940
+ },
8941
+ gasMode,
8942
+ onBeforeSubmission,
8943
+ assertOperation,
8944
+ submission?.operationId
8945
+ );
7882
8946
  } catch (error) {
7883
8947
  if (error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED") {
7884
8948
  this.withdrawalVersionCache.clear();
@@ -7894,7 +8958,7 @@ var YieldseekerAgent = class {
7894
8958
  throw error;
7895
8959
  }
7896
8960
  }
7897
- async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto", onBeforeSubmission, assertOperation) {
8961
+ async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto", onBeforeSubmission, assertOperation, operationId) {
7898
8962
  this.assertChain(chainId);
7899
8963
  this.assertAsset(asset);
7900
8964
  if (amount !== void 0 && BigInt(amount) <= 0n) {
@@ -8026,13 +9090,13 @@ var YieldseekerAgent = class {
8026
9090
  const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
8027
9091
  const verifiedPositions = [];
8028
9092
  if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
8029
- let active2 = true;
9093
+ let active = true;
8030
9094
  const checks = this.readVaultShares(values.map((position2) => position2.vaultOption), agentWallet, blockNumber).catch(() => []);
8031
9095
  const reads = values.map(async (position2, index) => {
8032
9096
  const address = getAddress6(position2.vaultOption.address);
8033
9097
  try {
8034
9098
  const group = (await checks).slice(index * 2, index * 2 + 2);
8035
- if (!active2 || group.length !== 2 || group.some((value) => value.status !== "success")) return null;
9099
+ if (!active || group.length !== 2 || group.some((value) => value.status !== "success")) return null;
8036
9100
  this.assertSession(generation, chainId, asset);
8037
9101
  const [underlying, shares] = group.map((value) => value.result);
8038
9102
  if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n) return null;
@@ -8045,7 +9109,7 @@ var YieldseekerAgent = class {
8045
9109
  shares,
8046
9110
  blockNumber
8047
9111
  );
8048
- if (!active2 || value === void 0) return null;
9112
+ if (!active || value === void 0) return null;
8049
9113
  this.assertSession(generation, chainId, asset);
8050
9114
  verified.withdrawableAssetsRaw = value.toString();
8051
9115
  verifiedPositions.push(verified);
@@ -8062,7 +9126,7 @@ var YieldseekerAgent = class {
8062
9126
  args: [shares],
8063
9127
  blockNumber
8064
9128
  });
8065
- if (!active2 || typeof value !== "bigint" || value < 0n) return null;
9129
+ if (!active || typeof value !== "bigint" || value < 0n) return null;
8066
9130
  this.assertSession(generation, chainId, asset);
8067
9131
  if (value > 0n && !positiveValue) {
8068
9132
  positiveValue = true;
@@ -8090,19 +9154,20 @@ var YieldseekerAgent = class {
8090
9154
  }
8091
9155
  });
8092
9156
  const result = await boundedVerification(Promise.all(reads), timeoutMs);
8093
- active2 = false;
9157
+ active = false;
8094
9158
  this.assertSession(generation, chainId, asset);
8095
9159
  return {
8096
9160
  verified: result !== void 0 && result.every((position2) => position2 !== null),
8097
9161
  positions: [...verifiedPositions]
8098
9162
  };
8099
9163
  };
8100
- const refreshPositions = async () => {
9164
+ const refreshPositions = async (promptless = false) => {
8101
9165
  this.assertSession(generation, chainId, asset);
8102
9166
  const response = await this.walletRequest(
8103
9167
  state,
8104
9168
  chainId,
8105
- this.agentPath(context, "vault-positions")
9169
+ this.agentPath(context, "vault-positions"),
9170
+ { promptless }
8106
9171
  );
8107
9172
  this.assertSession(generation, chainId, asset);
8108
9173
  if (!Array.isArray(response?.vaultPositions)) throw this.invalidResponse("vault positions");
@@ -8333,6 +9398,8 @@ var YieldseekerAgent = class {
8333
9398
  agentWallet: getAddress6(context.wallet.walletAddress),
8334
9399
  asset: metadata.address,
8335
9400
  callData: data,
9401
+ // The EOA send opts in to the server-owned send with it.
9402
+ operationId,
8336
9403
  onPending: (pending) => {
8337
9404
  submissionEvidence = { ...submissionEvidence, ...pending };
8338
9405
  notifyFunding(onProgress, submissionEvidence);
@@ -8341,6 +9408,11 @@ var YieldseekerAgent = class {
8341
9408
  assertOperation?.();
8342
9409
  await onBeforeSubmission?.({ ...base7, ...pending });
8343
9410
  onOwnerTransferAttempted?.();
9411
+ },
9412
+ // The owner's signed operation was accepted: another agent may start.
9413
+ onAccepted: (accepted) => {
9414
+ submissionEvidence = { ...submissionEvidence, ...accepted, ownerAuthorization: "accepted" };
9415
+ notifyFunding(onProgress, submissionEvidence);
8344
9416
  }
8345
9417
  });
8346
9418
  txHash = submitted.txHash;
@@ -8357,7 +9429,10 @@ var YieldseekerAgent = class {
8357
9429
  value: "0",
8358
9430
  chainId
8359
9431
  },
8360
- (hash2) => notifyFunding(onProgress, { ...base7, txHash: hash2 }),
9432
+ (hash2) => {
9433
+ submissionEvidence = { ...submissionEvidence, txHash: hash2, ownerAuthorization: "accepted" };
9434
+ notifyFunding(onProgress, submissionEvidence);
9435
+ },
8361
9436
  (value) => {
8362
9437
  receipt = value;
8363
9438
  },
@@ -8388,7 +9463,9 @@ var YieldseekerAgent = class {
8388
9463
  const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
8389
9464
  const knownAtPayout = new Set(knownPositions.keys());
8390
9465
  const [catalogVerified, knownProof] = await Promise.all([
8391
- boundedVerification(refreshPositions().then((values) => {
9466
+ // Never a sign-in prompt here: a refused or missing cached sign-in
9467
+ // leaves the catalog unverified, the existing unverified-MAX outcome.
9468
+ boundedVerification(refreshPositions(true).then((values) => {
8392
9469
  rememberPositions(values);
8393
9470
  return true;
8394
9471
  }).catch(() => false)),
@@ -8496,14 +9573,14 @@ var YieldseekerAgent = class {
8496
9573
  }
8497
9574
  /** Recheck MAX without re-unwinding, paying out, or requesting a signature. */
8498
9575
  async verifyWithdrawalCompletion(state, evidence) {
8499
- const unknown = (reason2) => ({ status: "unavailable", reason: reason2 });
9576
+ const unknown2 = (reason2) => ({ status: "unavailable", reason: reason2 });
8500
9577
  const proof = evidence.maxCompletion;
8501
- if (evidence.agentId !== this.id || evidence.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || evidence.chainId !== 8453 || !proof || !evidence.agentWallet || !isAddress4(evidence.agentWallet) || !evidence.blockNumber || !/^\d+$/.test(evidence.blockNumber) || evidence.transfer !== "confirmed" || evidence.delivery !== "confirmed" || !this.isTransactionHash(evidence.txHash)) return unknown("PAYOUT_PROOF_UNAVAILABLE");
9578
+ if (evidence.agentId !== this.id || evidence.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || evidence.chainId !== 8453 || !proof || !evidence.agentWallet || !isAddress4(evidence.agentWallet) || !evidence.blockNumber || !/^\d+$/.test(evidence.blockNumber) || evidence.transfer !== "confirmed" || evidence.delivery !== "confirmed" || !this.isTransactionHash(evidence.txHash)) return unknown2("PAYOUT_PROOF_UNAVAILABLE");
8502
9579
  this.assertAsset(evidence.token);
8503
9580
  const asset = evidence.token;
8504
9581
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
8505
9582
  const signature = this.auth.peekToken(state, evidence.chainId);
8506
- if (!signature) return unknown("AUTHENTICATION_REQUIRED");
9583
+ if (!signature) return unknown2("AUTHENTICATION_REQUIRED");
8507
9584
  let reason = "CATALOG_UNAVAILABLE";
8508
9585
  try {
8509
9586
  return await bounded(async () => {
@@ -8511,7 +9588,7 @@ var YieldseekerAgent = class {
8511
9588
  `/users/${encodeURIComponent(proof.userId)}/agents/${encodeURIComponent(proof.agentId)}/vault-positions`,
8512
9589
  { signature }
8513
9590
  );
8514
- if (!Array.isArray(response?.vaultPositions)) return unknown("CATALOG_INVALID");
9591
+ if (!Array.isArray(response?.vaultPositions)) return unknown2("CATALOG_INVALID");
8515
9592
  const positions = this.validateWithdrawalPositions(response.vaultPositions, evidence.chainId, asset);
8516
9593
  const vaults = new Map(proof.vaults.map((address) => [address.toLowerCase(), { address }]));
8517
9594
  positions.forEach((p) => vaults.set(p.vaultOption.address.toLowerCase(), p.vaultOption));
@@ -8519,14 +9596,14 @@ var YieldseekerAgent = class {
8519
9596
  if (needsInventory || [...vaults.values()].some((option) => !option.provider)) {
8520
9597
  const catalog = await this.loadVaultOptions(asset).catch(() => []);
8521
9598
  const eligible = catalog.filter((o) => o.chainId === evidence.chainId && o.assetAddress?.toLowerCase() === metadata.address.toLowerCase() && isAddress4(o.address));
8522
- if (needsInventory && !eligible.length) return unknown("INVENTORY_INCOMPLETE");
9599
+ if (needsInventory && !eligible.length) return unknown2("INVENTORY_INCOMPLETE");
8523
9600
  for (const option of eligible) {
8524
9601
  const key2 = option.address.toLowerCase();
8525
9602
  if (needsInventory || vaults.has(key2)) vaults.set(key2, option);
8526
9603
  }
8527
9604
  }
8528
9605
  if (vaults.size > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT || [...vaults.keys()].some((a) => !isAddress4(a)))
8529
- return unknown("INVENTORY_INVALID");
9606
+ return unknown2("INVENTORY_INVALID");
8530
9607
  reason = "VAULT_PROOF_UNAVAILABLE";
8531
9608
  const blockNumber = BigInt(evidence.blockNumber);
8532
9609
  const options = [...vaults.values()];
@@ -8557,7 +9634,7 @@ var YieldseekerAgent = class {
8557
9634
  };
8558
9635
  }, 8e3, () => new Error("MAX proof read timed out"));
8559
9636
  } catch {
8560
- return unknown(reason);
9637
+ return unknown2(reason);
8561
9638
  }
8562
9639
  }
8563
9640
  async readVaultShares(options, wallet, blockNumber) {
@@ -8638,12 +9715,12 @@ var YieldseekerAgent = class {
8638
9715
  { asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
8639
9716
  this.id
8640
9717
  );
8641
- let active2 = true;
9718
+ let active = true;
8642
9719
  let knownProof;
8643
9720
  const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.resolvedVaults.includes(position2.vaultOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
8644
9721
  const assertReadCurrent = () => {
8645
9722
  this.assertSession(generation, chainId, context.asset);
8646
- if (!active2 || (this.liquidityVersions.get(contextKey) ?? 0) !== version) throw unavailable();
9723
+ if (!active || (this.liquidityVersions.get(contextKey) ?? 0) !== version) throw unavailable();
8647
9724
  };
8648
9725
  const read2 = async () => {
8649
9726
  const client = this.getReceiptClient();
@@ -8832,7 +9909,7 @@ var YieldseekerAgent = class {
8832
9909
  if (error instanceof OwneyError) throw error;
8833
9910
  throw unavailable();
8834
9911
  } finally {
8835
- active2 = false;
9912
+ active = false;
8836
9913
  if (timer) clearTimeout(timer);
8837
9914
  }
8838
9915
  }
@@ -8917,6 +9994,7 @@ var YieldseekerAgent = class {
8917
9994
  this.assertAsset(asset);
8918
9995
  const until = Math.min(refreshUntil, Date.now() + YIELDSEEKER_SETTLEMENT_SNAPSHOT_WINDOW_MS);
8919
9996
  const activityUntil = Math.min(refreshUntil, Date.now() + YIELDSEEKER_SETTLEMENT_WINDOW_MS);
9997
+ const hash2 = typeof txHash === "string" && /^0x[0-9a-fA-F]{64}$/.test(txHash);
8920
9998
  this.invalidateSlot(state, chainId, asset, (key2) => {
8921
9999
  const failure = this.snapshotFailures.get(key2);
8922
10000
  this.advancePortfolioVersion(key2);
@@ -8928,6 +10006,7 @@ var YieldseekerAgent = class {
8928
10006
  }
8929
10007
  this.reconcileUntil.set(key2, Math.max(this.reconcileUntil.get(key2) ?? 0, until));
8930
10008
  this.activityRefreshUntil.set(key2, Math.max(this.activityRefreshUntil.get(key2) ?? 0, activityUntil));
10009
+ if (hash2) this.forceNextSnapshot.add(key2);
8931
10010
  });
8932
10011
  }
8933
10012
  endFundingBalanceRefresh(state, chainId, asset, read2) {
@@ -9763,7 +10842,7 @@ var YieldseekerAgent = class {
9763
10842
  return this.walletRequest(
9764
10843
  state,
9765
10844
  chainId,
9766
- `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`
10845
+ this.snapshotPath(context, forceRefresh)
9767
10846
  ).then((response) => {
9768
10847
  if (!response?.agentSnapshot) {
9769
10848
  throw this.invalidResponse("agent snapshot");
@@ -9793,6 +10872,16 @@ var YieldseekerAgent = class {
9793
10872
  throw error;
9794
10873
  });
9795
10874
  }
10875
+ snapshotPath(context, forceRefresh) {
10876
+ return `${this.agentPath(context, "snapshot")}${query(forceRefresh ? { shouldForceRefresh: true } : { shouldOnlyUseRecentValue: true, shouldAllowStaleOnError: true })}`;
10877
+ }
10878
+ /** True while `walletRequest` would refuse a read of `path`: this user's
10879
+ * rate-limit cooldown or that endpoint's outage cooldown. */
10880
+ readCoolingDown(state, chainId, path) {
10881
+ const userKey = this.userKey(state, chainId);
10882
+ const now = Date.now();
10883
+ return (this.standardReadFailures.get(userKey)?.retryAt ?? 0) > now || (this.endpointReadFailures.get(`${userKey}:${path}`)?.retryAt ?? 0) > now;
10884
+ }
9796
10885
  portfolioSnapshot(state, chainId, context, contextKey) {
9797
10886
  const key2 = `snapshot:${contextKey}`;
9798
10887
  const cached = this.readCache.get(key2);
@@ -9811,6 +10900,7 @@ var YieldseekerAgent = class {
9811
10900
  const settling = inWindow && !this.deferredToLedger(contextKey);
9812
10901
  if (inWindow) this.settlementChecks.set(contextKey, (this.settlementChecks.get(contextKey) ?? 0) + 1);
9813
10902
  const ready = this.readyForces.delete(contextKey) && this.noteReadyForce(contextKey);
10903
+ const movementForce = this.forceNextSnapshot.has(contextKey) && !this.readCoolingDown(state, chainId, this.snapshotPath(context, true)) && this.forceNextSnapshot.delete(contextKey);
9814
10904
  return this.requestPortfolioSnapshot(
9815
10905
  state,
9816
10906
  chainId,
@@ -9818,7 +10908,7 @@ var YieldseekerAgent = class {
9818
10908
  contextKey,
9819
10909
  version,
9820
10910
  generation,
9821
- allocationChanged || settling || ready
10911
+ allocationChanged || settling || ready || movementForce
9822
10912
  );
9823
10913
  })
9824
10914
  };
@@ -10268,7 +11358,7 @@ var YieldseekerAgent = class {
10268
11358
  }
10269
11359
  const ttl = this.portfolioCacheMs(contextKey);
10270
11360
  this.settlementChecks.set(contextKey, 1);
10271
- const force = movement === "withdrawal" || !this.deferredToLedger(contextKey);
11361
+ const force = true;
10272
11362
  await this.cachedRead(
10273
11363
  `snapshot:${contextKey}`,
10274
11364
  ttl,
@@ -10323,13 +11413,22 @@ var YieldseekerAgent = class {
10323
11413
  options.assertCurrent?.();
10324
11414
  return this.api.request(path, { method: options.method, body: options.body, signature: signature2 });
10325
11415
  };
10326
- let signature = await this.auth.getToken(state, chainId);
11416
+ const cached = options.promptless ? this.auth.peekToken(state, chainId) : void 0;
11417
+ if (options.promptless && !cached)
11418
+ throw new OwneyError(
11419
+ "AGENT_AUTH_FAILED",
11420
+ "No Yieldseeker sign-in is available without a prompt.",
11421
+ { reason: "YIELDSEEKER_SIGN_IN_REQUIRED" },
11422
+ this.id
11423
+ );
11424
+ let signature = cached ?? await this.auth.getToken(state, chainId);
10327
11425
  try {
10328
11426
  return await request2(signature);
10329
11427
  } catch (error) {
10330
11428
  if (!(error instanceof YieldseekerApiError)) throw error;
10331
11429
  if (error.providerCode === "NO_USER") throw error;
10332
11430
  if (!error.isAuthenticationError) throw error;
11431
+ if (options.promptless) throw error;
10333
11432
  signature = await this.auth.refreshToken(state, chainId, signature);
10334
11433
  try {
10335
11434
  return await request2(signature);
@@ -10557,6 +11656,16 @@ var YieldseekerAgent = class {
10557
11656
  };
10558
11657
 
10559
11658
  // src/lib/swap/swap-api.ts
11659
+ var FUSION_SECRET_SERVER_PUBLISH = 1;
11660
+ var FUSION_SECRET_HANDOFF_TIMEOUT_MS = 5e3;
11661
+ function handoffFailure(error) {
11662
+ const statusCode2 = error instanceof OwneyError && typeof error.details?.statusCode === "number" ? error.details.statusCode : void 0;
11663
+ return new OwneyError(
11664
+ error instanceof OwneyError ? error.code : "SWAP_REQUEST_FAILED",
11665
+ "The swap secret handoff did not complete.",
11666
+ statusCode2 === void 0 ? {} : { statusCode: statusCode2 }
11667
+ );
11668
+ }
10560
11669
  async function request(baseUrl, apiKey, path, init) {
10561
11670
  const url = `${baseUrl}/api/v1/swap${path}`;
10562
11671
  const controller = new AbortController();
@@ -10569,7 +11678,8 @@ async function request(baseUrl, apiKey, path, init) {
10569
11678
  "Content-Type": "application/json",
10570
11679
  "x-owney-api-key": apiKey
10571
11680
  },
10572
- ...init ? { body: JSON.stringify(init.body) } : {}
11681
+ ...init ? { body: JSON.stringify(init.body) } : {},
11682
+ ...init?.redirect ? { redirect: init.redirect } : {}
10573
11683
  });
10574
11684
  if (!res.ok) {
10575
11685
  const text = await res.text().catch(() => "");
@@ -10602,7 +11712,7 @@ async function request(baseUrl, apiKey, path, init) {
10602
11712
  );
10603
11713
  }
10604
11714
  return json.data;
10605
- }, 2e4, () => new OwneyError(
11715
+ }, init?.timeoutMs ?? 2e4, () => new OwneyError(
10606
11716
  "SWAP_REQUEST_FAILED",
10607
11717
  "Swap request timed out. Its outcome may still need confirmation.",
10608
11718
  { requestTimedOut: true, phase: path === "/order" ? "submit" : "read" }
@@ -10686,8 +11796,9 @@ function createSwapApi(baseUrl, apiKey) {
10686
11796
  /**
10687
11797
  * Builds a Fusion+ order server-side and returns EIP-712 typed data.
10688
11798
  *
10689
- * Only HASHES go over the wire. The preimages never leave the browser —
10690
- * see swap.secrets.
11799
+ * Only HASHES go over the wire. The preimages leave the browser only
11800
+ * through `handoffSecrets`, and only with the caller's opt-in — see
11801
+ * swap.secrets.
10691
11802
  */
10692
11803
  buildOrder: (params) => request(baseUrl, apiKey, "/order/build", {
10693
11804
  method: "POST",
@@ -10714,6 +11825,23 @@ function createSwapApi(baseUrl, apiKey) {
10714
11825
  method: "POST",
10715
11826
  body: { orderHash, secret }
10716
11827
  }),
11828
+ /**
11829
+ * Hands a signed Fusion+ order's secrets to our routing API, which checks
11830
+ * them against the order and publishes each one only once 1inch reports
11831
+ * its destination escrow ready (part 5). The browser keeps publishing too;
11832
+ * a double publish is a no-op upstream.
11833
+ *
11834
+ * Only our own API: a redirect is refused, since the body carries the
11835
+ * preimages. Bounded to 5 s.
11836
+ */
11837
+ handoffSecrets: (body) => request(baseUrl, apiKey, "/order/secrets", {
11838
+ method: "POST",
11839
+ body,
11840
+ redirect: "error",
11841
+ timeoutMs: FUSION_SECRET_HANDOFF_TIMEOUT_MS
11842
+ }).catch((error) => {
11843
+ throw handoffFailure(error);
11844
+ }),
10717
11845
  /**
10718
11846
  * @param chainId present only for a SAME-CHAIN order. That product keys its
10719
11847
  * orders per chain and the Fusion+ endpoint does not know them, so asking
@@ -10897,14 +12025,14 @@ function swapChain(chainId) {
10897
12025
  }
10898
12026
 
10899
12027
  // src/lib/swap/swap.arrival.ts
10900
- var DEFAULT_TIMEOUT_MS = 18e4;
12028
+ var DEFAULT_TIMEOUT_MS2 = 18e4;
10901
12029
  var DEFAULT_POLL_MS = 4e3;
10902
12030
  var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
10903
12031
  async function awaitBalanceIncrease(options) {
10904
12032
  const {
10905
12033
  readBalance,
10906
12034
  baseline,
10907
- timeoutMs = DEFAULT_TIMEOUT_MS,
12035
+ timeoutMs = DEFAULT_TIMEOUT_MS2,
10908
12036
  pollMs = DEFAULT_POLL_MS
10909
12037
  } = options;
10910
12038
  const deadline = Date.now() + timeoutMs;
@@ -10998,14 +12126,14 @@ var isSwapTerminal = (status) => SWAP_TERMINAL_STATUSES.includes(status);
10998
12126
  var DEFAULT_POLL_MS2 = 5e3;
10999
12127
  var MAX_BACKOFF_MS = 3e4;
11000
12128
  var backoffFor = (failures, base7) => Math.min(base7 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS);
11001
- var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
12129
+ var DEFAULT_TIMEOUT_MS3 = 15 * 60 * 1e3;
11002
12130
  async function runFusionOrderInternal(deps, options) {
11003
12131
  const {
11004
12132
  orderHash,
11005
12133
  secrets,
11006
12134
  onStage,
11007
12135
  pollIntervalMs = DEFAULT_POLL_MS2,
11008
- timeoutMs = DEFAULT_TIMEOUT_MS2
12136
+ timeoutMs = DEFAULT_TIMEOUT_MS3
11009
12137
  } = options;
11010
12138
  const deadline = deps.now() + timeoutMs;
11011
12139
  let partial = options.hasPartialFill === true;
@@ -11086,14 +12214,14 @@ async function runFusionOrderInternal(deps, options) {
11086
12214
  }
11087
12215
  }
11088
12216
  function runFusionOrder(deps, options) {
11089
- return observeOrder(options.orderHash, () => runFusionOrderInternal(deps, options));
12217
+ return observeOrder(options.orderHash, () => runFusionOrderInternal(deps, options), { wait: options.waitForWatcher === true });
11090
12218
  }
11091
12219
 
11092
12220
  // src/lib/swap/swap.same-chain-runner.ts
11093
12221
  var DEFAULT_POLL_MS3 = 5e3;
11094
12222
  var MAX_BACKOFF_MS2 = 3e4;
11095
12223
  var backoffFor2 = (failures, base7) => Math.min(base7 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS2);
11096
- var DEFAULT_TIMEOUT_MS3 = 3 * 60 * 1e3;
12224
+ var DEFAULT_TIMEOUT_MS4 = 3 * 60 * 1e3;
11097
12225
  function explain(status) {
11098
12226
  switch (status) {
11099
12227
  case "expired":
@@ -11117,7 +12245,7 @@ async function runSameChainOrderInternal(deps, options) {
11117
12245
  orderHash,
11118
12246
  onStage,
11119
12247
  pollIntervalMs = DEFAULT_POLL_MS3,
11120
- timeoutMs = DEFAULT_TIMEOUT_MS3
12248
+ timeoutMs = DEFAULT_TIMEOUT_MS4
11121
12249
  } = options;
11122
12250
  const deadline = deps.now() + timeoutMs;
11123
12251
  let partial = options.hasPartialFill === true;
@@ -11170,7 +12298,7 @@ async function runSameChainOrderInternal(deps, options) {
11170
12298
  }
11171
12299
  }
11172
12300
  function runSameChainOrder(deps, options) {
11173
- return observeOrder(options.orderHash, () => runSameChainOrderInternal(deps, options));
12301
+ return observeOrder(options.orderHash, () => runSameChainOrderInternal(deps, options), { wait: options.waitForWatcher === true });
11174
12302
  }
11175
12303
 
11176
12304
  // src/lib/swap/swap.secret-store.ts
@@ -11195,6 +12323,21 @@ function saveOrder(order) {
11195
12323
  return false;
11196
12324
  }
11197
12325
  }
12326
+ function loadOrder(orderHash) {
12327
+ const store = storage5();
12328
+ if (!store) return null;
12329
+ try {
12330
+ const raw2 = store.getItem(keyFor2(orderHash));
12331
+ if (!raw2) return null;
12332
+ const parsed = JSON.parse(raw2);
12333
+ if (!Array.isArray(parsed.secrets) || parsed.secrets.length === 0) {
12334
+ return null;
12335
+ }
12336
+ return parsed;
12337
+ } catch {
12338
+ return null;
12339
+ }
12340
+ }
11198
12341
  function clearOrder(orderHash) {
11199
12342
  const store = storage5();
11200
12343
  if (!store) return;
@@ -11273,10 +12416,10 @@ async function affordableAmount(deps, quoted, reserve = 0n) {
11273
12416
  }
11274
12417
  async function executeSwap(deps, options) {
11275
12418
  const { quote, walletAddress, onStage } = options;
11276
- let active2 = true;
12419
+ let active = true;
11277
12420
  const original = deps;
11278
12421
  const assertActive = () => {
11279
- if (!active2) throw new OwneyError("SWAP_REQUEST_FAILED", "This swap attempt is no longer active.");
12422
+ if (!active) throw new OwneyError("SWAP_REQUEST_FAILED", "This swap attempt is no longer active.");
11280
12423
  };
11281
12424
  const call = async (work, write = false) => {
11282
12425
  assertActive();
@@ -11337,7 +12480,7 @@ async function executeSwap(deps, options) {
11337
12480
  }
11338
12481
  return { received: arrived.toString(), ...result };
11339
12482
  } finally {
11340
- active2 = false;
12483
+ active = false;
11341
12484
  }
11342
12485
  }
11343
12486
  async function runClassic(deps, options) {
@@ -11456,6 +12599,7 @@ async function runSameChainFusion(deps, options, walletAddress) {
11456
12599
  }
11457
12600
  await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
11458
12601
  orderHash: built.orderHash,
12602
+ waitForWatcher: true,
11459
12603
  ...onStage ? { onStage } : {}
11460
12604
  });
11461
12605
  return { orderHash: built.orderHash, spent: amount.toString() };
@@ -11524,67 +12668,118 @@ async function runFusion(deps, options, walletAddress) {
11524
12668
  ...direction ? { direction } : {},
11525
12669
  ...permit ? { permit } : {}
11526
12670
  });
11527
- const durable = saveOrder({
11528
- orderHash: built.orderHash,
11529
- walletAddress,
11530
- secrets,
11531
- srcChainId: quote.src.chainId,
11532
- srcSymbol: quote.src.symbol,
11533
- dstChainId: quote.dst.chainId,
11534
- dstSymbol: quote.dst.symbol,
11535
- amount: amount.toString(),
11536
- createdAt: Date.now()
11537
- });
11538
- debugLog("owney-sdk", "swap: order built", { orderHash: built.orderHash });
11539
- let signature;
12671
+ const release = claimOrder(built.orderHash);
11540
12672
  try {
11541
- onStage?.("signing");
11542
- await deps.ensureChain(quote.src.chainId);
11543
- signature = await awaitWalletPrompt(() => deps.signTypedData(built.typedData), "sign the swap");
11544
- options.assertCurrent?.();
11545
- options.onOrderPrepared?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount });
11546
- } catch (error) {
12673
+ const stored = {
12674
+ orderHash: built.orderHash,
12675
+ walletAddress,
12676
+ secrets,
12677
+ srcChainId: quote.src.chainId,
12678
+ srcSymbol: quote.src.symbol,
12679
+ dstChainId: quote.dst.chainId,
12680
+ dstSymbol: quote.dst.symbol,
12681
+ amount: amount.toString(),
12682
+ createdAt: Date.now(),
12683
+ ...options.pendingDeposit ? { pendingDeposit: withinOrderMinimum(options.pendingDeposit, built.dstAmountMin) } : {}
12684
+ };
12685
+ const durable = saveOrder(stored);
12686
+ debugLog("owney-sdk", "swap: order built", { orderHash: built.orderHash });
12687
+ let signature;
12688
+ try {
12689
+ onStage?.("signing");
12690
+ await deps.ensureChain(quote.src.chainId);
12691
+ signature = await awaitWalletPrompt(() => deps.signTypedData(built.typedData), "sign the swap");
12692
+ options.assertCurrent?.();
12693
+ options.onOrderPrepared?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount });
12694
+ } catch (error) {
12695
+ clearOrder(built.orderHash);
12696
+ throw error;
12697
+ }
12698
+ if (options.secretHandoff && deps.api.handoffSecrets) {
12699
+ if (await tryHandoff(deps, options.secretHandoff, built, quote, secretHashes, secrets))
12700
+ saveOrder({ ...stored, serverSecrets: true });
12701
+ }
12702
+ debugLog("owney-sdk", "swap: signed, submitting to relayer");
12703
+ try {
12704
+ await deps.api.submitOrder({
12705
+ srcChainId: quote.src.chainId,
12706
+ // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
12707
+ // the envelope here gets a bare 500 from the relayer.
12708
+ order: built.order,
12709
+ signature,
12710
+ quoteId: built.quoteId,
12711
+ // Single-fill orders must NOT carry secretHashes — the relayer rejects
12712
+ // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
12713
+ // order's hashlock, so repeating it here is redundant, and only a
12714
+ // multi-fill order (a Merkle tree of hashes) needs them listed.
12715
+ ...secretHashes.length > 1 ? { secretHashes } : {},
12716
+ ...built.extension ? { extension: built.extension } : {}
12717
+ });
12718
+ } catch (error) {
12719
+ throw submissionUnknown(error, built.orderHash);
12720
+ }
12721
+ debugLog("owney-sdk", "swap: order submitted, polling escrows");
12722
+ try {
12723
+ options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable });
12724
+ } catch {
12725
+ }
12726
+ try {
12727
+ await runFusionOrder(deps.runner, {
12728
+ orderHash: built.orderHash,
12729
+ // This run owns the order: wait out a watcher another tab started.
12730
+ waitForWatcher: true,
12731
+ secrets,
12732
+ ...onStage ? { onStage } : {}
12733
+ });
12734
+ } catch (error) {
12735
+ if (error instanceof OwneyError && error.code === "SWAP_ORDER_REFUNDED" && error.details?.outcome === "refunded") {
12736
+ clearOrder(built.orderHash);
12737
+ }
12738
+ throw error;
12739
+ }
11547
12740
  clearOrder(built.orderHash);
11548
- throw error;
11549
- }
11550
- debugLog("owney-sdk", "swap: signed, submitting to relayer");
12741
+ return { orderHash: built.orderHash, spent: amount.toString() };
12742
+ } finally {
12743
+ release();
12744
+ }
12745
+ }
12746
+ var HANDOFF_DECLINE_CODES = /* @__PURE__ */ new Set([
12747
+ "FUNDING_TASK_KIND_DISABLED",
12748
+ "SECRETS_INVALID",
12749
+ "SECRET_HASH_MISMATCH",
12750
+ "ORDER_UNREADABLE",
12751
+ "ORDER_HASH_MISMATCH",
12752
+ "HASHLOCK_MISMATCH",
12753
+ "MAKER_MISMATCH",
12754
+ "CHAIN_MISMATCH",
12755
+ "OPERATION_INVALID",
12756
+ "ORDER_CONFLICT"
12757
+ ]);
12758
+ async function tryHandoff(deps, handoff, built, quote, secretHashes, secrets) {
11551
12759
  try {
11552
- await deps.api.submitOrder({
12760
+ const answer = await bounded(() => deps.api.handoffSecrets({
12761
+ operation: handoff.operation,
12762
+ orderHash: built.orderHash,
11553
12763
  srcChainId: quote.src.chainId,
11554
- // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
11555
- // the envelope here gets a bare 500 from the relayer.
12764
+ dstChainId: quote.dst.chainId,
11556
12765
  order: built.order,
11557
- signature,
11558
- quoteId: built.quoteId,
11559
- // Single-fill orders must NOT carry secretHashes — the relayer rejects
11560
- // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
11561
- // order's hashlock, so repeating it here is redundant, and only a
11562
- // multi-fill order (a Merkle tree of hashes) needs them listed.
11563
- ...secretHashes.length > 1 ? { secretHashes } : {},
11564
- ...built.extension ? { extension: built.extension } : {}
11565
- });
11566
- } catch (error) {
11567
- throw submissionUnknown(error, built.orderHash);
11568
- }
11569
- debugLog("owney-sdk", "swap: order submitted, polling escrows");
11570
- try {
11571
- options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable });
11572
- } catch {
11573
- }
11574
- try {
11575
- await runFusionOrder(deps.runner, {
11576
- orderHash: built.orderHash,
11577
- secrets,
11578
- ...onStage ? { onStage } : {}
11579
- });
11580
- } catch (error) {
11581
- if (error instanceof OwneyError && error.code === "SWAP_ORDER_REFUNDED" && error.details?.outcome === "refunded") {
11582
- clearOrder(built.orderHash);
12766
+ extension: built.extension,
12767
+ secretHashes,
12768
+ fusionSecrets: secrets
12769
+ }), FUSION_SECRET_HANDOFF_TIMEOUT_MS, () => new Error("secret handoff timed out"));
12770
+ if (answer?.accepted === true) {
12771
+ debugLog("owney-sdk", "swap: secrets handed to the server", { orderHash: built.orderHash });
12772
+ return true;
11583
12773
  }
11584
- throw error;
12774
+ const code = typeof answer?.code === "string" && HANDOFF_DECLINE_CODES.has(answer.code) ? answer.code : "OTHER";
12775
+ debugLog("owney-sdk", "swap: secret handoff declined, the browser publishes", { code });
12776
+ } catch {
12777
+ debugLog("owney-sdk", "swap: secret handoff unavailable, the browser publishes");
11585
12778
  }
11586
- clearOrder(built.orderHash);
11587
- return { orderHash: built.orderHash, spent: amount.toString() };
12779
+ return false;
12780
+ }
12781
+ function withinOrderMinimum(deposit, orderMinimum) {
12782
+ return typeof orderMinimum === "string" && /^\d+$/.test(orderMinimum) && /^\d+$/.test(deposit.received) && BigInt(orderMinimum) < BigInt(deposit.received) ? { ...deposit, received: orderMinimum } : deposit;
11588
12783
  }
11589
12784
  function submissionUnknown(error, orderHash) {
11590
12785
  return new OwneyError(
@@ -11607,7 +12802,7 @@ async function awaitWithdrawalPayouts(options) {
11607
12802
  const payouts = new Map(Object.entries(options.results).map(([id, result]) => [id, { result, evidence: result.evidence }]));
11608
12803
  const timeoutMs = Math.max(0, options.timeoutMs ?? 18e4);
11609
12804
  const deadline = options.deadline ?? Date.now() + timeoutMs;
11610
- let active2 = true;
12805
+ let active = true;
11611
12806
  let timer;
11612
12807
  const incomplete = (reason, agentId) => new OwneyError(
11613
12808
  "WITHDRAW_SWAP_INCOMPLETE",
@@ -11625,7 +12820,7 @@ async function awaitWithdrawalPayouts(options) {
11625
12820
  );
11626
12821
  const assertActive = () => {
11627
12822
  options.assertCurrent();
11628
- if (!active2 || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
12823
+ if (!active || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
11629
12824
  };
11630
12825
  const pause = async () => {
11631
12826
  assertActive();
@@ -11802,7 +12997,7 @@ async function awaitWithdrawalPayouts(options) {
11802
12997
  timer = setTimeout(() => reject(incomplete("PAYOUT_CONFIRMATION_TIMEOUT")), Math.max(0, deadline - Date.now()));
11803
12998
  })]);
11804
12999
  } finally {
11805
- active2 = false;
13000
+ active = false;
11806
13001
  if (timer) clearTimeout(timer);
11807
13002
  }
11808
13003
  }
@@ -12370,7 +13565,8 @@ import {
12370
13565
  createPublicClient as createPublicClient4,
12371
13566
  createWalletClient as createWalletClient5,
12372
13567
  custom as custom5,
12373
- erc20Abi as erc20Abi7
13568
+ erc20Abi as erc20Abi7,
13569
+ isAddress as isAddress5
12374
13570
  } from "viem";
12375
13571
  import { base as base6, arbitrum as arbitrum3, mainnet as mainnet3 } from "viem/chains";
12376
13572
 
@@ -12379,21 +13575,42 @@ import {
12379
13575
  keccak256 as keccak2562,
12380
13576
  parseTransaction
12381
13577
  } from "viem";
13578
+ var SPONSORED_DEPOSIT_SERVER_BROADCAST = 1;
12382
13579
  async function executeTokenBatch(input, initial) {
12383
13580
  let body = initial, family, ended = false;
12384
13581
  const deadlineAt = Date.now() + 9e4, controller = new AbortController();
12385
13582
  let timer;
12386
- const pending = () => new OwneyError(
12387
- "DEPOSIT_IN_PROGRESS",
12388
- "Deposit is awaiting confirmation. Its transaction is still being checked.",
12389
- {
12390
- sponsorPending: true,
12391
- phase: body.serializedTransaction ? "deposit" : "approval",
12392
- txHash: body.serializedTransaction ? keccak2562(body.serializedTransaction) : body.serializedPermitTransaction ? keccak2562(body.serializedPermitTransaction) : void 0,
12393
- notSubmitted: false,
12394
- safeToFallback: false
12395
- }
12396
- );
13583
+ const serverSent = /* @__PURE__ */ new Set();
13584
+ let accepted;
13585
+ let maybeAccepted = !!input.resumeSaved, alreadySending = false;
13586
+ const verifiedFailures = /* @__PURE__ */ new WeakSet();
13587
+ const pending = () => {
13588
+ const deposit = body.serializedTransaction ? keccak2562(body.serializedTransaction) : accepted;
13589
+ const unknown2 = !deposit && maybeAccepted;
13590
+ const deadline = /^\d{1,15}$/.test(body.deadline) ? Number(body.deadline) : void 0;
13591
+ return new OwneyError(
13592
+ "DEPOSIT_IN_PROGRESS",
13593
+ "Deposit is awaiting confirmation. Its transaction is still being checked.",
13594
+ {
13595
+ sponsorPending: true,
13596
+ phase: deposit || unknown2 ? "deposit" : "approval",
13597
+ txHash: deposit ?? (body.serializedPermitTransaction && !unknown2 ? keccak2562(body.serializedPermitTransaction) : void 0),
13598
+ notSubmitted: false,
13599
+ safeToFallback: false,
13600
+ // Only while the current deposit is the one the server will send.
13601
+ ...deposit && serverSent.has(deposit) ? { serverBroadcast: true } : unknown2 ? { serverBroadcast: "unknown" } : {},
13602
+ ...unknown2 && alreadySending ? { code: "DEPOSIT_ALREADY_SENDING" } : {},
13603
+ // The Permit2 deadline (seconds): after it no deposit of this
13604
+ // authorization can execute, which bounds an unknown outcome. Never
13605
+ // after a 409 DEPOSIT_ALREADY_SENDING: this refused authorization is
13606
+ // not the deposit being sent.
13607
+ ...unknown2 && !alreadySending && deadline !== void 0 ? { authorizationDeadline: deadline } : {},
13608
+ // Its Permit2 nonce: with the deadline, the chain shows whether any
13609
+ // deposit of this authorization executed.
13610
+ ...unknown2 && !alreadySending || deposit && serverSent.has(deposit) ? { permit2Nonce: body.nonce } : {}
13611
+ }
13612
+ );
13613
+ };
12397
13614
  const assertCurrent = () => {
12398
13615
  if (ended || Date.now() >= deadlineAt) throw pending();
12399
13616
  input.assertCurrent?.();
@@ -12418,20 +13635,52 @@ async function executeTokenBatch(input, initial) {
12418
13635
  }
12419
13636
  return operationId ? { ...value, operationId } : value;
12420
13637
  };
12421
- const post2 = () => bounded2(
12422
- postSponsorBatchTransfer({
12423
- apiKey: input.apiKey,
12424
- baseUrl: input.baseUrl,
12425
- body: withOperationId(body),
12426
- signal: controller.signal
12427
- })
12428
- );
13638
+ const withServerBroadcast = (value) => {
13639
+ const request2 = withOperationId(value);
13640
+ return request2.operationId ? { ...request2, serverBroadcast: true } : request2;
13641
+ };
13642
+ const post2 = async () => {
13643
+ const request2 = withServerBroadcast(body);
13644
+ const flaggedPrepare = request2.serverBroadcast === true && !request2.serializedTransaction && !(request2.erc2612Permit && !request2.serializedPermitTransaction);
13645
+ let result;
13646
+ try {
13647
+ result = await Promise.race([
13648
+ postSponsorBatchTransfer({
13649
+ apiKey: input.apiKey,
13650
+ baseUrl: input.baseUrl,
13651
+ body: request2,
13652
+ signal: controller.signal
13653
+ }),
13654
+ timeout
13655
+ ]);
13656
+ } catch (error) {
13657
+ const details = error instanceof OwneyError ? error.details : void 0;
13658
+ const sending = details?.code === "DEPOSIT_ALREADY_SENDING";
13659
+ if (flaggedPrepare && (ended || !(error instanceof OwneyError) || details?.outcomeUnknown === true || sending)) {
13660
+ maybeAccepted = true;
13661
+ alreadySending = sending;
13662
+ }
13663
+ throw error;
13664
+ }
13665
+ maybeAccepted = false;
13666
+ alreadySending = false;
13667
+ if (result.serverBroadcast === true && result.serializedTransaction && !body.serializedTransaction) {
13668
+ serverSent.add(result.txHash);
13669
+ accepted = result.txHash;
13670
+ try {
13671
+ input.onSubmitted?.(result.txHash, reference(result.txHash), true);
13672
+ } catch {
13673
+ }
13674
+ }
13675
+ assertCurrent();
13676
+ return result;
13677
+ };
12429
13678
  const replacements = { approval: 0, deposit: 0 };
12430
13679
  const unavailable = /* @__PURE__ */ new Set();
12431
13680
  const observedApprovals = /* @__PURE__ */ new Set();
12432
13681
  const checkpointed = /* @__PURE__ */ new Set();
12433
13682
  const accounted = /* @__PURE__ */ new Set();
12434
- const remember = async (phase, bytes, hash2, predecessorHash) => {
13683
+ const remember = async (phase, bytes, hash2, predecessorHash, submissionAttempted = false) => {
12435
13684
  assertCurrent();
12436
13685
  if (keccak2562(bytes) !== hash2) throw pending();
12437
13686
  const tx = parseTransaction(bytes);
@@ -12450,7 +13699,7 @@ async function executeTokenBatch(input, initial) {
12450
13699
  nonce: String(tx.nonce),
12451
13700
  predecessorHash,
12452
13701
  preparedAt: Date.now(),
12453
- submissionAttempted: false
13702
+ submissionAttempted
12454
13703
  },
12455
13704
  assertCurrent
12456
13705
  )
@@ -12464,13 +13713,18 @@ async function executeTokenBatch(input, initial) {
12464
13713
  const markSubmission = async (phase) => {
12465
13714
  const hash2 = phase === "deposit" ? family?.current.deposit : family?.current.approval;
12466
13715
  if (!family || !hash2) throw pending();
13716
+ const flag = serverSent.has(hash2) ? [true] : [];
12467
13717
  if (phase === "deposit" && !checkpointed.has(hash2)) {
12468
13718
  try {
12469
13719
  await bounded2(
12470
- Promise.resolve(input.onBeforeSubmission?.(hash2, reference(hash2)))
13720
+ Promise.resolve(
13721
+ input.onBeforeSubmission?.(hash2, reference(hash2), ...flag)
13722
+ )
12471
13723
  );
13724
+ checkpointed.add(hash2);
12472
13725
  } catch (error) {
12473
- if (error instanceof OwneyError && error.code === "DEPOSIT_CHECKPOINT_FAILED")
13726
+ if (serverSent.has(hash2)) {
13727
+ } else if (error instanceof OwneyError && error.code === "DEPOSIT_CHECKPOINT_FAILED")
12474
13728
  throw new OwneyError(error.code, error.message, {
12475
13729
  ...error.details,
12476
13730
  notSubmitted: !family.attempts.some(
@@ -12478,9 +13732,8 @@ async function executeTokenBatch(input, initial) {
12478
13732
  ),
12479
13733
  safeToFallback: false
12480
13734
  });
12481
- throw error;
13735
+ else throw error;
12482
13736
  }
12483
- checkpointed.add(hash2);
12484
13737
  }
12485
13738
  assertCurrent();
12486
13739
  const attempt = family.attempts.find((a) => a.hash === hash2);
@@ -12495,7 +13748,7 @@ async function executeTokenBatch(input, initial) {
12495
13748
  assertCurrent();
12496
13749
  if (phase === "deposit")
12497
13750
  try {
12498
- input.onSubmitted?.(hash2, reference(hash2));
13751
+ input.onSubmitted?.(hash2, reference(hash2), ...flag);
12499
13752
  } catch {
12500
13753
  }
12501
13754
  assertCurrent();
@@ -12531,12 +13784,15 @@ async function executeTokenBatch(input, initial) {
12531
13784
  }
12532
13785
  return observation.txHash;
12533
13786
  }
12534
- if (observation.status === "failed" || observation.status === "expired")
12535
- throw new OwneyError(
13787
+ if (observation.status === "failed" || observation.status === "expired") {
13788
+ const failure = new OwneyError(
12536
13789
  "SPONSOR_REQUEST_FAILED",
12537
13790
  observation.status === "expired" ? "The deposit authorization expired without transferring funds. Start a new deposit." : "Sponsored deposit transactions reverted. Please retry.",
12538
13791
  { notSubmitted: true, safeToFallback: false }
12539
13792
  );
13793
+ verifiedFailures.add(failure);
13794
+ throw failure;
13795
+ }
12540
13796
  };
12541
13797
  try {
12542
13798
  assertCurrent();
@@ -12552,6 +13808,14 @@ async function executeTokenBatch(input, initial) {
12552
13808
  tokenBatchKey(input),
12553
13809
  JSON.stringify(tokenBatchStoredBody(body))
12554
13810
  );
13811
+ if (/^\d{1,78}$/.test(body.nonce) && /^\d{1,15}$/.test(body.deadline))
13812
+ try {
13813
+ input.onAuthorization?.({
13814
+ permit2Nonce: body.nonce,
13815
+ authorizationDeadline: Number(body.deadline)
13816
+ });
13817
+ } catch {
13818
+ }
12555
13819
  if (family) {
12556
13820
  const winner = await observe();
12557
13821
  if (winner) return winner;
@@ -12575,6 +13839,20 @@ async function executeTokenBatch(input, initial) {
12575
13839
  result = await post2();
12576
13840
  } catch (error) {
12577
13841
  assertCurrent();
13842
+ if (maybeAccepted && alreadySending) {
13843
+ if (family)
13844
+ finishTokenBatchFamily(input, family, {
13845
+ kind: "failed",
13846
+ blockNumber: "0"
13847
+ });
13848
+ else
13849
+ tokenBatchClear(
13850
+ tokenBatchKey(input),
13851
+ JSON.stringify(tokenBatchStoredBody(body))
13852
+ );
13853
+ throw pending();
13854
+ }
13855
+ if (maybeAccepted && !family) throw pending();
12578
13856
  if (!family) {
12579
13857
  tokenBatchClear(
12580
13858
  tokenBatchKey(input),
@@ -12597,7 +13875,9 @@ async function executeTokenBatch(input, initial) {
12597
13875
  await remember(
12598
13876
  "deposit",
12599
13877
  result.serializedTransaction,
12600
- result.txHash
13878
+ result.txHash,
13879
+ void 0,
13880
+ serverSent.has(result.txHash)
12601
13881
  );
12602
13882
  needsPost = true;
12603
13883
  continue;
@@ -12673,6 +13953,10 @@ async function executeTokenBatch(input, initial) {
12673
13953
  )
12674
13954
  );
12675
13955
  }
13956
+ } catch (error) {
13957
+ if ((serverSent.size || maybeAccepted) && !(error && typeof error === "object" && verifiedFailures.has(error)))
13958
+ throw pending();
13959
+ throw error;
12676
13960
  } finally {
12677
13961
  ended = true;
12678
13962
  if (timer) clearTimeout(timer);
@@ -12702,14 +13986,14 @@ async function releaseExpiredTokenBatch(i) {
12702
13986
  let timer;
12703
13987
  let expired = false;
12704
13988
  const check = async () => {
12705
- let active2 = read(key2);
12706
- if (active2 && i.expectedTxHash) {
12707
- const current = JSON.parse(active2);
13989
+ let active = read(key2);
13990
+ if (active && i.expectedTxHash) {
13991
+ const current = JSON.parse(active);
12708
13992
  if (!current.serializedTransaction || keccak2563(current.serializedTransaction).toLowerCase() !== i.expectedTxHash.toLowerCase())
12709
- active2 = void 0;
13993
+ active = void 0;
12710
13994
  }
12711
13995
  const archiveKey = i.expectedTxHash ? `${key2}:expired:${i.expectedTxHash.toLowerCase()}` : void 0;
12712
- const saved = active2 ?? (archiveKey && (expiredRecords.get(archiveKey) ?? (typeof window === "undefined" ? void 0 : window.localStorage.getItem(archiveKey))));
13996
+ const saved = active ?? (archiveKey && (expiredRecords.get(archiveKey) ?? (typeof window === "undefined" ? void 0 : window.localStorage.getItem(archiveKey))));
12713
13997
  if (!saved) return;
12714
13998
  const body = JSON.parse(saved);
12715
13999
  if (!body.serializedTransaction || body.chainId !== i.chainId || !isAddressEqual2(body.from, i.owner) || !isAddressEqual2(body.token, i.token) || !body.transfers?.length) return;
@@ -12733,8 +14017,8 @@ async function releaseExpiredTokenBatch(i) {
12733
14017
  args: [i.owner, nonce >> 8n],
12734
14018
  blockNumber: block.number
12735
14019
  });
12736
- if (expired || typeof bitmap !== "bigint" || bitmap < 0n || bitmap > MAX_UINT256 || (bitmap & 1n << (nonce & 255n)) !== 0n || active2 && read(key2) !== active2) return;
12737
- if (active2) {
14020
+ if (expired || typeof bitmap !== "bigint" || bitmap < 0n || bitmap > MAX_UINT256 || (bitmap & 1n << (nonce & 255n)) !== 0n || active && read(key2) !== active) return;
14021
+ if (active) {
12738
14022
  const terminalKey = `${key2}:expired:${txHash.toLowerCase()}`;
12739
14023
  expiredRecords.set(terminalKey, saved);
12740
14024
  let persisted = typeof window === "undefined";
@@ -12771,15 +14055,15 @@ function clear(key2, body) {
12771
14055
  function sponsorTokenBatch(i) {
12772
14056
  const key2 = pendingKey(i);
12773
14057
  const plan = planOf(i.transfers);
12774
- const active2 = inflight.get(key2);
12775
- if (active2) {
12776
- if (active2.plan !== plan)
14058
+ const active = inflight.get(key2);
14059
+ if (active) {
14060
+ if (active.plan !== plan)
12777
14061
  return Promise.reject(
12778
14062
  new Error(
12779
14063
  "A token deposit is already in progress. Wait for its result before depositing again."
12780
14064
  )
12781
14065
  );
12782
- return active2.promise;
14066
+ return active.promise;
12783
14067
  }
12784
14068
  const promise = execute(i, key2, plan).finally(() => inflight.delete(key2));
12785
14069
  inflight.set(key2, { plan, promise });
@@ -12791,8 +14075,15 @@ async function execute(i, key2, plan) {
12791
14075
  ) || new Set(i.transfers.map((t) => t.to.toLowerCase())).size !== i.transfers.length)
12792
14076
  throw new Error("Invalid token deposit shares.");
12793
14077
  const send = (body) => executeTokenBatch(i, body);
14078
+ if (!i.resumeTxHash) await dropExpiredHashlessTokenBatch(i, i.pub);
12794
14079
  const family = await loadTokenBatchFamily(i, i.resumeTxHash);
12795
14080
  const saved = family ? JSON.stringify(family.body) : read(key2);
14081
+ if (!saved && i.resumeSaved)
14082
+ throw new OwneyError(
14083
+ "DEPOSIT_IN_PROGRESS",
14084
+ "The saved deposit is no longer available. Check its status before depositing again.",
14085
+ { notSubmitted: false, safeToFallback: false }
14086
+ );
12796
14087
  if (saved) {
12797
14088
  const previous = JSON.parse(saved);
12798
14089
  if (previous.chainId !== i.chainId || !isAddressEqual2(previous.from, i.owner) || !isAddressEqual2(previous.token, i.token) || planOf(previous.transfers) !== plan)
@@ -12902,10 +14193,13 @@ function makeSponsoredTokenCallback(deps) {
12902
14193
  pub,
12903
14194
  wallet,
12904
14195
  onApproved: deps.onApproved,
12905
- onSubmitted: (hash2, reference) => deps.onSubmitted?.(hash2, transfers, reference),
12906
- onBeforeSubmission: (hash2, reference) => deps.onBeforeSubmission?.(hash2, transfers, reference),
14196
+ // Rest arguments keep the callbacks' arity: the flag appears only when set.
14197
+ onSubmitted: (hash2, ...rest) => deps.onSubmitted?.(hash2, transfers, ...rest),
14198
+ onBeforeSubmission: (hash2, ...rest) => deps.onBeforeSubmission?.(hash2, transfers, ...rest),
12907
14199
  assertCurrent: deps.assertCurrent,
12908
14200
  resumeTxHash: deps.resumeTxHash,
14201
+ ...deps.resumeSaved ? { resumeSaved: true } : {},
14202
+ ...deps.onAuthorization ? { onAuthorization: deps.onAuthorization } : {},
12909
14203
  operationId: deps.operationId
12910
14204
  });
12911
14205
  };
@@ -13212,6 +14506,33 @@ function sponsoredTokensFor(asset) {
13212
14506
  function shouldFallbackToUserPaid(error, asset, appCallback) {
13213
14507
  return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
13214
14508
  }
14509
+ function serverBroadcastOf(error) {
14510
+ const flag = (value) => {
14511
+ const details = value?.details;
14512
+ return details?.serverBroadcast === true || details?.serverBroadcast === "unknown" ? details.serverBroadcast : void 0;
14513
+ };
14514
+ return flag(error) ?? flag(error?.details?.cause);
14515
+ }
14516
+ function authorizationDeadlineOf(error) {
14517
+ const read2 = (value) => {
14518
+ const deadline = value?.details?.authorizationDeadline;
14519
+ return typeof deadline === "number" && Number.isSafeInteger(deadline) && deadline > 0 ? deadline : void 0;
14520
+ };
14521
+ return read2(error) ?? read2(error?.details?.cause);
14522
+ }
14523
+ function alreadySendingOf(error) {
14524
+ const code = (value) => value?.details?.code;
14525
+ return code(error) === "DEPOSIT_ALREADY_SENDING" || code(error?.details?.cause) === "DEPOSIT_ALREADY_SENDING";
14526
+ }
14527
+ function permit2NonceOf(error) {
14528
+ const read2 = (value) => {
14529
+ const nonce = value?.details?.permit2Nonce;
14530
+ return typeof nonce === "string" && /^\d{1,78}$/.test(nonce) ? nonce : void 0;
14531
+ };
14532
+ return read2(error) ?? read2(error?.details?.cause);
14533
+ }
14534
+ var serverMayOwn = (leg) => leg !== void 0 && leg.settled !== null;
14535
+ var SERVER_HANDOFF_WAIT_MS = 1e4;
13215
14536
  var OwneySDK = class {
13216
14537
  agents = /* @__PURE__ */ new Map();
13217
14538
  activeAgents = /* @__PURE__ */ new Set();
@@ -13240,13 +14561,21 @@ var OwneySDK = class {
13240
14561
  ledger;
13241
14562
  reservationMode;
13242
14563
  reservationTransport;
14564
+ /** The caller's Zyfai server tasks opt-in: `true`, a function read each
14565
+ * time a handoff would start (F-I2(iii)), or undefined (off). */
14566
+ zyfaiServerTasks;
14567
+ /** The caller opted in to the Fusion+ secret handoff (`fusionSecretHandoff: true`). */
14568
+ fusionSecretHandoff;
13243
14569
  cachedSponsoredCallback = null;
13244
14570
  cachedWethSponsoredCallback = null;
13245
14571
  paymasterServiceUrl;
13246
14572
  cachedSponsoredCallsCallbacks = /* @__PURE__ */ new Map();
13247
14573
  agentReads = new AgentReads();
13248
14574
  depositFamilyReads = /* @__PURE__ */ new Map();
13249
- recoveredFundingReads = /* @__PURE__ */ new Set();
14575
+ /** Movements (owner, chain, agent, token, tx hash) whose balance refresh this
14576
+ * session started: true when it armed the refresh, false when its deadline
14577
+ * had passed (it only dropped the cache and forced one snapshot read). */
14578
+ recoveredFundingReads = /* @__PURE__ */ new Map();
13250
14579
  fundingBalanceChecks = /* @__PURE__ */ new WeakMap();
13251
14580
  activationChecks = /* @__PURE__ */ new Map();
13252
14581
  // Protect the SDK entry point as well as the mini-app hook. This owns only
@@ -13284,6 +14613,8 @@ var OwneySDK = class {
13284
14613
  });
13285
14614
  this.reservationMode = config.withdrawalReservation === "fail-closed" || config.withdrawalReservation === "off" ? config.withdrawalReservation : "fail-open";
13286
14615
  this.reservationTransport = { apiKey: config.apiKey, baseUrl: config.routingApiBaseUrl, appName: config.appName };
14616
+ this.zyfaiServerTasks = config.zyfaiServerTasks === true || typeof config.zyfaiServerTasks === "function" ? config.zyfaiServerTasks : void 0;
14617
+ this.fusionSecretHandoff = config.fusionSecretHandoff === true;
13287
14618
  }
13288
14619
  /** Finishes a funding-ledger operation on success and adds its reference to
13289
14620
  * the result. The result object is copied, never mutated or replaced.
@@ -13349,16 +14680,34 @@ var OwneySDK = class {
13349
14680
  return void 0;
13350
14681
  }
13351
14682
  }
14683
+ /**
14684
+ * Part 5: the swap's ledger identity, for the executor's Fusion+ secret
14685
+ * handoff. Only with the `fusionSecretHandoff` opt-in and an open operation;
14686
+ * otherwise undefined, and the executor sends nothing. Owner and token are
14687
+ * lowercased, as the ledger reports them.
14688
+ */
14689
+ secretHandoffFor(op, input) {
14690
+ if (!this.fusionSecretHandoff || !op || !input || input.kind !== "swap_deposit" && input.kind !== "withdraw_swap")
14691
+ return void 0;
14692
+ return { operation: {
14693
+ clientOperationId: op.operationId,
14694
+ kind: input.kind,
14695
+ owner: input.owner.toLowerCase(),
14696
+ chainId: input.chainId,
14697
+ token: input.token.toLowerCase()
14698
+ } };
14699
+ }
13352
14700
  closeSwapLedger(op) {
13353
14701
  if (op && this.openSwapLedgers.get(op.operationId) === op) this.openSwapLedgers.delete(op.operationId);
13354
14702
  }
13355
- /** Reports one agent's withdrawal evidence as its leg. Never throws. */
13356
- ledgerWithdrawalLeg(op, evidence) {
14703
+ /** Reports one agent's withdrawal evidence as its leg (with `errorCode`, when
14704
+ * given). Never throws. */
14705
+ ledgerWithdrawalLeg(op, evidence, errorCode) {
13357
14706
  if (!op) return;
13358
14707
  try {
13359
14708
  const leg = ledgerLegFromEvidence(evidence, "withdraw");
13360
14709
  const rail = withdrawalLedgerRail(evidence);
13361
- if (leg) op.leg(rail ? { ...leg, rail } : leg);
14710
+ if (leg) op.leg({ ...rail ? { ...leg, rail } : leg, ...errorCode ? { errorCode } : {} });
13362
14711
  if (leg?.status === "confirmed") this.ledgerProviderSamples(op, evidence, "withdraw");
13363
14712
  } catch {
13364
14713
  }
@@ -13593,8 +14942,8 @@ var OwneySDK = class {
13593
14942
  return createPaidRpcClient(VIEM_CHAIN2[chainId], this.rpcUrls);
13594
14943
  }
13595
14944
  /** Builds the default USDC batch callback for the connected wallet. */
13596
- getDefaultSponsoredCallback(onApproved, onSubmitted, resumeTxHash, onBeforeSubmission, assertSubmissionCurrent, operationId) {
13597
- if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && this.cachedSponsoredCallback)
14945
+ getDefaultSponsoredCallback(onApproved, onSubmitted, resumeTxHash, onBeforeSubmission, assertSubmissionCurrent, operationId, resumeSaved, onAuthorization) {
14946
+ if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && !resumeSaved && !onAuthorization && this.cachedSponsoredCallback)
13598
14947
  return this.cachedSponsoredCallback;
13599
14948
  const provider = this.requireConnectedProvider();
13600
14949
  const connection = this.requireState();
@@ -13608,6 +14957,8 @@ var OwneySDK = class {
13608
14957
  onSubmitted,
13609
14958
  onBeforeSubmission,
13610
14959
  resumeTxHash,
14960
+ ...resumeSaved ? { resumeSaved: true } : {},
14961
+ ...onAuthorization ? { onAuthorization } : {},
13611
14962
  operationId,
13612
14963
  assertCurrent: () => {
13613
14964
  assertSubmissionCurrent?.();
@@ -13628,7 +14979,7 @@ var OwneySDK = class {
13628
14979
  transport: custom5(provider)
13629
14980
  })
13630
14981
  });
13631
- if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId) this.cachedSponsoredCallback = callback;
14982
+ if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && !resumeSaved && !onAuthorization) this.cachedSponsoredCallback = callback;
13632
14983
  return callback;
13633
14984
  }
13634
14985
  /** Builds the wallet-native sponsored calls callback for compatible paymasters. */
@@ -13657,8 +15008,8 @@ var OwneySDK = class {
13657
15008
  * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
13658
15009
  * single-use batch authorization instead of an EIP-3009 authorization.
13659
15010
  */
13660
- getDefaultWethSponsoredCallback(onApproved, onSubmitted, resumeTxHash, onBeforeSubmission, assertSubmissionCurrent, operationId) {
13661
- if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && this.cachedWethSponsoredCallback)
15011
+ getDefaultWethSponsoredCallback(onApproved, onSubmitted, resumeTxHash, onBeforeSubmission, assertSubmissionCurrent, operationId, resumeSaved, onAuthorization) {
15012
+ if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && !resumeSaved && !onAuthorization && this.cachedWethSponsoredCallback)
13662
15013
  return this.cachedWethSponsoredCallback;
13663
15014
  const provider = this.requireConnectedProvider();
13664
15015
  const connection = this.requireState();
@@ -13672,6 +15023,8 @@ var OwneySDK = class {
13672
15023
  onSubmitted,
13673
15024
  onBeforeSubmission,
13674
15025
  resumeTxHash,
15026
+ ...resumeSaved ? { resumeSaved: true } : {},
15027
+ ...onAuthorization ? { onAuthorization } : {},
13675
15028
  operationId,
13676
15029
  assertCurrent: () => {
13677
15030
  assertSubmissionCurrent?.();
@@ -13692,7 +15045,7 @@ var OwneySDK = class {
13692
15045
  transport: custom5(provider)
13693
15046
  })
13694
15047
  });
13695
- if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId) this.cachedWethSponsoredCallback = callback;
15048
+ if (!onApproved && !onSubmitted && !resumeTxHash && !onBeforeSubmission && !assertSubmissionCurrent && !operationId && !resumeSaved && !onAuthorization) this.cachedWethSponsoredCallback = callback;
13696
15049
  return callback;
13697
15050
  }
13698
15051
  getAgent(agentId) {
@@ -13706,12 +15059,12 @@ var OwneySDK = class {
13706
15059
  return [...this.getActiveAgents().keys()].sort();
13707
15060
  }
13708
15061
  getActiveAgents() {
13709
- const active2 = /* @__PURE__ */ new Map();
15062
+ const active = /* @__PURE__ */ new Map();
13710
15063
  for (const id of this.activeAgents) {
13711
15064
  const agent = this.agents.get(id);
13712
- if (agent) active2.set(id, agent);
15065
+ if (agent) active.set(id, agent);
13713
15066
  }
13714
- return active2;
15067
+ return active;
13715
15068
  }
13716
15069
  /**
13717
15070
  * Read the org's agent policy once and reuse it. fetchOrgAgentConfig never
@@ -13810,7 +15163,13 @@ var OwneySDK = class {
13810
15163
  this.routingApiBaseUrl
13811
15164
  ) : this.rpcUrls,
13812
15165
  this.referralSource,
13813
- { owneyApiKey: this.apiKey, routingApiBaseUrl: this.routingApiBaseUrl }
15166
+ {
15167
+ owneyApiKey: this.apiKey,
15168
+ routingApiBaseUrl: this.routingApiBaseUrl,
15169
+ // Only an explicit opt-in lets the agent hand the Zyfai session to
15170
+ // Owney; a function is passed as it is and read per handoff.
15171
+ ...this.zyfaiServerTasks !== void 0 ? { zyfaiServerTasks: this.zyfaiServerTasks } : {}
15172
+ }
13814
15173
  );
13815
15174
  }
13816
15175
  if (agentId === "yieldseeker") {
@@ -14032,10 +15391,21 @@ var OwneySDK = class {
14032
15391
  } catch {
14033
15392
  }
14034
15393
  };
14035
- const onSubmitted = (hash2, destinations, depositAuthorization) => {
15394
+ let permit;
15395
+ const onAuthorization = (value) => {
15396
+ permit = value;
15397
+ };
15398
+ const permitFields = () => permit ? { permit2Nonce: permit.permit2Nonce, authorizationDeadline: permit.authorizationDeadline } : {};
15399
+ const onSubmitted = (hash2, destinations, depositAuthorization, serverBroadcast) => {
14036
15400
  for (const destination of destinations) {
14037
15401
  const evidence = prepared.get(destination.to.toLowerCase());
14038
- if (evidence) notifyFunding(onProgress, { ...evidence, txHash: hash2, ...depositAuthorization ? { depositAuthorization } : {}, transfer: "pending" });
15402
+ if (evidence) notifyFunding(onProgress, {
15403
+ ...evidence,
15404
+ txHash: hash2,
15405
+ ...depositAuthorization ? { depositAuthorization } : {},
15406
+ ...serverBroadcast ? { serverBroadcast, ...permitFields() } : {},
15407
+ transfer: "pending"
15408
+ });
14039
15409
  }
14040
15410
  };
14041
15411
  const onCallsProgress = (callsId, destinations, transfer) => {
@@ -14044,6 +15414,7 @@ var OwneySDK = class {
14044
15414
  if (evidence) notifyFunding(onProgress, { ...evidence, callsId, transfer });
14045
15415
  }
14046
15416
  };
15417
+ const serverTaskContext = (agent) => this.zyfaiServerTasks !== void 0 && agent.supportsServerTasks === true && ledgerOp ? { operationId: ledgerOp.operationId } : void 0;
14047
15418
  const ledgerDone = (result) => this.ledgerSucceeded(ledgerOp, result);
14048
15419
  const ledgerFailed = (error) => this.ledgerFailed(ledgerOp, error);
14049
15420
  if (BigInt(amount) === 0n) {
@@ -14079,7 +15450,7 @@ var OwneySDK = class {
14079
15450
  });
14080
15451
  await checkpoint2(evidence);
14081
15452
  } : void 0;
14082
- const checkpointHash = checkpointDestinations ? (hash2, destinations, depositAuthorization) => checkpointDestinations(destinations, { txHash: hash2, depositAuthorization }) : void 0;
15453
+ const checkpointHash = checkpointDestinations ? (hash2, destinations, depositAuthorization, serverBroadcast) => checkpointDestinations(destinations, { txHash: hash2, depositAuthorization, ...serverBroadcast ? { serverBroadcast, ...permitFields() } : {} }) : void 0;
14083
15454
  const checkpointCalls = checkpointDestinations ? (id, destinations) => checkpointDestinations(destinations, { callsId: id }) : void 0;
14084
15455
  const checkpointPaid = checkpoint2 ? (evidence) => checkpoint2([evidence]) : void 0;
14085
15456
  const customCallback = depositCallback && checkpointDestinations ? makeVerificationAwareDepositCallback(async (to, cid, value, verification) => {
@@ -14087,9 +15458,10 @@ var OwneySDK = class {
14087
15458
  await checkpointDestinations([{ to, amount: value }], {});
14088
15459
  return depositCallback(to, cid, value);
14089
15460
  }) : depositCallback;
14090
- if (options.resumeTxHash && (depositCallback || this.paymasterServiceUrl || !/^0x[\da-f]{64}$/i.test(options.resumeTxHash)))
15461
+ if (options.resumeTxHash && (depositCallback || this.paymasterServiceUrl || !/^0x[\da-f]{64}$/i.test(options.resumeTxHash)) || options.resumeSavedBatch && (options.resumeTxHash || depositCallback || this.paymasterServiceUrl))
14091
15462
  throw new OwneyError("DEPOSIT_IN_PROGRESS", "This deposit cannot resume the saved relayer transaction.");
14092
- const effectiveCallback = customCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted, onCallsProgress, checkpointCalls) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash, checkpointHash, options.assertSubmissionCurrent, ledgerOperationId) : this.getDefaultSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash, checkpointHash, options.assertSubmissionCurrent, ledgerOperationId));
15463
+ const effectiveCallback = customCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved, onSubmitted, onCallsProgress, checkpointCalls) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash, checkpointHash, options.assertSubmissionCurrent, ledgerOperationId, options.resumeSavedBatch, onAuthorization) : this.getDefaultSponsoredCallback(onApproved, onSubmitted, options.resumeTxHash, checkpointHash, options.assertSubmissionCurrent, ledgerOperationId, options.resumeSavedBatch, onAuthorization));
15464
+ let resumedDeadline;
14093
15465
  try {
14094
15466
  const ledgerToken = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === asset);
14095
15467
  ledgerOp = this.startLedger({
@@ -14104,16 +15476,19 @@ var OwneySDK = class {
14104
15476
  } catch {
14105
15477
  }
14106
15478
  try {
14107
- if (options.resumeTxHash) {
15479
+ if (options.resumeTxHash || options.resumeSavedBatch) {
14108
15480
  const token = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === asset);
14109
- const family = token && await loadTokenBatchFamily({
15481
+ const scope = token && {
14110
15482
  apiKey: this.apiKey,
14111
15483
  baseUrl: this.routingApiBaseUrl,
14112
15484
  owner: state.walletAddress,
14113
15485
  chainId,
14114
15486
  token: token.address
14115
- }, options.resumeTxHash);
14116
- const pending = family && { transfers: family.body.transfers };
15487
+ };
15488
+ const family = scope && (options.resumeTxHash ? await loadTokenBatchFamily(scope, options.resumeTxHash) : { body: await savedTokenBatchBody(scope, this.getPaidRpcClient(chainId)) });
15489
+ if (options.resumeSavedBatch && family?.body && /^\d{1,15}$/.test(family.body.deadline))
15490
+ resumedDeadline = Number(family.body.deadline);
15491
+ const pending = family?.body && { transfers: family.body.transfers };
14117
15492
  const invalid2 = () => new OwneyError("DEPOSIT_IN_PROGRESS", "The saved deposit does not match this request. Check its status before depositing again.");
14118
15493
  if (!pending || pending.transfers.some((transfer) => !/^\d+$/.test(transfer.amount) || BigInt(transfer.amount) <= 0n) || pending.transfers.reduce((sum, transfer) => sum + BigInt(transfer.amount), 0n) !== BigInt(amount)) throw invalid2();
14119
15494
  const candidates = agentId ? [this.getAgent(agentId)] : this.getEligibleAgents(chainId, asset);
@@ -14133,12 +15508,10 @@ var OwneySDK = class {
14133
15508
  return {
14134
15509
  id: agent.id,
14135
15510
  amount: transfer.amount,
14136
- run: (callback) => withFailureReporting(
14137
- this.apiKey,
14138
- agent.id,
14139
- () => agent.deposit(state, chainId, transfer.amount, asset, callback, onProgress),
14140
- this.routingApiBaseUrl
14141
- )
15511
+ run: (callback) => withFailureReporting(this.apiKey, agent.id, () => {
15512
+ const context = serverTaskContext(agent);
15513
+ return context ? agent.deposit(state, chainId, transfer.amount, asset, callback, onProgress, void 0, context) : agent.deposit(state, chainId, transfer.amount, asset, callback, onProgress);
15514
+ }, this.routingApiBaseUrl)
14142
15515
  };
14143
15516
  });
14144
15517
  const batchTransfer2 = getDepositBatchTransfer(effectiveCallback);
@@ -14177,7 +15550,8 @@ var OwneySDK = class {
14177
15550
  onProgress,
14178
15551
  () => paidFallbackAgents.add(agent.id),
14179
15552
  !!options.resumeTxHash,
14180
- checkpointPaid
15553
+ checkpointPaid,
15554
+ serverTaskContext(agent)
14181
15555
  ));
14182
15556
  }
14183
15557
  const totalAmount = BigInt(amount);
@@ -14239,7 +15613,10 @@ var OwneySDK = class {
14239
15613
  run: (callback) => withFailureReporting(
14240
15614
  this.apiKey,
14241
15615
  agent.id,
14242
- () => agent.deposit(state, chainId, amount2, asset, callback, onProgress),
15616
+ () => {
15617
+ const context = serverTaskContext(agent);
15618
+ return context ? agent.deposit(state, chainId, amount2, asset, callback, onProgress, void 0, context) : agent.deposit(state, chainId, amount2, asset, callback, onProgress);
15619
+ },
14243
15620
  this.routingApiBaseUrl
14244
15621
  )
14245
15622
  })),
@@ -14280,7 +15657,8 @@ var OwneySDK = class {
14280
15657
  onProgress,
14281
15658
  () => paidFallbackAgents.add(agent.id),
14282
15659
  !!options.resumeTxHash,
14283
- checkpointPaid
15660
+ checkpointPaid,
15661
+ serverTaskContext(agent)
14284
15662
  );
14285
15663
  } catch (error) {
14286
15664
  if (Object.keys(agentResults).length === 0) throw error;
@@ -14300,7 +15678,22 @@ var OwneySDK = class {
14300
15678
  }
14301
15679
  return ledgerDone({ agentResults });
14302
15680
  } catch (error) {
14303
- if (checkpointFailure && !(error instanceof OwneyError && error.details?.notSubmitted === false)) {
15681
+ if (options.resumeSavedBatch && !serverBroadcastOf(error))
15682
+ error = new OwneyError(
15683
+ "DEPOSIT_IN_PROGRESS",
15684
+ "Deposit is awaiting confirmation. Its transaction is still being checked.",
15685
+ {
15686
+ sponsorPending: true,
15687
+ phase: "deposit",
15688
+ notSubmitted: false,
15689
+ safeToFallback: false,
15690
+ serverBroadcast: "unknown",
15691
+ ...resumedDeadline !== void 0 ? { authorizationDeadline: resumedDeadline } : {},
15692
+ cause: error
15693
+ }
15694
+ );
15695
+ const serverMaySend = serverBroadcastOf(error);
15696
+ if (checkpointFailure && !serverMaySend && !(error instanceof OwneyError && error.details?.notSubmitted === false)) {
14304
15697
  error = error instanceof OwneyError && error.code === "DEPOSIT_PARTIAL_FAILURE" ? new OwneyError(
14305
15698
  error.code,
14306
15699
  "Some deposits completed before another agent could be submitted.",
@@ -14318,6 +15711,19 @@ var OwneySDK = class {
14318
15711
  const rejected = error instanceof OwneyError && error.details?.notSubmitted === true;
14319
15712
  if (!rejected && paidFallbackAgents.has(id) || evidence?.transfer === "confirmed" || evidence?.transfer === "failed") continue;
14320
15713
  if (!rejected && (evidence?.txHash || evidence?.userOpHash || evidence?.callsId)) continue;
15714
+ if (serverMaySend) {
15715
+ const refused = alreadySendingOf(error);
15716
+ const deadline = refused ? void 0 : authorizationDeadlineOf(error) ?? permit?.authorizationDeadline;
15717
+ const nonce = refused ? void 0 : permit2NonceOf(error) ?? permit?.permit2Nonce;
15718
+ if (evidence) notifyFunding(onProgress, {
15719
+ ...evidence,
15720
+ serverBroadcast: evidence.serverBroadcast ?? "unknown",
15721
+ ...deadline !== void 0 && !evidence.authorizationDeadline ? { authorizationDeadline: deadline } : {},
15722
+ ...nonce !== void 0 && !evidence.permit2Nonce ? { permit2Nonce: nonce } : {},
15723
+ transfer: "pending"
15724
+ });
15725
+ continue;
15726
+ }
14321
15727
  notifyFunding(onProgress, { ...evidence ?? {
14322
15728
  agentId: id,
14323
15729
  chainId,
@@ -14364,11 +15770,11 @@ var OwneySDK = class {
14364
15770
  * propagates unchanged. Scoped per agent invocation so one agent's failure
14365
15771
  * doesn't force a retry of sibling agents in a multi-agent split.
14366
15772
  */
14367
- async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback, onProgress, onPaidFallback, resumeOnly = false, onBeforeSubmission) {
15773
+ async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback, onProgress, onPaidFallback, resumeOnly = false, onBeforeSubmission, serverTaskContext) {
14368
15774
  const attempt = (callback) => withFailureReporting(
14369
15775
  this.apiKey,
14370
15776
  agent.id,
14371
- () => onBeforeSubmission ? agent.deposit(state, chainId, amount, asset, callback, onProgress, onBeforeSubmission) : onProgress ? agent.deposit(state, chainId, amount, asset, callback, onProgress) : agent.deposit(state, chainId, amount, asset, callback),
15777
+ () => serverTaskContext ? agent.deposit(state, chainId, amount, asset, callback, onProgress, onBeforeSubmission, serverTaskContext) : onBeforeSubmission ? agent.deposit(state, chainId, amount, asset, callback, onProgress, onBeforeSubmission) : onProgress ? agent.deposit(state, chainId, amount, asset, callback, onProgress) : agent.deposit(state, chainId, amount, asset, callback),
14372
15778
  this.routingApiBaseUrl
14373
15779
  );
14374
15780
  let approvalAttempted = false;
@@ -14665,11 +16071,36 @@ var OwneySDK = class {
14665
16071
  const key2 = `${state.walletAddress.toLowerCase()}:${chainId}:${evidence.agentId}:${evidence.token}:${evidence.txHash}`;
14666
16072
  if (options.refreshBalance !== false && (verified.transfer === "confirmed" || verified.transfer === "failed" && verified.confirmedAmount === "0") && !this.recoveredFundingReads.has(key2)) {
14667
16073
  agent.invalidateFundingRead?.(state, chainId, evidence.token, options.balanceRefreshUntil, evidence.txHash);
14668
- this.recoveredFundingReads.add(key2);
14669
- if (this.recoveredFundingReads.size > 256) this.recoveredFundingReads.delete(this.recoveredFundingReads.values().next().value);
16074
+ const until = options.balanceRefreshUntil;
16075
+ this.noteFundingRefresh(key2, until === void 0 || until > Date.now());
14670
16076
  }
14671
16077
  return verified;
14672
16078
  }
16079
+ noteFundingRefresh(key2, armed) {
16080
+ this.recoveredFundingReads.delete(key2);
16081
+ this.recoveredFundingReads.set(key2, armed);
16082
+ if (this.recoveredFundingReads.size > 256) this.recoveredFundingReads.delete(this.recoveredFundingReads.keys().next().value);
16083
+ }
16084
+ /**
16085
+ * Re-arms an agent's balance refresh after a confirmed deposit or withdrawal
16086
+ * the caller already verified (e.g. after a page reload, which loses the
16087
+ * refresh the SDK started in memory). Same refresh as receipt recovery
16088
+ * (`inspectFunding`), without a receipt read, and shared with it: once per
16089
+ * movement (`txHash`) in this SDK session, unless that recovery came after
16090
+ * its own deadline and so armed nothing. `until` bounds it (the agent caps
16091
+ * it further). Returns false when the agent has no such refresh or `until`
16092
+ * has passed; throws when not connected or on another chain. Never prompts.
16093
+ */
16094
+ refreshAgentBalanceAfterMovement(input) {
16095
+ const { state, chainId } = this.captureReadScope(input.chainId);
16096
+ const agent = this.getAgent(input.agentId);
16097
+ if (typeof agent.invalidateFundingRead !== "function" || !Number.isFinite(input.until) || input.until <= Date.now()) return false;
16098
+ const key2 = input.txHash ? `${state.walletAddress.toLowerCase()}:${chainId}:${input.agentId}:${input.token}:${input.txHash}` : void 0;
16099
+ if (key2 && this.recoveredFundingReads.get(key2) === true) return true;
16100
+ agent.invalidateFundingRead(state, chainId, input.token, input.until, input.txHash);
16101
+ if (key2) this.noteFundingRefresh(key2, true);
16102
+ return true;
16103
+ }
14673
16104
  /**
14674
16105
  * Lists on-chain payout candidates for a withdrawal the provider accepted
14675
16106
  * without a hash: ERC-20 `Transfer` logs on the evidence's token that reach
@@ -14791,22 +16222,25 @@ var OwneySDK = class {
14791
16222
  try {
14792
16223
  if (this.paymasterServiceUrl || !evidences.length || !/^\d+$/.test(amount)) return false;
14793
16224
  const first = evidences[0];
14794
- if (!first.txHash) return false;
16225
+ const saved = !first.txHash && evidences.every((e) => !e.txHash && (e.serverBroadcast === "unknown" || e.serverBroadcast === true) && typeof e.authorizationDeadline === "number");
16226
+ if (!first.txHash && !saved) return false;
14795
16227
  const { state, chainId, assertCurrent } = this.captureReadScope(first.chainId);
14796
16228
  const token = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === first.token);
14797
16229
  if (!token) return false;
14798
- const family = await loadTokenBatchFamily({
16230
+ const scope = {
14799
16231
  apiKey: this.apiKey,
14800
16232
  baseUrl: this.routingApiBaseUrl,
14801
16233
  owner: state.walletAddress,
14802
16234
  chainId,
14803
16235
  token: token.address
14804
- }, first.txHash);
14805
- const transfers = family?.body.transfers;
14806
- if (!family || !transfers || transfers.length !== evidences.length || transfers.reduce((n, t) => n + BigInt(t.amount), 0n).toString() !== amount) return false;
16236
+ };
16237
+ const family = saved ? void 0 : await loadTokenBatchFamily(scope, first.txHash);
16238
+ const savedBody = saved ? await savedTokenBatchBody(scope, this.getPaidRpcClient(chainId)) : void 0;
16239
+ const transfers = saved ? savedBody?.transfers : family?.body.transfers;
16240
+ if (!family && !savedBody || !transfers || transfers.length !== evidences.length || transfers.reduce((n, t) => n + BigInt(t.amount), 0n).toString() !== amount) return false;
14807
16241
  const destinations = /* @__PURE__ */ new Set();
14808
16242
  for (const e of evidences) {
14809
- if (e.chainId !== chainId || e.token !== first.token || e.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || e.callsId || e.userOpHash || !family.attempts.some((a) => a.phase === "deposit" && a.hash.toLowerCase() === e.txHash?.toLowerCase()) || e.depositAuthorization && e.depositAuthorization.authorizationId !== family.authorizationId) return false;
16243
+ if (e.chainId !== chainId || e.token !== first.token || e.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || e.callsId || e.userOpHash || (family ? !family.attempts.some((a) => a.phase === "deposit" && a.hash.toLowerCase() === e.txHash?.toLowerCase()) || e.depositAuthorization && e.depositAuthorization.authorizationId !== family.authorizationId : String(e.authorizationDeadline) !== savedBody.deadline)) return false;
14810
16244
  const agent = this.getAgent(e.agentId);
14811
16245
  const wallet = agent.getFundingWallet ? await agent.getFundingWallet(state, chainId, e.token) : (await agent.getUserProfile(state, chainId)).smartWallet;
14812
16246
  assertCurrent();
@@ -14819,6 +16253,62 @@ var OwneySDK = class {
14819
16253
  return false;
14820
16254
  }
14821
16255
  }
16256
+ /** Whether a Permit2 deposit authorization can still execute and whether it
16257
+ * did: `expired` when the finalized block is more than 300 s past
16258
+ * `deadline` (chain time), `used` when the owner's Permit2 nonce bit is set
16259
+ * at that block. Null when anything cannot be read. Never throws. */
16260
+ async checkDepositAuthorization(input) {
16261
+ try {
16262
+ const { chainId, owner, permit2Nonce, deadline } = input;
16263
+ if (!SupportedAssets.some((asset) => asset.chainId === chainId) || !isAddress5(owner) || typeof permit2Nonce !== "string" || !/^\d{1,78}$/.test(permit2Nonce) || !Number.isSafeInteger(deadline) || deadline <= 0) return null;
16264
+ const nonce = BigInt(permit2Nonce);
16265
+ if (nonce >= 1n << 256n) return null;
16266
+ const pub = this.getPaidRpcClient(chainId);
16267
+ const block = await pub.getBlock({ blockTag: "finalized" });
16268
+ if (typeof block?.timestamp !== "bigint" || typeof block.number !== "bigint") return null;
16269
+ const bitmap = await pub.readContract({
16270
+ address: BATCH_PERMIT2_ADDRESS,
16271
+ abi: PERMIT2_BATCH_ABI,
16272
+ functionName: "nonceBitmap",
16273
+ args: [owner, nonce >> 8n],
16274
+ blockNumber: block.number
16275
+ });
16276
+ if (typeof bitmap !== "bigint" || bitmap < 0n) return null;
16277
+ return { expired: block.timestamp > BigInt(deadline) + 300n, used: (bitmap & 1n << (nonce & 255n)) !== 0n };
16278
+ } catch {
16279
+ return null;
16280
+ }
16281
+ }
16282
+ /** Drops the saved signed body of a hashless deposit the server may have
16283
+ * been sending (`serverBroadcast` 'unknown'/true, no hash) once the app has
16284
+ * settled it another way: by the ledger's hash or its expired deadline.
16285
+ * Only the body with this Permit2 deadline and these exact shares is
16286
+ * dropped, so it never resumes or blocks a different deposit. Never throws. */
16287
+ async discardSavedDepositBatch(evidences) {
16288
+ try {
16289
+ if (this.paymasterServiceUrl || !evidences.length) return false;
16290
+ const first = evidences[0];
16291
+ const deadline = first.authorizationDeadline;
16292
+ if (typeof deadline !== "number" || !Number.isSafeInteger(deadline) || deadline <= 0) return false;
16293
+ const { state, chainId } = this.captureReadScope(first.chainId);
16294
+ const token = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === first.token);
16295
+ if (!token) return false;
16296
+ if (evidences.some((e) => e.chainId !== chainId || e.token !== first.token || e.authorizationDeadline !== deadline || e.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || !e.agentWallet || e.serverBroadcast !== "unknown" && e.serverBroadcast !== true)) return false;
16297
+ return await discardHashlessTokenBatch(
16298
+ {
16299
+ apiKey: this.apiKey,
16300
+ baseUrl: this.routingApiBaseUrl,
16301
+ owner: state.walletAddress,
16302
+ chainId,
16303
+ token: token.address
16304
+ },
16305
+ String(deadline),
16306
+ (body) => evidences.every((e) => body.transfers.some((t) => t.to.toLowerCase() === e.agentWallet.toLowerCase() && t.amount === e.intendedAmount))
16307
+ );
16308
+ } catch {
16309
+ return false;
16310
+ }
16311
+ }
14822
16312
  /** Validate signed journal membership, never just a caller-supplied family ID. */
14823
16313
  async validateDepositReplacement(previous, next) {
14824
16314
  try {
@@ -14919,6 +16409,14 @@ var OwneySDK = class {
14919
16409
  if (evidence.agentId !== "zyfai" || !id || !state || state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase()) return null;
14920
16410
  return this.getAgent("zyfai").getWithdrawalLifecycle(state, id);
14921
16411
  }
16412
+ /** A Zyfai deposit's placement status by the id recorded on its evidence. Never signs,
16413
+ * prompts or authenticates; `null` when unavailable or not this wallet's Zyfai deposit. */
16414
+ async getZyfaiDeposit(evidence) {
16415
+ const state = this.state;
16416
+ const id = evidence.providerDepositId;
16417
+ if (evidence.agentId !== "zyfai" || !id || !state || state.walletAddress.toLowerCase() !== evidence.walletAddress.toLowerCase()) return null;
16418
+ return this.getAgent("zyfai").getDepositPlacement(state, id);
16419
+ }
14922
16420
  // --- Fund operations ---
14923
16421
  /** On-chain version and ownership status for this owner's Yieldseeker agent. */
14924
16422
  async getYieldseekerWalletStatus(asset) {
@@ -15501,6 +16999,17 @@ var OwneySDK = class {
15501
16999
  onStage: (stage) => options.onStage(order.orderHash, stage)
15502
17000
  } : {}
15503
17001
  });
17002
+ const deposit = order.pendingDeposit;
17003
+ if (deposit && typeof deposit.received === "string" && typeof deposit.symbol === "string" && typeof deposit.chainId === "number" && loadOrder(order.orderHash))
17004
+ savePendingDeposit({
17005
+ id: `${deposit.chainId}-${deposit.symbol}-${order.orderHash}`,
17006
+ received: deposit.received,
17007
+ symbol: deposit.symbol,
17008
+ chainId: deposit.chainId,
17009
+ createdAt: Date.now(),
17010
+ ...typeof deposit.agentId === "string" ? { agentId: deposit.agentId } : {},
17011
+ ...typeof deposit.operationId === "string" ? { operationId: deposit.operationId } : {}
17012
+ });
15504
17013
  clearOrder(order.orderHash);
15505
17014
  updateWithdrawSwap(order.orderHash, { status: "landed" });
15506
17015
  return { orderHash: order.orderHash, status: "completed" };
@@ -15671,14 +17180,16 @@ var OwneySDK = class {
15671
17180
  options.to.symbol
15672
17181
  );
15673
17182
  }
15674
- const ledgerOp = this.openSwapLedger(() => ({
17183
+ let ledgerInput;
17184
+ const ledgerOp = this.openSwapLedger(() => ledgerInput = {
15675
17185
  operationId: options.operationId,
15676
17186
  kind: "swap_deposit",
15677
17187
  owner: state.walletAddress,
15678
17188
  chainId: options.to.chainId,
15679
17189
  token: SupportedAssets.find((value) => value.chainId === options.to.chainId && value.symbol === options.to.symbol)?.address ?? "",
15680
17190
  asset: options.to.symbol.toLowerCase()
15681
- }));
17191
+ });
17192
+ const secretHandoff = this.secretHandoffFor(ledgerOp, ledgerInput);
15682
17193
  try {
15683
17194
  const api = this.swapApi();
15684
17195
  const quoted = await api.quote({
@@ -15706,6 +17217,17 @@ var OwneySDK = class {
15706
17217
  ...options.assertCurrent ? { assertCurrent: options.assertCurrent } : {},
15707
17218
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
15708
17219
  ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
17220
+ // Stored with a cross-chain order, so a resume that finishes the swap
17221
+ // can still lead to the deposit (ROUT-625). The guaranteed minimum,
17222
+ // never the quoted amount.
17223
+ pendingDeposit: {
17224
+ received: quote.dstAmountMin,
17225
+ symbol: options.to.symbol,
17226
+ chainId: quote.dst.chainId,
17227
+ ...options.agentId ? { agentId: options.agentId } : {},
17228
+ ...ledgerOp ? { operationId: ledgerOp.operationId } : {}
17229
+ },
17230
+ ...secretHandoff ? { secretHandoff } : {},
15709
17231
  ...ledgerOp ? { onOrderSubmitted: (orderHash) => {
15710
17232
  swapIds.orderHash = orderHash;
15711
17233
  this.ledgerSwap(ledgerOp, quote, { status: "submitted", orderHash });
@@ -15725,6 +17247,7 @@ var OwneySDK = class {
15725
17247
  receivedDstAmountRaw: swap.received
15726
17248
  });
15727
17249
  options.onSwapProgress?.("depositing");
17250
+ const backgroundDepositId = swap.orderHash ? `${quote.dst.chainId}-${options.to.symbol}-${swap.orderHash}` : void 0;
15728
17251
  try {
15729
17252
  options.assertCurrent?.();
15730
17253
  await this.ensureSwapChain(quote.dst.chainId);
@@ -15754,10 +17277,13 @@ var OwneySDK = class {
15754
17277
  ...options.assertCurrent ? { assertSubmissionCurrent: options.assertCurrent } : {},
15755
17278
  ...ledgerOp ? { operationId: ledgerOp.operationId } : {}
15756
17279
  });
17280
+ if (backgroundDepositId) clearPendingDeposit(backgroundDepositId);
15757
17281
  return this.ledgerSucceeded(ledgerOp, { swap, deposit, deposited: amount });
15758
17282
  } catch (error) {
15759
17283
  const pending = {
15760
- id: `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
17284
+ // Keyed by the order when there is one, so it replaces any record a
17285
+ // background resume wrote for the same order rather than doubling it.
17286
+ id: backgroundDepositId ?? `${quote.dst.chainId}-${options.to.symbol}-${Date.now()}`,
15761
17287
  received: swap.received,
15762
17288
  symbol: options.to.symbol,
15763
17289
  chainId: quote.dst.chainId,
@@ -15913,7 +17439,7 @@ var OwneySDK = class {
15913
17439
  const state = this.requireState();
15914
17440
  const activeChainId = this.requireChainId();
15915
17441
  const revision = this.connectionRevision;
15916
- let active2 = true;
17442
+ let active = true;
15917
17443
  let record2;
15918
17444
  let ledgerOp;
15919
17445
  let hadDurableRecord = false;
@@ -15921,7 +17447,7 @@ var OwneySDK = class {
15921
17447
  let confirmationDeadline = Infinity;
15922
17448
  const assertCurrent = () => {
15923
17449
  options.assertCurrent?.();
15924
- if (!active2) throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal attempt is no longer active.", { swapSubmitted: false });
17450
+ if (!active) throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal attempt is no longer active.", { swapSubmitted: false });
15925
17451
  if (this.state !== state || this.connectionRevision !== revision || state.chainId !== activeChainId)
15926
17452
  throw new OwneyError("NOT_CONNECTED", "Wallet changed during withdrawal and swap.");
15927
17453
  };
@@ -15966,9 +17492,10 @@ var OwneySDK = class {
15966
17492
  const saved = options.recoveryId ? listWithdrawSwaps(Date.now(), true).find((row) => row.orderHash === options.recoveryId) : void 0;
15967
17493
  if (options.recoveryId && (!saved?.recovery?.results || saved.recovery.phase !== "ready" || saved.walletAddress?.toLowerCase() !== state.walletAddress.toLowerCase() || saved.srcChainId !== activeChainId || saved.srcSymbol !== asset.symbol || saved.dstChainId !== options.to.chainId || saved.dstSymbol !== options.to.symbol))
15968
17494
  throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal is not ready to continue.");
17495
+ let ledgerInput;
15969
17496
  ledgerOp = this.openSwapLedger(() => {
15970
17497
  const requestedRaw = saved ? saved.recovery.requested : options.amount === void 0 ? void 0 : parseUnits4(options.amount, asset.decimals).toString();
15971
- return {
17498
+ return ledgerInput = {
15972
17499
  operationId: saved?.operationId ?? options.operationId,
15973
17500
  kind: "withdraw_swap",
15974
17501
  owner: state.walletAddress,
@@ -15979,6 +17506,7 @@ var OwneySDK = class {
15979
17506
  ...requestedRaw === void 0 ? {} : { requestedAmountRaw: requestedRaw }
15980
17507
  };
15981
17508
  });
17509
+ const secretHandoff = this.secretHandoffFor(ledgerOp, ledgerInput);
15982
17510
  const prepareTimeout = () => new OwneyError(
15983
17511
  "WITHDRAW_SWAP_INCOMPLETE",
15984
17512
  "Could not read the source wallet. No new withdrawal was submitted.",
@@ -16038,7 +17566,7 @@ var OwneySDK = class {
16038
17566
  evidence.set(next.agentId, next);
16039
17567
  if (record2?.recovery) {
16040
17568
  record2.recovery.evidence[next.agentId] = next;
16041
- if (active2) saveWithdrawSwap(record2);
17569
+ if (active) saveWithdrawSwap(record2);
16042
17570
  else {
16043
17571
  const latest = listWithdrawSwaps(Date.now(), true).find((row) => row.recovery?.attemptId === record2.recovery.attemptId);
16044
17572
  if (latest?.recovery) updateWithdrawSwap(latest.orderHash, { recovery: {
@@ -16154,6 +17682,7 @@ var OwneySDK = class {
16154
17682
  quote,
16155
17683
  walletAddress: state.walletAddress,
16156
17684
  direction: "withdraw",
17685
+ ...secretHandoff ? { secretHandoff } : {},
16157
17686
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
16158
17687
  ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
16159
17688
  onOrderPrepared: (orderHash, details) => {
@@ -16242,7 +17771,7 @@ var OwneySDK = class {
16242
17771
  this.ledgerFailed(ledgerOp, error);
16243
17772
  throw error;
16244
17773
  } finally {
16245
- active2 = false;
17774
+ active = false;
16246
17775
  releaseSubmission();
16247
17776
  this.closeSwapLedger(ledgerOp);
16248
17777
  }
@@ -16290,11 +17819,13 @@ var OwneySDK = class {
16290
17819
  });
16291
17820
  } catch {
16292
17821
  }
16293
- const ledgerLeg = (evidence) => this.ledgerWithdrawalLeg(ledgerOp, evidence);
17822
+ const ledgerLeg = (evidence, errorCode) => this.ledgerWithdrawalLeg(ledgerOp, evidence, errorCode);
16294
17823
  let hold;
16295
17824
  let reservationRejected = false;
16296
17825
  let innerStarted = false;
16297
17826
  const reservationLegs = /* @__PURE__ */ new Map();
17827
+ const plannedLegs = /* @__PURE__ */ new Map();
17828
+ const serverLegs = /* @__PURE__ */ new Map();
16298
17829
  const reservation = {
16299
17830
  dispatching: (id) => hold?.dispatch(id),
16300
17831
  settled: (id, leg) => {
@@ -16304,11 +17835,22 @@ var OwneySDK = class {
16304
17835
  const settleHold = () => {
16305
17836
  if (!hold) return;
16306
17837
  hold.stopHeartbeat();
16307
- this.settleWithdrawalReservation(hold, ids, attempted, reservationLegs);
17838
+ const serverOwned = new Set(ids.filter((id) => !attempted.has(id) && serverMayOwn(serverLegs.get(id))));
17839
+ this.settleWithdrawalReservation(hold, ids, attempted, reservationLegs, serverOwned);
16308
17840
  };
16309
17841
  const runAgents = () => {
16310
17842
  innerStarted = true;
16311
- const inner = this.withdrawFromAgents(options, state, chainId, attempted, ledgerLeg, reservation);
17843
+ const inner = this.withdrawFromAgents(
17844
+ options,
17845
+ state,
17846
+ chainId,
17847
+ attempted,
17848
+ ledgerLeg,
17849
+ reservation,
17850
+ plannedLegs,
17851
+ (timeoutMs) => ledgerOp?.settle?.(timeoutMs),
17852
+ serverLegs
17853
+ );
16312
17854
  void inner.then(settleHold, settleHold);
16313
17855
  return inner;
16314
17856
  };
@@ -16332,7 +17874,9 @@ var OwneySDK = class {
16332
17874
  } catch (error) {
16333
17875
  if (!reservationRejected) {
16334
17876
  for (const id of ids) if (!attempted.has(id)) {
16335
- notifyFunding(options.onProgress, {
17877
+ if (serverMayOwn(serverLegs.get(id))) continue;
17878
+ const planned = plannedLegs.get(id);
17879
+ notifyFunding(options.onProgress, planned ? { ...planned, transfer: "failed", delivery: "failed" } : {
16336
17880
  agentId: id,
16337
17881
  chainId,
16338
17882
  token: options.asset,
@@ -16454,12 +17998,13 @@ var OwneySDK = class {
16454
17998
  * - a throw without submission evidence: `not_submitted`;
16455
17999
  * - Zyfai "withdrawal already in progress": kept, dispatch `provider_in_progress`.
16456
18000
  */
16457
- settleWithdrawalReservation(hold, ids, attempted, legs) {
18001
+ settleWithdrawalReservation(hold, ids, attempted, legs, keep) {
16458
18002
  try {
16459
18003
  if ([...attempted].some((id) => !legs.has(id))) return;
16460
18004
  const releases = /* @__PURE__ */ new Map();
16461
18005
  for (const id of new Set(ids)) {
16462
18006
  if (!RESERVATION_AGENTS.includes(id)) continue;
18007
+ if (keep?.has(id)) continue;
16463
18008
  const leg = legs.get(id);
16464
18009
  const outcome = attempted.has(id) && leg ? reservationLegOutcome(leg) : "not_submitted";
16465
18010
  if (outcome === "keep") continue;
@@ -16490,6 +18035,31 @@ var OwneySDK = class {
16490
18035
  } catch {
16491
18036
  }
16492
18037
  }
18038
+ /**
18039
+ * Reports a withdrawal step the app closed as never sent (a reload before its
18040
+ * request was saved) as a failed ledger leg with `NOT_SUBMITTED`, so other
18041
+ * devices and later recovery stop treating it as in flight. Refuses any step
18042
+ * with a hash or a saved request. Never touches the hold; never throws.
18043
+ */
18044
+ async reportWithdrawalNotSubmitted(operationId, evidence) {
18045
+ try {
18046
+ if (typeof operationId !== "string" || !operationId || !evidence || evidence.transfer !== "failed" || evidence.txHash || evidence.userOpHash || evidence.callsId || evidence.withdrawalRequest?.attempted === true) return;
18047
+ const leg = ledgerLegFromEvidence(evidence, "withdraw");
18048
+ if (leg?.status !== "failed") return;
18049
+ const token = SupportedAssets.find((value) => value.chainId === evidence.chainId && value.symbol === evidence.token);
18050
+ const op = this.ledger.resume({
18051
+ operationId,
18052
+ kind: "withdraw",
18053
+ owner: evidence.walletAddress,
18054
+ chainId: evidence.chainId,
18055
+ token: token?.address ?? "",
18056
+ asset: String(evidence.token).toLowerCase()
18057
+ });
18058
+ op.leg({ ...leg, errorCode: "NOT_SUBMITTED" });
18059
+ op.finish({});
18060
+ } catch {
18061
+ }
18062
+ }
16493
18063
  /**
16494
18064
  * Reports a withdrawal leg that was proven after its call ended (a hashless
16495
18065
  * Zyfai payout found on chain or in history) to the funding ledger, then
@@ -16539,7 +18109,36 @@ var OwneySDK = class {
16539
18109
  } catch {
16540
18110
  }
16541
18111
  }
16542
- async withdrawFromAgents(options, state, chainId, attempted, ledgerLeg, reservation) {
18112
+ /**
18113
+ * Reports a deposit leg that the app's recovery verified on chain after its
18114
+ * call ended to the funding ledger. Only evidence whose transfer is confirmed
18115
+ * with a transaction hash is reported; anything else is ignored. The ledger
18116
+ * PATCHes the existing operation (creating it only on a 404) and the server
18117
+ * re-verifies the hash. Deposits hold no reservation, so nothing is released.
18118
+ * Never throws.
18119
+ */
18120
+ async reportDepositRecovery(operationId, evidence) {
18121
+ try {
18122
+ if (typeof operationId !== "string" || !operationId || !evidence || evidence.transfer !== "confirmed") return;
18123
+ const leg = ledgerLegFromEvidence(evidence, "deposit");
18124
+ if (leg?.status !== "confirmed" || !leg.txHash) return;
18125
+ const rail = depositLedgerRail(evidence);
18126
+ const token = SupportedAssets.find((value) => value.chainId === evidence.chainId && value.symbol === evidence.token);
18127
+ const op = this.ledger.resume({
18128
+ operationId,
18129
+ kind: "deposit",
18130
+ owner: evidence.walletAddress,
18131
+ chainId: evidence.chainId,
18132
+ token: token?.address ?? "",
18133
+ asset: String(evidence.token).toLowerCase()
18134
+ });
18135
+ op.leg(rail ? { ...leg, rail } : leg);
18136
+ op.finish({});
18137
+ this.ledgerProviderSamples(op, evidence, "deposit");
18138
+ } catch {
18139
+ }
18140
+ }
18141
+ async withdrawFromAgents(options, state, chainId, attempted, ledgerLeg, reservation, plannedLegs, settleLedger, serverLegs = /* @__PURE__ */ new Map()) {
16543
18142
  const { asset, amount, agentId, onAgentResult } = options;
16544
18143
  const notifyAgentResult = (id, result) => {
16545
18144
  try {
@@ -16585,7 +18184,22 @@ var OwneySDK = class {
16585
18184
  { asset, amount, agentErrors: agentErrors2, agentFailures, pendingSubmissions: pending }
16586
18185
  );
16587
18186
  };
16588
- const submit = async (agent, requested2) => {
18187
+ const awaitServerHandoffs = async () => {
18188
+ const pending = [...serverLegs.values()].filter((leg) => leg.settled === void 0).map((leg) => leg.answer);
18189
+ if (!pending.length) return;
18190
+ let timer;
18191
+ await Promise.race([
18192
+ Promise.all(pending),
18193
+ new Promise((resolve) => {
18194
+ timer = setTimeout(resolve, SERVER_HANDOFF_WAIT_MS);
18195
+ })
18196
+ ]);
18197
+ clearTimeout(timer);
18198
+ };
18199
+ const submit = async (agent, requested2, onAccepted) => {
18200
+ const plannedTask = serverLegs.get(agent.id);
18201
+ const serverTask = plannedTask ? await plannedTask.answer : void 0;
18202
+ if (serverTask) await settleLedger?.(3e3);
16589
18203
  if (this.state !== state || this.state.chainId !== chainId)
16590
18204
  throw new OwneyError("NOT_CONNECTED", "Wallet connection changed before withdrawal submission.");
16591
18205
  attempted.add(agent.id);
@@ -16607,6 +18221,7 @@ var OwneySDK = class {
16607
18221
  submissionEvidence.set(agent.id, evidence);
16608
18222
  notifyFunding(options.onProgress, evidence);
16609
18223
  ledgerLeg?.(evidence);
18224
+ if (onAccepted && evidence.ownerAuthorization === "accepted" && evidence.transfer !== "failed" && (evidence.userOpHash || evidence.callsId || evidence.txHash)) onAccepted();
16610
18225
  };
16611
18226
  progress(evidence);
16612
18227
  adapterReported = false;
@@ -16621,14 +18236,26 @@ var OwneySDK = class {
16621
18236
  reservation?.dispatching(agent.id);
16622
18237
  } catch {
16623
18238
  }
16624
- const result = options.onBeforeSubmission || options.assertCurrent ? await agent.withdraw(
18239
+ const context = typeof options.operationId === "string" ? { operationId: options.operationId, ...serverTask ? { planned: true } : {} } : void 0;
18240
+ const onBeforeSubmission = options.onBeforeSubmission ? (next) => options.onBeforeSubmission({ ...evidence, ...next }) : void 0;
18241
+ const result = context ? await agent.withdraw(
18242
+ state,
18243
+ chainId,
18244
+ token,
18245
+ requested2,
18246
+ progress,
18247
+ options.gasMode ?? "auto",
18248
+ onBeforeSubmission,
18249
+ options.assertCurrent,
18250
+ context
18251
+ ) : options.onBeforeSubmission || options.assertCurrent ? await agent.withdraw(
16625
18252
  state,
16626
18253
  chainId,
16627
18254
  token,
16628
18255
  requested2,
16629
18256
  progress,
16630
18257
  options.gasMode ?? "auto",
16631
- options.onBeforeSubmission ? (next) => options.onBeforeSubmission({ ...evidence, ...next }) : void 0,
18258
+ onBeforeSubmission,
16632
18259
  options.assertCurrent
16633
18260
  ) : await (options.gasMode === void 0 ? agent.withdraw(state, chainId, token, requested2, progress) : agent.withdraw(state, chainId, token, requested2, progress, options.gasMode));
16634
18261
  settled({
@@ -16639,6 +18266,10 @@ var OwneySDK = class {
16639
18266
  } catch (error) {
16640
18267
  if (!adapterReported && !evidence.txHash && !evidence.userOpHash && !evidence.callsId && (this.isUserRejectedWithdrawal(error) || /insufficient (balance|funds)/i.test(error instanceof Error ? error.message : "")))
16641
18268
  progress({ ...evidence, transfer: "failed", delivery: "failed" });
18269
+ else if (error instanceof OwneyError && error.details?.rejectedBeforeSubmission === true && evidence.transfer !== "confirmed")
18270
+ progress({ ...evidence, transfer: "failed", delivery: "failed" });
18271
+ if (serverLegs.size && requested2 === void 0 && agent.withdrawalRequiresWalletApproval && error instanceof OwneyError && error.code === "WITHDRAW_INSUFFICIENT_BALANCE" && error.details?.available === "0" && evidence.transfer === "failed" && !evidence.txHash && !evidence.userOpHash && !evidence.callsId)
18272
+ ledgerLeg?.(evidence, "WITHDRAW_INSUFFICIENT_BALANCE");
16642
18273
  settled({ settled: "threw", evidence });
16643
18274
  throw error;
16644
18275
  }
@@ -16664,40 +18295,151 @@ var OwneySDK = class {
16664
18295
  notifyAgentResult(agent.id, result);
16665
18296
  return result;
16666
18297
  }
18298
+ const dispatch = async (agent, requested2) => {
18299
+ let accept;
18300
+ const accepted = new Promise((resolve) => {
18301
+ accept = () => resolve(true);
18302
+ });
18303
+ const outcome = submit(agent, requested2, agent.withdrawalRequiresWalletApproval ? accept : void 0);
18304
+ const handedOver = await Promise.race([accepted, outcome.then(() => false, () => false)]);
18305
+ return { outcome, handedOver };
18306
+ };
18307
+ const prepareLaterAgents = async (agents) => {
18308
+ if (!agents.some((agent) => agent.withdrawalRequiresWalletApproval)) return;
18309
+ for (const agent of agents) {
18310
+ if (agent.withdrawalRequiresWalletApproval || typeof agent.prepareWithdrawal !== "function") continue;
18311
+ try {
18312
+ await agent.prepareWithdrawal(state, chainId, token);
18313
+ } catch (error) {
18314
+ if (this.isUserRejectedWithdrawal(error)) throw error;
18315
+ console.warn(`withdraw preparation failed for agent "${agent.id}":`, error);
18316
+ }
18317
+ }
18318
+ };
18319
+ const recordPlan = (legs) => {
18320
+ if (legs.length < 2) return;
18321
+ const handOffs = [];
18322
+ for (const [index, [agent, intendedAmount]] of legs.entries()) {
18323
+ const planned = {
18324
+ agentId: agent.id,
18325
+ chainId,
18326
+ token,
18327
+ walletAddress: state.walletAddress,
18328
+ intendedAmount,
18329
+ transfer: "pending",
18330
+ registration: "not-applicable",
18331
+ delivery: "pending",
18332
+ planned: true
18333
+ };
18334
+ plannedLegs?.set(planned.agentId, planned);
18335
+ notifyFunding(options.onProgress, planned);
18336
+ const after = legs.slice(0, index).filter(([earlier]) => earlier.withdrawalRequiresWalletApproval).map(([earlier]) => `agent:${earlier.id}`);
18337
+ if (agent.supportsServerTasks === true && typeof agent.handOffPlannedWithdrawal === "function" && after.length && zyfaiServerTasksEnabled(this.zyfaiServerTasks)) handOffs.push([agent, planned, after]);
18338
+ }
18339
+ const operationId = options.operationId;
18340
+ if (typeof operationId !== "string" || !operationId) return;
18341
+ for (const [agent, planned, after] of handOffs) {
18342
+ const leg = { answer: Promise.resolve(void 0), share: planned.intendedAmount };
18343
+ leg.answer = (async () => {
18344
+ let serverTask;
18345
+ let noTask = false;
18346
+ try {
18347
+ const answer = await agent.handOffPlannedWithdrawal(
18348
+ state,
18349
+ chainId,
18350
+ token,
18351
+ planned.intendedAmount,
18352
+ {
18353
+ operationId,
18354
+ after,
18355
+ // T10-I1: saved before the request leaves, as the dispatch
18356
+ // handoff's checkpoint is, so a reload while the server
18357
+ // answers finds a leg the server may own.
18358
+ beforeRequest: () => notifyFunding(options.onProgress, { ...planned, serverTask: "unknown" })
18359
+ }
18360
+ );
18361
+ serverTask = answer.type === "accepted" ? true : answer.type === "unknown" || answer.code === "LEG_ALREADY_SENT" ? "unknown" : void 0;
18362
+ noTask = answer.type === "declined" && ZYFAI_HANDOFF_NO_TASK_CODES.has(answer.code);
18363
+ } catch {
18364
+ serverTask = "unknown";
18365
+ }
18366
+ leg.settled = serverTask ?? null;
18367
+ if (serverTask) {
18368
+ const marked = { ...planned, serverTask };
18369
+ plannedLegs?.set(planned.agentId, marked);
18370
+ notifyFunding(options.onProgress, marked);
18371
+ } else if (noTask) {
18372
+ notifyFunding(options.onProgress, { ...planned, serverTask: false });
18373
+ }
18374
+ return serverTask;
18375
+ })();
18376
+ serverLegs.set(planned.agentId, leg);
18377
+ }
18378
+ };
16667
18379
  const eligibleAgents = this.getEligibleAgents(chainId, asset).filter((agent) => !options.excludeAgentIds?.includes(agent.id));
16668
18380
  const withdrawalAgents = this.orderAgentsForWithdrawal(eligibleAgents);
16669
18381
  if (!amount) {
16670
18382
  const results2 = {};
16671
18383
  const agentErrors2 = {};
16672
18384
  const incompleteWithdrawals = {};
16673
- for (const agent of withdrawalAgents) {
16674
- try {
16675
- results2[agent.id] = await submit(agent);
16676
- notifyAgentResult(agent.id, results2[agent.id]);
16677
- } catch (err) {
18385
+ const failed2 = (agent, err, handedOver) => {
18386
+ if (!handedOver) {
16678
18387
  if (err instanceof OwneyError && err.code === "SPONSOR_REQUEST_FAILED" && err.details?.safeToFallback === true) throw err;
16679
18388
  if (this.isUserRejectedWithdrawal(err)) throw err;
16680
- if (err instanceof OwneyError && err.code === "WITHDRAW_INSUFFICIENT_BALANCE" && err.details?.available === "0" && submissionEvidence.get(agent.id)?.transfer === "failed") continue;
16681
- if (agent.id === "yieldseeker" && err instanceof OwneyError && err.code === "WITHDRAW_PARTIAL_FAILURE" && err.details?.reason === "YIELDSEEKER_MAX_INCOMPLETE") {
16682
- incompleteWithdrawals[agent.id] = { positionsStatus: err.details.positionsStatus };
16683
- const evidence = submissionEvidence.get(agent.id);
16684
- if (evidence?.agentId === agent.id && evidence.chainId === chainId && evidence.token === token && evidence.walletAddress.toLowerCase() === state.walletAddress.toLowerCase() && evidence.intendedAmount === "MAX" && evidence.transfer === "confirmed" && evidence.delivery === "confirmed" && evidence.txHash && /^0x[\da-f]{64}$/i.test(evidence.txHash) && evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) && BigInt(evidence.confirmedAmount) > 0n) {
16685
- results2[agent.id] = { txHash: evidence.txHash, type: "partial", amount: evidence.confirmedAmount, evidence };
16686
- notifyAgentResult(agent.id, results2[agent.id]);
16687
- }
18389
+ }
18390
+ if (err instanceof OwneyError && err.code === "WITHDRAW_INSUFFICIENT_BALANCE" && err.details?.available === "0" && submissionEvidence.get(agent.id)?.transfer === "failed") return;
18391
+ if (agent.id === "yieldseeker" && err instanceof OwneyError && err.code === "WITHDRAW_PARTIAL_FAILURE" && err.details?.reason === "YIELDSEEKER_MAX_INCOMPLETE") {
18392
+ incompleteWithdrawals[agent.id] = { positionsStatus: err.details.positionsStatus };
18393
+ const evidence = submissionEvidence.get(agent.id);
18394
+ if (evidence?.agentId === agent.id && evidence.chainId === chainId && evidence.token === token && evidence.walletAddress.toLowerCase() === state.walletAddress.toLowerCase() && evidence.intendedAmount === "MAX" && evidence.transfer === "confirmed" && evidence.delivery === "confirmed" && evidence.txHash && /^0x[\da-f]{64}$/i.test(evidence.txHash) && evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) && BigInt(evidence.confirmedAmount) > 0n) {
18395
+ results2[agent.id] = { txHash: evidence.txHash, type: "partial", amount: evidence.confirmedAmount, evidence };
18396
+ notifyAgentResult(agent.id, results2[agent.id]);
18397
+ }
18398
+ }
18399
+ console.error(`withdraw failed for agent "${agent.id}":`, err);
18400
+ agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
18401
+ recordAgentFailure(agent.id, err);
18402
+ const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
18403
+ void reportAgentFailure(
18404
+ this.apiKey,
18405
+ agent.id,
18406
+ code,
18407
+ this.routingApiBaseUrl
18408
+ );
18409
+ };
18410
+ const handedOverLegs2 = [];
18411
+ const settleHandedOver2 = async () => {
18412
+ for (const { agent, outcome } of handedOverLegs2.splice(0)) {
18413
+ try {
18414
+ results2[agent.id] = await outcome;
18415
+ notifyAgentResult(agent.id, results2[agent.id]);
18416
+ } catch (err) {
18417
+ failed2(agent, err, true);
18418
+ }
18419
+ }
18420
+ };
18421
+ await prepareLaterAgents(withdrawalAgents);
18422
+ recordPlan(withdrawalAgents.map((agent) => [agent, "MAX"]));
18423
+ try {
18424
+ for (const agent of withdrawalAgents) {
18425
+ const leg = await dispatch(agent);
18426
+ if (leg.handedOver) {
18427
+ handedOverLegs2.push({ agent, outcome: leg.outcome });
18428
+ continue;
18429
+ }
18430
+ try {
18431
+ results2[agent.id] = await leg.outcome;
18432
+ notifyAgentResult(agent.id, results2[agent.id]);
18433
+ } catch (err) {
18434
+ failed2(agent, err, false);
16688
18435
  }
16689
- console.error(`withdraw failed for agent "${agent.id}":`, err);
16690
- agentErrors2[agent.id] = err instanceof Error ? err.message : String(err);
16691
- recordAgentFailure(agent.id, err);
16692
- const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
16693
- void reportAgentFailure(
16694
- this.apiKey,
16695
- agent.id,
16696
- code,
16697
- this.routingApiBaseUrl
16698
- );
16699
18436
  }
18437
+ } catch (error) {
18438
+ if (serverLegs.size) await awaitServerHandoffs();
18439
+ await settleHandedOver2();
18440
+ throw error;
16700
18441
  }
18442
+ await settleHandedOver2();
16701
18443
  if (Object.keys(results2).length === 0) {
16702
18444
  throwUnconfirmed(agentErrors2);
16703
18445
  if (Object.keys(agentErrors2).length === 0)
@@ -16842,32 +18584,65 @@ var OwneySDK = class {
16842
18584
  const agentErrors = {
16843
18585
  ...aggregated.agentErrors ?? {}
16844
18586
  };
16845
- for (let i = 0; i < orderedPlans.length; i++) {
16846
- const p = orderedPlans[i];
16847
- assertPlan();
16848
- if (p.planned <= 0n) continue;
16849
- try {
16850
- results[p.agent.id] = await submit(p.agent, p.planned.toString());
16851
- notifyAgentResult(p.agent.id, results[p.agent.id]);
16852
- } catch (err) {
18587
+ const failed = (p, index, err, handedOver) => {
18588
+ if (!handedOver) {
16853
18589
  if (err instanceof OwneyError && err.code === "SPONSOR_REQUEST_FAILED" && err.details?.safeToFallback === true) throw err;
16854
18590
  if (this.isUserRejectedWithdrawal(err)) throw err;
16855
- console.error(`withdraw failed for agent "${p.agent.id}":`, err);
16856
- agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
16857
- recordAgentFailure(p.agent.id, err);
16858
- const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
16859
- void reportAgentFailure(
16860
- this.apiKey,
16861
- p.agent.id,
16862
- code,
16863
- this.routingApiBaseUrl
16864
- );
16865
- const failedAmount = p.planned;
16866
- p.planned = 0n;
16867
- if (submissionEvidence.get(p.agent.id)?.transfer === "failed")
16868
- redistributeShare(orderedPlans, i, failedAmount);
16869
18591
  }
18592
+ console.error(`withdraw failed for agent "${p.agent.id}":`, err);
18593
+ agentErrors[p.agent.id] = err instanceof Error ? err.message : String(err);
18594
+ recordAgentFailure(p.agent.id, err);
18595
+ const code = err instanceof OwneyError ? err.code : "SDK_AGENT_FAILURE";
18596
+ void reportAgentFailure(
18597
+ this.apiKey,
18598
+ p.agent.id,
18599
+ code,
18600
+ this.routingApiBaseUrl
18601
+ );
18602
+ const failedAmount = p.planned;
18603
+ p.planned = 0n;
18604
+ if (!handedOver && submissionEvidence.get(p.agent.id)?.transfer === "failed")
18605
+ redistributeShare(orderedPlans, index, failedAmount);
18606
+ };
18607
+ const handedOverLegs = [];
18608
+ const settleHandedOver = async () => {
18609
+ for (const { plan, index, outcome } of handedOverLegs.splice(0)) {
18610
+ try {
18611
+ results[plan.agent.id] = await outcome;
18612
+ notifyAgentResult(plan.agent.id, results[plan.agent.id]);
18613
+ } catch (err) {
18614
+ failed(plan, index, err, true);
18615
+ }
18616
+ }
18617
+ };
18618
+ const dispatchedPlans = orderedPlans.filter((plan) => plan.planned > 0n);
18619
+ await prepareLaterAgents(dispatchedPlans.map((plan) => plan.agent));
18620
+ recordPlan(dispatchedPlans.map((plan) => [plan.agent, plan.planned.toString()]));
18621
+ try {
18622
+ for (let i = 0; i < orderedPlans.length; i++) {
18623
+ const p = orderedPlans[i];
18624
+ assertPlan();
18625
+ if (p.planned <= 0n) continue;
18626
+ const serverLeg = serverLegs.get(p.agent.id);
18627
+ if (serverLeg && await serverLeg.answer && /^[1-9][0-9]*$/.test(serverLeg.share)) p.planned = BigInt(serverLeg.share);
18628
+ const leg = await dispatch(p.agent, p.planned.toString());
18629
+ if (leg.handedOver) {
18630
+ handedOverLegs.push({ plan: p, index: i, outcome: leg.outcome });
18631
+ continue;
18632
+ }
18633
+ try {
18634
+ results[p.agent.id] = await leg.outcome;
18635
+ notifyAgentResult(p.agent.id, results[p.agent.id]);
18636
+ } catch (err) {
18637
+ failed(p, i, err, false);
18638
+ }
18639
+ }
18640
+ } catch (error) {
18641
+ if (serverLegs.size) await awaitServerHandoffs();
18642
+ await settleHandedOver();
18643
+ throw error;
16870
18644
  }
18645
+ await settleHandedOver();
16871
18646
  if (Object.keys(results).length === 0) {
16872
18647
  throwUnconfirmed(agentErrors);
16873
18648
  throw new OwneyError(
@@ -17803,12 +19578,20 @@ function createOwneySIWX(config) {
17803
19578
  export {
17804
19579
  AgentChainIncompatibleError,
17805
19580
  AgentNotFoundError,
19581
+ FUNDING_DEPOSIT_RECOVERY_REPORT,
19582
+ FUSION_SECRET_SERVER_PUBLISH,
17806
19583
  InvalidHistoryCursorError,
17807
19584
  NotConnectedError,
17808
19585
  OwneyError,
17809
19586
  OwneySDK,
17810
19587
  SDK_VERSION,
19588
+ SPONSORED_DEPOSIT_SERVER_BROADCAST,
19589
+ YIELDSEEKER_WITHDRAW_SERVER_SUBMIT,
17811
19590
  YieldseekerAgent,
19591
+ ZYFAI_REGISTRATION_NOT_SENT_CODES,
19592
+ ZYFAI_SERVER_TASKS,
19593
+ ZYFAI_WITHDRAWAL_NOT_SENT_CODES,
19594
+ ZYFAI_WITHDRAW_DISPATCH_CHECKPOINT,
17812
19595
  clearWithdrawSwap,
17813
19596
  createOwneySIWX,
17814
19597
  listPendingDeposits,