@owney/sdk 0.7.27-beta.1 → 0.7.27-beta.2

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
@@ -1,5 +1,213 @@
1
+ // src/errors.ts
2
+ var OwneyError = class extends Error {
3
+ code;
4
+ details;
5
+ agentId;
6
+ constructor(code, message, details, agentId) {
7
+ const prefix = agentId ? `[${code}][agent:${agentId}]` : `[${code}]`;
8
+ super(`${prefix} ${message}`);
9
+ this.name = "OwneyError";
10
+ this.code = code;
11
+ this.details = details;
12
+ this.agentId = agentId;
13
+ }
14
+ };
15
+ var AgentNotFoundError = class extends OwneyError {
16
+ constructor(agentId, available) {
17
+ super(
18
+ "AGENT_NOT_FOUND",
19
+ `Unknown agent "${agentId}". Available agents: ${available.join(", ")}`,
20
+ { agentId, available },
21
+ agentId
22
+ );
23
+ this.name = "AgentNotFoundError";
24
+ }
25
+ };
26
+ var NotConnectedError = class extends OwneyError {
27
+ constructor() {
28
+ super("NOT_CONNECTED", "Not connected. Call sdk.connect(provider) first.");
29
+ this.name = "NotConnectedError";
30
+ }
31
+ };
32
+ var AgentChainIncompatibleError = class extends OwneyError {
33
+ incompatibleAgents;
34
+ connectedChainId;
35
+ constructor(incompatibleAgents, connectedChainId) {
36
+ const details = incompatibleAgents.map(
37
+ ({ agentId, supportedChainIds }) => `"${agentId}" supports chains [${supportedChainIds.join(", ")}]`
38
+ ).join("; ");
39
+ super(
40
+ "AGENT_CHAIN_INCOMPATIBLE",
41
+ `Chain ${connectedChainId} is not supported by the following agents: ${details}`,
42
+ { incompatibleAgents, connectedChainId }
43
+ );
44
+ this.name = "AgentChainIncompatibleError";
45
+ this.incompatibleAgents = incompatibleAgents;
46
+ this.connectedChainId = connectedChainId;
47
+ }
48
+ };
49
+
50
+ // src/lib/swap/swap.observation.ts
51
+ var active = /* @__PURE__ */ new Set();
52
+ var isOrderObserved = (key2) => active.has(key2);
53
+ async function observeOrder(key2, run) {
54
+ const busy = () => new OwneyError(
55
+ "SWAP_REQUEST_FAILED",
56
+ "This order is already being observed.",
57
+ { orderHash: key2, watchActive: true, settlementUnconfirmed: true }
58
+ );
59
+ if (active.has(key2)) throw busy();
60
+ active.add(key2);
61
+ try {
62
+ if (typeof navigator !== "undefined" && navigator.locks)
63
+ return await navigator.locks.request(`owney.swap.observe:${key2}`, { ifAvailable: true }, (lock) => {
64
+ if (!lock) throw busy();
65
+ return run();
66
+ });
67
+ return await run();
68
+ } finally {
69
+ active.delete(key2);
70
+ }
71
+ }
72
+
73
+ // src/lib/swap/swap.deadline.ts
74
+ async function bounded(work, timeoutMs, error) {
75
+ if (timeoutMs <= 0) throw error();
76
+ let timer;
77
+ try {
78
+ return await Promise.race([
79
+ Promise.resolve().then(work),
80
+ new Promise((_, reject) => {
81
+ timer = setTimeout(() => reject(error()), timeoutMs);
82
+ })
83
+ ]);
84
+ } finally {
85
+ clearTimeout(timer);
86
+ }
87
+ }
88
+ async function mapConcurrent(items, run, concurrency = 3) {
89
+ const results = new Array(items.length);
90
+ let next = 0;
91
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async () => {
92
+ for (; ; ) {
93
+ const index = next++;
94
+ if (index >= items.length) return;
95
+ results[index] = await run(items[index]);
96
+ }
97
+ }));
98
+ return results;
99
+ }
100
+
101
+ // src/lib/permit2-batch.ts
102
+ import { parseAbi, hashStruct } from "viem";
103
+ var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
104
+ var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
105
+ var ARBITRUM_USDC_ADDRESS = "0xaf88d065e77c8cc2239327c5edb3a432268e5831";
106
+ function supportsSponsoredErc2612(chainId, token) {
107
+ const expected = chainId === 8453 ? BASE_USDC_ADDRESS : chainId === 42161 ? ARBITRUM_USDC_ADDRESS : void 0;
108
+ return expected !== void 0 && token.toLowerCase() === expected.toLowerCase();
109
+ }
110
+ var ERC2612_PERMIT_TYPES = {
111
+ Permit: [
112
+ { name: "owner", type: "address" },
113
+ { name: "spender", type: "address" },
114
+ { name: "value", type: "uint256" },
115
+ { name: "nonce", type: "uint256" },
116
+ { name: "deadline", type: "uint256" }
117
+ ]
118
+ };
119
+ var ERC2612_READ_ABI = parseAbi([
120
+ "function name() view returns (string)",
121
+ "function version() view returns (string)",
122
+ "function nonces(address owner) view returns (uint256)"
123
+ ]);
124
+ function erc2612TypedData(input) {
125
+ return {
126
+ domain: {
127
+ name: input.permit.tokenName,
128
+ version: input.permit.tokenVersion,
129
+ chainId: input.chainId,
130
+ verifyingContract: input.token
131
+ },
132
+ types: ERC2612_PERMIT_TYPES,
133
+ primaryType: "Permit",
134
+ message: {
135
+ owner: input.owner,
136
+ spender: BATCH_PERMIT2_ADDRESS,
137
+ value: BigInt(input.permit.value),
138
+ nonce: BigInt(input.permit.nonce),
139
+ deadline: BigInt(input.permit.deadline)
140
+ }
141
+ };
142
+ }
143
+ var PERMIT_BATCH_TYPES = {
144
+ PermitBatchWitnessTransferFrom: [
145
+ { name: "permitted", type: "TokenPermissions[]" },
146
+ { name: "spender", type: "address" },
147
+ { name: "nonce", type: "uint256" },
148
+ { name: "deadline", type: "uint256" },
149
+ { name: "witness", type: "Deposit" }
150
+ ],
151
+ Deposit: [{ name: "recipients", type: "address[]" }],
152
+ TokenPermissions: [
153
+ { name: "token", type: "address" },
154
+ { name: "amount", type: "uint256" }
155
+ ]
156
+ };
157
+ var PERMIT2_BATCH_ABI = parseAbi([
158
+ "struct TokenPermissions { address token; uint256 amount; }",
159
+ "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
160
+ "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
161
+ "function permitWitnessTransferFrom(PermitBatchTransferFrom permit,SignatureTransferDetails[] transferDetails,address owner,bytes32 witness,string witnessTypeString,bytes signature)",
162
+ "function nonceBitmap(address owner,uint256 wordPos) view returns (uint256)"
163
+ ]);
164
+ function batchPermit(b) {
165
+ return {
166
+ permitted: b.transfers.map((t) => ({
167
+ token: b.token,
168
+ amount: BigInt(t.amount)
169
+ })),
170
+ nonce: BigInt(b.nonce),
171
+ deadline: BigInt(b.deadline)
172
+ };
173
+ }
174
+ function batchTypedData(b, spender) {
175
+ return {
176
+ domain: {
177
+ name: "Permit2",
178
+ chainId: b.chainId,
179
+ verifyingContract: BATCH_PERMIT2_ADDRESS
180
+ },
181
+ types: PERMIT_BATCH_TYPES,
182
+ primaryType: "PermitBatchWitnessTransferFrom",
183
+ message: {
184
+ ...batchPermit(b),
185
+ spender,
186
+ witness: { recipients: b.transfers.map((t) => t.to) }
187
+ }
188
+ };
189
+ }
190
+ var BATCH_WITNESS_TYPE = "Deposit witness)Deposit(address[] recipients)TokenPermissions(address token,uint256 amount)";
191
+ function batchWitness(b) {
192
+ return hashStruct({
193
+ types: PERMIT_BATCH_TYPES,
194
+ primaryType: "Deposit",
195
+ data: { recipients: b.transfers.map((t) => t.to) }
196
+ });
197
+ }
198
+ function batchArgs(b) {
199
+ return [
200
+ batchPermit(b),
201
+ b.transfers.map((t) => ({ to: t.to, requestedAmount: BigInt(t.amount) })),
202
+ b.from,
203
+ batchWitness(b),
204
+ BATCH_WITNESS_TYPE,
205
+ b.signature
206
+ ];
207
+ }
208
+
1
209
  // src/lib/funding-evidence.ts
2
- import { decodeEventLog, decodeFunctionData, erc20Abi, parseAbi } from "viem";
210
+ import { decodeEventLog, decodeFunctionData, erc20Abi, parseAbi as parseAbi2 } from "viem";
3
211
  import { entryPoint06Abi, entryPoint07Abi, getUserOperationHash, toUserOperation } from "viem/account-abstraction";
4
212
  function notifyFunding(callback, evidence) {
5
213
  try {
@@ -8,9 +216,9 @@ function notifyFunding(callback, evidence) {
8
216
  } catch {
9
217
  }
10
218
  }
11
- var fullWithdrawalAbi = parseAbi(["function withdrawAllAssetToUser(address recipient, address asset)"]);
12
- var withdrawalEventAbi = parseAbi(["event WithdrewTokenToUser(address indexed owner, address indexed recipient, address indexed token, uint256 amount)"]);
13
- var accountExecutionAbi = parseAbi([
219
+ var fullWithdrawalAbi = parseAbi2(["function withdrawAllAssetToUser(address recipient, address asset)"]);
220
+ var withdrawalEventAbi = parseAbi2(["event WithdrewTokenToUser(address indexed owner, address indexed recipient, address indexed token, uint256 amount)"]);
221
+ var accountExecutionAbi = parseAbi2([
14
222
  "function execute(address target,uint256 value,bytes data)",
15
223
  "function executeBatch((address target,uint256 value,bytes data)[] calls)"
16
224
  ]);
@@ -203,55 +411,6 @@ function registerDepositBatch(callback, transfer) {
203
411
  batchCallbacks.set(callback, transfer);
204
412
  }
205
413
 
206
- // src/errors.ts
207
- var OwneyError = class extends Error {
208
- code;
209
- details;
210
- agentId;
211
- constructor(code, message, details, agentId) {
212
- const prefix = agentId ? `[${code}][agent:${agentId}]` : `[${code}]`;
213
- super(`${prefix} ${message}`);
214
- this.name = "OwneyError";
215
- this.code = code;
216
- this.details = details;
217
- this.agentId = agentId;
218
- }
219
- };
220
- var AgentNotFoundError = class extends OwneyError {
221
- constructor(agentId, available) {
222
- super(
223
- "AGENT_NOT_FOUND",
224
- `Unknown agent "${agentId}". Available agents: ${available.join(", ")}`,
225
- { agentId, available },
226
- agentId
227
- );
228
- this.name = "AgentNotFoundError";
229
- }
230
- };
231
- var NotConnectedError = class extends OwneyError {
232
- constructor() {
233
- super("NOT_CONNECTED", "Not connected. Call sdk.connect(provider) first.");
234
- this.name = "NotConnectedError";
235
- }
236
- };
237
- var AgentChainIncompatibleError = class extends OwneyError {
238
- incompatibleAgents;
239
- connectedChainId;
240
- constructor(incompatibleAgents, connectedChainId) {
241
- const details = incompatibleAgents.map(
242
- ({ agentId, supportedChainIds }) => `"${agentId}" supports chains [${supportedChainIds.join(", ")}]`
243
- ).join("; ");
244
- super(
245
- "AGENT_CHAIN_INCOMPATIBLE",
246
- `Chain ${connectedChainId} is not supported by the following agents: ${details}`,
247
- { incompatibleAgents, connectedChainId }
248
- );
249
- this.name = "AgentChainIncompatibleError";
250
- this.incompatibleAgents = incompatibleAgents;
251
- this.connectedChainId = connectedChainId;
252
- }
253
- };
254
-
255
414
  // src/lib/transfer-auth.ts
256
415
  import { bytesToHex } from "viem";
257
416
 
@@ -518,21 +677,21 @@ function rateLimitDelay(error, now = Date.now()) {
518
677
  }
519
678
  if (typeof value !== "object" || seen.has(value)) return;
520
679
  seen.add(value);
521
- const record = value;
522
- if ([record.status, record.statusCode, record.code].some(
680
+ const record2 = value;
681
+ if ([record2.status, record2.statusCode, record2.code].some(
523
682
  (code) => String(code) === "429"
524
683
  )) {
525
684
  limited = true;
526
685
  }
527
686
  for (const key2 of ["retryAfterSeconds", "retryAfter"]) {
528
- const seconds = Number(record[key2]);
687
+ const seconds = Number(record2[key2]);
529
688
  if (Number.isFinite(seconds) && seconds > 0)
530
689
  delay = Math.max(delay, seconds * 1e3);
531
690
  }
532
- const retryAt = Number(record.retryAt);
691
+ const retryAt = Number(record2.retryAt);
533
692
  if (Number.isFinite(retryAt) && retryAt > now)
534
693
  delay = Math.max(delay, retryAt - now);
535
- const headers = record.headers;
694
+ const headers = record2.headers;
536
695
  const header = typeof headers?.get === "function" ? headers.get("Retry-After") : headers?.["retry-after"] ?? headers?.["Retry-After"];
537
696
  if (typeof header === "string" || typeof header === "number") {
538
697
  const seconds = Number(header);
@@ -548,10 +707,10 @@ function rateLimitDelay(error, now = Date.now()) {
548
707
  "data",
549
708
  "fields"
550
709
  ]) {
551
- visit(record[key2], depth + 1);
710
+ visit(record2[key2], depth + 1);
552
711
  }
553
712
  for (const key2 of ["agentErrors", "failures"]) {
554
- const entries = record[key2];
713
+ const entries = record2[key2];
555
714
  if (entries && typeof entries === "object") {
556
715
  for (const entry of Object.values(entries)) visit(entry, depth + 1);
557
716
  }
@@ -569,7 +728,7 @@ function rateLimitDiagnostics(error) {
569
728
  if (depth > 6 || !value || typeof value !== "object" || seen.has(value))
570
729
  return;
571
730
  seen.add(value);
572
- const record = value;
731
+ const record2 = value;
573
732
  for (const key2 of [
574
733
  "rpcSource",
575
734
  "chainId",
@@ -577,12 +736,12 @@ function rateLimitDiagnostics(error) {
577
736
  "providerRequestId",
578
737
  "statusCode"
579
738
  ]) {
580
- const candidate = record[key2];
739
+ const candidate = record2[key2];
581
740
  if (candidate !== void 0 && details[key2] === void 0)
582
741
  details = { ...details, [key2]: candidate };
583
742
  }
584
743
  for (const key2 of ["details", "cause", "response", "fields", "error"])
585
- visit(record[key2], depth + 1);
744
+ visit(record2[key2], depth + 1);
586
745
  }
587
746
  visit(error);
588
747
  return {
@@ -650,12 +809,19 @@ var AgentReads = class {
650
809
  };
651
810
 
652
811
  // src/agents/zyfai/zyfai.withdraw-submission.ts
812
+ var BELOW_MINIMUM = /^Idle balance (\d+(?:\.\d+)?) (USDC|WETH|ETH|EURC) is below the minimum withdrawal of (\d+(?:\.\d+)?) \2\.?$/;
653
813
  var listenersByClient = /* @__PURE__ */ new WeakMap();
654
814
  function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
655
815
  const client = sdk.httpClient;
656
816
  if (!client || typeof client.get !== "function" || typeof client.post !== "function")
657
- return { attempted: () => void 0, rejected: () => false, request: () => void 0, stop: () => {
658
- } };
817
+ return {
818
+ attempted: () => void 0,
819
+ rejected: () => false,
820
+ request: () => void 0,
821
+ belowMinimum: () => void 0,
822
+ stop: () => {
823
+ }
824
+ };
659
825
  let listeners2 = listenersByClient.get(client);
660
826
  if (!listeners2) {
661
827
  listeners2 = /* @__PURE__ */ new Set();
@@ -675,6 +841,7 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
675
841
  method: method === "get" ? "GET" : "POST",
676
842
  path: method === "get" ? "/users/withdraw" : "/users/partial-withdraw"
677
843
  };
844
+ let belowMinimum2;
678
845
  const notify2 = (outcome) => {
679
846
  if (outcome === "rejected" && matching.length !== 1) return;
680
847
  const evidence = matching.length === 1 ? { ...request3 } : {
@@ -685,7 +852,7 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
685
852
  };
686
853
  for (const observer of matching) {
687
854
  try {
688
- observer.notify(outcome, evidence);
855
+ observer.notify(outcome, evidence, outcome === "rejected" ? belowMinimum2 : void 0);
689
856
  } catch {
690
857
  }
691
858
  }
@@ -710,10 +877,12 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
710
877
  return await original.apply(this, callArgs);
711
878
  } catch (error) {
712
879
  const message = error instanceof Error ? error.message : "";
880
+ const minimum = BELOW_MINIMUM.exec(message);
881
+ if (minimum) belowMinimum2 = { symbol: minimum[2], idle: minimum[1], minimum: minimum[3] };
713
882
  const status = error?.response?.status;
714
883
  if (request3.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
715
884
  request3.httpStatus = status;
716
- 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" : /^Idle balance \d+(?:\.\d+)? (USDC|WETH|ETH|EURC) is below the minimum withdrawal of \d+(?:\.\d+)? \1\.?$/.test(message) ? "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";
885
+ 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";
717
886
  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");
718
887
  throw error;
719
888
  } finally {
@@ -726,32 +895,45 @@ function observeWithdrawalSubmission(sdk, chainId, token, assertCurrent) {
726
895
  let submitted = false;
727
896
  let rejected = false;
728
897
  let request2 = { attempted: false };
729
- const listener = { chainId, token, assertCurrent, notify: (outcome, evidence) => {
898
+ let belowMinimum;
899
+ const listener = { chainId, token, assertCurrent, notify: (outcome, evidence, detail) => {
730
900
  request2 = evidence;
731
901
  if (outcome === "attempted") submitted = true;
732
- else if (outcome === "rejected") rejected = true;
902
+ else if (outcome === "rejected") {
903
+ rejected = true;
904
+ belowMinimum = detail;
905
+ }
733
906
  } };
734
907
  listeners2.add(listener);
735
- return { attempted: () => submitted, rejected: () => rejected, request: () => ({ ...request2 }), stop: () => {
736
- listeners2.delete(listener);
737
- } };
908
+ return {
909
+ attempted: () => submitted,
910
+ rejected: () => rejected,
911
+ request: () => ({ ...request2 }),
912
+ belowMinimum: () => belowMinimum && { ...belowMinimum },
913
+ stop: () => {
914
+ listeners2.delete(listener);
915
+ }
916
+ };
738
917
  }
739
918
 
740
919
  // src/agents/zyfai/zyfai.withdraw-empty.ts
741
- function hasEmptyWithdrawalSnapshot(raw2, chainId, token) {
742
- const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
920
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
921
+ var symbol = (value) => typeof value === "string" ? value.toUpperCase() === "ETH" ? "WETH" : value.toUpperCase() : void 0;
922
+ var chain = (value) => {
923
+ if (typeof value === "number") return value;
924
+ if (typeof value !== "string") return void 0;
925
+ return /^\d+$/.test(value) ? Number(value) : { base: 8453, arbitrum: 42161, ethereum: 1, mainnet: 1 }[value.trim().toLowerCase()];
926
+ };
927
+ var zero = (value) => typeof value === "string" && /^(?:0x)?0+$/i.test(value);
928
+ var units = (value) => typeof value === "string" && /^(?:0x[0-9a-f]+|\d+)$/i.test(value) ? BigInt(value) : void 0;
929
+ function snapshotPortfolio(raw2) {
743
930
  const response = record(raw2);
744
931
  const portfolio = record(response?.portfolio);
745
932
  const chains = record(portfolio?.portfolioByChain);
746
- if (response?.success === false || !chains || !Array.isArray(portfolio?.positions) || !Array.isArray(portfolio.staleBalances)) return false;
747
- if (portfolio.pendingAsyncWithdrawals !== void 0 && (!Array.isArray(portfolio.pendingAsyncWithdrawals) || portfolio.pendingAsyncWithdrawals.length > 0)) return false;
748
- const symbol = (value) => typeof value === "string" ? value.toUpperCase() === "ETH" ? "WETH" : value.toUpperCase() : void 0;
749
- const chain = (value) => {
750
- if (typeof value === "number") return value;
751
- if (typeof value !== "string") return void 0;
752
- return /^\d+$/.test(value) ? Number(value) : { base: 8453, arbitrum: 42161, ethereum: 1, mainnet: 1 }[value.trim().toLowerCase()];
753
- };
754
- const zero = (value) => typeof value === "string" && /^(?:0x)?0+$/i.test(value);
933
+ if (response?.success === false || !portfolio || !chains || !Array.isArray(portfolio.positions) || !Array.isArray(portfolio.staleBalances)) return void 0;
934
+ return { portfolio, chains, entries: [...portfolio.positions, ...portfolio.staleBalances] };
935
+ }
936
+ function chainBalancesMatch(chains, chainId, token, accept) {
755
937
  for (const [id, assets] of Object.entries(chains)) {
756
938
  const idNumber = chain(id);
757
939
  if (idNumber === void 0) return false;
@@ -761,10 +943,13 @@ function hasEmptyWithdrawalSnapshot(raw2, chainId, token) {
761
943
  for (const [asset, value] of Object.entries(entries)) {
762
944
  if (symbol(asset) !== token) continue;
763
945
  const balance = record(value);
764
- if (!balance || !zero(balance.balance)) return false;
946
+ if (!balance || !accept(balance)) return false;
765
947
  }
766
948
  }
767
- for (const value of [...portfolio.positions, ...portfolio.staleBalances]) {
949
+ return true;
950
+ }
951
+ function positionsEmpty(entries, chainId, token) {
952
+ for (const value of entries) {
768
953
  const entry = record(value);
769
954
  if (!entry) return false;
770
955
  const id = chain(entry.chainId ?? entry.chain);
@@ -776,6 +961,23 @@ function hasEmptyWithdrawalSnapshot(raw2, chainId, token) {
776
961
  }
777
962
  return true;
778
963
  }
964
+ function hasEmptyWithdrawalSnapshot(raw2, chainId, token) {
965
+ const snapshot = snapshotPortfolio(raw2);
966
+ if (!snapshot) return false;
967
+ const pending = snapshot.portfolio.pendingAsyncWithdrawals;
968
+ if (pending !== void 0 && (!Array.isArray(pending) || pending.length > 0)) return false;
969
+ return chainBalancesMatch(snapshot.chains, chainId, token, (balance) => zero(balance.balance)) && positionsEmpty(snapshot.entries, chainId, token);
970
+ }
971
+ function hasIdleOnlyWithdrawalSnapshot(raw2, chainId, token, decimals, idle) {
972
+ const snapshot = snapshotPortfolio(raw2);
973
+ if (!snapshot) return false;
974
+ const pending = snapshot.portfolio.pendingAsyncWithdrawals;
975
+ if (pending !== void 0 && (!Array.isArray(pending) || pending.some((value) => record(value)?.status !== "CLAIMED"))) return false;
976
+ return chainBalancesMatch(snapshot.chains, chainId, token, (balance) => {
977
+ const amount = units(balance.balance);
978
+ return amount !== void 0 && amount <= idle && (balance.decimals === void 0 || balance.decimals === decimals);
979
+ }) && positionsEmpty(snapshot.entries, chainId, token);
980
+ }
779
981
 
780
982
  // src/agents/zyfai/zyfai.deposit.ts
781
983
  import { createWalletClient, custom, encodeFunctionData, erc20Abi as erc20Abi2, isAddress } from "viem";
@@ -828,20 +1030,20 @@ var SupportedAssets = [
828
1030
  ];
829
1031
 
830
1032
  // src/agents/zyfai/zyfai.deposit.ts
831
- async function submitZyfaiDeposit({ sdk, state, chain, amount, asset, smartWallet, assertCurrent, onBeforeSubmission }) {
1033
+ async function submitZyfaiDeposit({ sdk, state, chain: chain2, amount, asset, smartWallet, assertCurrent, onBeforeSubmission }) {
832
1034
  const owner = state.walletAddress;
833
1035
  const provider = state.provider;
834
- const token = SupportedAssets.find((value) => value.chainId === chain.id && value.symbol === asset);
1036
+ const token = SupportedAssets.find((value) => value.chainId === chain2.id && value.symbol === asset);
835
1037
  if (!token || !isAddress(owner) || !isAddress(smartWallet) || !/^\d+$/.test(amount) || BigInt(amount) <= 0n)
836
- throw new OwneyError("AGENT_INVALID_RESPONSE", "Invalid Zyfai deposit transfer.", { chainId: chain.id, asset }, "zyfai");
1038
+ throw new OwneyError("AGENT_INVALID_RESPONSE", "Invalid Zyfai deposit transfer.", { chainId: chain2.id, asset }, "zyfai");
837
1039
  assertCurrent();
838
- const prepared = await sdk.prepareEnterPosition({ userAddress: owner, chainId: chain.id, amount, asset, strategy: "aggressive" });
1040
+ const prepared = await sdk.prepareEnterPosition({ userAddress: owner, chainId: chain2.id, amount, asset, strategy: "aggressive" });
839
1041
  assertCurrent();
840
1042
  const expectedData = encodeFunctionData({ abi: erc20Abi2, functionName: "transfer", args: [smartWallet, BigInt(amount)] });
841
1043
  const transfer = prepared?.transfer;
842
1044
  if (!transfer || transfer.safeAddress?.toLowerCase() !== smartWallet.toLowerCase() || transfer.tokenAddress?.toLowerCase() !== token.address.toLowerCase() || transfer.to?.toLowerCase() !== token.address.toLowerCase() || transfer.data?.toLowerCase() !== expectedData.toLowerCase() || transfer.value !== "0")
843
- throw new OwneyError("AGENT_INVALID_RESPONSE", "Zyfai prepared a different deposit transfer.", { chainId: chain.id, asset }, "zyfai");
844
- const wallet = createWalletClient({ account: owner, chain, transport: custom({
1045
+ throw new OwneyError("AGENT_INVALID_RESPONSE", "Zyfai prepared a different deposit transfer.", { chainId: chain2.id, asset }, "zyfai");
1046
+ const wallet = createWalletClient({ account: owner, chain: chain2, transport: custom({
845
1047
  request: async (args) => {
846
1048
  assertCurrent();
847
1049
  if (args.method === "eth_sendTransaction") {
@@ -850,7 +1052,7 @@ async function submitZyfaiDeposit({ sdk, state, chain, amount, asset, smartWalle
850
1052
  provider.request({ method: "eth_chainId" })
851
1053
  ]);
852
1054
  assertCurrent();
853
- if (!Array.isArray(accounts) || typeof accounts[0] !== "string" || accounts[0].toLowerCase() !== owner.toLowerCase() || Number(chainId) !== chain.id)
1055
+ if (!Array.isArray(accounts) || typeof accounts[0] !== "string" || accounts[0].toLowerCase() !== owner.toLowerCase() || Number(chainId) !== chain2.id)
854
1056
  throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai deposit.", void 0, "zyfai");
855
1057
  await onBeforeSubmission?.();
856
1058
  assertCurrent();
@@ -858,12 +1060,12 @@ async function submitZyfaiDeposit({ sdk, state, chain, amount, asset, smartWalle
858
1060
  return provider.request(args);
859
1061
  }
860
1062
  }, { retryCount: 0 }) });
861
- return wallet.sendTransaction({ account: owner, chain, to: token.address, data: expectedData, value: 0n });
1063
+ return wallet.sendTransaction({ account: owner, chain: chain2, to: token.address, data: expectedData, value: 0n });
862
1064
  }
863
1065
 
864
1066
  // src/agents/zyfai/zyfai.agent.ts
865
1067
  import { SMART_SESSIONS_VALIDATOR, ZyfaiSDK } from "@zyfai/sdk";
866
- import { erc20Abi as erc20Abi3, formatUnits as formatUnits2, isAddress as isAddress2, parseAbi as parseAbi2 } from "viem";
1068
+ import { erc20Abi as erc20Abi3, formatUnits as formatUnits2, isAddress as isAddress2, parseAbi as parseAbi3, parseUnits } from "viem";
867
1069
  import { base, arbitrum, mainnet } from "viem/chains";
868
1070
 
869
1071
  // src/lib/debug.ts
@@ -914,8 +1116,8 @@ function assetAliases(asset) {
914
1116
  const normalised = asset.toLowerCase();
915
1117
  return normalised === "weth" ? ["weth", "eth"] : [normalised];
916
1118
  }
917
- function matchesAsset(symbol, asset) {
918
- return assetAliases(asset).includes(symbol.toLowerCase());
1119
+ function matchesAsset(symbol2, asset) {
1120
+ return assetAliases(asset).includes(symbol2.toLowerCase());
919
1121
  }
920
1122
 
921
1123
  // src/agents/zyfai/zyfai.mapper.ts
@@ -929,10 +1131,10 @@ var DECIMALS_BY_SYMBOL = {
929
1131
  wbtc: 8,
930
1132
  cbbtc: 8
931
1133
  };
932
- function tokenDecimals(symbol, explicit) {
1134
+ function tokenDecimals(symbol2, explicit) {
933
1135
  if (typeof explicit === "number" && Number.isFinite(explicit))
934
1136
  return explicit;
935
- return DECIMALS_BY_SYMBOL[(symbol ?? "").toLowerCase()] ?? 6;
1137
+ return DECIMALS_BY_SYMBOL[(symbol2 ?? "").toLowerCase()] ?? 6;
936
1138
  }
937
1139
  function mapWithdraw(raw2) {
938
1140
  return {
@@ -952,10 +1154,10 @@ var NAME_TO_CHAIN_ID = Object.entries(
952
1154
  acc[name.toLowerCase()] = Number(id);
953
1155
  return acc;
954
1156
  }, {});
955
- function resolveChainId(chain) {
956
- const asNum = Number(chain);
1157
+ function resolveChainId(chain2) {
1158
+ const asNum = Number(chain2);
957
1159
  if (Number.isFinite(asNum) && asNum > 0) return asNum;
958
- return NAME_TO_CHAIN_ID[chain.trim().toLowerCase()] ?? NaN;
1160
+ return NAME_TO_CHAIN_ID[chain2.trim().toLowerCase()] ?? NaN;
959
1161
  }
960
1162
  function mapPendingAllocations(raw2) {
961
1163
  if (!Array.isArray(raw2)) return void 0;
@@ -995,16 +1197,16 @@ function mapBalances(raw2, _chainId, smartWallet) {
995
1197
  for (const [chainIdKey, tokensBySymbol] of Object.entries(portfolioByChain)) {
996
1198
  const positionChainId = Number(chainIdKey);
997
1199
  for (const [
998
- symbol,
1200
+ symbol2,
999
1201
  { balance, balanceWithFee, decimals }
1000
1202
  ] of Object.entries(tokensBySymbol ?? {})) {
1001
1203
  if (balanceWithFee === void 0 && balance === void 0) throw new Error("Zyfai asset balance is unavailable.");
1002
- const amount = exactHexAmount(balanceWithFee ?? balance, tokenDecimals(symbol, decimals));
1204
+ const amount = exactHexAmount(balanceWithFee ?? balance, tokenDecimals(symbol2, decimals));
1003
1205
  totalBalance += Number(amount);
1004
1206
  tokens.push({
1005
1207
  chain: CHAIN_ID_TO_NAME[positionChainId] ?? "BASE",
1006
1208
  chainId: positionChainId,
1007
- asset: symbol,
1209
+ asset: symbol2,
1008
1210
  amount
1009
1211
  });
1010
1212
  }
@@ -1072,12 +1274,12 @@ function mapEarnings(raw2, smartWallet) {
1072
1274
  totalEarningsByChain
1073
1275
  )) {
1074
1276
  const positionChainId = Number(chainIdKey);
1075
- for (const [symbol, earnings] of Object.entries(tokensBySymbol ?? {})) {
1277
+ for (const [symbol2, earnings] of Object.entries(tokensBySymbol ?? {})) {
1076
1278
  const amount = Number(earnings);
1077
1279
  tokens.push({
1078
1280
  chain: CHAIN_ID_TO_NAME[positionChainId] ?? "BASE",
1079
1281
  chainId: positionChainId,
1080
- asset: symbol,
1282
+ asset: symbol2,
1081
1283
  amount: String(amount)
1082
1284
  });
1083
1285
  }
@@ -1097,8 +1299,8 @@ function mapWeightedApyByChain(raw2) {
1097
1299
  const chainId = Number(chainKey);
1098
1300
  if (!SUPPORTED_CHAIN_IDS.includes(chainId)) continue;
1099
1301
  const perAsset = {};
1100
- for (const [symbol, apy] of Object.entries(tokenApy)) {
1101
- const token = symbol;
1302
+ for (const [symbol2, apy] of Object.entries(tokenApy)) {
1303
+ const token = symbol2;
1102
1304
  if (!SUPPORTED_TOKENS.includes(token)) continue;
1103
1305
  perAsset[token] = apy;
1104
1306
  }
@@ -1329,8 +1531,8 @@ function computeAllocationApy(positions) {
1329
1531
  function earningsForToken(bucket, chainId, asset) {
1330
1532
  const tokens = bucket?.[String(chainId)];
1331
1533
  if (!tokens) return null;
1332
- for (const [symbol, value] of Object.entries(tokens)) {
1333
- if (!matchesAsset(symbol, asset)) continue;
1534
+ for (const [symbol2, value] of Object.entries(tokens)) {
1535
+ if (!matchesAsset(symbol2, asset)) continue;
1334
1536
  const amount = Number(value);
1335
1537
  return Number.isFinite(amount) ? amount : null;
1336
1538
  }
@@ -1347,8 +1549,8 @@ function assetsInSnapshot(entry, chainId) {
1347
1549
  entry.current_earnings_by_token,
1348
1550
  entry.total_earnings_by_token
1349
1551
  ]) {
1350
- for (const symbol of Object.keys(bucket?.[key2] ?? {})) {
1351
- seen.add(symbol.toUpperCase());
1552
+ for (const symbol2 of Object.keys(bucket?.[key2] ?? {})) {
1553
+ seen.add(symbol2.toUpperCase());
1352
1554
  }
1353
1555
  }
1354
1556
  return [...seen];
@@ -1717,13 +1919,13 @@ function resolveRpcUrls(rpcUrls, apiKey = "", baseUrl) {
1717
1919
  42161: resolveRpcUrl(rpcUrls, 42161, apiKey, baseUrl)
1718
1920
  };
1719
1921
  }
1720
- function createPaidRpcClient(chain, rpcUrls) {
1922
+ function createPaidRpcClient(chain2, rpcUrls) {
1721
1923
  const url = resolveRpcUrl(
1722
1924
  rpcUrls,
1723
- chain.id
1925
+ chain2.id
1724
1926
  );
1725
1927
  return createPublicClient({
1726
- chain,
1928
+ chain: chain2,
1727
1929
  transport: http(url, {
1728
1930
  retryCount: PAID_RPC_RETRY_COUNT,
1729
1931
  retryDelay: PAID_RPC_RETRY_DELAY_MS
@@ -1738,8 +1940,8 @@ function headerValue(error, name) {
1738
1940
  return;
1739
1941
  if (seen.has(candidate)) return;
1740
1942
  seen.add(candidate);
1741
- const record = candidate;
1742
- const headers = record.headers;
1943
+ const record2 = candidate;
1944
+ const headers = record2.headers;
1743
1945
  let found;
1744
1946
  if (typeof headers?.get === "function") {
1745
1947
  found = headers.get(name);
@@ -1752,7 +1954,7 @@ function headerValue(error, name) {
1752
1954
  return;
1753
1955
  }
1754
1956
  for (const key2 of ["cause", "details", "response", "error"])
1755
- visit(record[key2], depth + 1);
1957
+ visit(record2[key2], depth + 1);
1756
1958
  }
1757
1959
  visit(error);
1758
1960
  return value;
@@ -1765,16 +1967,16 @@ function statusCode(error) {
1765
1967
  return;
1766
1968
  if (seen.has(candidate)) return;
1767
1969
  seen.add(candidate);
1768
- const record = candidate;
1970
+ const record2 = candidate;
1769
1971
  for (const key2 of ["status", "statusCode"]) {
1770
- const parsed = Number(record[key2]);
1972
+ const parsed = Number(record2[key2]);
1771
1973
  if (Number.isInteger(parsed) && parsed >= 100 && parsed <= 599) {
1772
1974
  status = parsed;
1773
1975
  return;
1774
1976
  }
1775
1977
  }
1776
1978
  for (const key2 of ["cause", "details", "response", "error"])
1777
- visit(record[key2], depth + 1);
1979
+ visit(record2[key2], depth + 1);
1778
1980
  }
1779
1981
  visit(error);
1780
1982
  return status;
@@ -1821,7 +2023,7 @@ async function withPaidRpcDiagnostics(operation, chainId, rpcMethod, agentId) {
1821
2023
  }
1822
2024
 
1823
2025
  // src/agents/zyfai/zyfai.agent.ts
1824
- var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi2([
2026
+ var ERC7579_IS_MODULE_INSTALLED_ABI = parseAbi3([
1825
2027
  "function isModuleInstalled(uint256 moduleTypeId, address module, bytes additionalContext) view returns (bool)"
1826
2028
  ]);
1827
2029
  var WETH_ADDRESS_BY_CHAIN = {
@@ -1834,6 +2036,17 @@ var SPLITTING_ASSETS = ["USDC", "WETH"];
1834
2036
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
1835
2037
  var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
1836
2038
  var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
2039
+ function belowMinimumCeiling(rejection, token, decimals) {
2040
+ if ((rejection.symbol === "ETH" ? "WETH" : rejection.symbol) !== token) return void 0;
2041
+ const ceiling = (display) => {
2042
+ const places = display.split(".")[1]?.length ?? 0;
2043
+ if (places >= decimals) return void 0;
2044
+ return parseUnits(display, decimals) + 5n * 10n ** BigInt(decimals - places - 1);
2045
+ };
2046
+ const idle = ceiling(rejection.idle);
2047
+ const minimum = ceiling(rejection.minimum);
2048
+ return idle === void 0 || minimum === void 0 ? void 0 : idle < minimum ? idle : minimum;
2049
+ }
1837
2050
  var ZyfaiAgent = class _ZyfaiAgent {
1838
2051
  constructor(apiKey, rpcUrls, referralSource) {
1839
2052
  this.apiKey = apiKey;
@@ -2790,8 +3003,10 @@ var ZyfaiAgent = class _ZyfaiAgent {
2790
3003
  // --- IAgent: Fund operations ---
2791
3004
  /** Only after a definitive endpoint rejection, combine a new provider
2792
3005
  * snapshot with the Safe's exact idle balance. Never parse its rounded error
2793
- * amount as proof of zero, or use this to skip sending a new MAX request. */
2794
- async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner) {
3006
+ * amount as proof of zero, or use this to skip sending a new MAX request.
3007
+ * After Zyfai's minimum-withdrawal rejection, exact idle-only dust that fits
3008
+ * within the message's rounded amounts is reported as not withdrawable. */
3009
+ async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner, belowMinimum) {
2795
3010
  const internals = this.sdk;
2796
3011
  const generation = this.earningsGeneration;
2797
3012
  let expired = false;
@@ -2822,22 +3037,31 @@ var ZyfaiAgent = class _ZyfaiAgent {
2822
3037
  { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2823
3038
  this.id
2824
3039
  );
2825
- if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
2826
- const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
2827
- const idle = asset ? await this.getPublicClient(chainId).readContract({
2828
- address: asset.address,
2829
- abi: erc20Abi3,
2830
- functionName: "balanceOf",
2831
- args: [wallet.address]
2832
- }).catch(() => void 0) : void 0;
2833
- assertScope();
2834
- if (idle === 0n) throw new OwneyError(
3040
+ const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
3041
+ if (!asset || portfolio === void 0) return;
3042
+ const empty = hasEmptyWithdrawalSnapshot(portfolio, chainId, token);
3043
+ const ceiling = belowMinimum && belowMinimumCeiling(belowMinimum, token, asset.decimals);
3044
+ if (!empty && ceiling === void 0) return;
3045
+ const idle = await this.getPublicClient(chainId).readContract({
3046
+ address: asset.address,
3047
+ abi: erc20Abi3,
3048
+ functionName: "balanceOf",
3049
+ args: [wallet.address]
3050
+ }).catch(() => void 0);
3051
+ assertScope();
3052
+ if (idle === 0n && empty) throw new OwneyError(
3053
+ "WITHDRAW_INSUFFICIENT_BALANCE",
3054
+ "No funds remain in Zyfai for this asset.",
3055
+ { available: "0", chainId, token },
3056
+ this.id
3057
+ );
3058
+ if (typeof idle === "bigint" && idle > 0n && ceiling !== void 0 && idle < ceiling && hasIdleOnlyWithdrawalSnapshot(portfolio, chainId, token, asset.decimals, idle))
3059
+ throw new OwneyError(
2835
3060
  "WITHDRAW_INSUFFICIENT_BALANCE",
2836
- "No funds remain in Zyfai for this asset.",
2837
- { available: "0", chainId, token },
3061
+ "The remaining Zyfai balance is below Zyfai's minimum withdrawal.",
3062
+ { available: "0", chainId, token, reason: "ZYFAI_BELOW_MINIMUM_WITHDRAWAL", remainingRaw: idle.toString() },
2838
3063
  this.id
2839
3064
  );
2840
- }
2841
3065
  };
2842
3066
  let timer;
2843
3067
  try {
@@ -2888,7 +3112,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
2888
3112
  throw unavailable();
2889
3113
  }
2890
3114
  }
2891
- async withdraw(state, chainId, token, amount, onProgress) {
3115
+ async withdraw(state, chainId, token, amount, onProgress, _gasMode, _onBeforeSubmission, assertOperation) {
2892
3116
  if (amount !== void 0 && (typeof amount !== "string" || !/^[0-9]+$/.test(amount) || BigInt(amount) <= 0n))
2893
3117
  throw new OwneyError("WITHDRAW_INVALID_AMOUNT", "Withdrawal amount must be a positive integer in token base units.", void 0, this.id);
2894
3118
  const validChainId = isValidChainId(chainId);
@@ -2924,7 +3148,11 @@ var ZyfaiAgent = class _ZyfaiAgent {
2924
3148
  throw staleSessionError;
2925
3149
  }
2926
3150
  };
2927
- let observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertCurrent);
3151
+ const assertSubmission = () => {
3152
+ assertCurrent();
3153
+ assertOperation?.();
3154
+ };
3155
+ let observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission);
2928
3156
  const authToken = internals.httpClient?.authToken;
2929
3157
  let raw2;
2930
3158
  try {
@@ -2943,7 +3171,7 @@ var ZyfaiAgent = class _ZyfaiAgent {
2943
3171
  }
2944
3172
  if (!amount && observation.rejected() && observation.request()?.failure === "insufficient-balance") {
2945
3173
  try {
2946
- await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner);
3174
+ await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner, observation.belowMinimum());
2947
3175
  } catch (proofError) {
2948
3176
  if (proofError instanceof OwneyError && (proofError.code === "WITHDRAW_INSUFFICIENT_BALANCE" || proofError.code === "NOT_CONNECTED")) throw proofError;
2949
3177
  }
@@ -2956,9 +3184,10 @@ var ZyfaiAgent = class _ZyfaiAgent {
2956
3184
  { reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
2957
3185
  this.id
2958
3186
  );
3187
+ assertSubmission();
2959
3188
  await this.recoverWithdrawalSession(state, validChainId, token, owner);
2960
3189
  observation.stop();
2961
- observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertCurrent);
3190
+ observation = observeWithdrawalSubmission(executionSdk, validChainId, token, assertSubmission);
2962
3191
  }
2963
3192
  }
2964
3193
  if (!raw2) throw new OwneyError(
@@ -3352,7 +3581,7 @@ import {
3352
3581
  erc4626Abi as erc4626Abi2,
3353
3582
  getAddress as getAddress5,
3354
3583
  isAddress as isAddress4,
3355
- parseUnits
3584
+ parseUnits as parseUnits2
3356
3585
  } from "viem";
3357
3586
  import { base as base4 } from "viem/chains";
3358
3587
 
@@ -3503,6 +3732,11 @@ var YieldseekerAuth = class {
3503
3732
  key(state, chainId) {
3504
3733
  return `${state.walletAddress.toLowerCase()}:${chainId}:${resolveSiweOrigin(this.dependencies.origin).origin}`;
3505
3734
  }
3735
+ /** Observation must never open a sign-in prompt. */
3736
+ peekToken(state, chainId) {
3737
+ const token = this.tokens.get(this.key(state, chainId)) ?? readYieldseekerSession(state.walletAddress, chainId);
3738
+ return token && this.isReusable(token, state, chainId) ? token : void 0;
3739
+ }
3506
3740
  async getToken(state, chainId) {
3507
3741
  const key2 = this.key(state, chainId);
3508
3742
  const scope = { address: state.walletAddress, chainId };
@@ -3687,10 +3921,10 @@ var YieldseekerApiError = class extends Error {
3687
3921
  };
3688
3922
  function providerError(body, fallback2) {
3689
3923
  if (!body || typeof body !== "object") return { code: fallback2 };
3690
- const record = body;
3924
+ const record2 = body;
3691
3925
  return {
3692
- code: typeof record.message === "string" ? record.message : fallback2,
3693
- fields: record.fields && typeof record.fields === "object" ? record.fields : void 0
3926
+ code: typeof record2.message === "string" ? record2.message : fallback2,
3927
+ fields: record2.fields && typeof record2.fields === "object" ? record2.fields : void 0
3694
3928
  };
3695
3929
  }
3696
3930
  var YieldseekerApiClient = class {
@@ -3844,16 +4078,16 @@ function riskAdjustedApyForDays(option, days) {
3844
4078
  if (days === "30D") return option.riskAdjustedApy30dAverage;
3845
4079
  return (option.riskAdjustedApy7dAverage + option.riskAdjustedApy30dAverage) / 2;
3846
4080
  }
3847
- function assetAddressValue(record, address) {
3848
- const entry = Object.entries(record).find(
4081
+ function assetAddressValue(record2, address) {
4082
+ const entry = Object.entries(record2).find(
3849
4083
  ([key2]) => key2.toLowerCase() === address.toLowerCase()
3850
4084
  );
3851
4085
  return entry?.[1] ?? "0";
3852
4086
  }
3853
4087
  function position(value, asset, baseAssetDecimals) {
3854
- const option = value?.yieldOption;
4088
+ const option = value?.vaultOption;
3855
4089
  if (!option || typeof option.provider !== "string" || typeof option.name !== "string" || typeof option.address !== "string" || !isAddress3(option.address)) {
3856
- return invalid("yield positions", "missing vault metadata");
4090
+ return invalid("vault positions", "missing vault metadata");
3857
4091
  }
3858
4092
  return {
3859
4093
  chain: "BASE",
@@ -3869,10 +4103,10 @@ function position(value, asset, baseAssetDecimals) {
3869
4103
  amount: balanceDecimal(
3870
4104
  value.assetsBase,
3871
4105
  baseAssetDecimals,
3872
- "yield positions"
4106
+ "vault positions"
3873
4107
  ),
3874
- amountRaw: balanceRaw(value.assetsRaw, "yield positions").toString(),
3875
- withdrawableAmountRaw: balanceRaw(value.withdrawableAssetsRaw, "yield positions").toString(),
4108
+ amountRaw: balanceRaw(value.assetsRaw, "vault positions").toString(),
4109
+ withdrawableAmountRaw: balanceRaw(value.withdrawableAssetsRaw, "vault positions").toString(),
3876
4110
  apy: percent(option.riskAdjustedApy),
3877
4111
  tvl: Number(option.totalDepositsUsd),
3878
4112
  liquidity: Number(option.withdrawableDepositsUsd)
@@ -4091,52 +4325,82 @@ function actionEntry(action) {
4091
4325
  rebalanceLog: []
4092
4326
  };
4093
4327
  }
4094
- function allocationEntry(action, context) {
4095
- if (action.actionType.toLowerCase() !== "autoseek") return void 0;
4096
- const hash2 = action.details?.depositTransactionHash;
4097
- const deposits = action.details?.deposits;
4098
- if (typeof hash2 !== "string" || !/^0x[a-fA-F0-9]{64}$/.test(hash2) || !Array.isArray(deposits) || deposits.length === 0) return void 0;
4099
- const positions = [];
4100
- let amountRaw = 0n;
4101
- for (const deposit of deposits) {
4102
- if (!deposit || typeof deposit !== "object") return void 0;
4103
- const leg = deposit;
4104
- const symbol = typeof leg.assetSymbol === "string" ? leg.assetSymbol.toUpperCase() : "";
4105
- if (symbol !== context.asset && !(context.asset === "WETH" && symbol === "ETH")) return void 0;
4328
+ var TX_HASH = /^0x[a-fA-F0-9]{64}$/;
4329
+ function allocationLegs(value, context, requireAmount) {
4330
+ if (!Array.isArray(value)) return void 0;
4331
+ const legs = [];
4332
+ for (const item of value) {
4333
+ if (!item || typeof item !== "object") return void 0;
4334
+ const leg = item;
4335
+ const symbol2 = typeof leg.assetSymbol === "string" ? leg.assetSymbol.toUpperCase() : "";
4336
+ if ((requireAmount || symbol2) && symbol2 !== context.asset && !(context.asset === "WETH" && symbol2 === "ETH"))
4337
+ return void 0;
4106
4338
  if (typeof leg.vaultAddress !== "string" || !isAddress3(leg.vaultAddress)) return void 0;
4107
4339
  const rawAmount2 = typeof leg.rawAmount === "string" && /^\d+$/.test(leg.rawAmount) ? BigInt(leg.rawAmount) : typeof leg.rawAmount === "number" && Number.isSafeInteger(leg.rawAmount) ? BigInt(leg.rawAmount) : void 0;
4108
- if (rawAmount2 === void 0 || rawAmount2 <= 0n) return void 0;
4109
- const option = context.positions.find((position2) => position2.yieldOption.address.toLowerCase() === leg.vaultAddress.toLowerCase())?.yieldOption;
4110
- positions.push({
4340
+ if (requireAmount && (rawAmount2 === void 0 || rawAmount2 <= 0n)) return void 0;
4341
+ const vault = leg.vaultAddress.toLowerCase();
4342
+ const option = context.positions.find((position2) => position2.vaultOption.address.toLowerCase() === vault)?.vaultOption;
4343
+ const receipt = context.historic?.assets?.find((asset) => asset.address.toLowerCase() === vault && !asset.isSpam);
4344
+ legs.push({
4111
4345
  protocol: typeof leg.vaultProvider === "string" && leg.vaultProvider ? leg.vaultProvider : option?.provider || "Vault",
4112
- pool: typeof leg.vaultName === "string" && leg.vaultName ? leg.vaultName : option?.name,
4113
- tokenSymbol: context.asset,
4114
- amount: formatUnits3(rawAmount2, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
4346
+ pool: typeof leg.vaultName === "string" && leg.vaultName ? leg.vaultName : option?.name ?? (receipt?.name || receipt?.symbol || void 0),
4347
+ rawAmount: rawAmount2
4115
4348
  });
4116
- amountRaw += rawAmount2;
4117
4349
  }
4350
+ return legs;
4351
+ }
4352
+ function allocationEntry(action, context) {
4353
+ if (action.actionType.toLowerCase() !== "autoseek") return void 0;
4354
+ const hash2 = action.details?.depositTransactionHash;
4355
+ const withdrawHash = action.details?.withdrawTransactionHash;
4356
+ if (typeof hash2 !== "string" || !TX_HASH.test(hash2)) return null;
4357
+ const deposits = allocationLegs(action.details?.deposits, context, true);
4358
+ const withdrawals = action.details?.withdrawals == null ? [] : allocationLegs(action.details.withdrawals, context, false);
4359
+ if (!deposits?.length || !withdrawals) return null;
4360
+ const decimals = YIELDSEEKER_ASSET_METADATA[context.asset].decimals;
4361
+ const positions = deposits.map((leg) => ({
4362
+ protocol: leg.protocol,
4363
+ pool: leg.pool,
4364
+ tokenSymbol: context.asset,
4365
+ amount: formatUnits3(leg.rawAmount, decimals)
4366
+ }));
4367
+ const amountRaw = deposits.reduce((total, leg) => total + leg.rawAmount, 0n);
4368
+ const rebalance = withdrawals.length > 0;
4369
+ const hashes = rebalance && typeof withdrawHash === "string" && TX_HASH.test(withdrawHash) && withdrawHash.toLowerCase() !== hash2.toLowerCase() ? [hash2, withdrawHash] : [hash2];
4370
+ const source = withdrawals.length === 1 ? withdrawals[0] : {
4371
+ protocol: [...new Set(withdrawals.map((leg) => leg.protocol))].join(" + "),
4372
+ pool: withdrawals.map((leg) => leg.pool).filter(Boolean).join(" + ") || void 0
4373
+ };
4118
4374
  return {
4119
4375
  agent: "yieldseeker",
4120
- action: "Deposit",
4376
+ action: rebalance ? "Rebalance" : "Deposit",
4121
4377
  date: action.createdDate,
4122
4378
  oldApy: null,
4123
4379
  newApy: null,
4124
4380
  positions,
4125
- rebalanceLog: [],
4381
+ rebalanceLog: rebalance ? deposits.map((leg) => ({
4382
+ fromProtocol: source.protocol,
4383
+ ...source.pool ? { fromPool: source.pool } : {},
4384
+ toProtocol: leg.protocol,
4385
+ ...leg.pool ? { toPool: leg.pool } : {},
4386
+ tokenSymbol: context.asset,
4387
+ amount: formatUnits3(leg.rawAmount, decimals),
4388
+ status: "success"
4389
+ })) : [],
4126
4390
  transactions: [{
4127
- txHashes: [hash2],
4391
+ txHashes: hashes,
4128
4392
  chainId: context.agent.chainId,
4129
4393
  tokenAddress: context.agent.assetAddress,
4130
4394
  tokenSymbol: context.asset,
4131
4395
  agentWallet: context.wallet.walletAddress,
4132
4396
  amountRaw: String(amountRaw),
4133
- amount: formatUnits3(amountRaw, YIELDSEEKER_ASSET_METADATA[context.asset].decimals)
4397
+ amount: formatUnits3(amountRaw, decimals)
4134
4398
  }]
4135
4399
  };
4136
4400
  }
4137
4401
  function depositDestination(context, movement) {
4138
4402
  const to = movement.toAddress.toLowerCase();
4139
- const option = context.positions.find(({ yieldOption }) => yieldOption.chainId === movement.chainId && yieldOption.assetAddress.toLowerCase() === context.agent.assetAddress.toLowerCase() && yieldOption.address.toLowerCase() === to)?.yieldOption;
4403
+ const option = context.positions.find(({ vaultOption }) => vaultOption.chainId === movement.chainId && vaultOption.assetAddress.toLowerCase() === context.agent.assetAddress.toLowerCase() && vaultOption.address.toLowerCase() === to)?.vaultOption;
4140
4404
  if (option) return { protocol: option.provider || "Vault", pool: option.name, tokenSymbol: context.asset };
4141
4405
  const receipt = context.historic?.assets.find((asset) => asset.chainId === movement.chainId && !asset.isSpam && asset.address.toLowerCase() === to && asset.address.toLowerCase() !== context.agent.assetAddress.toLowerCase());
4142
4406
  const minted = receipt && context.historic?.movements.some((candidate) => candidate.chainId === movement.chainId && candidate.transactionHash.toLowerCase() === movement.transactionHash.toLowerCase() && candidate.assetAddress.toLowerCase() === receipt.address.toLowerCase() && candidate.fromAddress.toLowerCase() === "0x0000000000000000000000000000000000000000" && candidate.toAddress.toLowerCase() === context.wallet.walletAddress.toLowerCase() && raw(candidate.assetAmount, "historic position") > 0n);
@@ -4201,6 +4465,23 @@ function mapYieldseekerHistory(contexts, options) {
4201
4465
  seenMovements.add(key2);
4202
4466
  return true;
4203
4467
  });
4468
+ const allocations = /* @__PURE__ */ new Map();
4469
+ const rebalanceHashes = /* @__PURE__ */ new Set();
4470
+ const actions = [];
4471
+ for (const action of context.actions ?? []) {
4472
+ const allocation = allocationEntry(action, context);
4473
+ if (allocation === null) continue;
4474
+ if (!allocation) {
4475
+ const entry = actionEntry(action);
4476
+ if (entry) actions.push(entry);
4477
+ continue;
4478
+ }
4479
+ const hash2 = allocation.transactions[0].txHashes[0].toLowerCase();
4480
+ if (allocations.has(hash2)) continue;
4481
+ allocations.set(hash2, allocation);
4482
+ if (allocation.action === "Rebalance")
4483
+ for (const value of allocation.transactions[0].txHashes) rebalanceHashes.add(value.toLowerCase());
4484
+ }
4204
4485
  const movementEntries = movements.map(
4205
4486
  (movement) => movementEntry(
4206
4487
  movement,
@@ -4211,19 +4492,10 @@ function mapYieldseekerHistory(contexts, options) {
4211
4492
  options.vaultAddresses,
4212
4493
  depositDestination(context, movement)
4213
4494
  )
4214
- ).filter((entry) => entry !== void 0);
4495
+ ).filter((entry) => entry !== void 0 && !((entry.action === "Deposit" || entry.action === "Top up") && rebalanceHashes.has(entry.transactions[0]?.txHashes[0]?.toLowerCase() ?? "")));
4215
4496
  const deposits = new Set(movementEntries.filter((entry) => entry.action === "Deposit").map((entry) => entry.transactions[0]?.txHashes[0]?.toLowerCase()));
4216
- const actions = (context.actions ?? []).flatMap((action) => {
4217
- const allocation = allocationEntry(action, context);
4218
- if (allocation) {
4219
- const hash2 = allocation.transactions[0].txHashes[0].toLowerCase();
4220
- if (deposits.has(hash2)) return [];
4221
- deposits.add(hash2);
4222
- return [allocation];
4223
- }
4224
- const entry = actionEntry(action);
4225
- return entry ? [entry] : [];
4226
- });
4497
+ for (const [hash2, allocation] of allocations)
4498
+ if (allocation.action === "Rebalance" || !deposits.has(hash2)) actions.push(allocation);
4227
4499
  return [...movementEntries, ...actions];
4228
4500
  });
4229
4501
  const grouped = /* @__PURE__ */ new Map();
@@ -4301,8 +4573,8 @@ function mapYieldseekerProfile(address, contexts) {
4301
4573
  const protocols = /* @__PURE__ */ new Set();
4302
4574
  for (const context of contexts) {
4303
4575
  for (const current of context.positions) {
4304
- if (current.yieldOption?.provider) {
4305
- protocols.add(String(current.yieldOption.provider));
4576
+ if (current.vaultOption?.provider) {
4577
+ protocols.add(String(current.vaultOption.provider));
4306
4578
  }
4307
4579
  }
4308
4580
  }
@@ -4320,7 +4592,7 @@ function mapYieldseekerAgentApy(options, days) {
4320
4592
  const perAsset = {};
4321
4593
  const all = [];
4322
4594
  for (const entry of options) {
4323
- const apys = entry.yieldOptions.map(
4595
+ const apys = entry.vaultOptions.map(
4324
4596
  (option) => publicApyAfterYieldseekerFee(riskAdjustedApyForDays(option, days))
4325
4597
  ).filter(Number.isFinite);
4326
4598
  if (apys.length === 0) continue;
@@ -4384,13 +4656,13 @@ async function remainingVaultBalance(client, wallet, asset, vaults, blockNumber)
4384
4656
  import {
4385
4657
  getAddress as getAddress3,
4386
4658
  isAddressEqual,
4387
- parseAbi as parseAbi3
4659
+ parseAbi as parseAbi4
4388
4660
  } from "viem";
4389
4661
  var YIELDSEEKER_FACTORY = "0x9c7410a0faCAC60850C46eE5b58B518daec95130";
4390
4662
  var YIELDSEEKER_V1_IMPLEMENTATION = "0x32403455357589523435dB18b414057D7cE05051";
4391
4663
  var YIELDSEEKER_V2_IMPLEMENTATION = "0x613Db9c924ADB7A7A90c11b02Cce20c913E3Fe50";
4392
4664
  var ERC1967_IMPLEMENTATION_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
4393
- var walletAbi = parseAbi3([
4665
+ var walletAbi = parseAbi4([
4394
4666
  "function owner() view returns (address)",
4395
4667
  "function ownerAgentIndex() view returns (uint256)",
4396
4668
  "function baseAsset() view returns (address)",
@@ -4398,7 +4670,7 @@ var walletAbi = parseAbi3([
4398
4670
  "function upgradeToLatest()",
4399
4671
  "event Upgraded(address indexed implementation)"
4400
4672
  ]);
4401
- var factoryAbi = parseAbi3([
4673
+ var factoryAbi = parseAbi4([
4402
4674
  "function userWallets(address owner, uint256 index) view returns (address)",
4403
4675
  "function agentWalletImplementation() view returns (address)"
4404
4676
  ]);
@@ -4548,11 +4820,11 @@ function isHash(value) {
4548
4820
  return typeof value === "string" && /^0x[0-9a-fA-F]{64}$/.test(value);
4549
4821
  }
4550
4822
  async function assertOwnerSession(state, owner) {
4551
- const [chain, accounts] = await Promise.all([
4823
+ const [chain2, accounts] = await Promise.all([
4552
4824
  state.provider.request({ method: "eth_chainId" }),
4553
4825
  state.provider.request({ method: "eth_accounts" })
4554
4826
  ]);
4555
- if (Number(chain) !== 8453)
4827
+ if (Number(chain2) !== 8453)
4556
4828
  throw new OwneyError(
4557
4829
  "CHAIN_MISMATCH",
4558
4830
  "Switch back to Base before withdrawing.",
@@ -4803,6 +5075,7 @@ async function submitEoa(input) {
4803
5075
  chain: base3,
4804
5076
  transport: custom3(input.state.provider)
4805
5077
  });
5078
+ input.assertCurrent?.();
4806
5079
  const signature = await wallet.signMessage({ message: { raw: userOpHash } });
4807
5080
  operation = { ...operation, signature };
4808
5081
  await assertOwnerSession(input.state, input.owner);
@@ -4820,7 +5093,9 @@ async function submitEoa(input) {
4820
5093
  paymasterAndData: operation.paymasterAndData
4821
5094
  };
4822
5095
  savePending(input.onPending, { userOpHash, userOperation }, true);
5096
+ input.assertCurrent?.();
4823
5097
  await checkpoint(input.onBeforeSubmission, { userOpHash, userOperation });
5098
+ input.assertCurrent?.();
4824
5099
  const submittedHash = await rpc(
4825
5100
  input.rpcUrl,
4826
5101
  "eth_sendUserOperation",
@@ -4887,7 +5162,9 @@ async function submitSmart(input) {
4887
5162
  assertIntentFresh(input.expiresAt);
4888
5163
  const requestedId = toHex(globalThis.crypto.getRandomValues(new Uint8Array(32)));
4889
5164
  savePending(input.onPending, { callsId: requestedId }, true);
5165
+ input.assertCurrent?.();
4890
5166
  await checkpoint(input.onBeforeSubmission, { callsId: requestedId });
5167
+ input.assertCurrent?.();
4891
5168
  try {
4892
5169
  sent = await input.state.provider.request({
4893
5170
  method: "wallet_sendCalls",
@@ -5014,7 +5291,7 @@ var OWNEY_AGENT_RULE_PRESET = "explorative";
5014
5291
  var YIELDSEEKER_USERNAME_PREFIX = "owney_";
5015
5292
  var YIELDSEEKER_USERNAME_RANDOM_LENGTH = 14;
5016
5293
  var YIELDSEEKER_USERNAME_CREATE_ATTEMPTS = 3;
5017
- var YIELDSEEKER_YIELD_OPTIONS_CACHE_MS = 6e4;
5294
+ var YIELDSEEKER_VAULT_OPTIONS_CACHE_MS = 6e4;
5018
5295
  var YIELDSEEKER_AGENT_LIST_CACHE_MS = 12e4;
5019
5296
  var YIELDSEEKER_PORTFOLIO_CACHE_MS = 12e4;
5020
5297
  var YIELDSEEKER_SETTLEMENT_CACHE_MS = 5e3;
@@ -5117,6 +5394,7 @@ var YieldseekerAgent = class {
5117
5394
  pendingReads = /* @__PURE__ */ new Map();
5118
5395
  readGeneration = 0;
5119
5396
  portfolioVersions = /* @__PURE__ */ new Map();
5397
+ liquidityVersions = /* @__PURE__ */ new Map();
5120
5398
  snapshotFailures = /* @__PURE__ */ new Map();
5121
5399
  standardReadFailures = /* @__PURE__ */ new Map();
5122
5400
  reconcileUntil = /* @__PURE__ */ new Map();
@@ -5126,8 +5404,8 @@ var YieldseekerAgent = class {
5126
5404
  // settlement; keep slow reconciliation after the fast window expires.
5127
5405
  snapshotMovements = /* @__PURE__ */ new Map();
5128
5406
  movementEpochs = /* @__PURE__ */ new Map();
5129
- yieldOptions = /* @__PURE__ */ new Map();
5130
- pendingYieldOptions = /* @__PURE__ */ new Map();
5407
+ vaultOptions = /* @__PURE__ */ new Map();
5408
+ pendingVaultOptions = /* @__PURE__ */ new Map();
5131
5409
  constructor(owneyApiKey, options = {}) {
5132
5410
  this.owneyApiKey = owneyApiKey;
5133
5411
  this.routingApiBaseUrl = options.routingApiBaseUrl;
@@ -5421,6 +5699,7 @@ var YieldseekerAgent = class {
5421
5699
  this.readCache.clear();
5422
5700
  this.pendingReads.clear();
5423
5701
  this.portfolioVersions.clear();
5702
+ this.liquidityVersions.clear();
5424
5703
  this.snapshotFailures.clear();
5425
5704
  this.standardReadFailures.clear();
5426
5705
  this.reconcileUntil.clear();
@@ -5528,7 +5807,7 @@ var YieldseekerAgent = class {
5528
5807
  notifyFunding(onProgress, evidence);
5529
5808
  return { txHash, smartWallet: context.wallet.walletAddress, amount, evidence };
5530
5809
  }
5531
- async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto", onBeforeSubmission) {
5810
+ async withdraw(state, chainId, asset, amount, onProgress, gasMode = "auto", onBeforeSubmission, assertOperation) {
5532
5811
  let ownerTransferAttempted = false;
5533
5812
  let evidence = {
5534
5813
  agentId: this.id,
@@ -5546,7 +5825,7 @@ var YieldseekerAgent = class {
5546
5825
  notifyFunding(onProgress, evidence);
5547
5826
  }, () => {
5548
5827
  ownerTransferAttempted = true;
5549
- }, gasMode, onBeforeSubmission);
5828
+ }, gasMode, onBeforeSubmission, assertOperation);
5550
5829
  } catch (error) {
5551
5830
  if (error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED") this.withdrawalVersionCache.clear();
5552
5831
  if (!ownerTransferAttempted || error?.code === 4001 || error instanceof OwneyError && (error.code === "AGENT_TRANSACTION_REVERTED" || error.details?.ownerTransferNotExecuted === true || error.code === "SPONSOR_REQUEST_FAILED" && (error.details?.safeToFallback === true || error.details?.executionFailed === true)))
@@ -5559,7 +5838,7 @@ var YieldseekerAgent = class {
5559
5838
  throw error;
5560
5839
  }
5561
5840
  }
5562
- async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto", onBeforeSubmission) {
5841
+ async withdrawInternal(state, chainId, asset, amount, onProgress, onOwnerTransferAttempted, gasMode = "auto", onBeforeSubmission, assertOperation) {
5563
5842
  this.assertChain(chainId);
5564
5843
  this.assertAsset(asset);
5565
5844
  if (amount !== void 0 && BigInt(amount) <= 0n) {
@@ -5677,7 +5956,7 @@ var YieldseekerAgent = class {
5677
5956
  const initialFunds = funds;
5678
5957
  const idle = initialFunds.idle;
5679
5958
  const maxLiquidity = amount === void 0 ? funds : void 0;
5680
- let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.resolvedVaults.includes(position2.yieldOption.address.toLowerCase()))];
5959
+ let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.resolvedVaults.includes(position2.vaultOption.address.toLowerCase()))];
5681
5960
  let positionsStatus = "empty";
5682
5961
  const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
5683
5962
  validatePositions(positions);
@@ -5685,18 +5964,18 @@ var YieldseekerAgent = class {
5685
5964
  const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
5686
5965
  const knownPositions = /* @__PURE__ */ new Map();
5687
5966
  const rememberPositions = (values) => {
5688
- for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
5967
+ for (const position2 of values) knownPositions.set(position2.vaultOption.address.toLowerCase(), position2);
5689
5968
  };
5690
5969
  const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
5691
5970
  const verifiedPositions = [];
5692
5971
  if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
5693
- let active = true;
5694
- const checks = this.readVaultShares(values.map((position2) => position2.yieldOption), agentWallet, blockNumber).catch(() => []);
5972
+ let active2 = true;
5973
+ const checks = this.readVaultShares(values.map((position2) => position2.vaultOption), agentWallet, blockNumber).catch(() => []);
5695
5974
  const reads = values.map(async (position2, index) => {
5696
- const address = getAddress5(position2.yieldOption.address);
5975
+ const address = getAddress5(position2.vaultOption.address);
5697
5976
  try {
5698
5977
  const group = (await checks).slice(index * 2, index * 2 + 2);
5699
- if (!active || group.length !== 2 || group.some((value) => value.status !== "success")) return null;
5978
+ if (!active2 || group.length !== 2 || group.some((value) => value.status !== "success")) return null;
5700
5979
  this.assertSession(generation, chainId, asset);
5701
5980
  const [underlying, shares] = group.map((value) => value.result);
5702
5981
  if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n) return null;
@@ -5712,7 +5991,7 @@ var YieldseekerAgent = class {
5712
5991
  args: [shares],
5713
5992
  blockNumber
5714
5993
  });
5715
- if (!active || typeof value !== "bigint" || value < 0n) return null;
5994
+ if (!active2 || typeof value !== "bigint" || value < 0n) return null;
5716
5995
  this.assertSession(generation, chainId, asset);
5717
5996
  if (value > 0n && !positiveValue) {
5718
5997
  positiveValue = true;
@@ -5740,7 +6019,7 @@ var YieldseekerAgent = class {
5740
6019
  }
5741
6020
  });
5742
6021
  const result = await boundedVerification(Promise.all(reads), timeoutMs);
5743
- active = false;
6022
+ active2 = false;
5744
6023
  this.assertSession(generation, chainId, asset);
5745
6024
  return {
5746
6025
  verified: result !== void 0 && result.every((position2) => position2 !== null),
@@ -5752,13 +6031,14 @@ var YieldseekerAgent = class {
5752
6031
  const response = await this.walletRequest(
5753
6032
  state,
5754
6033
  chainId,
5755
- this.agentPath(context, "yield-positions")
6034
+ this.agentPath(context, "vault-positions")
5756
6035
  );
5757
6036
  this.assertSession(generation, chainId, asset);
5758
- if (!Array.isArray(response?.yieldPositions)) throw this.invalidResponse("yield positions");
5759
- return validatePositions(response.yieldPositions);
6037
+ if (!Array.isArray(response?.vaultPositions)) throw this.invalidResponse("vault positions");
6038
+ return validatePositions(response.vaultPositions);
5760
6039
  };
5761
6040
  if (amount === void 0) {
6041
+ rememberPositions(priorPositions);
5762
6042
  rememberPositions(funds.indexedPositions);
5763
6043
  rememberPositions(positions);
5764
6044
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
@@ -5785,7 +6065,7 @@ var YieldseekerAgent = class {
5785
6065
  throw new OwneyError(
5786
6066
  "WITHDRAW_FAILED",
5787
6067
  "Yieldseeker vault funds are not currently withdrawable.",
5788
- { asset, positionsStatus, remainingPositions: positions.map((position2) => position2.yieldOption.address) },
6068
+ { asset, positionsStatus, remainingPositions: positions.map((position2) => position2.vaultOption.address) },
5789
6069
  this.id
5790
6070
  );
5791
6071
  }
@@ -5834,6 +6114,7 @@ var YieldseekerAgent = class {
5834
6114
  let latestUnwindBlock;
5835
6115
  const unwind = async (position2, assetsRaw) => {
5836
6116
  this.assertSession(generation, chainId, asset);
6117
+ assertOperation?.();
5837
6118
  let unwindTransactionHash;
5838
6119
  try {
5839
6120
  const response = await this.walletRequest(
@@ -5842,7 +6123,8 @@ var YieldseekerAgent = class {
5842
6123
  this.agentPath(context, "withdraw-from-position"),
5843
6124
  {
5844
6125
  method: "POST",
5845
- body: { chainId, vaultAddress: position2.yieldOption.address, assetsRaw: assetsRaw.toString() }
6126
+ body: { chainId, vaultAddress: position2.vaultOption.address, assetsRaw: assetsRaw.toString() },
6127
+ assertCurrent: assertOperation
5846
6128
  }
5847
6129
  );
5848
6130
  if (!this.isTransactionHash(response?.transactionHash)) throw this.invalidResponse("position withdrawal");
@@ -5861,7 +6143,7 @@ var YieldseekerAgent = class {
5861
6143
  ...error instanceof OwneyError ? error.details : {},
5862
6144
  withdrawalPhase: "unwind",
5863
6145
  reason: "YIELDSEEKER_UNWIND_UNRESOLVED",
5864
- vaultAddress: position2.yieldOption.address,
6146
+ vaultAddress: position2.vaultOption.address,
5865
6147
  unwindTransactionHash,
5866
6148
  ownerTransferSubmitted: false
5867
6149
  },
@@ -5874,7 +6156,7 @@ var YieldseekerAgent = class {
5874
6156
  };
5875
6157
  if (amount === void 0) {
5876
6158
  for (let sweep = 0; sweep < YIELDSEEKER_MAX_WITHDRAW_SWEEPS && hasPositions(positions); sweep++) {
5877
- const before = new Map(positions.map((position2) => [position2.yieldOption.address.toLowerCase(), BigInt(position2.withdrawableAssetsRaw)]));
6159
+ const before = new Map(positions.map((position2) => [position2.vaultOption.address.toLowerCase(), BigInt(position2.withdrawableAssetsRaw)]));
5878
6160
  let unwound = false;
5879
6161
  for (const position2 of positions) {
5880
6162
  const available = BigInt(position2.withdrawableAssetsRaw);
@@ -5891,16 +6173,16 @@ var YieldseekerAgent = class {
5891
6173
  if (catalog) rememberPositions(catalog);
5892
6174
  this.assertSession(generation, chainId, asset);
5893
6175
  if (proof.verified) {
5894
- positions = [...proof.positions, ...(catalog ?? []).filter((position2) => !knownBeforeRefresh.has(position2.yieldOption.address.toLowerCase()))];
6176
+ positions = [...proof.positions, ...(catalog ?? []).filter((position2) => !knownBeforeRefresh.has(position2.vaultOption.address.toLowerCase()))];
5895
6177
  } else if (catalog) {
5896
- const checked = new Set(proof.positions.map((position2) => position2.yieldOption.address.toLowerCase()));
5897
- positions = [...proof.positions, ...catalog.filter((position2) => !checked.has(position2.yieldOption.address.toLowerCase()))];
6178
+ const checked = new Set(proof.positions.map((position2) => position2.vaultOption.address.toLowerCase()));
6179
+ positions = [...proof.positions, ...catalog.filter((position2) => !checked.has(position2.vaultOption.address.toLowerCase()))];
5898
6180
  } else {
5899
6181
  positionsStatus = "unavailable";
5900
6182
  break;
5901
6183
  }
5902
6184
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
5903
- if (positions.some((position2) => BigInt(position2.withdrawableAssetsRaw) > 0n && before.has(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) >= before.get(position2.yieldOption.address.toLowerCase()))) break;
6185
+ if (positions.some((position2) => BigInt(position2.withdrawableAssetsRaw) > 0n && before.has(position2.vaultOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) >= before.get(position2.vaultOption.address.toLowerCase()))) break;
5904
6186
  }
5905
6187
  } else {
5906
6188
  for (const position2 of positions) {
@@ -5913,7 +6195,7 @@ var YieldseekerAgent = class {
5913
6195
  }
5914
6196
  }
5915
6197
  if (amount !== void 0 && remaining > 0n) {
5916
- throw this.invalidResponse("yield positions", {
6198
+ throw this.invalidResponse("vault positions", {
5917
6199
  reason: "Withdrawable positions could not cover the request.",
5918
6200
  remaining: remaining.toString()
5919
6201
  });
@@ -5948,6 +6230,13 @@ var YieldseekerAgent = class {
5948
6230
  const account = getAddress5(state.walletAddress);
5949
6231
  let receipt;
5950
6232
  const base7 = { agentId: this.id, chainId, token: asset, walletAddress: state.walletAddress, agentWallet: context.wallet.walletAddress, intendedAmount: amount ?? "MAX", transfer: "pending", registration: "not-applicable", delivery: "pending" };
6233
+ if (amount === void 0) base7.maxCompletion = {
6234
+ userId: context.user.userId,
6235
+ agentId: context.agent.agentId,
6236
+ vaults: [...knownPositions.keys()],
6237
+ inventoryComplete: funds.complete
6238
+ };
6239
+ if (base7.maxCompletion) notifyFunding(onProgress, base7);
5951
6240
  let submissionEvidence = base7;
5952
6241
  const data = amount === void 0 ? encodeFunctionData2({
5953
6242
  abi: YIELDSEEKER_AGENT_WALLET_ABI,
@@ -5965,6 +6254,7 @@ var YieldseekerAgent = class {
5965
6254
  client: this.getReceiptClient(),
5966
6255
  apiKey: this.owneyApiKey,
5967
6256
  routingApiBaseUrl: this.routingApiBaseUrl,
6257
+ assertCurrent: assertOperation,
5968
6258
  yieldseekerSignature: await this.auth.getToken(state, chainId),
5969
6259
  refreshYieldseekerSignature: (rejected) => this.auth.refreshToken(state, chainId, rejected),
5970
6260
  userId: context.user.userId,
@@ -5977,6 +6267,7 @@ var YieldseekerAgent = class {
5977
6267
  notifyFunding(onProgress, submissionEvidence);
5978
6268
  },
5979
6269
  onBeforeSubmission: async (pending) => {
6270
+ assertOperation?.();
5980
6271
  await onBeforeSubmission?.({ ...base7, ...pending });
5981
6272
  onOwnerTransferAttempted?.();
5982
6273
  }
@@ -5985,15 +6276,26 @@ var YieldseekerAgent = class {
5985
6276
  receipt = submitted.receipt;
5986
6277
  } else {
5987
6278
  onOwnerTransferAttempted?.();
5988
- txHash = await this.submitTransaction(state, chainId, {
5989
- from: account,
5990
- to: getAddress5(context.wallet.walletAddress),
5991
- data,
5992
- value: "0",
5993
- chainId
5994
- }, (hash2) => notifyFunding(onProgress, { ...base7, txHash: hash2 }), (value) => {
5995
- receipt = value;
5996
- });
6279
+ txHash = await this.submitTransaction(
6280
+ state,
6281
+ chainId,
6282
+ {
6283
+ from: account,
6284
+ to: getAddress5(context.wallet.walletAddress),
6285
+ data,
6286
+ value: "0",
6287
+ chainId
6288
+ },
6289
+ (hash2) => notifyFunding(onProgress, { ...base7, txHash: hash2 }),
6290
+ (value) => {
6291
+ receipt = value;
6292
+ },
6293
+ async () => {
6294
+ assertOperation?.();
6295
+ await onBeforeSubmission?.(base7);
6296
+ assertOperation?.();
6297
+ }
6298
+ );
5997
6299
  }
5998
6300
  const evidence = receipt ? inspectFundingReceipt(receipt, { ...submissionEvidence, txHash }, metadata.address, "withdraw") : { ...submissionEvidence, txHash, transfer: "pending", delivery: "pending" };
5999
6301
  notifyFunding(onProgress, evidence);
@@ -6038,7 +6340,7 @@ var YieldseekerAgent = class {
6038
6340
  asset,
6039
6341
  positionsStatus,
6040
6342
  remainingPositions: positionsStatus === "remaining" ? positions.filter((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n)).map((position2) => ({
6041
- vaultAddress: position2.yieldOption.address,
6343
+ vaultAddress: position2.vaultOption.address,
6042
6344
  sharesRaw: position2.assetsRaw,
6043
6345
  withdrawableAmountRaw: position2.withdrawableAssetsRaw
6044
6346
  })) : void 0,
@@ -6047,6 +6349,7 @@ var YieldseekerAgent = class {
6047
6349
  this.id
6048
6350
  );
6049
6351
  }
6352
+ if (evidence.maxCompletion) evidence.maxCompletion.vaults = [...knownPositions.keys()];
6050
6353
  if (!proof.verified) completion = "unverified";
6051
6354
  }
6052
6355
  if (explicitEmptyPayout)
@@ -6104,6 +6407,65 @@ var YieldseekerAgent = class {
6104
6407
  );
6105
6408
  return proof.available;
6106
6409
  }
6410
+ /** Recheck MAX without re-unwinding, paying out, or requesting a signature. */
6411
+ async verifyWithdrawalCompletion(state, evidence) {
6412
+ const unknown = (reason2) => ({ status: "unavailable", reason: reason2 });
6413
+ const proof = evidence.maxCompletion;
6414
+ 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");
6415
+ this.assertAsset(evidence.token);
6416
+ const asset = evidence.token;
6417
+ const metadata = YIELDSEEKER_ASSET_METADATA[asset];
6418
+ const signature = this.auth.peekToken(state, evidence.chainId);
6419
+ if (!signature) return unknown("AUTHENTICATION_REQUIRED");
6420
+ let reason = "CATALOG_UNAVAILABLE";
6421
+ try {
6422
+ return await bounded(async () => {
6423
+ const response = await this.api.request(
6424
+ `/users/${encodeURIComponent(proof.userId)}/agents/${encodeURIComponent(proof.agentId)}/vault-positions`,
6425
+ { signature }
6426
+ );
6427
+ if (!Array.isArray(response?.vaultPositions)) return unknown("CATALOG_INVALID");
6428
+ const positions = this.validateWithdrawalPositions(response.vaultPositions, evidence.chainId, asset);
6429
+ const vaults = new Set([...proof.vaults, ...positions.map((p) => p.vaultOption.address)].map((a) => a.toLowerCase()));
6430
+ if (!proof.inventoryComplete) {
6431
+ const catalog = await this.loadVaultOptions(asset);
6432
+ const eligible = catalog.filter((o) => o.chainId === evidence.chainId && o.assetAddress?.toLowerCase() === metadata.address.toLowerCase() && isAddress4(o.address));
6433
+ if (!eligible.length) return unknown("INVENTORY_INCOMPLETE");
6434
+ eligible.forEach((o) => vaults.add(o.address.toLowerCase()));
6435
+ }
6436
+ if (vaults.size > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT || [...vaults].some((a) => !isAddress4(a)))
6437
+ return unknown("INVENTORY_INVALID");
6438
+ reason = "VAULT_PROOF_UNAVAILABLE";
6439
+ const blockNumber = BigInt(evidence.blockNumber);
6440
+ const addresses = [...vaults];
6441
+ const shares = await this.readVaultShares(addresses.map((address) => ({ address })), evidence.agentWallet, blockNumber);
6442
+ const checks = await Promise.all(addresses.map(async (address, i) => {
6443
+ const group = shares.slice(i * 2, i * 2 + 2);
6444
+ if (group.length !== 2 || group.some((r) => r.status !== "success")) return "unavailable";
6445
+ const [underlying, value] = group.map((r) => r.result);
6446
+ if (typeof underlying !== "string" || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof value !== "bigint" || value < 0n) return "unavailable";
6447
+ if (value === 0n) return "complete";
6448
+ const amounts = await Promise.allSettled(["convertToAssets", "previewRedeem"].map((functionName) => this.getReceiptClient().readContract({
6449
+ address: getAddress5(address),
6450
+ abi: erc4626Abi2,
6451
+ functionName,
6452
+ args: [value],
6453
+ blockNumber
6454
+ })));
6455
+ if (amounts.some((r) => r.status === "fulfilled" && typeof r.value === "bigint" && r.value > 0n)) return "remaining";
6456
+ return amounts.every((r) => r.status === "fulfilled" && r.value === 0n) ? "complete" : "unavailable";
6457
+ }));
6458
+ const status = checks.includes("remaining") ? "remaining" : checks.includes("unavailable") ? "unavailable" : "complete";
6459
+ return {
6460
+ status,
6461
+ checkedVaults: addresses.length,
6462
+ ...status === "unavailable" ? { reason } : status === "remaining" ? { reason: "VAULT_REMAINDER" } : {}
6463
+ };
6464
+ }, 8e3, () => new Error("MAX proof read timed out"));
6465
+ } catch {
6466
+ return unknown(reason);
6467
+ }
6468
+ }
6107
6469
  async readVaultShares(options, wallet, blockNumber) {
6108
6470
  const owner = getAddress5(wallet);
6109
6471
  const batches = Array.from({ length: Math.ceil(options.length / YIELDSEEKER_VAULTS_PER_RPC_BATCH) }, (_, index) => options.slice(index * YIELDSEEKER_VAULTS_PER_RPC_BATCH, (index + 1) * YIELDSEEKER_VAULTS_PER_RPC_BATCH));
@@ -6124,23 +6486,23 @@ var YieldseekerAgent = class {
6124
6486
  const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
6125
6487
  const seen = /* @__PURE__ */ new Set();
6126
6488
  for (const value of values) {
6127
- const option = value?.yieldOption;
6489
+ const option = value?.vaultOption;
6128
6490
  if (!option || typeof option.address !== "string" || !isAddress4(option.address) || seen.has(option.address.toLowerCase()) || option.chainId !== void 0 && option.chainId !== chainId || option.assetAddress !== void 0 && (typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== underlying) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
6129
- throw this.invalidResponse("yield positions");
6491
+ throw this.invalidResponse("vault positions");
6130
6492
  seen.add(option.address.toLowerCase());
6131
6493
  }
6132
6494
  return values;
6133
6495
  }
6134
6496
  withdrawalVaultHints(state, chainId, context) {
6135
6497
  const key2 = this.contextKey(state, chainId, context.asset);
6136
- const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
6137
- const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
6498
+ const positions = this.readCache.get(`positions:${key2}`)?.value?.vaultPositions ?? [];
6499
+ const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.vaultOption.address.toLowerCase(), position2]));
6138
6500
  const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
6139
6501
  const receivedAssets = new Set((historic?.movements ?? []).filter((movement) => movement.chainId === chainId && movement.agentWalletId === context.wallet.agentWalletId && movement.toAddress?.toLowerCase() === context.wallet.walletAddress.toLowerCase()).filter((movement) => typeof movement.assetAddress === "string").map((movement) => movement.assetAddress.toLowerCase()));
6140
- for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
6502
+ for (const option of this.vaultOptions.get(context.asset)?.value ?? []) {
6141
6503
  if (option.chainId !== chainId || typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== YIELDSEEKER_ASSET_METADATA[context.asset].address.toLowerCase() || !isAddress4(option.address) || !receivedAssets.has(option.address.toLowerCase()) || hints.has(option.address.toLowerCase())) continue;
6142
6504
  hints.set(option.address.toLowerCase(), {
6143
- yieldOption: option,
6505
+ vaultOption: option,
6144
6506
  assetsRaw: "0",
6145
6507
  assetsBase: "0",
6146
6508
  assetsUsd: 0,
@@ -6174,7 +6536,7 @@ var YieldseekerAgent = class {
6174
6536
  async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
6175
6537
  const generation = this.readGeneration;
6176
6538
  const contextKey = this.contextKey(state, chainId, context.asset);
6177
- const version = this.portfolioVersion(contextKey);
6539
+ const version = this.liquidityVersions.get(contextKey) ?? 0;
6178
6540
  const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
6179
6541
  const unavailable = () => new OwneyError(
6180
6542
  "WITHDRAW_BALANCE_UNAVAILABLE",
@@ -6182,12 +6544,12 @@ var YieldseekerAgent = class {
6182
6544
  { asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
6183
6545
  this.id
6184
6546
  );
6185
- let active = true;
6547
+ let active2 = true;
6186
6548
  let knownProof;
6187
- const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.resolvedVaults.includes(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
6549
+ const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.resolvedVaults.includes(position2.vaultOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
6188
6550
  const assertReadCurrent = () => {
6189
6551
  this.assertSession(generation, chainId, context.asset);
6190
- if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
6552
+ if (!active2 || (this.liquidityVersions.get(contextKey) ?? 0) !== version) throw unavailable();
6191
6553
  };
6192
6554
  const read2 = async () => {
6193
6555
  const client = this.getReceiptClient();
@@ -6198,8 +6560,8 @@ var YieldseekerAgent = class {
6198
6560
  const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
6199
6561
  const candidates = /* @__PURE__ */ new Map();
6200
6562
  for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
6201
- candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
6202
- for (const position2 of indexedPositions) candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
6563
+ candidates.set(position2.vaultOption.address.toLowerCase(), position2.vaultOption);
6564
+ for (const position2 of indexedPositions) candidates.set(position2.vaultOption.address.toLowerCase(), position2.vaultOption);
6203
6565
  const options = [...candidates.values()];
6204
6566
  if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
6205
6567
  const [idle, feeTracker] = await Promise.all([
@@ -6243,15 +6605,15 @@ var YieldseekerAgent = class {
6243
6605
  assertReadCurrent();
6244
6606
  knownProof = proof(false);
6245
6607
  if (requested !== void 0 && knownProof.available >= requested) return knownProof;
6246
- const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
6608
+ const indexed = new Map(indexedPositions.map((position2) => [position2.vaultOption.address.toLowerCase(), position2]));
6247
6609
  const indexedValue = indexedPositions.reduce((sum, position2) => sum + BigInt(position2.withdrawableAssetsRaw), 0n);
6248
- const missingKnownPosition = previousPositions.some((position2) => !indexed.has(position2.yieldOption.address.toLowerCase()));
6610
+ const missingKnownPosition = previousPositions.some((position2) => !indexed.has(position2.vaultOption.address.toLowerCase()));
6249
6611
  const unvaluedPosition = indexedPositions.some((position2) => BigInt(position2.assetsRaw) > 0n && BigInt(position2.withdrawableAssetsRaw) === 0n);
6250
6612
  const indexedAvailable = idle + indexedValue > fees ? idle + indexedValue - fees : 0n;
6251
6613
  if (indexedValue > 0n && !missingKnownPosition && !unvaluedPosition && (requested === void 0 || indexedAvailable >= requested)) {
6252
6614
  const known = this.balanceVaults.get(contextKey) ?? /* @__PURE__ */ new Set();
6253
6615
  for (const position2 of indexedPositions) if (BigInt(position2.withdrawableAssetsRaw) > 0n)
6254
- known.add(position2.yieldOption.address.toLowerCase());
6616
+ known.add(position2.vaultOption.address.toLowerCase());
6255
6617
  this.balanceVaults.set(contextKey, known);
6256
6618
  return {
6257
6619
  blockNumber,
@@ -6275,7 +6637,7 @@ var YieldseekerAgent = class {
6275
6637
  if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n) return false;
6276
6638
  if (shares > 0n) known.add(option.address.toLowerCase());
6277
6639
  const position2 = {
6278
- yieldOption: option,
6640
+ vaultOption: option,
6279
6641
  assetsRaw: shares.toString(),
6280
6642
  assetsBase: "0",
6281
6643
  assetsUsd: 0,
@@ -6331,7 +6693,7 @@ var YieldseekerAgent = class {
6331
6693
  knownProof = proof(false);
6332
6694
  if (requested !== void 0 && knownProof.available >= requested) return knownProof;
6333
6695
  const discrepancy = verified.some((position2) => {
6334
- const row = indexed.get(position2.yieldOption.address.toLowerCase());
6696
+ const row = indexed.get(position2.vaultOption.address.toLowerCase());
6335
6697
  return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
6336
6698
  });
6337
6699
  const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
@@ -6340,7 +6702,7 @@ var YieldseekerAgent = class {
6340
6702
  assertReadCurrent();
6341
6703
  return proof(true);
6342
6704
  }
6343
- const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
6705
+ const catalog = await this.loadVaultOptions(context.asset).catch(() => void 0);
6344
6706
  assertReadCurrent();
6345
6707
  const eligible = /* @__PURE__ */ new Map();
6346
6708
  for (const option of catalog ?? []) {
@@ -6366,7 +6728,7 @@ var YieldseekerAgent = class {
6366
6728
  if (error instanceof OwneyError) throw error;
6367
6729
  throw unavailable();
6368
6730
  } finally {
6369
- active = false;
6731
+ active2 = false;
6370
6732
  if (timer) clearTimeout(timer);
6371
6733
  }
6372
6734
  }
@@ -6380,7 +6742,7 @@ var YieldseekerAgent = class {
6380
6742
  this.balanceContexts.set(read2, { context, generation: this.readGeneration });
6381
6743
  const key2 = this.contextKey(state, chainId, context.asset);
6382
6744
  const known = this.balanceVaults.get(key2) ?? /* @__PURE__ */ new Set();
6383
- for (const position2 of context.positions) known.add(position2.yieldOption.address.toLowerCase());
6745
+ for (const position2 of context.positions) known.add(position2.vaultOption.address.toLowerCase());
6384
6746
  this.balanceVaults.set(key2, known);
6385
6747
  }
6386
6748
  return balance;
@@ -6419,7 +6781,7 @@ var YieldseekerAgent = class {
6419
6781
  const total = idle + invested > fees ? idle + invested - fees : 0n;
6420
6782
  const displayed = balance.assetBalances?.find((item) => item.chainId === evidence.chainId && item.asset === evidence.token);
6421
6783
  if (!displayed || !evidence.confirmedAmount || !/^\d+$/.test(evidence.confirmedAmount)) return void 0;
6422
- const amount = parseUnits(displayed.amount, asset.decimals);
6784
+ const amount = parseUnits2(displayed.amount, asset.decimals);
6423
6785
  if (amount < 0n) return void 0;
6424
6786
  const displayUnit = evidence.token === "USDC" ? 5000n : 500000000000n;
6425
6787
  const movementUnit = BigInt(evidence.confirmedAmount) / 1000n;
@@ -6518,13 +6880,13 @@ var YieldseekerAgent = class {
6518
6880
  });
6519
6881
  const catalog = await Promise.all(
6520
6882
  [...new Set(contexts.map((context) => context.asset))].map(
6521
- (contextAsset) => this.loadYieldOptions(contextAsset)
6883
+ (contextAsset) => this.loadVaultOptions(contextAsset)
6522
6884
  )
6523
6885
  );
6524
6886
  const vaultAddresses = new Set(
6525
6887
  catalog.flat().filter(
6526
- (yieldOption) => yieldOption.chainId === chainId && isAddress4(yieldOption.address)
6527
- ).map((yieldOption) => yieldOption.address.toLowerCase())
6888
+ (vaultOption) => vaultOption.chainId === chainId && isAddress4(vaultOption.address)
6889
+ ).map((vaultOption) => vaultOption.address.toLowerCase())
6528
6890
  );
6529
6891
  return mapYieldseekerHistory(contexts, {
6530
6892
  limit: options?.limit ?? 10,
@@ -6549,30 +6911,30 @@ var YieldseekerAgent = class {
6549
6911
  const assets = requested ? [requested] : ["USDC", "WETH"];
6550
6912
  const values = await Promise.all(
6551
6913
  assets.map(async (asset) => {
6552
- return { asset, yieldOptions: await this.loadYieldOptions(asset) };
6914
+ return { asset, vaultOptions: await this.loadVaultOptions(asset) };
6553
6915
  })
6554
6916
  );
6555
6917
  return mapYieldseekerAgentApy(values, days);
6556
6918
  }
6557
- async loadYieldOptions(asset) {
6558
- const cached = this.yieldOptions.get(asset);
6919
+ async loadVaultOptions(asset) {
6920
+ const cached = this.vaultOptions.get(asset);
6559
6921
  if (cached && cached.expiresAt > Date.now()) return cached.value;
6560
- const pending = this.pendingYieldOptions.get(asset);
6922
+ const pending = this.pendingVaultOptions.get(asset);
6561
6923
  if (pending) return pending;
6562
6924
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
6563
6925
  const request2 = this.api.request(
6564
- `/chains/8453/assets/${metadata.address}/yield-options`
6926
+ `/chains/8453/assets/${metadata.address}/vault-options`
6565
6927
  ).then((response) => {
6566
- if (!Array.isArray(response?.yieldOptions)) {
6567
- throw this.invalidResponse("yield options");
6928
+ if (!Array.isArray(response?.vaultOptions)) {
6929
+ throw this.invalidResponse("vault options");
6568
6930
  }
6569
- this.yieldOptions.set(asset, {
6570
- expiresAt: Date.now() + YIELDSEEKER_YIELD_OPTIONS_CACHE_MS,
6571
- value: response.yieldOptions
6931
+ this.vaultOptions.set(asset, {
6932
+ expiresAt: Date.now() + YIELDSEEKER_VAULT_OPTIONS_CACHE_MS,
6933
+ value: response.vaultOptions
6572
6934
  });
6573
- return response.yieldOptions;
6574
- }).finally(() => this.pendingYieldOptions.delete(asset));
6575
- this.pendingYieldOptions.set(asset, request2);
6935
+ return response.vaultOptions;
6936
+ }).finally(() => this.pendingVaultOptions.delete(asset));
6937
+ this.pendingVaultOptions.set(asset, request2);
6576
6938
  return request2;
6577
6939
  }
6578
6940
  userKey(state, chainId) {
@@ -6882,9 +7244,10 @@ var YieldseekerAgent = class {
6882
7244
  expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
6883
7245
  };
6884
7246
  }
6885
- advancePortfolioVersion(contextKey) {
7247
+ advancePortfolioVersion(contextKey, { snapshotOnly = false } = {}) {
6886
7248
  const version = this.portfolioVersion(contextKey) + 1;
6887
7249
  this.portfolioVersions.set(contextKey, version);
7250
+ if (!snapshotOnly) this.liquidityVersions.set(contextKey, version);
6888
7251
  this.snapshotFailures.delete(contextKey);
6889
7252
  for (const kind of ["snapshot", "positions", "historic", "actions"]) {
6890
7253
  const key2 = `${kind}:${contextKey}`;
@@ -6932,7 +7295,7 @@ var YieldseekerAgent = class {
6932
7295
  if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
6933
7296
  const failure = this.snapshotFailures.get(contextKey);
6934
7297
  if (failure && failure.retryAt > Date.now()) throw failure.error;
6935
- this.advancePortfolioVersion(contextKey);
7298
+ this.advancePortfolioVersion(contextKey, { snapshotOnly: true });
6936
7299
  }
6937
7300
  const version = this.portfolioVersion(contextKey);
6938
7301
  const generation = this.readGeneration;
@@ -7015,17 +7378,17 @@ var YieldseekerAgent = class {
7015
7378
  }
7016
7379
  async loadPortfolioParts(state, chainId, context, options, contextKey) {
7017
7380
  const [positions, historic, actions] = await Promise.all([
7018
- options.includePositions === false ? Promise.resolve({ yieldPositions: [] }) : this.cachedRead(
7381
+ options.includePositions === false ? Promise.resolve({ vaultPositions: [] }) : this.cachedRead(
7019
7382
  `positions:${contextKey}`,
7020
7383
  this.portfolioCacheMs(contextKey),
7021
7384
  async () => {
7022
7385
  const response = await this.walletRequest(
7023
7386
  state,
7024
7387
  chainId,
7025
- this.agentPath(context, "yield-positions")
7388
+ this.agentPath(context, "vault-positions")
7026
7389
  );
7027
- if (!Array.isArray(response?.yieldPositions)) {
7028
- throw this.invalidResponse("yield positions");
7390
+ if (!Array.isArray(response?.vaultPositions)) {
7391
+ throw this.invalidResponse("vault positions");
7029
7392
  }
7030
7393
  return response;
7031
7394
  }
@@ -7050,7 +7413,7 @@ var YieldseekerAgent = class {
7050
7413
  ) : Promise.resolve(void 0)
7051
7414
  ]);
7052
7415
  return {
7053
- positions: positions.yieldPositions,
7416
+ positions: positions.vaultPositions,
7054
7417
  ...historic?.position ? { historic: historic.position } : {},
7055
7418
  ...actions?.actions ? { actions: actions.actions } : {}
7056
7419
  };
@@ -7121,10 +7484,10 @@ var YieldseekerAgent = class {
7121
7484
  }
7122
7485
  async providerRequest(state, chainId, path, options = {}) {
7123
7486
  this.assertChain(chainId);
7124
- const request2 = (signature2) => this.api.request(path, {
7125
- ...options,
7126
- signature: signature2
7127
- });
7487
+ const request2 = (signature2) => {
7488
+ options.assertCurrent?.();
7489
+ return this.api.request(path, { method: options.method, body: options.body, signature: signature2 });
7490
+ };
7128
7491
  let signature = await this.auth.getToken(state, chainId);
7129
7492
  try {
7130
7493
  return await request2(signature);
@@ -7426,45 +7789,57 @@ async function fetchAgentKeys(apiKey, baseUrl = ROUTING_API_BASE_URL2) {
7426
7789
  // src/lib/swap/swap-api.ts
7427
7790
  async function request(baseUrl, apiKey, path, init) {
7428
7791
  const url = `${baseUrl}/api/v1/swap${path}`;
7429
- const res = await fetch(url, {
7430
- method: init?.method ?? "GET",
7431
- headers: {
7432
- "Content-Type": "application/json",
7433
- "x-owney-api-key": apiKey
7434
- },
7435
- ...init ? { body: JSON.stringify(init.body) } : {}
7436
- });
7437
- if (!res.ok) {
7438
- const text = await res.text().catch(() => "");
7439
- if (res.status === 429) {
7440
- throw new OwneyError(
7441
- "SWAP_RATE_LIMITED",
7442
- "Swap provider is rate limiting, retry shortly",
7443
- { statusCode: res.status }
7444
- );
7445
- }
7446
- if (res.status === 403) {
7447
- throw new OwneyError(
7448
- "SWAP_DISABLED",
7449
- "Swap is not enabled for this organization",
7450
- { statusCode: res.status }
7451
- );
7452
- }
7453
- throw new OwneyError(
7454
- "SWAP_REQUEST_FAILED",
7455
- `Swap API error ${res.status}: ${text}`,
7456
- { statusCode: res.status, responseBody: text }
7457
- );
7458
- }
7459
- const json = await res.json();
7460
- if (!json.success) {
7461
- throw new OwneyError(
7792
+ const controller = new AbortController();
7793
+ try {
7794
+ return await bounded(async () => {
7795
+ const res = await fetch(url, {
7796
+ signal: controller.signal,
7797
+ method: init?.method ?? "GET",
7798
+ headers: {
7799
+ "Content-Type": "application/json",
7800
+ "x-owney-api-key": apiKey
7801
+ },
7802
+ ...init ? { body: JSON.stringify(init.body) } : {}
7803
+ });
7804
+ if (!res.ok) {
7805
+ const text = await res.text().catch(() => "");
7806
+ if (res.status === 429) {
7807
+ throw new OwneyError(
7808
+ "SWAP_RATE_LIMITED",
7809
+ "Swap provider is rate limiting, retry shortly",
7810
+ { statusCode: res.status }
7811
+ );
7812
+ }
7813
+ if (res.status === 403) {
7814
+ throw new OwneyError(
7815
+ "SWAP_DISABLED",
7816
+ "Swap is not enabled for this organization",
7817
+ { statusCode: res.status }
7818
+ );
7819
+ }
7820
+ throw new OwneyError(
7821
+ "SWAP_REQUEST_FAILED",
7822
+ `Swap API error ${res.status}: ${text}`,
7823
+ { statusCode: res.status, responseBody: text }
7824
+ );
7825
+ }
7826
+ const json = await res.json();
7827
+ if (!json.success) {
7828
+ throw new OwneyError(
7829
+ "SWAP_REQUEST_FAILED",
7830
+ `Swap API request failed: ${json.message ?? "unknown error"}`,
7831
+ { message: json.message }
7832
+ );
7833
+ }
7834
+ return json.data;
7835
+ }, 2e4, () => new OwneyError(
7462
7836
  "SWAP_REQUEST_FAILED",
7463
- `Swap API request failed: ${json.message ?? "unknown error"}`,
7464
- { message: json.message }
7465
- );
7837
+ "Swap request timed out. Its outcome may still need confirmation.",
7838
+ { requestTimedOut: true, phase: path === "/order" ? "submit" : "read" }
7839
+ ));
7840
+ } finally {
7841
+ controller.abort();
7466
7842
  }
7467
- return json.data;
7468
7843
  }
7469
7844
  function createSwapApi(baseUrl, apiKey) {
7470
7845
  return {
@@ -7770,8 +8145,12 @@ async function awaitBalanceIncrease(options) {
7770
8145
  let lastError;
7771
8146
  for (; ; ) {
7772
8147
  try {
7773
- const balance = await readBalance();
7774
- if (balance > baseline) {
8148
+ const balance = await bounded(
8149
+ readBalance,
8150
+ Math.max(1, Math.min(1e4, deadline - Date.now())),
8151
+ () => new Error("Balance observation timed out")
8152
+ );
8153
+ if (Date.now() <= deadline && balance > baseline) {
7775
8154
  const arrived = balance - baseline;
7776
8155
  debugLog("owney-sdk", "arrival: funds landed", {
7777
8156
  arrived: arrived.toString()
@@ -7797,7 +8176,7 @@ async function awaitBalanceIncrease(options) {
7797
8176
  lastReadError: lastError instanceof Error ? lastError.message : String(lastError)
7798
8177
  } : { arrived: null };
7799
8178
  }
7800
- await sleep(pollMs);
8179
+ await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
7801
8180
  }
7802
8181
  }
7803
8182
 
@@ -7850,7 +8229,7 @@ var DEFAULT_POLL_MS2 = 5e3;
7850
8229
  var MAX_BACKOFF_MS = 3e4;
7851
8230
  var backoffFor = (failures, base7) => Math.min(base7 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS);
7852
8231
  var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
7853
- async function runFusionOrder(deps, options) {
8232
+ async function runFusionOrderInternal(deps, options) {
7854
8233
  const {
7855
8234
  orderHash,
7856
8235
  secrets,
@@ -7859,39 +8238,51 @@ async function runFusionOrder(deps, options) {
7859
8238
  timeoutMs = DEFAULT_TIMEOUT_MS2
7860
8239
  } = options;
7861
8240
  const deadline = deps.now() + timeoutMs;
8241
+ let partial = options.hasPartialFill === true;
8242
+ const timeout = () => new OwneyError(
8243
+ "SWAP_REQUEST_FAILED",
8244
+ "Swap settlement is not confirmed yet. Check its status before starting another swap.",
8245
+ { orderHash, settlementUnconfirmed: true, outcome: partial ? "partially-filled" : "submitted-unknown" }
8246
+ );
8247
+ const assertTime = () => {
8248
+ if (deps.now() >= deadline) throw timeout();
8249
+ };
8250
+ const read2 = async (work) => {
8251
+ assertTime();
8252
+ const result = await bounded(work, Math.min(1e4, deadline - deps.now()), timeout);
8253
+ assertTime();
8254
+ return result;
8255
+ };
8256
+ const pause = async (ms) => {
8257
+ assertTime();
8258
+ await bounded(() => deps.sleep(Math.min(ms, deadline - deps.now())), deadline - deps.now(), timeout);
8259
+ assertTime();
8260
+ };
7862
8261
  let failures = 0;
7863
8262
  const published = /* @__PURE__ */ new Set();
7864
8263
  onStage?.("swapping");
7865
8264
  for (; ; ) {
7866
- if (deps.now() >= deadline) {
7867
- throw new OwneyError(
7868
- "SWAP_REQUEST_FAILED",
7869
- "Timed out waiting for the swap to settle. It may still complete \u2014 check the order status before retrying.",
7870
- // Marks the one failure that means "we stopped watching a live order",
7871
- // as opposed to a verdict or a prompt nobody answered. The executor
7872
- // checks the wallet before believing it: resolvers do not stop filling
7873
- // because we stopped looking (ROUT-242, comment 15980).
7874
- { orderHash, settlementUnconfirmed: true }
7875
- );
7876
- }
8265
+ assertTime();
7877
8266
  let ready;
7878
8267
  try {
7879
- ready = await deps.readyForSecrets(orderHash);
8268
+ ready = await read2(() => deps.readyForSecrets(orderHash));
7880
8269
  } catch {
7881
8270
  ready = {};
7882
8271
  }
8272
+ assertTime();
7883
8273
  for (const fill of ready.fills ?? []) {
8274
+ assertTime();
7884
8275
  if (published.has(fill.idx)) continue;
7885
8276
  const secret = secrets[fill.idx];
7886
8277
  if (secret === void 0) {
7887
8278
  throw new OwneyError(
7888
8279
  "SWAP_REQUEST_FAILED",
7889
8280
  `Swap needs a secret for fill ${fill.idx} that this session does not have. The order will refund once its timelock expires.`,
7890
- { orderHash, fillIndex: fill.idx }
8281
+ { orderHash, fillIndex: fill.idx, settlementUnconfirmed: true, outcome: "submitted-unknown" }
7891
8282
  );
7892
8283
  }
7893
8284
  try {
7894
- await deps.submitSecret(orderHash, secret);
8285
+ await read2(() => deps.submitSecret(orderHash, secret));
7895
8286
  published.add(fill.idx);
7896
8287
  } catch {
7897
8288
  failures += 1;
@@ -7899,29 +8290,34 @@ async function runFusionOrder(deps, options) {
7899
8290
  }
7900
8291
  let status;
7901
8292
  try {
7902
- ({ status } = await deps.orderStatus(orderHash));
8293
+ const state = await read2(() => deps.orderStatus(orderHash));
8294
+ status = state.status;
8295
+ partial ||= status === "partially-filled" || !!state.fills?.length;
7903
8296
  failures = 0;
7904
8297
  } catch {
7905
8298
  failures += 1;
7906
- await deps.sleep(backoffFor(failures, pollIntervalMs));
8299
+ await pause(backoffFor(failures, pollIntervalMs));
7907
8300
  continue;
7908
8301
  }
7909
8302
  if (status === "refunding") onStage?.("refunding");
7910
8303
  if (isSwapTerminal(status)) {
7911
- if (status === "executed") {
8304
+ if (status === "executed" || status === "filled") {
7912
8305
  onStage?.("swapped");
7913
8306
  return { status, filled: true };
7914
8307
  }
7915
- if (status === "refunded") onStage?.("refunded");
8308
+ if (status === "refunded") onStage?.(partial ? "swap-partial" : "refunded");
7916
8309
  throw new OwneyError(
7917
8310
  status === "refunded" ? "SWAP_ORDER_REFUNDED" : status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
7918
- status === "refunded" ? "The swap did not complete and your funds have been returned." : "The swap did not complete in time. Your funds will be returned once the timelock expires.",
7919
- { orderHash, status }
8311
+ partial ? "The swap filled partly. Check the order and both balances before retrying." : status === "refunded" ? "The swap did not complete and your funds have been returned." : "The swap did not complete in time. Your funds will be returned once the timelock expires.",
8312
+ { orderHash, status, outcome: status === "refunded" && !partial ? "refunded" : partial ? "partially-filled" : "refund-pending" }
7920
8313
  );
7921
8314
  }
7922
- await deps.sleep(pollIntervalMs);
8315
+ await pause(pollIntervalMs);
7923
8316
  }
7924
8317
  }
8318
+ function runFusionOrder(deps, options) {
8319
+ return observeOrder(options.orderHash, () => runFusionOrderInternal(deps, options));
8320
+ }
7925
8321
 
7926
8322
  // src/lib/swap/swap.same-chain-runner.ts
7927
8323
  var DEFAULT_POLL_MS3 = 5e3;
@@ -7946,7 +8342,7 @@ function explain(status) {
7946
8342
  return "The swap did not complete. Nothing was exchanged.";
7947
8343
  }
7948
8344
  }
7949
- async function runSameChainOrder(deps, options) {
8345
+ async function runSameChainOrderInternal(deps, options) {
7950
8346
  const {
7951
8347
  orderHash,
7952
8348
  onStage,
@@ -7954,23 +8350,39 @@ async function runSameChainOrder(deps, options) {
7954
8350
  timeoutMs = DEFAULT_TIMEOUT_MS3
7955
8351
  } = options;
7956
8352
  const deadline = deps.now() + timeoutMs;
8353
+ let partial = options.hasPartialFill === true;
8354
+ const timeout = () => new OwneyError(
8355
+ "SWAP_REQUEST_FAILED",
8356
+ "Swap settlement is not confirmed yet. Check its status before starting another swap.",
8357
+ { orderHash, settlementUnconfirmed: true, outcome: partial ? "partially-filled" : "submitted-unknown" }
8358
+ );
8359
+ const assertTime = () => {
8360
+ if (deps.now() >= deadline) throw timeout();
8361
+ };
8362
+ const read2 = async (work) => {
8363
+ assertTime();
8364
+ const result = await bounded(work, Math.min(1e4, deadline - deps.now()), timeout);
8365
+ assertTime();
8366
+ return result;
8367
+ };
8368
+ const pause = async (ms) => {
8369
+ assertTime();
8370
+ await bounded(() => deps.sleep(Math.min(ms, deadline - deps.now())), deadline - deps.now(), timeout);
8371
+ assertTime();
8372
+ };
7957
8373
  let failures = 0;
7958
8374
  onStage?.("swapping");
7959
8375
  for (; ; ) {
7960
- if (deps.now() >= deadline) {
7961
- throw new OwneyError(
7962
- "SWAP_ORDER_EXPIRED",
7963
- "No one filled the swap in time. Nothing was exchanged and your funds are still in your wallet.",
7964
- { orderHash }
7965
- );
7966
- }
8376
+ assertTime();
7967
8377
  let status;
7968
8378
  try {
7969
- ({ status } = await deps.orderStatus(orderHash));
8379
+ const state = await read2(() => deps.orderStatus(orderHash));
8380
+ status = state.status;
8381
+ partial ||= status === "partially-filled" || !!state.fills?.length;
7970
8382
  failures = 0;
7971
8383
  } catch {
7972
8384
  failures += 1;
7973
- await deps.sleep(backoffFor2(failures, pollIntervalMs));
8385
+ await pause(backoffFor2(failures, pollIntervalMs));
7974
8386
  continue;
7975
8387
  }
7976
8388
  if (isSwapTerminal(status)) {
@@ -7980,17 +8392,19 @@ async function runSameChainOrder(deps, options) {
7980
8392
  }
7981
8393
  throw new OwneyError(
7982
8394
  status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
7983
- explain(status),
7984
- { orderHash, status }
8395
+ partial ? "The swap filled partly. Check the order and both balances before retrying." : explain(status),
8396
+ { orderHash, status, outcome: partial ? "partially-filled" : "not-submitted" }
7985
8397
  );
7986
8398
  }
7987
- await deps.sleep(pollIntervalMs);
8399
+ await pause(pollIntervalMs);
7988
8400
  }
7989
8401
  }
8402
+ function runSameChainOrder(deps, options) {
8403
+ return observeOrder(options.orderHash, () => runSameChainOrderInternal(deps, options));
8404
+ }
7990
8405
 
7991
8406
  // src/lib/swap/swap.secret-store.ts
7992
8407
  var KEY_PREFIX4 = "owney.swap.order";
7993
- var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7994
8408
  var storage4 = () => {
7995
8409
  if (typeof window === "undefined") return null;
7996
8410
  try {
@@ -8002,10 +8416,13 @@ var storage4 = () => {
8002
8416
  var keyFor2 = (orderHash) => `${KEY_PREFIX4}.${orderHash}`;
8003
8417
  function saveOrder(order) {
8004
8418
  const store = storage4();
8005
- if (!store) return;
8419
+ if (!store) return false;
8006
8420
  try {
8007
- store.setItem(keyFor2(order.orderHash), JSON.stringify(order));
8421
+ const value = JSON.stringify(order);
8422
+ store.setItem(keyFor2(order.orderHash), value);
8423
+ return store.getItem(keyFor2(order.orderHash)) === value;
8008
8424
  } catch {
8425
+ return false;
8009
8426
  }
8010
8427
  }
8011
8428
  function clearOrder(orderHash) {
@@ -8016,7 +8433,7 @@ function clearOrder(orderHash) {
8016
8433
  } catch {
8017
8434
  }
8018
8435
  }
8019
- function listOrders(now = Date.now()) {
8436
+ function listOrders(_now = Date.now()) {
8020
8437
  const store = storage4();
8021
8438
  if (!store) return [];
8022
8439
  const out = [];
@@ -8031,10 +8448,6 @@ function listOrders(now = Date.now()) {
8031
8448
  if (!raw2) continue;
8032
8449
  try {
8033
8450
  const parsed = JSON.parse(raw2);
8034
- if (now - parsed.createdAt > MAX_AGE_MS) {
8035
- store.removeItem(key2);
8036
- continue;
8037
- }
8038
8451
  if (Array.isArray(parsed.secrets) && parsed.secrets.length > 0) {
8039
8452
  out.push(parsed);
8040
8453
  }
@@ -8075,12 +8488,11 @@ async function awaitWalletPrompt(prompt, what) {
8075
8488
  if (timer) clearTimeout(timer);
8076
8489
  }
8077
8490
  }
8078
- function stoppedWatching(error) {
8079
- return error instanceof OwneyError && error.details?.settlementUnconfirmed === true;
8080
- }
8081
8491
  var DEFAULT_SLIPPAGE = 1;
8082
- async function affordableAmount(deps, quoted) {
8083
- const balance = await deps.readSourceBalance();
8492
+ async function affordableAmount(deps, quoted, reserve = 0n) {
8493
+ const raw2 = await deps.readSourceBalance();
8494
+ const balance = raw2 > reserve ? raw2 - reserve : 0n;
8495
+ if (balance <= 0n) throw new OwneyError("SWAP_REQUEST_FAILED", "No spendable source funds remain for this swap.", { outcome: "not-submitted" });
8084
8496
  if (balance >= quoted) return quoted;
8085
8497
  debugLog("owney-sdk", "swap: trimming to the current source balance", {
8086
8498
  quoted: quoted.toString(),
@@ -8091,48 +8503,72 @@ async function affordableAmount(deps, quoted) {
8091
8503
  }
8092
8504
  async function executeSwap(deps, options) {
8093
8505
  const { quote, walletAddress, onStage } = options;
8094
- debugLog("owney-sdk", "swap: start", {
8095
- rail: quote.rail,
8096
- from: `${quote.src.amount} ${quote.src.symbol} on ${quote.src.chainId}`,
8097
- to: `${quote.dst.symbol} on ${quote.dst.chainId}`,
8098
- expected: quote.dst.amount,
8099
- floor: quote.dstAmountMin
8100
- });
8101
- const before = await deps.readTargetBalance();
8102
- debugLog("owney-sdk", "swap: target balance before", before.toString());
8103
- let result;
8104
- let railError;
8506
+ let active2 = true;
8507
+ const original = deps;
8508
+ const assertActive = () => {
8509
+ if (!active2) throw new OwneyError("SWAP_REQUEST_FAILED", "This swap attempt is no longer active.");
8510
+ };
8511
+ const call = async (work, write = false) => {
8512
+ assertActive();
8513
+ if (write) options.assertCurrent?.();
8514
+ const value = await bounded(
8515
+ work,
8516
+ write ? 3e5 : 2e4,
8517
+ () => new OwneyError(
8518
+ "SWAP_REQUEST_FAILED",
8519
+ "The swap step could not be confirmed. Check its status before retrying.",
8520
+ { settlementUnconfirmed: write, outcome: write ? "submitted-unknown" : "not-submitted" }
8521
+ )
8522
+ );
8523
+ assertActive();
8524
+ if (write) options.assertCurrent?.();
8525
+ return value;
8526
+ };
8527
+ deps = {
8528
+ ...original,
8529
+ api: {
8530
+ ...original.api,
8531
+ buildOrder: (args) => call(() => original.api.buildOrder(args)),
8532
+ swapTx: (args) => call(() => original.api.swapTx(args)),
8533
+ submitOrder: (args) => call(() => original.api.submitOrder(args), true)
8534
+ },
8535
+ readTargetBalance: () => call(original.readTargetBalance),
8536
+ readSourceBalance: () => call(original.readSourceBalance),
8537
+ readAllowance: (spender) => call(() => original.readAllowance(spender)),
8538
+ ensureChain: (chain2) => call(() => original.ensureChain(chain2), true),
8539
+ approve: (spender, amount) => call(() => original.approve(spender, amount), true),
8540
+ sendTransaction: (tx) => call(() => original.sendTransaction(tx), true),
8541
+ signTypedData: (data) => call(() => original.signTypedData(data), true),
8542
+ ...original.buildPermit ? { buildPermit: (spender, amount) => call(() => original.buildPermit(spender, amount), true) } : {}
8543
+ };
8105
8544
  try {
8106
- result = quote.rail === "classic" ? await runClassic(deps, options) : quote.rail === "fusion" ? await runSameChainFusion(deps, options, walletAddress) : await runFusion(deps, options, walletAddress);
8107
- } catch (error) {
8108
- if (!stoppedWatching(error)) throw error;
8109
- railError = error;
8110
- debugLog("owney-sdk", "swap: rail failed, checking whether funds landed", {
8111
- message: error instanceof Error ? error.message : String(error)
8545
+ debugLog("owney-sdk", "swap: start", {
8546
+ rail: quote.rail,
8547
+ from: `${quote.src.amount} ${quote.src.symbol} on ${quote.src.chainId}`,
8548
+ to: `${quote.dst.symbol} on ${quote.dst.chainId}`,
8549
+ expected: quote.dst.amount,
8550
+ floor: quote.dstAmountMin
8112
8551
  });
8113
- }
8114
- const { arrived } = await awaitBalanceIncrease({
8115
- readBalance: () => deps.readTargetBalance(),
8116
- baseline: before,
8117
- ...options.arrivalTimeoutMs === void 0 ? {} : { timeoutMs: options.arrivalTimeoutMs },
8118
- ...options.arrivalPollMs === void 0 ? {} : { pollMs: options.arrivalPollMs }
8119
- });
8120
- if (arrived === null) {
8121
- if (railError) throw railError;
8122
- throw new OwneyError(
8123
- "SWAP_REQUEST_FAILED",
8124
- "The swap completed but no funds arrived in the wallet. Check the transaction before retrying.",
8125
- { rail: quote.rail, ...result ?? {} }
8126
- );
8127
- }
8128
- if (railError) {
8129
- debugLog("owney-sdk", "swap: rail gave up but the funds are here", {
8130
- received: arrived.toString()
8552
+ const before = await deps.readTargetBalance();
8553
+ debugLog("owney-sdk", "swap: target balance before", before.toString());
8554
+ const result = quote.rail === "classic" ? await runClassic(deps, options) : quote.rail === "fusion" ? await runSameChainFusion(deps, options, walletAddress) : await runFusion(deps, options, walletAddress);
8555
+ const { arrived } = await awaitBalanceIncrease({
8556
+ readBalance: () => deps.readTargetBalance(),
8557
+ baseline: before,
8558
+ ...options.arrivalTimeoutMs === void 0 ? {} : { timeoutMs: options.arrivalTimeoutMs },
8559
+ ...options.arrivalPollMs === void 0 ? {} : { pollMs: options.arrivalPollMs }
8131
8560
  });
8132
- const orderHash = railError.details?.orderHash;
8133
- if (orderHash) clearOrder(orderHash);
8561
+ if (arrived === null) {
8562
+ throw new OwneyError(
8563
+ "SWAP_REQUEST_FAILED",
8564
+ "The swap completed but no funds arrived in the wallet. Check the transaction before retrying.",
8565
+ { rail: quote.rail, ...result, outcome: "filled", settlementUnconfirmed: true }
8566
+ );
8567
+ }
8568
+ return { received: arrived.toString(), ...result };
8569
+ } finally {
8570
+ active2 = false;
8134
8571
  }
8135
- return { received: arrived.toString(), ...result ?? {} };
8136
8572
  }
8137
8573
  async function runClassic(deps, options) {
8138
8574
  const {
@@ -8143,7 +8579,7 @@ async function runClassic(deps, options) {
8143
8579
  onStage
8144
8580
  } = options;
8145
8581
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
8146
- const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
8582
+ let amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount), options.sourceReserve);
8147
8583
  const swapTxRequest = {
8148
8584
  from: {
8149
8585
  chainId: quote.src.chainId,
@@ -8175,25 +8611,36 @@ async function runClassic(deps, options) {
8175
8611
  "owney-sdk",
8176
8612
  "swap: re-fetching classic calldata after approval"
8177
8613
  );
8614
+ amount = await affordableAmount(deps, amount, options.sourceReserve);
8615
+ swapTxRequest.from.amount = amount.toString();
8178
8616
  ({ tx } = await deps.api.swapTx(swapTxRequest));
8617
+ if (await deps.readAllowance(tx.to) < amount)
8618
+ throw new OwneyError("SWAP_REQUEST_FAILED", "Swap approval was not confirmed for the current spender.", { outcome: "not-submitted" });
8179
8619
  }
8180
8620
  }
8181
8621
  onStage?.("signing");
8182
8622
  await deps.ensureChain(quote.src.chainId);
8183
8623
  debugLog("owney-sdk", "swap: sending classic swap tx", { to: tx.to });
8624
+ options.assertCurrent?.();
8625
+ options.onTransactionRequested?.();
8184
8626
  const txHash = await deps.sendTransaction({
8185
8627
  to: tx.to,
8186
8628
  data: tx.data,
8187
8629
  value: tx.value ?? "0"
8630
+ }).catch((error) => {
8631
+ if (error?.code === 4001 || error?.code === "ACTION_REJECTED")
8632
+ throw new OwneyError("SWAP_REQUEST_FAILED", "The swap transaction was rejected in the wallet.", { outcome: "not-submitted" });
8633
+ throw error;
8188
8634
  });
8189
8635
  onStage?.("swapped");
8190
- return { txHash };
8636
+ return { txHash, spent: amount.toString() };
8191
8637
  }
8192
8638
  async function runSameChainFusion(deps, options, walletAddress) {
8193
8639
  const { quote, direction, onStage } = options;
8194
8640
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
8195
- const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
8196
- const permit = await authoriseSpend(deps, options, amount, isNativeSource);
8641
+ let amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount), options.sourceReserve);
8642
+ const { permit, paid } = await authoriseSpend(deps, options, amount, isNativeSource);
8643
+ amount = await authorisedAmount(deps, options, amount, paid);
8197
8644
  onStage?.("building");
8198
8645
  debugLog("owney-sdk", "swap: building same-chain fusion order");
8199
8646
  const built = await deps.api.buildOrder({
@@ -8217,36 +8664,42 @@ async function runSameChainFusion(deps, options, walletAddress) {
8217
8664
  "sign the swap"
8218
8665
  );
8219
8666
  debugLog("owney-sdk", "swap: signed, submitting same-chain order");
8220
- await deps.api.submitOrder({
8221
- // Without this the routing API hands the order to the Fusion+ relayer,
8222
- // which does not know it and answers with an error that names nothing.
8223
- rail: "fusion",
8224
- srcChainId: quote.src.chainId,
8225
- order: built.order,
8226
- signature,
8227
- quoteId: built.quoteId,
8228
- ...built.extension ? { extension: built.extension } : {}
8229
- });
8667
+ options.assertCurrent?.();
8668
+ options.onOrderPrepared?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount });
8669
+ try {
8670
+ await deps.api.submitOrder({
8671
+ // Without this the routing API hands the order to the Fusion+ relayer,
8672
+ // which does not know it and answers with an error that names nothing.
8673
+ rail: "fusion",
8674
+ srcChainId: quote.src.chainId,
8675
+ order: built.order,
8676
+ signature,
8677
+ quoteId: built.quoteId,
8678
+ ...built.extension ? { extension: built.extension } : {}
8679
+ });
8680
+ } catch (error) {
8681
+ throw submissionUnknown(error, built.orderHash);
8682
+ }
8230
8683
  try {
8231
- options.onOrderSubmitted?.(built.orderHash);
8684
+ options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable: true });
8232
8685
  } catch {
8233
8686
  }
8234
8687
  await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
8235
8688
  orderHash: built.orderHash,
8236
8689
  ...onStage ? { onStage } : {}
8237
8690
  });
8238
- return { orderHash: built.orderHash };
8691
+ return { orderHash: built.orderHash, spent: amount.toString() };
8239
8692
  }
8240
8693
  async function authoriseSpend(deps, options, amount, isNativeSource) {
8241
8694
  const { quote, onStage } = options;
8242
- if (!quote.spender || isNativeSource) return void 0;
8695
+ if (!quote.spender || isNativeSource) return { paid: false };
8243
8696
  const current = await deps.readAllowance(quote.spender);
8244
8697
  debugLog("owney-sdk", "swap: fusion allowance", {
8245
8698
  spender: quote.spender,
8246
8699
  current: current.toString(),
8247
8700
  needed: amount.toString()
8248
8701
  });
8249
- if (current >= amount) return void 0;
8702
+ if (current >= amount) return { paid: false };
8250
8703
  onStage?.("approving");
8251
8704
  await deps.ensureChain(quote.src.chainId);
8252
8705
  const permit = quote.permitSupported ? await awaitWalletPrompt(
@@ -8257,11 +8710,18 @@ async function authoriseSpend(deps, options, amount, isNativeSource) {
8257
8710
  debugLog("owney-sdk", "swap: permitting limit order protocol", {
8258
8711
  bytes: (permit.length - 2) / 2
8259
8712
  });
8260
- return permit;
8713
+ return { permit, paid: false };
8261
8714
  }
8262
8715
  await deps.approve(quote.spender, MAX_UINT256);
8263
8716
  debugLog("owney-sdk", "swap: approved limit order protocol");
8264
- return void 0;
8717
+ return { paid: true };
8718
+ }
8719
+ async function authorisedAmount(deps, options, amount, paid) {
8720
+ if (!paid) return amount;
8721
+ const remaining = await affordableAmount(deps, amount, options.sourceReserve);
8722
+ if (!options.quote.spender || await deps.readAllowance(options.quote.spender) < remaining)
8723
+ throw new OwneyError("SWAP_REQUEST_FAILED", "Swap approval was not confirmed.", { outcome: "not-submitted" });
8724
+ return remaining;
8265
8725
  }
8266
8726
  async function runFusion(deps, options, walletAddress) {
8267
8727
  const { quote, direction, onStage } = options;
@@ -8273,8 +8733,9 @@ async function runFusion(deps, options, walletAddress) {
8273
8733
  { chainId: quote.src.chainId, symbol: quote.src.symbol }
8274
8734
  );
8275
8735
  }
8276
- const amount = await affordableAmount(deps, BigInt(quote.src.amount));
8277
- const permit = await authoriseSpend(deps, options, amount, false);
8736
+ let amount = await affordableAmount(deps, BigInt(quote.src.amount), options.sourceReserve);
8737
+ const { permit, paid } = await authoriseSpend(deps, options, amount, false);
8738
+ amount = await authorisedAmount(deps, options, amount, paid);
8278
8739
  const { secrets, secretHashes } = mintSecrets(quote.secretsCount ?? 1);
8279
8740
  onStage?.("building");
8280
8741
  debugLog("owney-sdk", "swap: building fusion order", {
@@ -8293,8 +8754,9 @@ async function runFusion(deps, options, walletAddress) {
8293
8754
  ...direction ? { direction } : {},
8294
8755
  ...permit ? { permit } : {}
8295
8756
  });
8296
- saveOrder({
8757
+ const durable = saveOrder({
8297
8758
  orderHash: built.orderHash,
8759
+ walletAddress,
8298
8760
  secrets,
8299
8761
  srcChainId: quote.src.chainId,
8300
8762
  srcSymbol: quote.src.symbol,
@@ -8304,33 +8766,39 @@ async function runFusion(deps, options, walletAddress) {
8304
8766
  createdAt: Date.now()
8305
8767
  });
8306
8768
  debugLog("owney-sdk", "swap: order built", { orderHash: built.orderHash });
8307
- onStage?.("signing");
8308
- await deps.ensureChain(quote.src.chainId);
8309
- debugLog("owney-sdk", "swap: awaiting signature in wallet", {
8310
- signingOnChain: quote.src.chainId
8311
- });
8312
- const signature = await awaitWalletPrompt(
8313
- () => deps.signTypedData(built.typedData),
8314
- "sign the swap"
8315
- );
8769
+ let signature;
8770
+ try {
8771
+ onStage?.("signing");
8772
+ await deps.ensureChain(quote.src.chainId);
8773
+ signature = await awaitWalletPrompt(() => deps.signTypedData(built.typedData), "sign the swap");
8774
+ options.assertCurrent?.();
8775
+ options.onOrderPrepared?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount });
8776
+ } catch (error) {
8777
+ clearOrder(built.orderHash);
8778
+ throw error;
8779
+ }
8316
8780
  debugLog("owney-sdk", "swap: signed, submitting to relayer");
8317
- await deps.api.submitOrder({
8318
- srcChainId: quote.src.chainId,
8319
- // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
8320
- // the envelope here gets a bare 500 from the relayer.
8321
- order: built.order,
8322
- signature,
8323
- quoteId: built.quoteId,
8324
- // Single-fill orders must NOT carry secretHashes — the relayer rejects
8325
- // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
8326
- // order's hashlock, so repeating it here is redundant, and only a
8327
- // multi-fill order (a Merkle tree of hashes) needs them listed.
8328
- ...secretHashes.length > 1 ? { secretHashes } : {},
8329
- ...built.extension ? { extension: built.extension } : {}
8330
- });
8781
+ try {
8782
+ await deps.api.submitOrder({
8783
+ srcChainId: quote.src.chainId,
8784
+ // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
8785
+ // the envelope here gets a bare 500 from the relayer.
8786
+ order: built.order,
8787
+ signature,
8788
+ quoteId: built.quoteId,
8789
+ // Single-fill orders must NOT carry secretHashes — the relayer rejects
8790
+ // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
8791
+ // order's hashlock, so repeating it here is redundant, and only a
8792
+ // multi-fill order (a Merkle tree of hashes) needs them listed.
8793
+ ...secretHashes.length > 1 ? { secretHashes } : {},
8794
+ ...built.extension ? { extension: built.extension } : {}
8795
+ });
8796
+ } catch (error) {
8797
+ throw submissionUnknown(error, built.orderHash);
8798
+ }
8331
8799
  debugLog("owney-sdk", "swap: order submitted, polling escrows");
8332
8800
  try {
8333
- options.onOrderSubmitted?.(built.orderHash);
8801
+ options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable });
8334
8802
  } catch {
8335
8803
  }
8336
8804
  try {
@@ -8340,13 +8808,25 @@ async function runFusion(deps, options, walletAddress) {
8340
8808
  ...onStage ? { onStage } : {}
8341
8809
  });
8342
8810
  } catch (error) {
8343
- if (error instanceof OwneyError && (error.code === "SWAP_ORDER_REFUNDED" || error.code === "SWAP_ORDER_CANCELLED")) {
8811
+ if (error instanceof OwneyError && error.code === "SWAP_ORDER_REFUNDED" && error.details?.outcome === "refunded") {
8344
8812
  clearOrder(built.orderHash);
8345
8813
  }
8346
8814
  throw error;
8347
8815
  }
8348
8816
  clearOrder(built.orderHash);
8349
- return { orderHash: built.orderHash };
8817
+ return { orderHash: built.orderHash, spent: amount.toString() };
8818
+ }
8819
+ function submissionUnknown(error, orderHash) {
8820
+ return new OwneyError(
8821
+ "SWAP_REQUEST_FAILED",
8822
+ "The order submission could not be confirmed. Check this order before starting another swap.",
8823
+ {
8824
+ orderHash,
8825
+ settlementUnconfirmed: true,
8826
+ outcome: "submitted-unknown",
8827
+ ...error instanceof OwneyError ? { causeCode: error.code } : {}
8828
+ }
8829
+ );
8350
8830
  }
8351
8831
 
8352
8832
  // src/lib/swap/swap.withdrawal-settlement.ts
@@ -8356,8 +8836,8 @@ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8356
8836
  async function awaitWithdrawalPayouts(options) {
8357
8837
  const payouts = new Map(Object.entries(options.results).map(([id, result]) => [id, { result, evidence: result.evidence }]));
8358
8838
  const timeoutMs = Math.max(0, options.timeoutMs ?? 18e4);
8359
- const deadline = Date.now() + timeoutMs;
8360
- let active = true;
8839
+ const deadline = options.deadline ?? Date.now() + timeoutMs;
8840
+ let active2 = true;
8361
8841
  let timer;
8362
8842
  const incomplete = (reason, agentId) => new OwneyError(
8363
8843
  "WITHDRAW_SWAP_INCOMPLETE",
@@ -8375,7 +8855,7 @@ async function awaitWithdrawalPayouts(options) {
8375
8855
  );
8376
8856
  const assertActive = () => {
8377
8857
  options.assertCurrent();
8378
- if (!active || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
8858
+ if (!active2 || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
8379
8859
  };
8380
8860
  const pause = async () => {
8381
8861
  assertActive();
@@ -8386,10 +8866,11 @@ async function awaitWithdrawalPayouts(options) {
8386
8866
  if (evidence.agentId !== id || evidence.chainId !== options.chainId || evidence.token !== options.token || evidence.walletAddress.toLowerCase() !== options.walletAddress.toLowerCase())
8387
8867
  throw incomplete("PAYOUT_SCOPE_MISMATCH", id);
8388
8868
  };
8389
- const record = (id, payout, evidence) => {
8869
+ const record2 = (id, payout, evidence) => {
8390
8870
  assertActive();
8391
8871
  validate(id, evidence);
8392
8872
  payout.evidence = evidence;
8873
+ payout.result.evidence = evidence;
8393
8874
  if (evidence.transfer === "confirmed" && evidence.delivery === "confirmed") payout.amount = rawAmount(evidence.confirmedAmount);
8394
8875
  options.onProgress(evidence);
8395
8876
  };
@@ -8454,8 +8935,30 @@ async function awaitWithdrawalPayouts(options) {
8454
8935
  if (payout.amount === void 0 || payout.amount <= 0n) throw incomplete("INVALID_PAYOUT_AMOUNT", id);
8455
8936
  if (["yieldseeker", "zyfai"].includes(id) && (!hash(evidence.txHash) || rawAmount(evidence.blockNumber) === void 0 || !evidence.logIndexes?.length))
8456
8937
  throw incomplete("PAYOUT_RECEIPT_UNVERIFIED", id);
8457
- if (options.requested === void 0 && payout.result.completion === "unverified")
8458
- throw incomplete("MAX_COMPLETION_UNVERIFIED", id);
8938
+ if (options.requested === void 0 && payout.result.completion === "unverified") {
8939
+ if (!options.verifyCompletion || id === "yieldseeker" && !evidence.maxCompletion)
8940
+ throw incomplete("MAX_COMPLETION_UNVERIFIED", id);
8941
+ let proof = { status: "unavailable" };
8942
+ try {
8943
+ proof = await bounded(
8944
+ () => options.verifyCompletion(evidence),
8945
+ Math.min(1e4, deadline - Date.now()),
8946
+ () => incomplete("MAX_COMPLETION_UNVERIFIED", id)
8947
+ );
8948
+ } catch {
8949
+ assertActive();
8950
+ }
8951
+ assertActive();
8952
+ evidence.maxCompletionCheck = proof;
8953
+ options.onProgress(evidence);
8954
+ if (proof.status === "remaining") throw incomplete("MAX_REMAINDER_VERIFIED", id);
8955
+ if (proof.status !== "complete") {
8956
+ await pause();
8957
+ continue;
8958
+ }
8959
+ payout.result.completion = void 0;
8960
+ payout.result.type = "full";
8961
+ }
8459
8962
  const intended = rawAmount(evidence.intendedAmount);
8460
8963
  if (intended !== void 0 && payout.amount < intended && verifiedFee(payout) !== intended - payout.amount)
8461
8964
  throw incomplete("PAYOUT_SHORTFALL", id);
@@ -8465,24 +8968,24 @@ async function awaitWithdrawalPayouts(options) {
8465
8968
  let next = evidence;
8466
8969
  if (!next.txHash && (next.userOpHash || next.callsId)) {
8467
8970
  next = await options.resolve(next);
8468
- record(id, payout, next);
8971
+ record2(id, payout, next);
8469
8972
  }
8470
8973
  if (next.transfer === "failed" || next.delivery === "failed") continue;
8471
8974
  if (next.txHash) {
8472
8975
  next = await options.inspect(next, false);
8473
- record(id, payout, next);
8976
+ record2(id, payout, next);
8474
8977
  if (id === "zyfai" && next.transfer === "unknown" && rawAmount(next.blockNumber) !== void 0 && next.logIndexes?.length === 0 && next.transferLogs?.length === 0 && !next.userOpHash && !next.callsId) {
8475
8978
  const found = await findPayout(id, next);
8476
8979
  if (found) {
8477
8980
  next = found;
8478
- record(id, payout, next);
8981
+ record2(id, payout, next);
8479
8982
  }
8480
8983
  }
8481
8984
  } else if (!next.userOpHash && !next.callsId) {
8482
8985
  const found = await findPayout(id, next);
8483
8986
  if (found) {
8484
8987
  next = found;
8485
- record(id, payout, next);
8988
+ record2(id, payout, next);
8486
8989
  }
8487
8990
  }
8488
8991
  if (next.transfer === "confirmed" && next.delivery === "confirmed" || next.transfer === "failed" || next.delivery === "failed") continue;
@@ -8526,10 +9029,10 @@ async function awaitWithdrawalPayouts(options) {
8526
9029
  };
8527
9030
  try {
8528
9031
  return await Promise.race([read2(), new Promise((_, reject) => {
8529
- timer = setTimeout(() => reject(incomplete("PAYOUT_CONFIRMATION_TIMEOUT")), timeoutMs);
9032
+ timer = setTimeout(() => reject(incomplete("PAYOUT_CONFIRMATION_TIMEOUT")), Math.max(0, deadline - Date.now()));
8530
9033
  })]);
8531
9034
  } finally {
8532
- active = false;
9035
+ active2 = false;
8533
9036
  if (timer) clearTimeout(timer);
8534
9037
  }
8535
9038
  }
@@ -8550,10 +9053,10 @@ function verifiedFee(payout) {
8550
9053
  import {
8551
9054
  domainSeparator,
8552
9055
  encodeAbiParameters,
8553
- parseAbi as parseAbi4,
9056
+ parseAbi as parseAbi5,
8554
9057
  parseSignature
8555
9058
  } from "viem";
8556
- var PERMIT_ABI = parseAbi4([
9059
+ var PERMIT_ABI = parseAbi5([
8557
9060
  "function nonces(address owner) view returns (uint256)",
8558
9061
  "function name() view returns (string)",
8559
9062
  "function version() view returns (string)",
@@ -8651,7 +9154,7 @@ async function buildErc2612Permit(reads, input) {
8651
9154
 
8652
9155
  // src/lib/swap/swap.withdraw-swap.ts
8653
9156
  var KEY_PREFIX5 = "owney.swap.withdraw";
8654
- var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
9157
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
8655
9158
  var storage5 = () => {
8656
9159
  if (typeof window === "undefined") return null;
8657
9160
  try {
@@ -8676,15 +9179,19 @@ var notify = () => {
8676
9179
  }
8677
9180
  }
8678
9181
  };
8679
- function saveWithdrawSwap(record) {
9182
+ function saveWithdrawSwap(record2) {
8680
9183
  const store = storage5();
8681
- if (!store) return;
9184
+ if (!store) return false;
8682
9185
  try {
8683
- store.setItem(keyFor3(record.orderHash), JSON.stringify(record));
9186
+ const current = store.getItem(keyFor3(record2.orderHash));
9187
+ const value = JSON.stringify({ ...current ? JSON.parse(current) : {}, ...record2 });
9188
+ store.setItem(keyFor3(record2.orderHash), value);
9189
+ if (store.getItem(keyFor3(record2.orderHash)) !== value) return false;
8684
9190
  } catch {
8685
- return;
9191
+ return false;
8686
9192
  }
8687
9193
  notify();
9194
+ return true;
8688
9195
  }
8689
9196
  function updateWithdrawSwap(orderHash, patch) {
8690
9197
  const store = storage5();
@@ -8707,13 +9214,17 @@ function clearWithdrawSwap(orderHash) {
8707
9214
  const store = storage5();
8708
9215
  if (!store) return;
8709
9216
  try {
8710
- store.removeItem(keyFor3(orderHash));
9217
+ const raw2 = store.getItem(keyFor3(orderHash));
9218
+ const record2 = raw2 ? JSON.parse(raw2) : void 0;
9219
+ if (record2 && !["landed", "refunded", "not-swapped"].includes(record2.status))
9220
+ store.setItem(keyFor3(orderHash), JSON.stringify({ ...record2, dismissed: true }));
9221
+ else store.removeItem(keyFor3(orderHash));
8711
9222
  } catch {
8712
9223
  return;
8713
9224
  }
8714
9225
  notify();
8715
9226
  }
8716
- function listWithdrawSwaps(now = Date.now()) {
9227
+ function listWithdrawSwaps(now = Date.now(), includeDismissed = false) {
8717
9228
  const store = storage5();
8718
9229
  if (!store) return [];
8719
9230
  const out = [];
@@ -8728,11 +9239,11 @@ function listWithdrawSwaps(now = Date.now()) {
8728
9239
  if (!raw2) continue;
8729
9240
  try {
8730
9241
  const parsed = JSON.parse(raw2);
8731
- if (now - parsed.createdAt > MAX_AGE_MS2) {
9242
+ if (["landed", "refunded", "not-swapped"].includes(parsed.status) && now - (parsed.settledAt ?? parsed.createdAt) > MAX_AGE_MS) {
8732
9243
  store.removeItem(key2);
8733
9244
  continue;
8734
9245
  }
8735
- out.push(parsed);
9246
+ if (includeDismissed || !parsed.dismissed) out.push(parsed);
8736
9247
  } catch {
8737
9248
  store.removeItem(key2);
8738
9249
  }
@@ -8742,10 +9253,15 @@ function listWithdrawSwaps(now = Date.now()) {
8742
9253
  }
8743
9254
  return out.sort((a, b) => b.createdAt - a.createdAt);
8744
9255
  }
9256
+ if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
9257
+ window.addEventListener("storage", (event) => {
9258
+ if (event.key === null || event.key.startsWith(`${KEY_PREFIX5}.`)) notify();
9259
+ });
9260
+ }
8745
9261
 
8746
9262
  // src/lib/swap/swap.pending-deposit.ts
8747
9263
  var KEY_PREFIX6 = "owney.swap.pending-deposit";
8748
- var MAX_AGE_MS3 = 24 * 60 * 60 * 1e3;
9264
+ var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
8749
9265
  var storage6 = () => {
8750
9266
  if (typeof window === "undefined") return null;
8751
9267
  try {
@@ -8788,7 +9304,7 @@ function listPendingDeposits(now = Date.now()) {
8788
9304
  if (!raw2) continue;
8789
9305
  try {
8790
9306
  const parsed = JSON.parse(raw2);
8791
- if (now - parsed.createdAt > MAX_AGE_MS3) {
9307
+ if (now - parsed.createdAt > MAX_AGE_MS2) {
8792
9308
  store.removeItem(key2);
8793
9309
  continue;
8794
9310
  }
@@ -8837,7 +9353,7 @@ async function withFailureReporting(apiKey, agentType, fn, baseUrl) {
8837
9353
  }
8838
9354
 
8839
9355
  // src/lib/helpers/withdraw-helper.ts
8840
- import { parseUnits as parseUnits2 } from "viem";
9356
+ import { parseUnits as parseUnits3 } from "viem";
8841
9357
  function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decimals) {
8842
9358
  return agents.map((agent) => {
8843
9359
  let invalidBalance = false;
@@ -8847,7 +9363,7 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
8847
9363
  return 0n;
8848
9364
  }
8849
9365
  const [whole, fraction] = value.split(".");
8850
- return raw2 ? BigInt(value) : parseUnits2(fraction === void 0 ? whole : `${whole}.${fraction.slice(0, decimals)}`, decimals);
9366
+ return raw2 ? BigInt(value) : parseUnits3(fraction === void 0 ? whole : `${whole}.${fraction.slice(0, decimals)}`, decimals);
8851
9367
  };
8852
9368
  const agentBalance = aggregated[agent.id];
8853
9369
  const tokenBalance = agentBalance?.tokens.find(
@@ -9047,11 +9563,11 @@ function aggregateDailyEarnings(series, tokenSymbol) {
9047
9563
  const byAsset = /* @__PURE__ */ new Map();
9048
9564
  for (const account of series) {
9049
9565
  for (const { asset, points } of account.assets) {
9050
- const symbol = asset.toUpperCase();
9051
- if (!points.length || wanted && symbol !== wanted) continue;
9052
- const components = byAsset.get(symbol) ?? [];
9566
+ const symbol2 = asset.toUpperCase();
9567
+ if (!points.length || wanted && symbol2 !== wanted) continue;
9568
+ const components = byAsset.get(symbol2) ?? [];
9053
9569
  components.push([...points].sort((a, b) => a.date.localeCompare(b.date)));
9054
- byAsset.set(symbol, components);
9570
+ byAsset.set(symbol2, components);
9055
9571
  }
9056
9572
  }
9057
9573
  return [...byAsset.entries()].map(([asset, components]) => {
@@ -9080,7 +9596,7 @@ function aggregateDailyEarnings(series, tokenSymbol) {
9080
9596
 
9081
9597
  // src/client.ts
9082
9598
  import {
9083
- parseUnits as parseUnits3,
9599
+ parseUnits as parseUnits4,
9084
9600
  createPublicClient as createPublicClient4,
9085
9601
  createWalletClient as createWalletClient5,
9086
9602
  custom as custom5,
@@ -9096,111 +9612,6 @@ import {
9096
9612
  parseTransaction,
9097
9613
  toBytes
9098
9614
  } from "viem";
9099
-
9100
- // src/lib/permit2-batch.ts
9101
- import { parseAbi as parseAbi5, hashStruct } from "viem";
9102
- var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
9103
- var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
9104
- var ERC2612_PERMIT_TYPES = {
9105
- Permit: [
9106
- { name: "owner", type: "address" },
9107
- { name: "spender", type: "address" },
9108
- { name: "value", type: "uint256" },
9109
- { name: "nonce", type: "uint256" },
9110
- { name: "deadline", type: "uint256" }
9111
- ]
9112
- };
9113
- var ERC2612_READ_ABI = parseAbi5([
9114
- "function name() view returns (string)",
9115
- "function version() view returns (string)",
9116
- "function nonces(address owner) view returns (uint256)"
9117
- ]);
9118
- function erc2612TypedData(input) {
9119
- return {
9120
- domain: {
9121
- name: input.permit.tokenName,
9122
- version: input.permit.tokenVersion,
9123
- chainId: input.chainId,
9124
- verifyingContract: input.token
9125
- },
9126
- types: ERC2612_PERMIT_TYPES,
9127
- primaryType: "Permit",
9128
- message: {
9129
- owner: input.owner,
9130
- spender: BATCH_PERMIT2_ADDRESS,
9131
- value: BigInt(input.permit.value),
9132
- nonce: BigInt(input.permit.nonce),
9133
- deadline: BigInt(input.permit.deadline)
9134
- }
9135
- };
9136
- }
9137
- var PERMIT_BATCH_TYPES = {
9138
- PermitBatchWitnessTransferFrom: [
9139
- { name: "permitted", type: "TokenPermissions[]" },
9140
- { name: "spender", type: "address" },
9141
- { name: "nonce", type: "uint256" },
9142
- { name: "deadline", type: "uint256" },
9143
- { name: "witness", type: "Deposit" }
9144
- ],
9145
- Deposit: [{ name: "recipients", type: "address[]" }],
9146
- TokenPermissions: [
9147
- { name: "token", type: "address" },
9148
- { name: "amount", type: "uint256" }
9149
- ]
9150
- };
9151
- var PERMIT2_BATCH_ABI = parseAbi5([
9152
- "struct TokenPermissions { address token; uint256 amount; }",
9153
- "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
9154
- "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
9155
- "function permitWitnessTransferFrom(PermitBatchTransferFrom permit,SignatureTransferDetails[] transferDetails,address owner,bytes32 witness,string witnessTypeString,bytes signature)",
9156
- "function nonceBitmap(address owner,uint256 wordPos) view returns (uint256)"
9157
- ]);
9158
- function batchPermit(b) {
9159
- return {
9160
- permitted: b.transfers.map((t) => ({
9161
- token: b.token,
9162
- amount: BigInt(t.amount)
9163
- })),
9164
- nonce: BigInt(b.nonce),
9165
- deadline: BigInt(b.deadline)
9166
- };
9167
- }
9168
- function batchTypedData(b, spender) {
9169
- return {
9170
- domain: {
9171
- name: "Permit2",
9172
- chainId: b.chainId,
9173
- verifyingContract: BATCH_PERMIT2_ADDRESS
9174
- },
9175
- types: PERMIT_BATCH_TYPES,
9176
- primaryType: "PermitBatchWitnessTransferFrom",
9177
- message: {
9178
- ...batchPermit(b),
9179
- spender,
9180
- witness: { recipients: b.transfers.map((t) => t.to) }
9181
- }
9182
- };
9183
- }
9184
- var BATCH_WITNESS_TYPE = "Deposit witness)Deposit(address[] recipients)TokenPermissions(address token,uint256 amount)";
9185
- function batchWitness(b) {
9186
- return hashStruct({
9187
- types: PERMIT_BATCH_TYPES,
9188
- primaryType: "Deposit",
9189
- data: { recipients: b.transfers.map((t) => t.to) }
9190
- });
9191
- }
9192
- function batchArgs(b) {
9193
- return [
9194
- batchPermit(b),
9195
- b.transfers.map((t) => ({ to: t.to, requestedAmount: BigInt(t.amount) })),
9196
- b.from,
9197
- batchWitness(b),
9198
- BATCH_WITNESS_TYPE,
9199
- b.signature
9200
- ];
9201
- }
9202
-
9203
- // src/lib/sponsored-token-batch.ts
9204
9615
  var memory = /* @__PURE__ */ new Map();
9205
9616
  var settledRecords = /* @__PURE__ */ new Map();
9206
9617
  var expiredRecords = /* @__PURE__ */ new Map();
@@ -9228,14 +9639,14 @@ async function releaseExpiredTokenBatch(i) {
9228
9639
  let timer;
9229
9640
  let expired = false;
9230
9641
  const check = async () => {
9231
- let active = read(key2);
9232
- if (active && i.expectedTxHash) {
9233
- const current = JSON.parse(active);
9642
+ let active2 = read(key2);
9643
+ if (active2 && i.expectedTxHash) {
9644
+ const current = JSON.parse(active2);
9234
9645
  if (!current.serializedTransaction || keccak2562(current.serializedTransaction).toLowerCase() !== i.expectedTxHash.toLowerCase())
9235
- active = void 0;
9646
+ active2 = void 0;
9236
9647
  }
9237
9648
  const archiveKey = i.expectedTxHash ? `${key2}:expired:${i.expectedTxHash.toLowerCase()}` : void 0;
9238
- const saved = active ?? (archiveKey && (expiredRecords.get(archiveKey) ?? (typeof window === "undefined" ? void 0 : window.localStorage.getItem(archiveKey))));
9649
+ const saved = active2 ?? (archiveKey && (expiredRecords.get(archiveKey) ?? (typeof window === "undefined" ? void 0 : window.localStorage.getItem(archiveKey))));
9239
9650
  if (!saved) return;
9240
9651
  const body = JSON.parse(saved);
9241
9652
  if (!body.serializedTransaction || body.chainId !== i.chainId || !isAddressEqual2(body.from, i.owner) || !isAddressEqual2(body.token, i.token) || !body.transfers?.length) return;
@@ -9259,8 +9670,8 @@ async function releaseExpiredTokenBatch(i) {
9259
9670
  args: [i.owner, nonce >> 8n],
9260
9671
  blockNumber: block.number
9261
9672
  });
9262
- if (expired || typeof bitmap !== "bigint" || bitmap < 0n || bitmap > MAX_UINT256 || (bitmap & 1n << (nonce & 255n)) !== 0n || active && read(key2) !== active) return;
9263
- if (active) {
9673
+ if (expired || typeof bitmap !== "bigint" || bitmap < 0n || bitmap > MAX_UINT256 || (bitmap & 1n << (nonce & 255n)) !== 0n || active2 && read(key2) !== active2) return;
9674
+ if (active2) {
9264
9675
  const terminalKey = `${key2}:expired:${txHash.toLowerCase()}`;
9265
9676
  expiredRecords.set(terminalKey, saved);
9266
9677
  let persisted = typeof window === "undefined";
@@ -9318,15 +9729,15 @@ function clear(key2, body) {
9318
9729
  function sponsorTokenBatch(i) {
9319
9730
  const key2 = pendingKey(i);
9320
9731
  const plan = planOf(i.transfers);
9321
- const active = inflight.get(key2);
9322
- if (active) {
9323
- if (active.plan !== plan || i.resumeTxHash && pendingTokenBatch(i)?.txHash !== i.resumeTxHash)
9732
+ const active2 = inflight.get(key2);
9733
+ if (active2) {
9734
+ if (active2.plan !== plan || i.resumeTxHash && pendingTokenBatch(i)?.txHash !== i.resumeTxHash)
9324
9735
  return Promise.reject(
9325
9736
  new Error(
9326
9737
  "A token deposit is already in progress. Wait for its result before depositing again."
9327
9738
  )
9328
9739
  );
9329
- return active.promise;
9740
+ return active2.promise;
9330
9741
  }
9331
9742
  const promise = execute(i, key2, plan).finally(() => inflight.delete(key2));
9332
9743
  inflight.set(key2, { plan, promise });
@@ -9480,7 +9891,7 @@ async function execute(i, key2, plan) {
9480
9891
  "DEPOSIT_INSUFFICIENT_BALANCE",
9481
9892
  "Insufficient token balance for this deposit."
9482
9893
  );
9483
- if (allowance < total && (i.chainId !== 8453 || !isAddressEqual2(i.token, BASE_USDC_ADDRESS)))
9894
+ if (allowance < total && !supportsSponsoredErc2612(i.chainId, i.token))
9484
9895
  throw new OwneyError(
9485
9896
  "PERMIT2_APPROVAL_REQUIRED",
9486
9897
  "token deposits need a one-time Permit2 approval."
@@ -9516,7 +9927,7 @@ async function execute(i, key2, plan) {
9516
9927
  const signature2 = await i.wallet.signTypedData({
9517
9928
  account: i.owner,
9518
9929
  ...erc2612TypedData({
9519
- chainId: 8453,
9930
+ chainId: i.chainId,
9520
9931
  token: i.token,
9521
9932
  owner: i.owner,
9522
9933
  permit: unsignedPermit
@@ -10144,12 +10555,12 @@ var OwneySDK = class {
10144
10555
  return [...this.getActiveAgents().keys()].sort();
10145
10556
  }
10146
10557
  getActiveAgents() {
10147
- const active = /* @__PURE__ */ new Map();
10558
+ const active2 = /* @__PURE__ */ new Map();
10148
10559
  for (const id of this.activeAgents) {
10149
10560
  const agent = this.agents.get(id);
10150
- if (agent) active.set(id, agent);
10561
+ if (agent) active2.set(id, agent);
10151
10562
  }
10152
- return active;
10563
+ return active2;
10153
10564
  }
10154
10565
  /**
10155
10566
  * Read the org's agent policy once and reuse it. fetchOrgAgentConfig never
@@ -10732,10 +11143,10 @@ var OwneySDK = class {
10732
11143
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
10733
11144
  * two independent auto-recovery mechanisms:
10734
11145
  *
10735
- * 1. Missing Permit2 allowance outside the atomic Base-USDC path: when the
11146
+ * 1. Missing Permit2 allowance outside the sponsored ERC-2612 path: when the
10736
11147
  * app did not supply its own callback and the attempt fails with
10737
11148
  * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
10738
- * for that token. Base USDC bundles a gasless ERC-2612 approval inside
11149
+ * for that token. Native Base/Arbitrum USDC uses gasless ERC-2612 approval in
10739
11150
  * its sponsored deposit and never reaches this branch. Other tokens send
10740
11151
  * the one-time user-paid Permit2 approval via `approvePermit2()` and
10741
11152
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
@@ -11242,8 +11653,8 @@ var OwneySDK = class {
11242
11653
  async ensureSwapChain(chainId) {
11243
11654
  const provider = this.requireConnectedProvider();
11244
11655
  const state = this.requireState();
11245
- const chain = swapChain(chainId);
11246
- if (!chain) {
11656
+ const chain2 = swapChain(chainId);
11657
+ if (!chain2) {
11247
11658
  throw new OwneyError(
11248
11659
  "CHAIN_UNSUPPORTED",
11249
11660
  `Chain ${chainId} is not supported`,
@@ -11251,10 +11662,10 @@ var OwneySDK = class {
11251
11662
  );
11252
11663
  }
11253
11664
  await ensureWalletOnChain(
11254
- createPublicClient4({ chain, transport: custom5(provider) }),
11665
+ createPublicClient4({ chain: chain2, transport: custom5(provider) }),
11255
11666
  createWalletClient5({
11256
11667
  account: state.walletAddress,
11257
- chain,
11668
+ chain: chain2,
11258
11669
  transport: custom5(provider)
11259
11670
  }),
11260
11671
  chainId
@@ -11278,15 +11689,15 @@ var OwneySDK = class {
11278
11689
  async wrapNativeSource(quote, walletAddress, onStage, direction) {
11279
11690
  const wrapper = requireWrapper(quote);
11280
11691
  const chainId = quote.src.chainId;
11281
- const chain = swapChain(chainId);
11692
+ const chain2 = swapChain(chainId);
11282
11693
  const provider = this.requireConnectedProvider();
11283
11694
  const publicClient = createPublicClient4({
11284
- chain,
11695
+ chain: chain2,
11285
11696
  transport: swapReadTransport(chainId, this.zyfaiRpcUrls)
11286
11697
  });
11287
11698
  const wallet = createWalletClient5({
11288
11699
  account: walletAddress,
11289
- chain,
11700
+ chain: chain2,
11290
11701
  transport: custom5(provider)
11291
11702
  });
11292
11703
  onStage?.("wrapping");
@@ -11313,7 +11724,7 @@ var OwneySDK = class {
11313
11724
  data: tx.data,
11314
11725
  value: BigInt(tx.value),
11315
11726
  account: walletAddress,
11316
- chain
11727
+ chain: chain2
11317
11728
  });
11318
11729
  onSubmitted?.(hash2);
11319
11730
  const receipt = await publicClient.waitForTransactionReceipt({
@@ -11383,15 +11794,27 @@ var OwneySDK = class {
11383
11794
  * Kept as a builder rather than baked into the executor so the whole swap
11384
11795
  * flow stays testable without a provider — the executor never imports viem.
11385
11796
  */
11386
- buildSwapDeps(quote) {
11797
+ buildSwapDeps(quote, assertOperation, onTransactionHash) {
11387
11798
  const state = this.requireState();
11388
11799
  const provider = this.requireConnectedProvider();
11800
+ const revision = this.connectionRevision;
11801
+ const assertWallet = () => {
11802
+ assertOperation?.();
11803
+ if (this.state !== state || this.connectionRevision !== revision)
11804
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed before swap authorization.");
11805
+ };
11389
11806
  const srcChain = swapChain(quote.src.chainId);
11390
11807
  const dstChain = swapChain(quote.dst.chainId);
11391
11808
  const wallet = createWalletClient5({
11392
11809
  account: state.walletAddress,
11393
11810
  chain: srcChain,
11394
- transport: custom5(provider)
11811
+ // viem can await chain/account preparation before dispatching a wallet
11812
+ // request. Recheck at the transport boundary so an expired attempt or
11813
+ // replaced session cannot send after that internal await completes.
11814
+ transport: custom5({ request: (args) => {
11815
+ assertWallet();
11816
+ return provider.request(args);
11817
+ } }, { retryCount: 0 })
11395
11818
  });
11396
11819
  const srcPublic = createPublicClient4({
11397
11820
  chain: srcChain,
@@ -11420,6 +11843,7 @@ var OwneySDK = class {
11420
11843
  });
11421
11844
  },
11422
11845
  sendTransaction: async (tx) => {
11846
+ assertWallet();
11423
11847
  const hash2 = await wallet.sendTransaction({
11424
11848
  to: tx.to,
11425
11849
  data: tx.data,
@@ -11427,24 +11851,34 @@ var OwneySDK = class {
11427
11851
  account: state.walletAddress,
11428
11852
  chain: srcChain
11429
11853
  });
11854
+ onTransactionHash?.(hash2);
11430
11855
  const receipt = await srcPublic.waitForTransactionReceipt({
11431
11856
  timeout: receiptTimeoutMs(quote.src.chainId),
11432
11857
  hash: hash2,
11433
11858
  confirmations: 1
11859
+ }).catch(() => {
11860
+ throw new OwneyError(
11861
+ "SWAP_REQUEST_FAILED",
11862
+ "Swap receipt is not confirmed yet.",
11863
+ { txHash: hash2, settlementUnconfirmed: true, outcome: "submitted-unknown" }
11864
+ );
11434
11865
  });
11435
11866
  if (receipt.status !== "success") {
11436
11867
  throw new OwneyError(
11437
11868
  "SWAP_REQUEST_FAILED",
11438
11869
  `Swap transaction reverted (tx ${hash2})`,
11439
- { hash: hash2 }
11870
+ { txHash: hash2, outcome: "not-submitted", executionFailed: true }
11440
11871
  );
11441
11872
  }
11442
11873
  return hash2;
11443
11874
  },
11444
- signTypedData: (typedData) => wallet.signTypedData({
11445
- account: state.walletAddress,
11446
- ...typedData
11447
- }),
11875
+ signTypedData: (typedData) => {
11876
+ assertWallet();
11877
+ return wallet.signTypedData({
11878
+ account: state.walletAddress,
11879
+ ...typedData
11880
+ });
11881
+ },
11448
11882
  // Chain-bound like every other read here: the wallet provider's chain is
11449
11883
  // not ours to rely on mid-swap.
11450
11884
  readSourceBalance: async () => {
@@ -11466,6 +11900,7 @@ var OwneySDK = class {
11466
11900
  args: [state.walletAddress, spender]
11467
11901
  }),
11468
11902
  approve: async (spender, amount) => {
11903
+ assertWallet();
11469
11904
  const hash2 = await wallet.writeContract({
11470
11905
  address: quote.src.address,
11471
11906
  abi: ERC20_ALLOWANCE_ABI,
@@ -11474,14 +11909,22 @@ var OwneySDK = class {
11474
11909
  account: state.walletAddress,
11475
11910
  chain: srcChain
11476
11911
  });
11477
- await srcPublic.waitForTransactionReceipt({
11912
+ const receipt = await srcPublic.waitForTransactionReceipt({
11478
11913
  hash: hash2,
11479
11914
  confirmations: 1,
11480
11915
  timeout: receiptTimeoutMs(quote.src.chainId)
11481
11916
  });
11917
+ if (receipt.status !== "success") throw new OwneyError(
11918
+ "SWAP_REQUEST_FAILED",
11919
+ "Swap approval reverted.",
11920
+ { hash: hash2, phase: "approval", outcome: "not-submitted" }
11921
+ );
11482
11922
  return hash2;
11483
11923
  },
11484
- ensureChain: (chainId) => this.ensureSwapChain(chainId),
11924
+ ensureChain: (chainId) => {
11925
+ assertWallet();
11926
+ return this.ensureSwapChain(chainId);
11927
+ },
11485
11928
  /**
11486
11929
  * Sign the approval instead of paying for it. Null means the token
11487
11930
  * cannot be permitted (WETH has no `permit`) or its domain could not be
@@ -11501,10 +11944,13 @@ var OwneySDK = class {
11501
11944
  spender,
11502
11945
  value: amount,
11503
11946
  chainId: quote.src.chainId,
11504
- signTypedData: (payload) => wallet.signTypedData({
11505
- account: state.walletAddress,
11506
- ...payload
11507
- })
11947
+ signTypedData: (payload) => {
11948
+ assertWallet();
11949
+ return wallet.signTypedData({
11950
+ account: state.walletAddress,
11951
+ ...payload
11952
+ });
11953
+ }
11508
11954
  }
11509
11955
  ),
11510
11956
  /**
@@ -11554,36 +12000,20 @@ var OwneySDK = class {
11554
12000
  );
11555
12001
  }
11556
12002
  /**
11557
- * How much of a landed swap to actually deposit.
11558
- *
11559
- * The swap reports what ARRIVED, and the deposit used to be handed that
11560
- * figure directly. It is the entire arrival, so there is no slack: anything
11561
- * that spends the target asset between the two fails the deposit outright.
11562
- *
11563
- * Something does. A first WETH deposit needs a one-time Permit2 approval,
11564
- * and that approval is a transaction. A wallet holding no native gas token -
11565
- * the normal state of someone who just swapped everything INTO WETH - pays
11566
- * for it out of the WETH. The deposit then asked for ~2e12 wei more than was
11567
- * left and refused with DEPOSIT_INSUFFICIENT_BALANCE, under a wrapper saying
11568
- * the funds were safe but unadded (ROUT-242).
11569
- *
11570
- * So the approval happens here, before the amount is settled, and the amount
11571
- * comes from a read taken after it. Two things this deliberately is not:
11572
- *
11573
- * - It is not a balance sweep. `received` is a DELTA over the pre-swap
11574
- * balance, so a wallet already holding WETH reads far above it; depositing
11575
- * the balance would move funds the user never offered. The arrival is the
11576
- * ceiling and the balance is only ever allowed to lower it.
11577
- * - It does not touch USDC, which moves on signed typed data and so spends
11578
- * nothing of the user's in between, nor a paymaster-configured client,
11579
- * where the sponsor covers the transfer and Permit2 never applies.
12003
+ * Finish any user-paid Permit2 approval before measuring the deposit.
12004
+ * Wallets can charge approval gas in the received token (ROUT-242/544).
12005
+ * Cap the result at the swap arrival to preserve pre-existing wallet funds.
12006
+ * Native Base/Arbitrum USDC uses sponsored ERC-2612; configured paymaster
12007
+ * wallet calls also cover approval without spending the received tokens.
11580
12008
  */
11581
- async settleSwapDepositAmount(chainId, token, symbol, received) {
11582
- if (symbol !== "WETH" || this.paymasterServiceUrl) {
12009
+ async settleSwapDepositAmount(chainId, token, symbol2, received) {
12010
+ if (symbol2 !== "WETH" && symbol2 !== "USDC" || this.paymasterServiceUrl || supportsSponsoredErc2612(chainId, token)) {
11583
12011
  return received.toString();
11584
12012
  }
11585
12013
  const allowance = await this.readTokenPermit2Allowance(chainId, token);
11586
- if (allowance < received) await this.approvePermit2();
12014
+ if (allowance < received) {
12015
+ await this.approvePermit2(symbol2, received, chainId);
12016
+ }
11587
12017
  const balance = await this.readTokenBalance(chainId, token);
11588
12018
  return (balance < received ? balance : received).toString();
11589
12019
  }
@@ -11710,21 +12140,30 @@ var OwneySDK = class {
11710
12140
  * Every order is attempted even if an earlier one throws: they are unrelated
11711
12141
  * escrows, and one ending badly must not abandon the next one's secrets.
11712
12142
  * `unfinished` is not a verdict — the order may still settle, so unless the
11713
- * chain has said refunded or cancelled its preimages are kept.
12143
+ * provider has confirmed full settlement or refund, its preimages are kept.
11714
12144
  */
11715
12145
  async resumePendingSwaps(options = {}) {
11716
- const pending = listOrders();
11717
- if (pending.length === 0) {
11718
- await this.resumeSameChainSwaps(options);
11719
- return [];
11720
- }
12146
+ await mapConcurrent(listWithdrawSwaps(Date.now(), true).filter((record2) => record2.txHash && record2.status === "unknown" && (!options.orderHash || options.orderHash === record2.orderHash)), async (record2) => {
12147
+ try {
12148
+ const rpc2 = createPublicClient4({ chain: swapChain(record2.srcChainId), transport: swapReadTransport(record2.srcChainId, this.zyfaiRpcUrls) });
12149
+ const receipt = await bounded(
12150
+ () => rpc2.getTransactionReceipt({ hash: record2.txHash }),
12151
+ 8e3,
12152
+ () => new Error("Receipt unavailable")
12153
+ );
12154
+ updateWithdrawSwap(record2.orderHash, { status: receipt.status === "success" ? "landed" : "not-swapped" });
12155
+ } catch {
12156
+ }
12157
+ });
12158
+ const pending = listOrders().filter((order) => !isOrderObserved(order.orderHash) && (!options.orderHash || order.orderHash === options.orderHash));
11721
12159
  const runner = this.swapRunnerDeps();
11722
- const results = [];
11723
- for (const order of pending) {
12160
+ const sameChain = this.resumeSameChainSwaps(options);
12161
+ const results = await mapConcurrent(pending, async (order) => {
11724
12162
  try {
11725
12163
  await runFusionOrder(runner, {
11726
12164
  orderHash: order.orderHash,
11727
12165
  secrets: order.secrets,
12166
+ hasPartialFill: listWithdrawSwaps(Date.now(), true).some((record2) => record2.orderHash === order.orderHash && record2.status === "partially-filled"),
11728
12167
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
11729
12168
  ...options.onStage ? {
11730
12169
  onStage: (stage) => options.onStage(order.orderHash, stage)
@@ -11732,25 +12171,23 @@ var OwneySDK = class {
11732
12171
  });
11733
12172
  clearOrder(order.orderHash);
11734
12173
  updateWithdrawSwap(order.orderHash, { status: "landed" });
11735
- results.push({
11736
- orderHash: order.orderHash,
11737
- status: "completed"
11738
- });
12174
+ return { orderHash: order.orderHash, status: "completed" };
11739
12175
  } catch (error) {
11740
- if (error instanceof OwneyError && (error.code === "SWAP_ORDER_REFUNDED" || error.code === "SWAP_ORDER_CANCELLED")) {
12176
+ if (error instanceof OwneyError && error.code === "SWAP_ORDER_REFUNDED" && error.details?.outcome === "refunded") {
11741
12177
  clearOrder(order.orderHash);
11742
12178
  updateWithdrawSwap(order.orderHash, { status: "refunded" });
11743
12179
  } else {
11744
- updateWithdrawSwap(order.orderHash, { status: "unknown" });
12180
+ if (!(error instanceof OwneyError && error.details?.watchActive))
12181
+ updateWithdrawSwap(order.orderHash, { status: error instanceof OwneyError && error.details?.outcome === "refund-pending" ? "refund-pending" : error instanceof OwneyError && error.details?.outcome === "partially-filled" ? "partially-filled" : "unknown" });
11745
12182
  }
11746
- results.push({
12183
+ return {
11747
12184
  orderHash: order.orderHash,
11748
12185
  status: "unfinished",
11749
12186
  ...error instanceof OwneyError ? { error } : {}
11750
- });
12187
+ };
11751
12188
  }
11752
- }
11753
- await this.resumeSameChainSwaps(options);
12189
+ });
12190
+ await sameChain;
11754
12191
  return results;
11755
12192
  }
11756
12193
  /**
@@ -11771,34 +12208,32 @@ var OwneySDK = class {
11771
12208
  * anything else.
11772
12209
  */
11773
12210
  async resumeSameChainSwaps(options = {}) {
11774
- const running = listWithdrawSwaps().filter(
11775
- (record) => record.status === "swapping" && record.srcChainId === record.dstChainId
11776
- );
11777
- for (const record of running) {
12211
+ const running = listWithdrawSwaps(Date.now(), true).filter((record2) => ["swapping", "unknown", "partially-filled", "refund-pending"].includes(record2.status) && record2.srcChainId === record2.dstChainId && /^0x[0-9a-f]{64}$/i.test(record2.orderHash) && !isOrderObserved(record2.orderHash) && (!options.orderHash || record2.orderHash === options.orderHash));
12212
+ await mapConcurrent(running, async (record2) => {
11778
12213
  const runner = {
11779
12214
  // The chain bound in. That product keys its orders per chain, and the
11780
12215
  // Fusion+ endpoint answers 404 for one — which would read as an order
11781
12216
  // that vanished rather than one still in the auction.
11782
- orderStatus: (h) => this.swapApi().orderStatus(h, record.srcChainId),
12217
+ orderStatus: (h) => this.swapApi().orderStatus(h, record2.srcChainId),
11783
12218
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
11784
12219
  now: () => Date.now()
11785
12220
  };
11786
12221
  try {
11787
12222
  await runSameChainOrder(runner, {
11788
- orderHash: record.orderHash,
12223
+ orderHash: record2.orderHash,
12224
+ hasPartialFill: record2.status === "partially-filled",
11789
12225
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
11790
12226
  ...options.onStage ? {
11791
- onStage: (stage) => options.onStage(record.orderHash, stage)
12227
+ onStage: (stage) => options.onStage(record2.orderHash, stage)
11792
12228
  } : {}
11793
12229
  });
11794
- updateWithdrawSwap(record.orderHash, { status: "landed" });
12230
+ updateWithdrawSwap(record2.orderHash, { status: "landed" });
11795
12231
  } catch (error) {
11796
- const code = error instanceof OwneyError ? error.code : void 0;
11797
- updateWithdrawSwap(record.orderHash, {
11798
- status: code === "SWAP_ORDER_EXPIRED" || code === "SWAP_ORDER_CANCELLED" ? "refunded" : "unknown"
11799
- });
12232
+ if (error instanceof OwneyError && error.details?.watchActive) return;
12233
+ const outcome = error instanceof OwneyError ? error.details?.outcome : void 0;
12234
+ updateWithdrawSwap(record2.orderHash, { status: outcome === "not-submitted" ? "not-swapped" : outcome === "partially-filled" ? "partially-filled" : "unknown" });
11800
12235
  }
11801
- }
12236
+ });
11802
12237
  }
11803
12238
  /**
11804
12239
  * What the wallet actually holds, across every supported chain, restricted
@@ -11954,6 +12389,92 @@ var OwneySDK = class {
11954
12389
  );
11955
12390
  }
11956
12391
  }
12392
+ /** Read-only confirmation of a saved pre-swap withdrawal. Never withdraws or swaps. */
12393
+ async recheckWithdrawalSwap(id) {
12394
+ const record2 = listWithdrawSwaps(Date.now(), true).find((row) => row.orderHash === id);
12395
+ const state = this.requireState();
12396
+ if (!record2?.recovery || record2.walletAddress?.toLowerCase() !== state.walletAddress.toLowerCase() || record2.srcChainId !== this.requireChainId() || record2.recovery.phase === "submitted")
12397
+ throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "Reconnect the original wallet and source chain to check this withdrawal.");
12398
+ if (this.withdrawalSwapSubmissions.has(`${state.walletAddress.toLowerCase()}:${record2.srcChainId}:${record2.srcSymbol}`))
12399
+ throw new OwneyError("WITHDRAW_IN_PROGRESS", "This withdrawal is still being observed by its original attempt.");
12400
+ const revision = this.connectionRevision;
12401
+ const asset = SupportedAssets.find((a) => a.chainId === record2.srcChainId && a.symbol === record2.srcSymbol);
12402
+ if (!asset || !record2.recovery.results)
12403
+ throw new OwneyError(
12404
+ "WITHDRAW_SWAP_INCOMPLETE",
12405
+ "Some withdrawal results remain unknown. Check Activity and balances before continuing.",
12406
+ { reason: "WITHDRAWAL_RESULTS_UNAVAILABLE", swapSubmitted: false }
12407
+ );
12408
+ const rpc2 = createPublicClient4({
12409
+ chain: VIEM_CHAIN2[record2.srcChainId],
12410
+ transport: swapReadTransport(record2.srcChainId, this.zyfaiRpcUrls)
12411
+ });
12412
+ const recovery = record2.recovery;
12413
+ const assertRecoveryCurrent = () => {
12414
+ if (this.state !== state || this.connectionRevision !== revision)
12415
+ throw new OwneyError("NOT_CONNECTED", "Wallet changed during confirmation.");
12416
+ const latest = listWithdrawSwaps(Date.now(), true).find((row) => row.orderHash === id);
12417
+ if (!latest?.recovery || latest.recovery.phase === "submitted")
12418
+ throw new OwneyError("WITHDRAW_IN_PROGRESS", "This intent has already advanced. Check its current order.");
12419
+ };
12420
+ const persist = () => {
12421
+ assertRecoveryCurrent();
12422
+ saveWithdrawSwap(record2);
12423
+ };
12424
+ for (const [id2, result] of Object.entries(recovery.results)) {
12425
+ const evidence = recovery.evidence[id2] ?? result.evidence;
12426
+ if (evidence) result.evidence = { ...evidence, transfer: "pending", delivery: "pending" };
12427
+ if (id2 === "yieldseeker" && recovery.requested === void 0) result.completion = "unverified";
12428
+ }
12429
+ try {
12430
+ const arrived = await awaitWithdrawalPayouts({
12431
+ results: recovery.results,
12432
+ requested: recovery.requested === void 0 ? void 0 : BigInt(recovery.requested),
12433
+ walletAddress: state.walletAddress,
12434
+ chainId: record2.srcChainId,
12435
+ token: asset.symbol,
12436
+ submittedAt: recovery.submittedAt,
12437
+ baseline: BigInt(recovery.baseline),
12438
+ baselineBlock: BigInt(recovery.baselineBlock),
12439
+ timeoutMs: 3e4,
12440
+ assertCurrent: assertRecoveryCurrent,
12441
+ readBalance: () => rpc2.readContract({ address: asset.address, abi: erc20Abi7, functionName: "balanceOf", args: [state.walletAddress] }),
12442
+ inspect: (value, fromHistory) => this.inspectFunding(value, "withdraw", { requireAmountMatch: fromHistory, refreshBalance: false }),
12443
+ resolve: (value) => this.resolveSponsoredWithdrawalEvidence(value, { submittedAt: recovery.submittedAt }),
12444
+ history: (value, fromDate, filters) => this.getFundingHistory(value, fromDate, filters),
12445
+ verifyCompletion: (value) => this.getAgent(value.agentId).verifyWithdrawalCompletion?.(state, value) ?? Promise.resolve({ status: "unavailable" }),
12446
+ onProgress: (value) => {
12447
+ recovery.evidence[value.agentId] = value;
12448
+ persist();
12449
+ }
12450
+ });
12451
+ record2.withdrawn = arrived.toString();
12452
+ record2.status = "ready";
12453
+ recovery.phase = "ready";
12454
+ recovery.reason = void 0;
12455
+ persist();
12456
+ return record2;
12457
+ } catch (error) {
12458
+ record2.status = "withdrawal-incomplete";
12459
+ recovery.reason = error instanceof OwneyError ? String(error.details?.reason ?? error.code) : "CONFIRMATION_UNAVAILABLE";
12460
+ try {
12461
+ persist();
12462
+ } catch {
12463
+ }
12464
+ throw error;
12465
+ }
12466
+ }
12467
+ /** Explicit foreground continuation: spends verified payouts, never withdraws again. */
12468
+ async continueWithdrawalSwap(id, callbacks = {}) {
12469
+ const record2 = await this.recheckWithdrawalSwap(id);
12470
+ return this.withdrawAndSwap({
12471
+ from: { chainId: record2.srcChainId, symbol: record2.srcSymbol },
12472
+ to: { chainId: record2.dstChainId, symbol: record2.dstSymbol, decimals: record2.dstDecimals },
12473
+ ...record2.recovery?.agentId ? { agentId: record2.recovery.agentId } : {},
12474
+ recoveryId: id,
12475
+ ...callbacks
12476
+ });
12477
+ }
11957
12478
  /**
11958
12479
  * Withdraw from an agent and swap the proceeds into whatever the user wants
11959
12480
  * to hold, delivered to their own wallet.
@@ -11982,7 +12503,14 @@ var OwneySDK = class {
11982
12503
  const state = this.requireState();
11983
12504
  const activeChainId = this.requireChainId();
11984
12505
  const revision = this.connectionRevision;
12506
+ let active2 = true;
12507
+ let record2;
12508
+ let hadDurableRecord = false;
12509
+ const startedAt = Date.now();
12510
+ let confirmationDeadline = Infinity;
11985
12511
  const assertCurrent = () => {
12512
+ options.assertCurrent?.();
12513
+ if (!active2) throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal attempt is no longer active.", { swapSubmitted: false });
11986
12514
  if (this.state !== state || this.connectionRevision !== revision || state.chainId !== activeChainId)
11987
12515
  throw new OwneyError("NOT_CONNECTED", "Wallet changed during withdrawal and swap.");
11988
12516
  };
@@ -12024,30 +12552,95 @@ var OwneySDK = class {
12024
12552
  functionName: "balanceOf",
12025
12553
  args: [state.walletAddress]
12026
12554
  });
12027
- const baselineBlock = await srcPublic.getBlockNumber({ cacheTime: 0 });
12028
- const baseline = await readWalletBalance();
12555
+ const saved = options.recoveryId ? listWithdrawSwaps(Date.now(), true).find((row) => row.orderHash === options.recoveryId) : void 0;
12556
+ 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))
12557
+ throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal is not ready to continue.");
12558
+ const prepareTimeout = () => new OwneyError(
12559
+ "WITHDRAW_SWAP_INCOMPLETE",
12560
+ "Could not read the source wallet. No new withdrawal was submitted.",
12561
+ { reason: "BASELINE_UNAVAILABLE", swapSubmitted: false }
12562
+ );
12563
+ const baselineBlock = saved ? BigInt(saved.recovery.baselineBlock) : await bounded(() => srcPublic.getBlockNumber({ cacheTime: 0 }), 2e4, prepareTimeout);
12564
+ const baseline = saved ? BigInt(saved.recovery.baseline) : await bounded(readWalletBalance, 2e4, prepareTimeout);
12029
12565
  assertCurrent();
12030
12566
  debugLog("owney-sdk", "withdrawAndSwap: baseline", {
12031
12567
  asset: `${asset.symbol}@${asset.chainId}`,
12032
12568
  baseline: baseline.toString()
12033
12569
  });
12034
12570
  options.onSwapProgress?.("withdrawing");
12035
- const submittedAt = Date.now();
12036
- const requested = options.amount === void 0 ? void 0 : parseUnits3(options.amount, asset.decimals);
12571
+ const submittedAt = saved?.recovery?.submittedAt ?? Date.now();
12572
+ const requested = saved ? saved.recovery.requested === void 0 ? void 0 : BigInt(saved.recovery.requested) : options.amount === void 0 ? void 0 : parseUnits4(options.amount, asset.decimals);
12573
+ confirmationDeadline = Date.now() + (options.arrivalTimeoutMs ?? 18e4);
12574
+ const attemptId = saved?.recovery?.attemptId ?? `withdraw-${globalThis.crypto.randomUUID()}`;
12575
+ record2 = saved ?? {
12576
+ orderHash: attemptId,
12577
+ walletAddress: state.walletAddress,
12578
+ status: "withdrawal-pending",
12579
+ withdrawn: "0",
12580
+ expected: "0",
12581
+ createdAt: Date.now(),
12582
+ srcSymbol: asset.symbol,
12583
+ srcChainId: activeChainId,
12584
+ srcDecimals: asset.decimals,
12585
+ dstSymbol: options.to.symbol,
12586
+ dstChainId: options.to.chainId,
12587
+ dstDecimals: options.to.decimals ?? 18,
12588
+ recovery: {
12589
+ attemptId,
12590
+ phase: "withdrawing",
12591
+ baseline: baseline.toString(),
12592
+ baselineBlock: baselineBlock.toString(),
12593
+ submittedAt,
12594
+ deadline: confirmationDeadline,
12595
+ requested: requested?.toString(),
12596
+ agentId: options.agentId,
12597
+ evidence: {}
12598
+ }
12599
+ };
12600
+ hadDurableRecord = saveWithdrawSwap(record2);
12601
+ const assertSubmission = () => {
12602
+ assertCurrent();
12603
+ if (Date.now() >= confirmationDeadline) throw new OwneyError(
12604
+ "WITHDRAW_SWAP_INCOMPLETE",
12605
+ "Withdrawal confirmation timed out. Check Activity before retrying.",
12606
+ { reason: "PAYOUT_CONFIRMATION_TIMEOUT", swapSubmitted: false }
12607
+ );
12608
+ };
12037
12609
  const evidence = /* @__PURE__ */ new Map();
12038
12610
  const onProgress = (next) => {
12039
12611
  if (next.chainId !== activeChainId || next.token !== asset.symbol || next.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase()) return;
12040
12612
  evidence.set(next.agentId, next);
12613
+ if (record2?.recovery) {
12614
+ record2.recovery.evidence[next.agentId] = next;
12615
+ if (active2) saveWithdrawSwap(record2);
12616
+ else {
12617
+ const latest = listWithdrawSwaps(Date.now(), true).find((row) => row.recovery?.attemptId === record2.recovery.attemptId);
12618
+ if (latest?.recovery) updateWithdrawSwap(latest.orderHash, { recovery: {
12619
+ ...latest.recovery,
12620
+ evidence: { ...latest.recovery.evidence, [next.agentId]: next }
12621
+ } });
12622
+ }
12623
+ }
12041
12624
  notifyFunding(options.onWithdrawalProgress, next);
12042
12625
  };
12043
12626
  let withdraw;
12044
12627
  try {
12045
- withdraw = await this.withdraw({
12628
+ withdraw = saved ? { agentResult: saved.recovery.results, totalWithdrawn: null } : await bounded(() => this.withdraw({
12046
12629
  asset: asset.symbol,
12047
12630
  ...requested === void 0 ? {} : { amount: requested.toString() },
12048
12631
  ...options.agentId ? { agentId: options.agentId } : {},
12049
- onProgress
12050
- });
12632
+ onProgress,
12633
+ assertCurrent: assertSubmission,
12634
+ onBeforeSubmission: (next) => {
12635
+ assertSubmission();
12636
+ onProgress(next);
12637
+ },
12638
+ observationDeadline: confirmationDeadline
12639
+ }), Math.max(0, confirmationDeadline - Date.now()), () => new OwneyError(
12640
+ "WITHDRAW_SWAP_INCOMPLETE",
12641
+ "Withdrawal confirmation timed out. Check Activity and balances before trying again.",
12642
+ { reason: "WITHDRAWAL_DISPATCH_TIMEOUT", swapSubmitted: false, withdrawalEvidence: Object.fromEntries(evidence) }
12643
+ ));
12051
12644
  } catch (error) {
12052
12645
  const details = error instanceof OwneyError ? error.details : void 0;
12053
12646
  const moved = [...evidence.values()].some((value) => value.transfer !== "failed" && (value.delivery === "confirmed" || value.transfer === "unknown" || value.txHash || value.userOpHash || value.callsId || value.withdrawalRequest?.attempted));
@@ -12069,6 +12662,9 @@ var OwneySDK = class {
12069
12662
  const results = "agentResult" in withdraw ? withdraw.agentResult : {
12070
12663
  [options.agentId ?? withdraw.evidence?.agentId ?? "unknown"]: withdraw
12071
12664
  };
12665
+ record2.recovery.results = results;
12666
+ record2.recovery.phase = "verifying";
12667
+ saveWithdrawSwap(record2);
12072
12668
  const arrived = await awaitWithdrawalPayouts({
12073
12669
  results,
12074
12670
  requested,
@@ -12080,7 +12676,8 @@ var OwneySDK = class {
12080
12676
  baselineBlock,
12081
12677
  assertCurrent,
12082
12678
  readBalance: readWalletBalance,
12083
- timeoutMs: options.arrivalTimeoutMs,
12679
+ deadline: confirmationDeadline,
12680
+ verifyCompletion: (value) => this.getAgent(value.agentId).verifyWithdrawalCompletion?.(state, value) ?? Promise.resolve({ status: "unavailable" }),
12084
12681
  inspect: (value, fromHistory) => this.inspectFunding(value, "withdraw", { requireAmountMatch: fromHistory, refreshBalance: false }),
12085
12682
  resolve: (value) => this.resolveSponsoredWithdrawalEvidence(value, { submittedAt }),
12086
12683
  history: (value, fromDate, filters) => this.getFundingHistory(value, fromDate, filters),
@@ -12088,34 +12685,89 @@ var OwneySDK = class {
12088
12685
  });
12089
12686
  assertCurrent();
12090
12687
  const withdrawn = arrived.toString();
12688
+ record2.withdrawn = withdrawn;
12689
+ record2.status = "ready";
12690
+ record2.recovery.phase = "ready";
12691
+ hadDurableRecord = saveWithdrawSwap(record2) || hadDurableRecord;
12091
12692
  options.onSwapProgress?.("withdrawn");
12092
12693
  try {
12093
- const quote = await this.swapApi().quote({
12694
+ const quote = await bounded(() => this.swapApi().quote({
12094
12695
  from: { ...options.from, amount: withdrawn },
12095
12696
  to: options.to,
12096
12697
  direction: "withdraw",
12097
12698
  walletAddress: state.walletAddress
12098
- });
12699
+ }), 2e4, () => new OwneyError("SWAP_REQUEST_FAILED", "Swap pricing timed out.", { outcome: "not-submitted" }));
12099
12700
  assertCurrent();
12100
- await this.ensureSwapChain(quote.src.chainId);
12701
+ await bounded(
12702
+ () => this.ensureSwapChain(quote.src.chainId),
12703
+ 18e4,
12704
+ () => new OwneyError("SWAP_WALLET_PROMPT_TIMEOUT", "The wallet did not confirm its chain. No swap was submitted.", { outcome: "not-submitted" })
12705
+ );
12101
12706
  assertCurrent();
12102
- const swap = await executeSwap(this.buildSwapDeps(quote), {
12707
+ const swap = await executeSwap(this.buildSwapDeps(quote, assertCurrent, (hash2) => {
12708
+ record2.txHash = hash2;
12709
+ saveWithdrawSwap(record2);
12710
+ }), {
12711
+ onTransactionRequested: () => {
12712
+ assertCurrent();
12713
+ record2.status = "unknown";
12714
+ record2.recovery.phase = "submitted";
12715
+ if (!saveWithdrawSwap(record2) && hadDurableRecord)
12716
+ throw new OwneyError("SWAP_REQUEST_FAILED", "Recovery storage is unavailable. No swap was submitted.", { outcome: "not-submitted" });
12717
+ },
12718
+ assertCurrent,
12719
+ sourceReserve: baseline,
12103
12720
  quote,
12104
12721
  walletAddress: state.walletAddress,
12105
12722
  direction: "withdraw",
12106
12723
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
12107
12724
  ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
12108
- onOrderSubmitted: (orderHash) => {
12725
+ onOrderPrepared: (orderHash, details) => {
12726
+ assertCurrent();
12727
+ const previous = record2.orderHash;
12728
+ record2.orderHash = orderHash;
12729
+ record2.status = "unknown";
12730
+ record2.recovery.phase = "submitted";
12731
+ record2.spent = details.spent;
12732
+ record2.expected = details.expected;
12733
+ if (!saveWithdrawSwap(record2) && hadDurableRecord) {
12734
+ record2.orderHash = previous;
12735
+ throw new OwneyError("SWAP_REQUEST_FAILED", "Recovery storage is unavailable. No swap was submitted.", { outcome: "not-submitted" });
12736
+ }
12737
+ if (previous !== orderHash) {
12738
+ updateWithdrawSwap(previous, { status: "not-swapped" });
12739
+ clearWithdrawSwap(previous);
12740
+ }
12741
+ },
12742
+ onOrderSubmitted: (orderHash, details) => {
12109
12743
  releaseSubmission();
12110
- options.onOrderSubmitted?.({ orderHash, withdrawn, expected: quote.dst.amount });
12744
+ record2.orderHash = orderHash;
12745
+ record2.status = "swapping";
12746
+ record2.recovery.phase = "submitted";
12747
+ record2.createdAt = Date.now();
12748
+ const durable = saveWithdrawSwap(record2);
12749
+ options.onOrderSubmitted?.({
12750
+ orderHash,
12751
+ withdrawn,
12752
+ expected: details?.expected ?? quote.dst.amount,
12753
+ spent: details?.spent,
12754
+ durable: durable && details?.durable !== false
12755
+ });
12111
12756
  }
12112
12757
  });
12758
+ record2.status = "landed";
12759
+ record2.received = swap.received;
12760
+ record2.spent = swap.spent;
12761
+ saveWithdrawSwap(record2);
12113
12762
  return { withdraw, withdrawn, swap };
12114
12763
  } catch (error) {
12115
12764
  throw new OwneyError(
12116
12765
  "WITHDRAW_SWAP_FAILED",
12117
- `Withdrew ${withdrawn} ${asset.symbol} to your wallet, but the swap to ${options.to.symbol} did not complete. The funds are in your wallet as ${asset.symbol}.`,
12766
+ `The withdrawal completed, but the swap to ${options.to.symbol} could not be confirmed. Check the order and both balances before retrying.`,
12118
12767
  {
12768
+ outcome: record2.recovery?.phase === "ready" ? "not-submitted" : "submitted-unknown",
12769
+ ...error instanceof OwneyError ? error.details : {},
12770
+ ...record2.orderHash.startsWith("0x") ? { orderHash: record2.orderHash, settlementUnconfirmed: true } : {},
12119
12771
  withdrawn,
12120
12772
  asset: asset.symbol,
12121
12773
  chainId: asset.chainId,
@@ -12126,7 +12778,25 @@ var OwneySDK = class {
12126
12778
  }
12127
12779
  );
12128
12780
  }
12781
+ } catch (error) {
12782
+ if (record2) {
12783
+ if (error instanceof OwneyError && error.details) Object.assign(error.details, {
12784
+ attemptId: record2.recovery?.attemptId,
12785
+ phase: record2.recovery?.phase,
12786
+ elapsedMs: Date.now() - startedAt,
12787
+ txHash: record2.txHash,
12788
+ withdrawalEvidence: record2.recovery?.evidence
12789
+ });
12790
+ const details = error instanceof OwneyError ? error.details : void 0;
12791
+ const outcome = details?.outcome;
12792
+ if (record2.recovery && outcome === "not-submitted") record2.recovery.phase = "ready";
12793
+ record2.status = outcome === "refunded" ? "refunded" : outcome === "refund-pending" ? "refund-pending" : outcome === "partially-filled" ? "partially-filled" : record2.recovery?.phase === "submitted" || details?.settlementUnconfirmed ? "unknown" : record2.recovery?.phase === "ready" ? "ready" : "withdrawal-incomplete";
12794
+ if (record2.recovery) record2.recovery.reason = typeof details?.reason === "string" ? details.reason : void 0;
12795
+ saveWithdrawSwap(record2);
12796
+ }
12797
+ throw error;
12129
12798
  } finally {
12799
+ active2 = false;
12130
12800
  releaseSubmission();
12131
12801
  }
12132
12802
  }
@@ -12159,7 +12829,15 @@ var OwneySDK = class {
12159
12829
  const attempted = /* @__PURE__ */ new Set();
12160
12830
  this.withdrawalSubmissions.add(submissionKey);
12161
12831
  try {
12162
- return await this.withdrawFromAgents(options, state, chainId, attempted);
12832
+ return options.observationDeadline === void 0 ? await this.withdrawFromAgents(options, state, chainId, attempted) : await bounded(
12833
+ () => this.withdrawFromAgents(options, state, chainId, attempted),
12834
+ Math.max(0, options.observationDeadline - Date.now()),
12835
+ () => new OwneyError(
12836
+ "WITHDRAW_UNCONFIRMED",
12837
+ "Withdrawal dispatch could not be confirmed before the deadline.",
12838
+ { phase: "observe", reason: "WITHDRAWAL_DISPATCH_TIMEOUT" }
12839
+ )
12840
+ );
12163
12841
  } catch (error) {
12164
12842
  for (const id of ids) if (!attempted.has(id)) {
12165
12843
  notifyFunding(options.onProgress, {
@@ -12249,7 +12927,7 @@ var OwneySDK = class {
12249
12927
  progress(evidence);
12250
12928
  adapterReported = false;
12251
12929
  try {
12252
- if (options.onBeforeSubmission)
12930
+ if (options.onBeforeSubmission || options.assertCurrent)
12253
12931
  return await agent.withdraw(
12254
12932
  state,
12255
12933
  chainId,
@@ -12257,7 +12935,8 @@ var OwneySDK = class {
12257
12935
  requested2,
12258
12936
  progress,
12259
12937
  options.gasMode ?? "auto",
12260
- (next) => options.onBeforeSubmission({ ...evidence, ...next })
12938
+ options.onBeforeSubmission ? (next) => options.onBeforeSubmission({ ...evidence, ...next }) : void 0,
12939
+ options.assertCurrent
12261
12940
  );
12262
12941
  return await (options.gasMode === void 0 ? agent.withdraw(state, chainId, token, requested2, progress) : agent.withdraw(state, chainId, token, requested2, progress, options.gasMode));
12263
12942
  } catch (error) {
@@ -12381,7 +13060,7 @@ var OwneySDK = class {
12381
13060
  if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
12382
13061
  if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
12383
13062
  const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
12384
- if (!total || parseUnits3(total.amount, assetInfo.decimals) <= entry.balance) return;
13063
+ if (!total || parseUnits4(total.amount, assetInfo.decimals) <= entry.balance) return;
12385
13064
  }
12386
13065
  try {
12387
13066
  const otherAvailable = indexedAvailable - entry.balance;