@owney/sdk 0.7.27-beta.0 → 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.verifiedVaults.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,23 +5964,23 @@ 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.readVaultLiquidity(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
- const group = (await checks).slice(index * 3, index * 3 + 3);
5699
- if (!active || group.length !== 3 || group.some((value) => value.status !== "success")) return null;
5977
+ const group = (await checks).slice(index * 2, index * 2 + 2);
5978
+ if (!active2 || group.length !== 2 || group.some((value) => value.status !== "success")) return null;
5700
5979
  this.assertSession(generation, chainId, asset);
5701
- const [underlying, shares, withdrawable] = group.map((value) => value.result);
5702
- if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
5703
- const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
5704
- if (shares > 0n && withdrawable === 0n) {
5980
+ const [underlying, shares] = group.map((value) => value.result);
5981
+ if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n) return null;
5982
+ const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: "0" };
5983
+ if (shares > 0n) {
5705
5984
  let positiveValue = false;
5706
5985
  const valuations = await Promise.all(["convertToAssets", "previewRedeem"].map(async (functionName) => {
5707
5986
  try {
@@ -5712,9 +5991,11 @@ var YieldseekerAgent = class {
5712
5991
  args: [shares],
5713
5992
  blockNumber
5714
5993
  });
5715
- if (typeof value !== "bigint" || value < 0n) return null;
5994
+ if (!active2 || typeof value !== "bigint" || value < 0n) return null;
5995
+ this.assertSession(generation, chainId, asset);
5716
5996
  if (value > 0n && !positiveValue) {
5717
5997
  positiveValue = true;
5998
+ verified.withdrawableAssetsRaw = value.toString();
5718
5999
  verifiedPositions.push(verified);
5719
6000
  }
5720
6001
  return value;
@@ -5722,7 +6003,12 @@ var YieldseekerAgent = class {
5722
6003
  return null;
5723
6004
  }
5724
6005
  }));
5725
- if (positiveValue) return verified;
6006
+ if (positiveValue) {
6007
+ const redemptionValue = valuations[1];
6008
+ if (redemptionValue !== null && redemptionValue > 0n)
6009
+ verified.withdrawableAssetsRaw = redemptionValue.toString();
6010
+ return verified;
6011
+ }
5726
6012
  if (blockNumber === void 0 || valuations.some((value) => value !== 0n)) return null;
5727
6013
  zeroValuePositions.add(verified);
5728
6014
  }
@@ -5733,7 +6019,7 @@ var YieldseekerAgent = class {
5733
6019
  }
5734
6020
  });
5735
6021
  const result = await boundedVerification(Promise.all(reads), timeoutMs);
5736
- active = false;
6022
+ active2 = false;
5737
6023
  this.assertSession(generation, chainId, asset);
5738
6024
  return {
5739
6025
  verified: result !== void 0 && result.every((position2) => position2 !== null),
@@ -5745,13 +6031,14 @@ var YieldseekerAgent = class {
5745
6031
  const response = await this.walletRequest(
5746
6032
  state,
5747
6033
  chainId,
5748
- this.agentPath(context, "yield-positions")
6034
+ this.agentPath(context, "vault-positions")
5749
6035
  );
5750
6036
  this.assertSession(generation, chainId, asset);
5751
- if (!Array.isArray(response?.yieldPositions)) throw this.invalidResponse("yield positions");
5752
- return validatePositions(response.yieldPositions);
6037
+ if (!Array.isArray(response?.vaultPositions)) throw this.invalidResponse("vault positions");
6038
+ return validatePositions(response.vaultPositions);
5753
6039
  };
5754
6040
  if (amount === void 0) {
6041
+ rememberPositions(priorPositions);
5755
6042
  rememberPositions(funds.indexedPositions);
5756
6043
  rememberPositions(positions);
5757
6044
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
@@ -5778,7 +6065,7 @@ var YieldseekerAgent = class {
5778
6065
  throw new OwneyError(
5779
6066
  "WITHDRAW_FAILED",
5780
6067
  "Yieldseeker vault funds are not currently withdrawable.",
5781
- { asset, positionsStatus, remainingPositions: positions.map((position2) => position2.yieldOption.address) },
6068
+ { asset, positionsStatus, remainingPositions: positions.map((position2) => position2.vaultOption.address) },
5782
6069
  this.id
5783
6070
  );
5784
6071
  }
@@ -5827,6 +6114,7 @@ var YieldseekerAgent = class {
5827
6114
  let latestUnwindBlock;
5828
6115
  const unwind = async (position2, assetsRaw) => {
5829
6116
  this.assertSession(generation, chainId, asset);
6117
+ assertOperation?.();
5830
6118
  let unwindTransactionHash;
5831
6119
  try {
5832
6120
  const response = await this.walletRequest(
@@ -5835,7 +6123,8 @@ var YieldseekerAgent = class {
5835
6123
  this.agentPath(context, "withdraw-from-position"),
5836
6124
  {
5837
6125
  method: "POST",
5838
- body: { chainId, vaultAddress: position2.yieldOption.address, assetsRaw: assetsRaw.toString() }
6126
+ body: { chainId, vaultAddress: position2.vaultOption.address, assetsRaw: assetsRaw.toString() },
6127
+ assertCurrent: assertOperation
5839
6128
  }
5840
6129
  );
5841
6130
  if (!this.isTransactionHash(response?.transactionHash)) throw this.invalidResponse("position withdrawal");
@@ -5854,7 +6143,7 @@ var YieldseekerAgent = class {
5854
6143
  ...error instanceof OwneyError ? error.details : {},
5855
6144
  withdrawalPhase: "unwind",
5856
6145
  reason: "YIELDSEEKER_UNWIND_UNRESOLVED",
5857
- vaultAddress: position2.yieldOption.address,
6146
+ vaultAddress: position2.vaultOption.address,
5858
6147
  unwindTransactionHash,
5859
6148
  ownerTransferSubmitted: false
5860
6149
  },
@@ -5867,7 +6156,7 @@ var YieldseekerAgent = class {
5867
6156
  };
5868
6157
  if (amount === void 0) {
5869
6158
  for (let sweep = 0; sweep < YIELDSEEKER_MAX_WITHDRAW_SWEEPS && hasPositions(positions); sweep++) {
5870
- 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)]));
5871
6160
  let unwound = false;
5872
6161
  for (const position2 of positions) {
5873
6162
  const available = BigInt(position2.withdrawableAssetsRaw);
@@ -5884,14 +6173,16 @@ var YieldseekerAgent = class {
5884
6173
  if (catalog) rememberPositions(catalog);
5885
6174
  this.assertSession(generation, chainId, asset);
5886
6175
  if (proof.verified) {
5887
- positions = [...proof.positions, ...(catalog ?? []).filter((position2) => !knownBeforeRefresh.has(position2.yieldOption.address.toLowerCase()))];
5888
- } else if (catalog) positions = catalog;
5889
- else {
6176
+ positions = [...proof.positions, ...(catalog ?? []).filter((position2) => !knownBeforeRefresh.has(position2.vaultOption.address.toLowerCase()))];
6177
+ } else if (catalog) {
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()))];
6180
+ } else {
5890
6181
  positionsStatus = "unavailable";
5891
6182
  break;
5892
6183
  }
5893
6184
  positionsStatus = hasPositions(positions) ? "remaining" : "empty";
5894
- 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;
5895
6186
  }
5896
6187
  } else {
5897
6188
  for (const position2 of positions) {
@@ -5904,7 +6195,7 @@ var YieldseekerAgent = class {
5904
6195
  }
5905
6196
  }
5906
6197
  if (amount !== void 0 && remaining > 0n) {
5907
- throw this.invalidResponse("yield positions", {
6198
+ throw this.invalidResponse("vault positions", {
5908
6199
  reason: "Withdrawable positions could not cover the request.",
5909
6200
  remaining: remaining.toString()
5910
6201
  });
@@ -5939,6 +6230,13 @@ var YieldseekerAgent = class {
5939
6230
  const account = getAddress5(state.walletAddress);
5940
6231
  let receipt;
5941
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);
5942
6240
  let submissionEvidence = base7;
5943
6241
  const data = amount === void 0 ? encodeFunctionData2({
5944
6242
  abi: YIELDSEEKER_AGENT_WALLET_ABI,
@@ -5956,6 +6254,7 @@ var YieldseekerAgent = class {
5956
6254
  client: this.getReceiptClient(),
5957
6255
  apiKey: this.owneyApiKey,
5958
6256
  routingApiBaseUrl: this.routingApiBaseUrl,
6257
+ assertCurrent: assertOperation,
5959
6258
  yieldseekerSignature: await this.auth.getToken(state, chainId),
5960
6259
  refreshYieldseekerSignature: (rejected) => this.auth.refreshToken(state, chainId, rejected),
5961
6260
  userId: context.user.userId,
@@ -5968,6 +6267,7 @@ var YieldseekerAgent = class {
5968
6267
  notifyFunding(onProgress, submissionEvidence);
5969
6268
  },
5970
6269
  onBeforeSubmission: async (pending) => {
6270
+ assertOperation?.();
5971
6271
  await onBeforeSubmission?.({ ...base7, ...pending });
5972
6272
  onOwnerTransferAttempted?.();
5973
6273
  }
@@ -5976,15 +6276,26 @@ var YieldseekerAgent = class {
5976
6276
  receipt = submitted.receipt;
5977
6277
  } else {
5978
6278
  onOwnerTransferAttempted?.();
5979
- txHash = await this.submitTransaction(state, chainId, {
5980
- from: account,
5981
- to: getAddress5(context.wallet.walletAddress),
5982
- data,
5983
- value: "0",
5984
- chainId
5985
- }, (hash2) => notifyFunding(onProgress, { ...base7, txHash: hash2 }), (value) => {
5986
- receipt = value;
5987
- });
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
+ );
5988
6299
  }
5989
6300
  const evidence = receipt ? inspectFundingReceipt(receipt, { ...submissionEvidence, txHash }, metadata.address, "withdraw") : { ...submissionEvidence, txHash, transfer: "pending", delivery: "pending" };
5990
6301
  notifyFunding(onProgress, evidence);
@@ -6029,7 +6340,7 @@ var YieldseekerAgent = class {
6029
6340
  asset,
6030
6341
  positionsStatus,
6031
6342
  remainingPositions: positionsStatus === "remaining" ? positions.filter((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n)).map((position2) => ({
6032
- vaultAddress: position2.yieldOption.address,
6343
+ vaultAddress: position2.vaultOption.address,
6033
6344
  sharesRaw: position2.assetsRaw,
6034
6345
  withdrawableAmountRaw: position2.withdrawableAssetsRaw
6035
6346
  })) : void 0,
@@ -6038,6 +6349,7 @@ var YieldseekerAgent = class {
6038
6349
  this.id
6039
6350
  );
6040
6351
  }
6352
+ if (evidence.maxCompletion) evidence.maxCompletion.vaults = [...knownPositions.keys()];
6041
6353
  if (!proof.verified) completion = "unverified";
6042
6354
  }
6043
6355
  if (explicitEmptyPayout)
@@ -6070,7 +6382,7 @@ var YieldseekerAgent = class {
6070
6382
  }
6071
6383
  }
6072
6384
  }
6073
- /** A withdrawal-only liquidity floor; display snapshots never authorize it. */
6385
+ /** Fresh position amounts for withdrawal planning, net of reserved fees. */
6074
6386
  async getWithdrawableBalance(state, chainId, asset, minimumAmount) {
6075
6387
  this.assertChain(chainId);
6076
6388
  this.assertAsset(asset);
@@ -6095,7 +6407,66 @@ var YieldseekerAgent = class {
6095
6407
  );
6096
6408
  return proof.available;
6097
6409
  }
6098
- async readVaultLiquidity(options, wallet, blockNumber) {
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
+ }
6469
+ async readVaultShares(options, wallet, blockNumber) {
6099
6470
  const owner = getAddress5(wallet);
6100
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));
6101
6472
  const results = await Promise.all(batches.map((batch) => this.getReceiptClient().multicall({
@@ -6105,8 +6476,7 @@ var YieldseekerAgent = class {
6105
6476
  const address = getAddress5(option.address);
6106
6477
  return [
6107
6478
  { address, abi: erc4626Abi2, functionName: "asset" },
6108
- { address, abi: erc20Abi5, functionName: "balanceOf", args: [owner] },
6109
- { address, abi: erc4626Abi2, functionName: "maxWithdraw", args: [owner] }
6479
+ { address, abi: erc20Abi5, functionName: "balanceOf", args: [owner] }
6110
6480
  ];
6111
6481
  })
6112
6482
  })));
@@ -6116,23 +6486,23 @@ var YieldseekerAgent = class {
6116
6486
  const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
6117
6487
  const seen = /* @__PURE__ */ new Set();
6118
6488
  for (const value of values) {
6119
- const option = value?.yieldOption;
6489
+ const option = value?.vaultOption;
6120
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))
6121
- throw this.invalidResponse("yield positions");
6491
+ throw this.invalidResponse("vault positions");
6122
6492
  seen.add(option.address.toLowerCase());
6123
6493
  }
6124
6494
  return values;
6125
6495
  }
6126
6496
  withdrawalVaultHints(state, chainId, context) {
6127
6497
  const key2 = this.contextKey(state, chainId, context.asset);
6128
- const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
6129
- 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]));
6130
6500
  const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
6131
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()));
6132
- for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
6502
+ for (const option of this.vaultOptions.get(context.asset)?.value ?? []) {
6133
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;
6134
6504
  hints.set(option.address.toLowerCase(), {
6135
- yieldOption: option,
6505
+ vaultOption: option,
6136
6506
  assetsRaw: "0",
6137
6507
  assetsBase: "0",
6138
6508
  assetsUsd: 0,
@@ -6166,7 +6536,7 @@ var YieldseekerAgent = class {
6166
6536
  async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
6167
6537
  const generation = this.readGeneration;
6168
6538
  const contextKey = this.contextKey(state, chainId, context.asset);
6169
- const version = this.portfolioVersion(contextKey);
6539
+ const version = this.liquidityVersions.get(contextKey) ?? 0;
6170
6540
  const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
6171
6541
  const unavailable = () => new OwneyError(
6172
6542
  "WITHDRAW_BALANCE_UNAVAILABLE",
@@ -6174,12 +6544,12 @@ var YieldseekerAgent = class {
6174
6544
  { asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
6175
6545
  this.id
6176
6546
  );
6177
- let active = true;
6547
+ let active2 = true;
6178
6548
  let knownProof;
6179
- const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.verifiedVaults.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);
6180
6550
  const assertReadCurrent = () => {
6181
6551
  this.assertSession(generation, chainId, context.asset);
6182
- if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
6552
+ if (!active2 || (this.liquidityVersions.get(contextKey) ?? 0) !== version) throw unavailable();
6183
6553
  };
6184
6554
  const read2 = async () => {
6185
6555
  const client = this.getReceiptClient();
@@ -6190,11 +6560,10 @@ var YieldseekerAgent = class {
6190
6560
  const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
6191
6561
  const candidates = /* @__PURE__ */ new Map();
6192
6562
  for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
6193
- candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
6194
- 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);
6195
6565
  const options = [...candidates.values()];
6196
6566
  if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
6197
- const checksRead = this.readVaultLiquidity(options, wallet, blockNumber).then((value) => ({ value }), (error) => ({ error }));
6198
6567
  const [idle, feeTracker] = await Promise.all([
6199
6568
  client.readContract({
6200
6569
  address: metadata.address,
@@ -6221,7 +6590,7 @@ var YieldseekerAgent = class {
6221
6590
  });
6222
6591
  if (typeof fees !== "bigint" || fees < 0n) throw unavailable();
6223
6592
  const verified = [];
6224
- const verifiedVaults = [];
6593
+ const resolvedVaults = [];
6225
6594
  let deployed = 0n;
6226
6595
  const proof = (complete) => ({
6227
6596
  blockNumber,
@@ -6229,53 +6598,102 @@ var YieldseekerAgent = class {
6229
6598
  fees,
6230
6599
  available: idle + deployed > fees ? idle + deployed - fees : 0n,
6231
6600
  positions: [...verified],
6232
- verifiedVaults: [...verifiedVaults],
6601
+ resolvedVaults: [...resolvedVaults],
6233
6602
  indexedPositions,
6234
6603
  complete
6235
6604
  });
6236
6605
  assertReadCurrent();
6237
6606
  knownProof = proof(false);
6238
6607
  if (requested !== void 0 && knownProof.available >= requested) return knownProof;
6239
- const collect = (options2, results) => {
6608
+ const indexed = new Map(indexedPositions.map((position2) => [position2.vaultOption.address.toLowerCase(), position2]));
6609
+ const indexedValue = indexedPositions.reduce((sum, position2) => sum + BigInt(position2.withdrawableAssetsRaw), 0n);
6610
+ const missingKnownPosition = previousPositions.some((position2) => !indexed.has(position2.vaultOption.address.toLowerCase()));
6611
+ const unvaluedPosition = indexedPositions.some((position2) => BigInt(position2.assetsRaw) > 0n && BigInt(position2.withdrawableAssetsRaw) === 0n);
6612
+ const indexedAvailable = idle + indexedValue > fees ? idle + indexedValue - fees : 0n;
6613
+ if (indexedValue > 0n && !missingKnownPosition && !unvaluedPosition && (requested === void 0 || indexedAvailable >= requested)) {
6614
+ const known = this.balanceVaults.get(contextKey) ?? /* @__PURE__ */ new Set();
6615
+ for (const position2 of indexedPositions) if (BigInt(position2.withdrawableAssetsRaw) > 0n)
6616
+ known.add(position2.vaultOption.address.toLowerCase());
6617
+ this.balanceVaults.set(contextKey, known);
6618
+ return {
6619
+ blockNumber,
6620
+ idle,
6621
+ fees,
6622
+ available: indexedAvailable,
6623
+ positions: indexedPositions,
6624
+ resolvedVaults: [...indexed.keys()],
6625
+ indexedPositions,
6626
+ complete: true
6627
+ };
6628
+ }
6629
+ const collect = async (options2, results) => {
6240
6630
  assertReadCurrent();
6241
6631
  const known = this.balanceVaults.get(contextKey) ?? /* @__PURE__ */ new Set();
6242
6632
  this.balanceVaults.set(contextKey, known);
6243
- let valid2 = true;
6244
- options2.forEach((option, index) => {
6245
- const group = results.slice(index * 3, index * 3 + 3);
6246
- const shareProbe = group[1];
6247
- if (shareProbe?.status === "success" && typeof shareProbe.result === "bigint" && shareProbe.result > 0n)
6248
- known.add(option.address.toLowerCase());
6249
- if (group.length !== 3 || group.some((value) => value.status !== "success")) {
6250
- valid2 = false;
6251
- return;
6252
- }
6253
- const [underlying, shares, withdrawable] = group.map((value) => value.result);
6254
- if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n || shares === 0n && withdrawable > 0n) {
6255
- valid2 = false;
6256
- return;
6257
- }
6258
- verifiedVaults.push(option.address.toLowerCase());
6259
- deployed += withdrawable;
6260
- verified.push({
6261
- yieldOption: option,
6633
+ const valid2 = await Promise.all(options2.map(async (option, index) => {
6634
+ const group = results.slice(index * 2, index * 2 + 2);
6635
+ if (group.length !== 2 || group.some((value2) => value2.status !== "success")) return false;
6636
+ const [underlying, shares] = group.map((value2) => value2.result);
6637
+ if (typeof underlying !== "string" || !isAddress4(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n) return false;
6638
+ if (shares > 0n) known.add(option.address.toLowerCase());
6639
+ const position2 = {
6640
+ vaultOption: option,
6262
6641
  assetsRaw: shares.toString(),
6263
6642
  assetsBase: "0",
6264
6643
  assetsUsd: 0,
6265
- withdrawableAssetsRaw: withdrawable.toString(),
6266
- withdrawableAssetsBase: withdrawable.toString()
6267
- });
6268
- });
6269
- return valid2;
6644
+ withdrawableAssetsRaw: "0",
6645
+ withdrawableAssetsBase: "0"
6646
+ };
6647
+ resolvedVaults.push(option.address.toLowerCase());
6648
+ verified.push(position2);
6649
+ knownProof = proof(false);
6650
+ if (shares === 0n) return true;
6651
+ let preview;
6652
+ try {
6653
+ const value2 = await client.readContract({
6654
+ address: getAddress5(option.address),
6655
+ abi: erc4626Abi2,
6656
+ functionName: "previewRedeem",
6657
+ args: [shares],
6658
+ blockNumber
6659
+ });
6660
+ if (typeof value2 === "bigint" && value2 >= 0n) preview = value2;
6661
+ } catch {
6662
+ }
6663
+ assertReadCurrent();
6664
+ let value = preview;
6665
+ if (value === void 0 || value === 0n) {
6666
+ try {
6667
+ const converted = await client.readContract({
6668
+ address: getAddress5(option.address),
6669
+ abi: erc4626Abi2,
6670
+ functionName: "convertToAssets",
6671
+ args: [shares],
6672
+ blockNumber
6673
+ });
6674
+ if (typeof converted === "bigint" && converted >= 0n && (converted > 0n || preview === 0n)) value = converted;
6675
+ else value = void 0;
6676
+ } catch {
6677
+ value = void 0;
6678
+ }
6679
+ }
6680
+ assertReadCurrent();
6681
+ if (value === void 0) return false;
6682
+ position2.withdrawableAssetsRaw = value.toString();
6683
+ position2.withdrawableAssetsBase = value.toString();
6684
+ deployed += value;
6685
+ knownProof = proof(false);
6686
+ return true;
6687
+ }));
6688
+ return valid2.every(Boolean);
6270
6689
  };
6271
- const checks = await checksRead;
6690
+ const checks = await this.readVaultShares(options, wallet, blockNumber);
6272
6691
  assertReadCurrent();
6273
- if ("error" in checks) throw checks.error;
6274
- const allKnownVerified = collect(options, checks.value);
6692
+ const allKnownVerified = await collect(options, checks);
6275
6693
  knownProof = proof(false);
6276
- const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
6694
+ if (requested !== void 0 && knownProof.available >= requested) return knownProof;
6277
6695
  const discrepancy = verified.some((position2) => {
6278
- const row = indexed.get(position2.yieldOption.address.toLowerCase());
6696
+ const row = indexed.get(position2.vaultOption.address.toLowerCase());
6279
6697
  return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
6280
6698
  });
6281
6699
  const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
@@ -6284,7 +6702,7 @@ var YieldseekerAgent = class {
6284
6702
  assertReadCurrent();
6285
6703
  return proof(true);
6286
6704
  }
6287
- const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
6705
+ const catalog = await this.loadVaultOptions(context.asset).catch(() => void 0);
6288
6706
  assertReadCurrent();
6289
6707
  const eligible = /* @__PURE__ */ new Map();
6290
6708
  for (const option of catalog ?? []) {
@@ -6293,7 +6711,7 @@ var YieldseekerAgent = class {
6293
6711
  }
6294
6712
  const extra = [...eligible].filter(([address]) => !candidates.has(address)).map(([, option]) => option);
6295
6713
  if (options.length + extra.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
6296
- const allExtraVerified = collect(extra, await this.readVaultLiquidity(extra, wallet, blockNumber));
6714
+ const allExtraVerified = await collect(extra, await this.readVaultShares(extra, wallet, blockNumber));
6297
6715
  assertReadCurrent();
6298
6716
  const result = proof(eligible.size > 0 && allKnownVerified && allExtraVerified);
6299
6717
  if (result.available === 0n && !result.complete && !hasUnverifiedIndexedFunds(result)) throw unavailable();
@@ -6310,7 +6728,7 @@ var YieldseekerAgent = class {
6310
6728
  if (error instanceof OwneyError) throw error;
6311
6729
  throw unavailable();
6312
6730
  } finally {
6313
- active = false;
6731
+ active2 = false;
6314
6732
  if (timer) clearTimeout(timer);
6315
6733
  }
6316
6734
  }
@@ -6324,7 +6742,7 @@ var YieldseekerAgent = class {
6324
6742
  this.balanceContexts.set(read2, { context, generation: this.readGeneration });
6325
6743
  const key2 = this.contextKey(state, chainId, context.asset);
6326
6744
  const known = this.balanceVaults.get(key2) ?? /* @__PURE__ */ new Set();
6327
- 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());
6328
6746
  this.balanceVaults.set(key2, known);
6329
6747
  }
6330
6748
  return balance;
@@ -6363,7 +6781,7 @@ var YieldseekerAgent = class {
6363
6781
  const total = idle + invested > fees ? idle + invested - fees : 0n;
6364
6782
  const displayed = balance.assetBalances?.find((item) => item.chainId === evidence.chainId && item.asset === evidence.token);
6365
6783
  if (!displayed || !evidence.confirmedAmount || !/^\d+$/.test(evidence.confirmedAmount)) return void 0;
6366
- const amount = parseUnits(displayed.amount, asset.decimals);
6784
+ const amount = parseUnits2(displayed.amount, asset.decimals);
6367
6785
  if (amount < 0n) return void 0;
6368
6786
  const displayUnit = evidence.token === "USDC" ? 5000n : 500000000000n;
6369
6787
  const movementUnit = BigInt(evidence.confirmedAmount) / 1000n;
@@ -6462,13 +6880,13 @@ var YieldseekerAgent = class {
6462
6880
  });
6463
6881
  const catalog = await Promise.all(
6464
6882
  [...new Set(contexts.map((context) => context.asset))].map(
6465
- (contextAsset) => this.loadYieldOptions(contextAsset)
6883
+ (contextAsset) => this.loadVaultOptions(contextAsset)
6466
6884
  )
6467
6885
  );
6468
6886
  const vaultAddresses = new Set(
6469
6887
  catalog.flat().filter(
6470
- (yieldOption) => yieldOption.chainId === chainId && isAddress4(yieldOption.address)
6471
- ).map((yieldOption) => yieldOption.address.toLowerCase())
6888
+ (vaultOption) => vaultOption.chainId === chainId && isAddress4(vaultOption.address)
6889
+ ).map((vaultOption) => vaultOption.address.toLowerCase())
6472
6890
  );
6473
6891
  return mapYieldseekerHistory(contexts, {
6474
6892
  limit: options?.limit ?? 10,
@@ -6493,30 +6911,30 @@ var YieldseekerAgent = class {
6493
6911
  const assets = requested ? [requested] : ["USDC", "WETH"];
6494
6912
  const values = await Promise.all(
6495
6913
  assets.map(async (asset) => {
6496
- return { asset, yieldOptions: await this.loadYieldOptions(asset) };
6914
+ return { asset, vaultOptions: await this.loadVaultOptions(asset) };
6497
6915
  })
6498
6916
  );
6499
6917
  return mapYieldseekerAgentApy(values, days);
6500
6918
  }
6501
- async loadYieldOptions(asset) {
6502
- const cached = this.yieldOptions.get(asset);
6919
+ async loadVaultOptions(asset) {
6920
+ const cached = this.vaultOptions.get(asset);
6503
6921
  if (cached && cached.expiresAt > Date.now()) return cached.value;
6504
- const pending = this.pendingYieldOptions.get(asset);
6922
+ const pending = this.pendingVaultOptions.get(asset);
6505
6923
  if (pending) return pending;
6506
6924
  const metadata = YIELDSEEKER_ASSET_METADATA[asset];
6507
6925
  const request2 = this.api.request(
6508
- `/chains/8453/assets/${metadata.address}/yield-options`
6926
+ `/chains/8453/assets/${metadata.address}/vault-options`
6509
6927
  ).then((response) => {
6510
- if (!Array.isArray(response?.yieldOptions)) {
6511
- throw this.invalidResponse("yield options");
6928
+ if (!Array.isArray(response?.vaultOptions)) {
6929
+ throw this.invalidResponse("vault options");
6512
6930
  }
6513
- this.yieldOptions.set(asset, {
6514
- expiresAt: Date.now() + YIELDSEEKER_YIELD_OPTIONS_CACHE_MS,
6515
- value: response.yieldOptions
6931
+ this.vaultOptions.set(asset, {
6932
+ expiresAt: Date.now() + YIELDSEEKER_VAULT_OPTIONS_CACHE_MS,
6933
+ value: response.vaultOptions
6516
6934
  });
6517
- return response.yieldOptions;
6518
- }).finally(() => this.pendingYieldOptions.delete(asset));
6519
- this.pendingYieldOptions.set(asset, request2);
6935
+ return response.vaultOptions;
6936
+ }).finally(() => this.pendingVaultOptions.delete(asset));
6937
+ this.pendingVaultOptions.set(asset, request2);
6520
6938
  return request2;
6521
6939
  }
6522
6940
  userKey(state, chainId) {
@@ -6826,9 +7244,10 @@ var YieldseekerAgent = class {
6826
7244
  expectedNetDeposits: BigInt(snapshot.netDepositsBase) + delta
6827
7245
  };
6828
7246
  }
6829
- advancePortfolioVersion(contextKey) {
7247
+ advancePortfolioVersion(contextKey, { snapshotOnly = false } = {}) {
6830
7248
  const version = this.portfolioVersion(contextKey) + 1;
6831
7249
  this.portfolioVersions.set(contextKey, version);
7250
+ if (!snapshotOnly) this.liquidityVersions.set(contextKey, version);
6832
7251
  this.snapshotFailures.delete(contextKey);
6833
7252
  for (const kind of ["snapshot", "positions", "historic", "actions"]) {
6834
7253
  const key2 = `${kind}:${contextKey}`;
@@ -6876,7 +7295,7 @@ var YieldseekerAgent = class {
6876
7295
  if ((!cached || cached.expiresAt <= Date.now()) && !this.pendingReads.has(key2)) {
6877
7296
  const failure = this.snapshotFailures.get(contextKey);
6878
7297
  if (failure && failure.retryAt > Date.now()) throw failure.error;
6879
- this.advancePortfolioVersion(contextKey);
7298
+ this.advancePortfolioVersion(contextKey, { snapshotOnly: true });
6880
7299
  }
6881
7300
  const version = this.portfolioVersion(contextKey);
6882
7301
  const generation = this.readGeneration;
@@ -6959,17 +7378,17 @@ var YieldseekerAgent = class {
6959
7378
  }
6960
7379
  async loadPortfolioParts(state, chainId, context, options, contextKey) {
6961
7380
  const [positions, historic, actions] = await Promise.all([
6962
- options.includePositions === false ? Promise.resolve({ yieldPositions: [] }) : this.cachedRead(
7381
+ options.includePositions === false ? Promise.resolve({ vaultPositions: [] }) : this.cachedRead(
6963
7382
  `positions:${contextKey}`,
6964
7383
  this.portfolioCacheMs(contextKey),
6965
7384
  async () => {
6966
7385
  const response = await this.walletRequest(
6967
7386
  state,
6968
7387
  chainId,
6969
- this.agentPath(context, "yield-positions")
7388
+ this.agentPath(context, "vault-positions")
6970
7389
  );
6971
- if (!Array.isArray(response?.yieldPositions)) {
6972
- throw this.invalidResponse("yield positions");
7390
+ if (!Array.isArray(response?.vaultPositions)) {
7391
+ throw this.invalidResponse("vault positions");
6973
7392
  }
6974
7393
  return response;
6975
7394
  }
@@ -6994,7 +7413,7 @@ var YieldseekerAgent = class {
6994
7413
  ) : Promise.resolve(void 0)
6995
7414
  ]);
6996
7415
  return {
6997
- positions: positions.yieldPositions,
7416
+ positions: positions.vaultPositions,
6998
7417
  ...historic?.position ? { historic: historic.position } : {},
6999
7418
  ...actions?.actions ? { actions: actions.actions } : {}
7000
7419
  };
@@ -7065,10 +7484,10 @@ var YieldseekerAgent = class {
7065
7484
  }
7066
7485
  async providerRequest(state, chainId, path, options = {}) {
7067
7486
  this.assertChain(chainId);
7068
- const request2 = (signature2) => this.api.request(path, {
7069
- ...options,
7070
- signature: signature2
7071
- });
7487
+ const request2 = (signature2) => {
7488
+ options.assertCurrent?.();
7489
+ return this.api.request(path, { method: options.method, body: options.body, signature: signature2 });
7490
+ };
7072
7491
  let signature = await this.auth.getToken(state, chainId);
7073
7492
  try {
7074
7493
  return await request2(signature);
@@ -7370,45 +7789,57 @@ async function fetchAgentKeys(apiKey, baseUrl = ROUTING_API_BASE_URL2) {
7370
7789
  // src/lib/swap/swap-api.ts
7371
7790
  async function request(baseUrl, apiKey, path, init) {
7372
7791
  const url = `${baseUrl}/api/v1/swap${path}`;
7373
- const res = await fetch(url, {
7374
- method: init?.method ?? "GET",
7375
- headers: {
7376
- "Content-Type": "application/json",
7377
- "x-owney-api-key": apiKey
7378
- },
7379
- ...init ? { body: JSON.stringify(init.body) } : {}
7380
- });
7381
- if (!res.ok) {
7382
- const text = await res.text().catch(() => "");
7383
- if (res.status === 429) {
7384
- throw new OwneyError(
7385
- "SWAP_RATE_LIMITED",
7386
- "Swap provider is rate limiting, retry shortly",
7387
- { statusCode: res.status }
7388
- );
7389
- }
7390
- if (res.status === 403) {
7391
- throw new OwneyError(
7392
- "SWAP_DISABLED",
7393
- "Swap is not enabled for this organization",
7394
- { statusCode: res.status }
7395
- );
7396
- }
7397
- throw new OwneyError(
7398
- "SWAP_REQUEST_FAILED",
7399
- `Swap API error ${res.status}: ${text}`,
7400
- { statusCode: res.status, responseBody: text }
7401
- );
7402
- }
7403
- const json = await res.json();
7404
- if (!json.success) {
7405
- 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(
7406
7836
  "SWAP_REQUEST_FAILED",
7407
- `Swap API request failed: ${json.message ?? "unknown error"}`,
7408
- { message: json.message }
7409
- );
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();
7410
7842
  }
7411
- return json.data;
7412
7843
  }
7413
7844
  function createSwapApi(baseUrl, apiKey) {
7414
7845
  return {
@@ -7714,8 +8145,12 @@ async function awaitBalanceIncrease(options) {
7714
8145
  let lastError;
7715
8146
  for (; ; ) {
7716
8147
  try {
7717
- const balance = await readBalance();
7718
- 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) {
7719
8154
  const arrived = balance - baseline;
7720
8155
  debugLog("owney-sdk", "arrival: funds landed", {
7721
8156
  arrived: arrived.toString()
@@ -7741,7 +8176,7 @@ async function awaitBalanceIncrease(options) {
7741
8176
  lastReadError: lastError instanceof Error ? lastError.message : String(lastError)
7742
8177
  } : { arrived: null };
7743
8178
  }
7744
- await sleep(pollMs);
8179
+ await sleep(Math.min(pollMs, Math.max(0, deadline - Date.now())));
7745
8180
  }
7746
8181
  }
7747
8182
 
@@ -7794,7 +8229,7 @@ var DEFAULT_POLL_MS2 = 5e3;
7794
8229
  var MAX_BACKOFF_MS = 3e4;
7795
8230
  var backoffFor = (failures, base7) => Math.min(base7 * 2 ** Math.min(failures, 5), MAX_BACKOFF_MS);
7796
8231
  var DEFAULT_TIMEOUT_MS2 = 15 * 60 * 1e3;
7797
- async function runFusionOrder(deps, options) {
8232
+ async function runFusionOrderInternal(deps, options) {
7798
8233
  const {
7799
8234
  orderHash,
7800
8235
  secrets,
@@ -7803,39 +8238,51 @@ async function runFusionOrder(deps, options) {
7803
8238
  timeoutMs = DEFAULT_TIMEOUT_MS2
7804
8239
  } = options;
7805
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
+ };
7806
8261
  let failures = 0;
7807
8262
  const published = /* @__PURE__ */ new Set();
7808
8263
  onStage?.("swapping");
7809
8264
  for (; ; ) {
7810
- if (deps.now() >= deadline) {
7811
- throw new OwneyError(
7812
- "SWAP_REQUEST_FAILED",
7813
- "Timed out waiting for the swap to settle. It may still complete \u2014 check the order status before retrying.",
7814
- // Marks the one failure that means "we stopped watching a live order",
7815
- // as opposed to a verdict or a prompt nobody answered. The executor
7816
- // checks the wallet before believing it: resolvers do not stop filling
7817
- // because we stopped looking (ROUT-242, comment 15980).
7818
- { orderHash, settlementUnconfirmed: true }
7819
- );
7820
- }
8265
+ assertTime();
7821
8266
  let ready;
7822
8267
  try {
7823
- ready = await deps.readyForSecrets(orderHash);
8268
+ ready = await read2(() => deps.readyForSecrets(orderHash));
7824
8269
  } catch {
7825
8270
  ready = {};
7826
8271
  }
8272
+ assertTime();
7827
8273
  for (const fill of ready.fills ?? []) {
8274
+ assertTime();
7828
8275
  if (published.has(fill.idx)) continue;
7829
8276
  const secret = secrets[fill.idx];
7830
8277
  if (secret === void 0) {
7831
8278
  throw new OwneyError(
7832
8279
  "SWAP_REQUEST_FAILED",
7833
8280
  `Swap needs a secret for fill ${fill.idx} that this session does not have. The order will refund once its timelock expires.`,
7834
- { orderHash, fillIndex: fill.idx }
8281
+ { orderHash, fillIndex: fill.idx, settlementUnconfirmed: true, outcome: "submitted-unknown" }
7835
8282
  );
7836
8283
  }
7837
8284
  try {
7838
- await deps.submitSecret(orderHash, secret);
8285
+ await read2(() => deps.submitSecret(orderHash, secret));
7839
8286
  published.add(fill.idx);
7840
8287
  } catch {
7841
8288
  failures += 1;
@@ -7843,29 +8290,34 @@ async function runFusionOrder(deps, options) {
7843
8290
  }
7844
8291
  let status;
7845
8292
  try {
7846
- ({ 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;
7847
8296
  failures = 0;
7848
8297
  } catch {
7849
8298
  failures += 1;
7850
- await deps.sleep(backoffFor(failures, pollIntervalMs));
8299
+ await pause(backoffFor(failures, pollIntervalMs));
7851
8300
  continue;
7852
8301
  }
7853
8302
  if (status === "refunding") onStage?.("refunding");
7854
8303
  if (isSwapTerminal(status)) {
7855
- if (status === "executed") {
8304
+ if (status === "executed" || status === "filled") {
7856
8305
  onStage?.("swapped");
7857
8306
  return { status, filled: true };
7858
8307
  }
7859
- if (status === "refunded") onStage?.("refunded");
8308
+ if (status === "refunded") onStage?.(partial ? "swap-partial" : "refunded");
7860
8309
  throw new OwneyError(
7861
8310
  status === "refunded" ? "SWAP_ORDER_REFUNDED" : status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
7862
- 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.",
7863
- { 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" }
7864
8313
  );
7865
8314
  }
7866
- await deps.sleep(pollIntervalMs);
8315
+ await pause(pollIntervalMs);
7867
8316
  }
7868
8317
  }
8318
+ function runFusionOrder(deps, options) {
8319
+ return observeOrder(options.orderHash, () => runFusionOrderInternal(deps, options));
8320
+ }
7869
8321
 
7870
8322
  // src/lib/swap/swap.same-chain-runner.ts
7871
8323
  var DEFAULT_POLL_MS3 = 5e3;
@@ -7890,7 +8342,7 @@ function explain(status) {
7890
8342
  return "The swap did not complete. Nothing was exchanged.";
7891
8343
  }
7892
8344
  }
7893
- async function runSameChainOrder(deps, options) {
8345
+ async function runSameChainOrderInternal(deps, options) {
7894
8346
  const {
7895
8347
  orderHash,
7896
8348
  onStage,
@@ -7898,23 +8350,39 @@ async function runSameChainOrder(deps, options) {
7898
8350
  timeoutMs = DEFAULT_TIMEOUT_MS3
7899
8351
  } = options;
7900
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
+ };
7901
8373
  let failures = 0;
7902
8374
  onStage?.("swapping");
7903
8375
  for (; ; ) {
7904
- if (deps.now() >= deadline) {
7905
- throw new OwneyError(
7906
- "SWAP_ORDER_EXPIRED",
7907
- "No one filled the swap in time. Nothing was exchanged and your funds are still in your wallet.",
7908
- { orderHash }
7909
- );
7910
- }
8376
+ assertTime();
7911
8377
  let status;
7912
8378
  try {
7913
- ({ 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;
7914
8382
  failures = 0;
7915
8383
  } catch {
7916
8384
  failures += 1;
7917
- await deps.sleep(backoffFor2(failures, pollIntervalMs));
8385
+ await pause(backoffFor2(failures, pollIntervalMs));
7918
8386
  continue;
7919
8387
  }
7920
8388
  if (isSwapTerminal(status)) {
@@ -7924,17 +8392,19 @@ async function runSameChainOrder(deps, options) {
7924
8392
  }
7925
8393
  throw new OwneyError(
7926
8394
  status === "cancelled" ? "SWAP_ORDER_CANCELLED" : "SWAP_ORDER_EXPIRED",
7927
- explain(status),
7928
- { 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" }
7929
8397
  );
7930
8398
  }
7931
- await deps.sleep(pollIntervalMs);
8399
+ await pause(pollIntervalMs);
7932
8400
  }
7933
8401
  }
8402
+ function runSameChainOrder(deps, options) {
8403
+ return observeOrder(options.orderHash, () => runSameChainOrderInternal(deps, options));
8404
+ }
7934
8405
 
7935
8406
  // src/lib/swap/swap.secret-store.ts
7936
8407
  var KEY_PREFIX4 = "owney.swap.order";
7937
- var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
7938
8408
  var storage4 = () => {
7939
8409
  if (typeof window === "undefined") return null;
7940
8410
  try {
@@ -7946,10 +8416,13 @@ var storage4 = () => {
7946
8416
  var keyFor2 = (orderHash) => `${KEY_PREFIX4}.${orderHash}`;
7947
8417
  function saveOrder(order) {
7948
8418
  const store = storage4();
7949
- if (!store) return;
8419
+ if (!store) return false;
7950
8420
  try {
7951
- 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;
7952
8424
  } catch {
8425
+ return false;
7953
8426
  }
7954
8427
  }
7955
8428
  function clearOrder(orderHash) {
@@ -7960,7 +8433,7 @@ function clearOrder(orderHash) {
7960
8433
  } catch {
7961
8434
  }
7962
8435
  }
7963
- function listOrders(now = Date.now()) {
8436
+ function listOrders(_now = Date.now()) {
7964
8437
  const store = storage4();
7965
8438
  if (!store) return [];
7966
8439
  const out = [];
@@ -7975,10 +8448,6 @@ function listOrders(now = Date.now()) {
7975
8448
  if (!raw2) continue;
7976
8449
  try {
7977
8450
  const parsed = JSON.parse(raw2);
7978
- if (now - parsed.createdAt > MAX_AGE_MS) {
7979
- store.removeItem(key2);
7980
- continue;
7981
- }
7982
8451
  if (Array.isArray(parsed.secrets) && parsed.secrets.length > 0) {
7983
8452
  out.push(parsed);
7984
8453
  }
@@ -8019,12 +8488,11 @@ async function awaitWalletPrompt(prompt, what) {
8019
8488
  if (timer) clearTimeout(timer);
8020
8489
  }
8021
8490
  }
8022
- function stoppedWatching(error) {
8023
- return error instanceof OwneyError && error.details?.settlementUnconfirmed === true;
8024
- }
8025
8491
  var DEFAULT_SLIPPAGE = 1;
8026
- async function affordableAmount(deps, quoted) {
8027
- 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" });
8028
8496
  if (balance >= quoted) return quoted;
8029
8497
  debugLog("owney-sdk", "swap: trimming to the current source balance", {
8030
8498
  quoted: quoted.toString(),
@@ -8035,48 +8503,72 @@ async function affordableAmount(deps, quoted) {
8035
8503
  }
8036
8504
  async function executeSwap(deps, options) {
8037
8505
  const { quote, walletAddress, onStage } = options;
8038
- debugLog("owney-sdk", "swap: start", {
8039
- rail: quote.rail,
8040
- from: `${quote.src.amount} ${quote.src.symbol} on ${quote.src.chainId}`,
8041
- to: `${quote.dst.symbol} on ${quote.dst.chainId}`,
8042
- expected: quote.dst.amount,
8043
- floor: quote.dstAmountMin
8044
- });
8045
- const before = await deps.readTargetBalance();
8046
- debugLog("owney-sdk", "swap: target balance before", before.toString());
8047
- let result;
8048
- 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
+ };
8049
8544
  try {
8050
- result = quote.rail === "classic" ? await runClassic(deps, options) : quote.rail === "fusion" ? await runSameChainFusion(deps, options, walletAddress) : await runFusion(deps, options, walletAddress);
8051
- } catch (error) {
8052
- if (!stoppedWatching(error)) throw error;
8053
- railError = error;
8054
- debugLog("owney-sdk", "swap: rail failed, checking whether funds landed", {
8055
- 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
8056
8551
  });
8057
- }
8058
- const { arrived } = await awaitBalanceIncrease({
8059
- readBalance: () => deps.readTargetBalance(),
8060
- baseline: before,
8061
- ...options.arrivalTimeoutMs === void 0 ? {} : { timeoutMs: options.arrivalTimeoutMs },
8062
- ...options.arrivalPollMs === void 0 ? {} : { pollMs: options.arrivalPollMs }
8063
- });
8064
- if (arrived === null) {
8065
- if (railError) throw railError;
8066
- throw new OwneyError(
8067
- "SWAP_REQUEST_FAILED",
8068
- "The swap completed but no funds arrived in the wallet. Check the transaction before retrying.",
8069
- { rail: quote.rail, ...result ?? {} }
8070
- );
8071
- }
8072
- if (railError) {
8073
- debugLog("owney-sdk", "swap: rail gave up but the funds are here", {
8074
- 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 }
8075
8560
  });
8076
- const orderHash = railError.details?.orderHash;
8077
- 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;
8078
8571
  }
8079
- return { received: arrived.toString(), ...result ?? {} };
8080
8572
  }
8081
8573
  async function runClassic(deps, options) {
8082
8574
  const {
@@ -8087,7 +8579,7 @@ async function runClassic(deps, options) {
8087
8579
  onStage
8088
8580
  } = options;
8089
8581
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
8090
- 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);
8091
8583
  const swapTxRequest = {
8092
8584
  from: {
8093
8585
  chainId: quote.src.chainId,
@@ -8119,25 +8611,36 @@ async function runClassic(deps, options) {
8119
8611
  "owney-sdk",
8120
8612
  "swap: re-fetching classic calldata after approval"
8121
8613
  );
8614
+ amount = await affordableAmount(deps, amount, options.sourceReserve);
8615
+ swapTxRequest.from.amount = amount.toString();
8122
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" });
8123
8619
  }
8124
8620
  }
8125
8621
  onStage?.("signing");
8126
8622
  await deps.ensureChain(quote.src.chainId);
8127
8623
  debugLog("owney-sdk", "swap: sending classic swap tx", { to: tx.to });
8624
+ options.assertCurrent?.();
8625
+ options.onTransactionRequested?.();
8128
8626
  const txHash = await deps.sendTransaction({
8129
8627
  to: tx.to,
8130
8628
  data: tx.data,
8131
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;
8132
8634
  });
8133
8635
  onStage?.("swapped");
8134
- return { txHash };
8636
+ return { txHash, spent: amount.toString() };
8135
8637
  }
8136
8638
  async function runSameChainFusion(deps, options, walletAddress) {
8137
8639
  const { quote, direction, onStage } = options;
8138
8640
  const isNativeSource = quote.src.address.toLowerCase().startsWith("0xeeee");
8139
- const amount = isNativeSource ? BigInt(quote.src.amount) : await affordableAmount(deps, BigInt(quote.src.amount));
8140
- 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);
8141
8644
  onStage?.("building");
8142
8645
  debugLog("owney-sdk", "swap: building same-chain fusion order");
8143
8646
  const built = await deps.api.buildOrder({
@@ -8161,36 +8664,42 @@ async function runSameChainFusion(deps, options, walletAddress) {
8161
8664
  "sign the swap"
8162
8665
  );
8163
8666
  debugLog("owney-sdk", "swap: signed, submitting same-chain order");
8164
- await deps.api.submitOrder({
8165
- // Without this the routing API hands the order to the Fusion+ relayer,
8166
- // which does not know it and answers with an error that names nothing.
8167
- rail: "fusion",
8168
- srcChainId: quote.src.chainId,
8169
- order: built.order,
8170
- signature,
8171
- quoteId: built.quoteId,
8172
- ...built.extension ? { extension: built.extension } : {}
8173
- });
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
+ }
8174
8683
  try {
8175
- options.onOrderSubmitted?.(built.orderHash);
8684
+ options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable: true });
8176
8685
  } catch {
8177
8686
  }
8178
8687
  await runSameChainOrder(deps.sameChainRunner ?? deps.runner, {
8179
8688
  orderHash: built.orderHash,
8180
8689
  ...onStage ? { onStage } : {}
8181
8690
  });
8182
- return { orderHash: built.orderHash };
8691
+ return { orderHash: built.orderHash, spent: amount.toString() };
8183
8692
  }
8184
8693
  async function authoriseSpend(deps, options, amount, isNativeSource) {
8185
8694
  const { quote, onStage } = options;
8186
- if (!quote.spender || isNativeSource) return void 0;
8695
+ if (!quote.spender || isNativeSource) return { paid: false };
8187
8696
  const current = await deps.readAllowance(quote.spender);
8188
8697
  debugLog("owney-sdk", "swap: fusion allowance", {
8189
8698
  spender: quote.spender,
8190
8699
  current: current.toString(),
8191
8700
  needed: amount.toString()
8192
8701
  });
8193
- if (current >= amount) return void 0;
8702
+ if (current >= amount) return { paid: false };
8194
8703
  onStage?.("approving");
8195
8704
  await deps.ensureChain(quote.src.chainId);
8196
8705
  const permit = quote.permitSupported ? await awaitWalletPrompt(
@@ -8201,11 +8710,18 @@ async function authoriseSpend(deps, options, amount, isNativeSource) {
8201
8710
  debugLog("owney-sdk", "swap: permitting limit order protocol", {
8202
8711
  bytes: (permit.length - 2) / 2
8203
8712
  });
8204
- return permit;
8713
+ return { permit, paid: false };
8205
8714
  }
8206
8715
  await deps.approve(quote.spender, MAX_UINT256);
8207
8716
  debugLog("owney-sdk", "swap: approved limit order protocol");
8208
- 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;
8209
8725
  }
8210
8726
  async function runFusion(deps, options, walletAddress) {
8211
8727
  const { quote, direction, onStage } = options;
@@ -8217,8 +8733,9 @@ async function runFusion(deps, options, walletAddress) {
8217
8733
  { chainId: quote.src.chainId, symbol: quote.src.symbol }
8218
8734
  );
8219
8735
  }
8220
- const amount = await affordableAmount(deps, BigInt(quote.src.amount));
8221
- 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);
8222
8739
  const { secrets, secretHashes } = mintSecrets(quote.secretsCount ?? 1);
8223
8740
  onStage?.("building");
8224
8741
  debugLog("owney-sdk", "swap: building fusion order", {
@@ -8237,8 +8754,9 @@ async function runFusion(deps, options, walletAddress) {
8237
8754
  ...direction ? { direction } : {},
8238
8755
  ...permit ? { permit } : {}
8239
8756
  });
8240
- saveOrder({
8757
+ const durable = saveOrder({
8241
8758
  orderHash: built.orderHash,
8759
+ walletAddress,
8242
8760
  secrets,
8243
8761
  srcChainId: quote.src.chainId,
8244
8762
  srcSymbol: quote.src.symbol,
@@ -8248,33 +8766,39 @@ async function runFusion(deps, options, walletAddress) {
8248
8766
  createdAt: Date.now()
8249
8767
  });
8250
8768
  debugLog("owney-sdk", "swap: order built", { orderHash: built.orderHash });
8251
- onStage?.("signing");
8252
- await deps.ensureChain(quote.src.chainId);
8253
- debugLog("owney-sdk", "swap: awaiting signature in wallet", {
8254
- signingOnChain: quote.src.chainId
8255
- });
8256
- const signature = await awaitWalletPrompt(
8257
- () => deps.signTypedData(built.typedData),
8258
- "sign the swap"
8259
- );
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
+ }
8260
8780
  debugLog("owney-sdk", "swap: signed, submitting to relayer");
8261
- await deps.api.submitOrder({
8262
- srcChainId: quote.src.chainId,
8263
- // The ORDER STRUCT, not the typed-data envelope we just signed. Sending
8264
- // the envelope here gets a bare 500 from the relayer.
8265
- order: built.order,
8266
- signature,
8267
- quoteId: built.quoteId,
8268
- // Single-fill orders must NOT carry secretHashes — the relayer rejects
8269
- // them with SECRET_HASHES_NOT_REQUIRED. The one hash is already inside the
8270
- // order's hashlock, so repeating it here is redundant, and only a
8271
- // multi-fill order (a Merkle tree of hashes) needs them listed.
8272
- ...secretHashes.length > 1 ? { secretHashes } : {},
8273
- ...built.extension ? { extension: built.extension } : {}
8274
- });
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
+ }
8275
8799
  debugLog("owney-sdk", "swap: order submitted, polling escrows");
8276
8800
  try {
8277
- options.onOrderSubmitted?.(built.orderHash);
8801
+ options.onOrderSubmitted?.(built.orderHash, { spent: amount.toString(), expected: built.dstAmount, durable });
8278
8802
  } catch {
8279
8803
  }
8280
8804
  try {
@@ -8284,13 +8808,25 @@ async function runFusion(deps, options, walletAddress) {
8284
8808
  ...onStage ? { onStage } : {}
8285
8809
  });
8286
8810
  } catch (error) {
8287
- 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") {
8288
8812
  clearOrder(built.orderHash);
8289
8813
  }
8290
8814
  throw error;
8291
8815
  }
8292
8816
  clearOrder(built.orderHash);
8293
- 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
+ );
8294
8830
  }
8295
8831
 
8296
8832
  // src/lib/swap/swap.withdrawal-settlement.ts
@@ -8300,8 +8836,8 @@ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8300
8836
  async function awaitWithdrawalPayouts(options) {
8301
8837
  const payouts = new Map(Object.entries(options.results).map(([id, result]) => [id, { result, evidence: result.evidence }]));
8302
8838
  const timeoutMs = Math.max(0, options.timeoutMs ?? 18e4);
8303
- const deadline = Date.now() + timeoutMs;
8304
- let active = true;
8839
+ const deadline = options.deadline ?? Date.now() + timeoutMs;
8840
+ let active2 = true;
8305
8841
  let timer;
8306
8842
  const incomplete = (reason, agentId) => new OwneyError(
8307
8843
  "WITHDRAW_SWAP_INCOMPLETE",
@@ -8319,7 +8855,7 @@ async function awaitWithdrawalPayouts(options) {
8319
8855
  );
8320
8856
  const assertActive = () => {
8321
8857
  options.assertCurrent();
8322
- if (!active || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
8858
+ if (!active2 || Date.now() > deadline) throw incomplete("PAYOUT_CONFIRMATION_TIMEOUT");
8323
8859
  };
8324
8860
  const pause = async () => {
8325
8861
  assertActive();
@@ -8330,10 +8866,11 @@ async function awaitWithdrawalPayouts(options) {
8330
8866
  if (evidence.agentId !== id || evidence.chainId !== options.chainId || evidence.token !== options.token || evidence.walletAddress.toLowerCase() !== options.walletAddress.toLowerCase())
8331
8867
  throw incomplete("PAYOUT_SCOPE_MISMATCH", id);
8332
8868
  };
8333
- const record = (id, payout, evidence) => {
8869
+ const record2 = (id, payout, evidence) => {
8334
8870
  assertActive();
8335
8871
  validate(id, evidence);
8336
8872
  payout.evidence = evidence;
8873
+ payout.result.evidence = evidence;
8337
8874
  if (evidence.transfer === "confirmed" && evidence.delivery === "confirmed") payout.amount = rawAmount(evidence.confirmedAmount);
8338
8875
  options.onProgress(evidence);
8339
8876
  };
@@ -8398,8 +8935,30 @@ async function awaitWithdrawalPayouts(options) {
8398
8935
  if (payout.amount === void 0 || payout.amount <= 0n) throw incomplete("INVALID_PAYOUT_AMOUNT", id);
8399
8936
  if (["yieldseeker", "zyfai"].includes(id) && (!hash(evidence.txHash) || rawAmount(evidence.blockNumber) === void 0 || !evidence.logIndexes?.length))
8400
8937
  throw incomplete("PAYOUT_RECEIPT_UNVERIFIED", id);
8401
- if (options.requested === void 0 && payout.result.completion === "unverified")
8402
- 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
+ }
8403
8962
  const intended = rawAmount(evidence.intendedAmount);
8404
8963
  if (intended !== void 0 && payout.amount < intended && verifiedFee(payout) !== intended - payout.amount)
8405
8964
  throw incomplete("PAYOUT_SHORTFALL", id);
@@ -8409,24 +8968,24 @@ async function awaitWithdrawalPayouts(options) {
8409
8968
  let next = evidence;
8410
8969
  if (!next.txHash && (next.userOpHash || next.callsId)) {
8411
8970
  next = await options.resolve(next);
8412
- record(id, payout, next);
8971
+ record2(id, payout, next);
8413
8972
  }
8414
8973
  if (next.transfer === "failed" || next.delivery === "failed") continue;
8415
8974
  if (next.txHash) {
8416
8975
  next = await options.inspect(next, false);
8417
- record(id, payout, next);
8976
+ record2(id, payout, next);
8418
8977
  if (id === "zyfai" && next.transfer === "unknown" && rawAmount(next.blockNumber) !== void 0 && next.logIndexes?.length === 0 && next.transferLogs?.length === 0 && !next.userOpHash && !next.callsId) {
8419
8978
  const found = await findPayout(id, next);
8420
8979
  if (found) {
8421
8980
  next = found;
8422
- record(id, payout, next);
8981
+ record2(id, payout, next);
8423
8982
  }
8424
8983
  }
8425
8984
  } else if (!next.userOpHash && !next.callsId) {
8426
8985
  const found = await findPayout(id, next);
8427
8986
  if (found) {
8428
8987
  next = found;
8429
- record(id, payout, next);
8988
+ record2(id, payout, next);
8430
8989
  }
8431
8990
  }
8432
8991
  if (next.transfer === "confirmed" && next.delivery === "confirmed" || next.transfer === "failed" || next.delivery === "failed") continue;
@@ -8470,10 +9029,10 @@ async function awaitWithdrawalPayouts(options) {
8470
9029
  };
8471
9030
  try {
8472
9031
  return await Promise.race([read2(), new Promise((_, reject) => {
8473
- timer = setTimeout(() => reject(incomplete("PAYOUT_CONFIRMATION_TIMEOUT")), timeoutMs);
9032
+ timer = setTimeout(() => reject(incomplete("PAYOUT_CONFIRMATION_TIMEOUT")), Math.max(0, deadline - Date.now()));
8474
9033
  })]);
8475
9034
  } finally {
8476
- active = false;
9035
+ active2 = false;
8477
9036
  if (timer) clearTimeout(timer);
8478
9037
  }
8479
9038
  }
@@ -8494,10 +9053,10 @@ function verifiedFee(payout) {
8494
9053
  import {
8495
9054
  domainSeparator,
8496
9055
  encodeAbiParameters,
8497
- parseAbi as parseAbi4,
9056
+ parseAbi as parseAbi5,
8498
9057
  parseSignature
8499
9058
  } from "viem";
8500
- var PERMIT_ABI = parseAbi4([
9059
+ var PERMIT_ABI = parseAbi5([
8501
9060
  "function nonces(address owner) view returns (uint256)",
8502
9061
  "function name() view returns (string)",
8503
9062
  "function version() view returns (string)",
@@ -8595,7 +9154,7 @@ async function buildErc2612Permit(reads, input) {
8595
9154
 
8596
9155
  // src/lib/swap/swap.withdraw-swap.ts
8597
9156
  var KEY_PREFIX5 = "owney.swap.withdraw";
8598
- var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
9157
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
8599
9158
  var storage5 = () => {
8600
9159
  if (typeof window === "undefined") return null;
8601
9160
  try {
@@ -8620,15 +9179,19 @@ var notify = () => {
8620
9179
  }
8621
9180
  }
8622
9181
  };
8623
- function saveWithdrawSwap(record) {
9182
+ function saveWithdrawSwap(record2) {
8624
9183
  const store = storage5();
8625
- if (!store) return;
9184
+ if (!store) return false;
8626
9185
  try {
8627
- 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;
8628
9190
  } catch {
8629
- return;
9191
+ return false;
8630
9192
  }
8631
9193
  notify();
9194
+ return true;
8632
9195
  }
8633
9196
  function updateWithdrawSwap(orderHash, patch) {
8634
9197
  const store = storage5();
@@ -8651,13 +9214,17 @@ function clearWithdrawSwap(orderHash) {
8651
9214
  const store = storage5();
8652
9215
  if (!store) return;
8653
9216
  try {
8654
- 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));
8655
9222
  } catch {
8656
9223
  return;
8657
9224
  }
8658
9225
  notify();
8659
9226
  }
8660
- function listWithdrawSwaps(now = Date.now()) {
9227
+ function listWithdrawSwaps(now = Date.now(), includeDismissed = false) {
8661
9228
  const store = storage5();
8662
9229
  if (!store) return [];
8663
9230
  const out = [];
@@ -8672,11 +9239,11 @@ function listWithdrawSwaps(now = Date.now()) {
8672
9239
  if (!raw2) continue;
8673
9240
  try {
8674
9241
  const parsed = JSON.parse(raw2);
8675
- if (now - parsed.createdAt > MAX_AGE_MS2) {
9242
+ if (["landed", "refunded", "not-swapped"].includes(parsed.status) && now - (parsed.settledAt ?? parsed.createdAt) > MAX_AGE_MS) {
8676
9243
  store.removeItem(key2);
8677
9244
  continue;
8678
9245
  }
8679
- out.push(parsed);
9246
+ if (includeDismissed || !parsed.dismissed) out.push(parsed);
8680
9247
  } catch {
8681
9248
  store.removeItem(key2);
8682
9249
  }
@@ -8686,10 +9253,15 @@ function listWithdrawSwaps(now = Date.now()) {
8686
9253
  }
8687
9254
  return out.sort((a, b) => b.createdAt - a.createdAt);
8688
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
+ }
8689
9261
 
8690
9262
  // src/lib/swap/swap.pending-deposit.ts
8691
9263
  var KEY_PREFIX6 = "owney.swap.pending-deposit";
8692
- var MAX_AGE_MS3 = 24 * 60 * 60 * 1e3;
9264
+ var MAX_AGE_MS2 = 24 * 60 * 60 * 1e3;
8693
9265
  var storage6 = () => {
8694
9266
  if (typeof window === "undefined") return null;
8695
9267
  try {
@@ -8732,7 +9304,7 @@ function listPendingDeposits(now = Date.now()) {
8732
9304
  if (!raw2) continue;
8733
9305
  try {
8734
9306
  const parsed = JSON.parse(raw2);
8735
- if (now - parsed.createdAt > MAX_AGE_MS3) {
9307
+ if (now - parsed.createdAt > MAX_AGE_MS2) {
8736
9308
  store.removeItem(key2);
8737
9309
  continue;
8738
9310
  }
@@ -8781,7 +9353,7 @@ async function withFailureReporting(apiKey, agentType, fn, baseUrl) {
8781
9353
  }
8782
9354
 
8783
9355
  // src/lib/helpers/withdraw-helper.ts
8784
- import { parseUnits as parseUnits2 } from "viem";
9356
+ import { parseUnits as parseUnits3 } from "viem";
8785
9357
  function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decimals) {
8786
9358
  return agents.map((agent) => {
8787
9359
  let invalidBalance = false;
@@ -8791,7 +9363,7 @@ function projectAgentBalancesForAsset(agents, aggregated, chainId, asset, decima
8791
9363
  return 0n;
8792
9364
  }
8793
9365
  const [whole, fraction] = value.split(".");
8794
- 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);
8795
9367
  };
8796
9368
  const agentBalance = aggregated[agent.id];
8797
9369
  const tokenBalance = agentBalance?.tokens.find(
@@ -8991,11 +9563,11 @@ function aggregateDailyEarnings(series, tokenSymbol) {
8991
9563
  const byAsset = /* @__PURE__ */ new Map();
8992
9564
  for (const account of series) {
8993
9565
  for (const { asset, points } of account.assets) {
8994
- const symbol = asset.toUpperCase();
8995
- if (!points.length || wanted && symbol !== wanted) continue;
8996
- const components = byAsset.get(symbol) ?? [];
9566
+ const symbol2 = asset.toUpperCase();
9567
+ if (!points.length || wanted && symbol2 !== wanted) continue;
9568
+ const components = byAsset.get(symbol2) ?? [];
8997
9569
  components.push([...points].sort((a, b) => a.date.localeCompare(b.date)));
8998
- byAsset.set(symbol, components);
9570
+ byAsset.set(symbol2, components);
8999
9571
  }
9000
9572
  }
9001
9573
  return [...byAsset.entries()].map(([asset, components]) => {
@@ -9024,7 +9596,7 @@ function aggregateDailyEarnings(series, tokenSymbol) {
9024
9596
 
9025
9597
  // src/client.ts
9026
9598
  import {
9027
- parseUnits as parseUnits3,
9599
+ parseUnits as parseUnits4,
9028
9600
  createPublicClient as createPublicClient4,
9029
9601
  createWalletClient as createWalletClient5,
9030
9602
  custom as custom5,
@@ -9040,111 +9612,6 @@ import {
9040
9612
  parseTransaction,
9041
9613
  toBytes
9042
9614
  } from "viem";
9043
-
9044
- // src/lib/permit2-batch.ts
9045
- import { parseAbi as parseAbi5, hashStruct } from "viem";
9046
- var BATCH_PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
9047
- var BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
9048
- var ERC2612_PERMIT_TYPES = {
9049
- Permit: [
9050
- { name: "owner", type: "address" },
9051
- { name: "spender", type: "address" },
9052
- { name: "value", type: "uint256" },
9053
- { name: "nonce", type: "uint256" },
9054
- { name: "deadline", type: "uint256" }
9055
- ]
9056
- };
9057
- var ERC2612_READ_ABI = parseAbi5([
9058
- "function name() view returns (string)",
9059
- "function version() view returns (string)",
9060
- "function nonces(address owner) view returns (uint256)"
9061
- ]);
9062
- function erc2612TypedData(input) {
9063
- return {
9064
- domain: {
9065
- name: input.permit.tokenName,
9066
- version: input.permit.tokenVersion,
9067
- chainId: input.chainId,
9068
- verifyingContract: input.token
9069
- },
9070
- types: ERC2612_PERMIT_TYPES,
9071
- primaryType: "Permit",
9072
- message: {
9073
- owner: input.owner,
9074
- spender: BATCH_PERMIT2_ADDRESS,
9075
- value: BigInt(input.permit.value),
9076
- nonce: BigInt(input.permit.nonce),
9077
- deadline: BigInt(input.permit.deadline)
9078
- }
9079
- };
9080
- }
9081
- var PERMIT_BATCH_TYPES = {
9082
- PermitBatchWitnessTransferFrom: [
9083
- { name: "permitted", type: "TokenPermissions[]" },
9084
- { name: "spender", type: "address" },
9085
- { name: "nonce", type: "uint256" },
9086
- { name: "deadline", type: "uint256" },
9087
- { name: "witness", type: "Deposit" }
9088
- ],
9089
- Deposit: [{ name: "recipients", type: "address[]" }],
9090
- TokenPermissions: [
9091
- { name: "token", type: "address" },
9092
- { name: "amount", type: "uint256" }
9093
- ]
9094
- };
9095
- var PERMIT2_BATCH_ABI = parseAbi5([
9096
- "struct TokenPermissions { address token; uint256 amount; }",
9097
- "struct PermitBatchTransferFrom { TokenPermissions[] permitted; uint256 nonce; uint256 deadline; }",
9098
- "struct SignatureTransferDetails { address to; uint256 requestedAmount; }",
9099
- "function permitWitnessTransferFrom(PermitBatchTransferFrom permit,SignatureTransferDetails[] transferDetails,address owner,bytes32 witness,string witnessTypeString,bytes signature)",
9100
- "function nonceBitmap(address owner,uint256 wordPos) view returns (uint256)"
9101
- ]);
9102
- function batchPermit(b) {
9103
- return {
9104
- permitted: b.transfers.map((t) => ({
9105
- token: b.token,
9106
- amount: BigInt(t.amount)
9107
- })),
9108
- nonce: BigInt(b.nonce),
9109
- deadline: BigInt(b.deadline)
9110
- };
9111
- }
9112
- function batchTypedData(b, spender) {
9113
- return {
9114
- domain: {
9115
- name: "Permit2",
9116
- chainId: b.chainId,
9117
- verifyingContract: BATCH_PERMIT2_ADDRESS
9118
- },
9119
- types: PERMIT_BATCH_TYPES,
9120
- primaryType: "PermitBatchWitnessTransferFrom",
9121
- message: {
9122
- ...batchPermit(b),
9123
- spender,
9124
- witness: { recipients: b.transfers.map((t) => t.to) }
9125
- }
9126
- };
9127
- }
9128
- var BATCH_WITNESS_TYPE = "Deposit witness)Deposit(address[] recipients)TokenPermissions(address token,uint256 amount)";
9129
- function batchWitness(b) {
9130
- return hashStruct({
9131
- types: PERMIT_BATCH_TYPES,
9132
- primaryType: "Deposit",
9133
- data: { recipients: b.transfers.map((t) => t.to) }
9134
- });
9135
- }
9136
- function batchArgs(b) {
9137
- return [
9138
- batchPermit(b),
9139
- b.transfers.map((t) => ({ to: t.to, requestedAmount: BigInt(t.amount) })),
9140
- b.from,
9141
- batchWitness(b),
9142
- BATCH_WITNESS_TYPE,
9143
- b.signature
9144
- ];
9145
- }
9146
-
9147
- // src/lib/sponsored-token-batch.ts
9148
9615
  var memory = /* @__PURE__ */ new Map();
9149
9616
  var settledRecords = /* @__PURE__ */ new Map();
9150
9617
  var expiredRecords = /* @__PURE__ */ new Map();
@@ -9172,14 +9639,14 @@ async function releaseExpiredTokenBatch(i) {
9172
9639
  let timer;
9173
9640
  let expired = false;
9174
9641
  const check = async () => {
9175
- let active = read(key2);
9176
- if (active && i.expectedTxHash) {
9177
- const current = JSON.parse(active);
9642
+ let active2 = read(key2);
9643
+ if (active2 && i.expectedTxHash) {
9644
+ const current = JSON.parse(active2);
9178
9645
  if (!current.serializedTransaction || keccak2562(current.serializedTransaction).toLowerCase() !== i.expectedTxHash.toLowerCase())
9179
- active = void 0;
9646
+ active2 = void 0;
9180
9647
  }
9181
9648
  const archiveKey = i.expectedTxHash ? `${key2}:expired:${i.expectedTxHash.toLowerCase()}` : void 0;
9182
- 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))));
9183
9650
  if (!saved) return;
9184
9651
  const body = JSON.parse(saved);
9185
9652
  if (!body.serializedTransaction || body.chainId !== i.chainId || !isAddressEqual2(body.from, i.owner) || !isAddressEqual2(body.token, i.token) || !body.transfers?.length) return;
@@ -9203,8 +9670,8 @@ async function releaseExpiredTokenBatch(i) {
9203
9670
  args: [i.owner, nonce >> 8n],
9204
9671
  blockNumber: block.number
9205
9672
  });
9206
- if (expired || typeof bitmap !== "bigint" || bitmap < 0n || bitmap > MAX_UINT256 || (bitmap & 1n << (nonce & 255n)) !== 0n || active && read(key2) !== active) return;
9207
- 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) {
9208
9675
  const terminalKey = `${key2}:expired:${txHash.toLowerCase()}`;
9209
9676
  expiredRecords.set(terminalKey, saved);
9210
9677
  let persisted = typeof window === "undefined";
@@ -9262,15 +9729,15 @@ function clear(key2, body) {
9262
9729
  function sponsorTokenBatch(i) {
9263
9730
  const key2 = pendingKey(i);
9264
9731
  const plan = planOf(i.transfers);
9265
- const active = inflight.get(key2);
9266
- if (active) {
9267
- 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)
9268
9735
  return Promise.reject(
9269
9736
  new Error(
9270
9737
  "A token deposit is already in progress. Wait for its result before depositing again."
9271
9738
  )
9272
9739
  );
9273
- return active.promise;
9740
+ return active2.promise;
9274
9741
  }
9275
9742
  const promise = execute(i, key2, plan).finally(() => inflight.delete(key2));
9276
9743
  inflight.set(key2, { plan, promise });
@@ -9424,7 +9891,7 @@ async function execute(i, key2, plan) {
9424
9891
  "DEPOSIT_INSUFFICIENT_BALANCE",
9425
9892
  "Insufficient token balance for this deposit."
9426
9893
  );
9427
- if (allowance < total && (i.chainId !== 8453 || !isAddressEqual2(i.token, BASE_USDC_ADDRESS)))
9894
+ if (allowance < total && !supportsSponsoredErc2612(i.chainId, i.token))
9428
9895
  throw new OwneyError(
9429
9896
  "PERMIT2_APPROVAL_REQUIRED",
9430
9897
  "token deposits need a one-time Permit2 approval."
@@ -9460,7 +9927,7 @@ async function execute(i, key2, plan) {
9460
9927
  const signature2 = await i.wallet.signTypedData({
9461
9928
  account: i.owner,
9462
9929
  ...erc2612TypedData({
9463
- chainId: 8453,
9930
+ chainId: i.chainId,
9464
9931
  token: i.token,
9465
9932
  owner: i.owner,
9466
9933
  permit: unsignedPermit
@@ -10088,12 +10555,12 @@ var OwneySDK = class {
10088
10555
  return [...this.getActiveAgents().keys()].sort();
10089
10556
  }
10090
10557
  getActiveAgents() {
10091
- const active = /* @__PURE__ */ new Map();
10558
+ const active2 = /* @__PURE__ */ new Map();
10092
10559
  for (const id of this.activeAgents) {
10093
10560
  const agent = this.agents.get(id);
10094
- if (agent) active.set(id, agent);
10561
+ if (agent) active2.set(id, agent);
10095
10562
  }
10096
- return active;
10563
+ return active2;
10097
10564
  }
10098
10565
  /**
10099
10566
  * Read the org's agent policy once and reuse it. fetchOrgAgentConfig never
@@ -10676,10 +11143,10 @@ var OwneySDK = class {
10676
11143
  * Invokes `agent.deposit` with the resolved sponsored callback, composing
10677
11144
  * two independent auto-recovery mechanisms:
10678
11145
  *
10679
- * 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
10680
11147
  * app did not supply its own callback and the attempt fails with
10681
11148
  * `PERMIT2_APPROVAL_REQUIRED`, this is the wallet's first Permit2 deposit
10682
- * 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
10683
11150
  * its sponsored deposit and never reaches this branch. Other tokens send
10684
11151
  * the one-time user-paid Permit2 approval via `approvePermit2()` and
10685
11152
  * retry the SAME sponsored attempt once. Bounded to one approval attempt
@@ -11186,8 +11653,8 @@ var OwneySDK = class {
11186
11653
  async ensureSwapChain(chainId) {
11187
11654
  const provider = this.requireConnectedProvider();
11188
11655
  const state = this.requireState();
11189
- const chain = swapChain(chainId);
11190
- if (!chain) {
11656
+ const chain2 = swapChain(chainId);
11657
+ if (!chain2) {
11191
11658
  throw new OwneyError(
11192
11659
  "CHAIN_UNSUPPORTED",
11193
11660
  `Chain ${chainId} is not supported`,
@@ -11195,10 +11662,10 @@ var OwneySDK = class {
11195
11662
  );
11196
11663
  }
11197
11664
  await ensureWalletOnChain(
11198
- createPublicClient4({ chain, transport: custom5(provider) }),
11665
+ createPublicClient4({ chain: chain2, transport: custom5(provider) }),
11199
11666
  createWalletClient5({
11200
11667
  account: state.walletAddress,
11201
- chain,
11668
+ chain: chain2,
11202
11669
  transport: custom5(provider)
11203
11670
  }),
11204
11671
  chainId
@@ -11222,15 +11689,15 @@ var OwneySDK = class {
11222
11689
  async wrapNativeSource(quote, walletAddress, onStage, direction) {
11223
11690
  const wrapper = requireWrapper(quote);
11224
11691
  const chainId = quote.src.chainId;
11225
- const chain = swapChain(chainId);
11692
+ const chain2 = swapChain(chainId);
11226
11693
  const provider = this.requireConnectedProvider();
11227
11694
  const publicClient = createPublicClient4({
11228
- chain,
11695
+ chain: chain2,
11229
11696
  transport: swapReadTransport(chainId, this.zyfaiRpcUrls)
11230
11697
  });
11231
11698
  const wallet = createWalletClient5({
11232
11699
  account: walletAddress,
11233
- chain,
11700
+ chain: chain2,
11234
11701
  transport: custom5(provider)
11235
11702
  });
11236
11703
  onStage?.("wrapping");
@@ -11257,7 +11724,7 @@ var OwneySDK = class {
11257
11724
  data: tx.data,
11258
11725
  value: BigInt(tx.value),
11259
11726
  account: walletAddress,
11260
- chain
11727
+ chain: chain2
11261
11728
  });
11262
11729
  onSubmitted?.(hash2);
11263
11730
  const receipt = await publicClient.waitForTransactionReceipt({
@@ -11327,15 +11794,27 @@ var OwneySDK = class {
11327
11794
  * Kept as a builder rather than baked into the executor so the whole swap
11328
11795
  * flow stays testable without a provider — the executor never imports viem.
11329
11796
  */
11330
- buildSwapDeps(quote) {
11797
+ buildSwapDeps(quote, assertOperation, onTransactionHash) {
11331
11798
  const state = this.requireState();
11332
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
+ };
11333
11806
  const srcChain = swapChain(quote.src.chainId);
11334
11807
  const dstChain = swapChain(quote.dst.chainId);
11335
11808
  const wallet = createWalletClient5({
11336
11809
  account: state.walletAddress,
11337
11810
  chain: srcChain,
11338
- 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 })
11339
11818
  });
11340
11819
  const srcPublic = createPublicClient4({
11341
11820
  chain: srcChain,
@@ -11364,6 +11843,7 @@ var OwneySDK = class {
11364
11843
  });
11365
11844
  },
11366
11845
  sendTransaction: async (tx) => {
11846
+ assertWallet();
11367
11847
  const hash2 = await wallet.sendTransaction({
11368
11848
  to: tx.to,
11369
11849
  data: tx.data,
@@ -11371,24 +11851,34 @@ var OwneySDK = class {
11371
11851
  account: state.walletAddress,
11372
11852
  chain: srcChain
11373
11853
  });
11854
+ onTransactionHash?.(hash2);
11374
11855
  const receipt = await srcPublic.waitForTransactionReceipt({
11375
11856
  timeout: receiptTimeoutMs(quote.src.chainId),
11376
11857
  hash: hash2,
11377
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
+ );
11378
11865
  });
11379
11866
  if (receipt.status !== "success") {
11380
11867
  throw new OwneyError(
11381
11868
  "SWAP_REQUEST_FAILED",
11382
11869
  `Swap transaction reverted (tx ${hash2})`,
11383
- { hash: hash2 }
11870
+ { txHash: hash2, outcome: "not-submitted", executionFailed: true }
11384
11871
  );
11385
11872
  }
11386
11873
  return hash2;
11387
11874
  },
11388
- signTypedData: (typedData) => wallet.signTypedData({
11389
- account: state.walletAddress,
11390
- ...typedData
11391
- }),
11875
+ signTypedData: (typedData) => {
11876
+ assertWallet();
11877
+ return wallet.signTypedData({
11878
+ account: state.walletAddress,
11879
+ ...typedData
11880
+ });
11881
+ },
11392
11882
  // Chain-bound like every other read here: the wallet provider's chain is
11393
11883
  // not ours to rely on mid-swap.
11394
11884
  readSourceBalance: async () => {
@@ -11410,6 +11900,7 @@ var OwneySDK = class {
11410
11900
  args: [state.walletAddress, spender]
11411
11901
  }),
11412
11902
  approve: async (spender, amount) => {
11903
+ assertWallet();
11413
11904
  const hash2 = await wallet.writeContract({
11414
11905
  address: quote.src.address,
11415
11906
  abi: ERC20_ALLOWANCE_ABI,
@@ -11418,14 +11909,22 @@ var OwneySDK = class {
11418
11909
  account: state.walletAddress,
11419
11910
  chain: srcChain
11420
11911
  });
11421
- await srcPublic.waitForTransactionReceipt({
11912
+ const receipt = await srcPublic.waitForTransactionReceipt({
11422
11913
  hash: hash2,
11423
11914
  confirmations: 1,
11424
11915
  timeout: receiptTimeoutMs(quote.src.chainId)
11425
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
+ );
11426
11922
  return hash2;
11427
11923
  },
11428
- ensureChain: (chainId) => this.ensureSwapChain(chainId),
11924
+ ensureChain: (chainId) => {
11925
+ assertWallet();
11926
+ return this.ensureSwapChain(chainId);
11927
+ },
11429
11928
  /**
11430
11929
  * Sign the approval instead of paying for it. Null means the token
11431
11930
  * cannot be permitted (WETH has no `permit`) or its domain could not be
@@ -11445,10 +11944,13 @@ var OwneySDK = class {
11445
11944
  spender,
11446
11945
  value: amount,
11447
11946
  chainId: quote.src.chainId,
11448
- signTypedData: (payload) => wallet.signTypedData({
11449
- account: state.walletAddress,
11450
- ...payload
11451
- })
11947
+ signTypedData: (payload) => {
11948
+ assertWallet();
11949
+ return wallet.signTypedData({
11950
+ account: state.walletAddress,
11951
+ ...payload
11952
+ });
11953
+ }
11452
11954
  }
11453
11955
  ),
11454
11956
  /**
@@ -11498,36 +12000,20 @@ var OwneySDK = class {
11498
12000
  );
11499
12001
  }
11500
12002
  /**
11501
- * How much of a landed swap to actually deposit.
11502
- *
11503
- * The swap reports what ARRIVED, and the deposit used to be handed that
11504
- * figure directly. It is the entire arrival, so there is no slack: anything
11505
- * that spends the target asset between the two fails the deposit outright.
11506
- *
11507
- * Something does. A first WETH deposit needs a one-time Permit2 approval,
11508
- * and that approval is a transaction. A wallet holding no native gas token -
11509
- * the normal state of someone who just swapped everything INTO WETH - pays
11510
- * for it out of the WETH. The deposit then asked for ~2e12 wei more than was
11511
- * left and refused with DEPOSIT_INSUFFICIENT_BALANCE, under a wrapper saying
11512
- * the funds were safe but unadded (ROUT-242).
11513
- *
11514
- * So the approval happens here, before the amount is settled, and the amount
11515
- * comes from a read taken after it. Two things this deliberately is not:
11516
- *
11517
- * - It is not a balance sweep. `received` is a DELTA over the pre-swap
11518
- * balance, so a wallet already holding WETH reads far above it; depositing
11519
- * the balance would move funds the user never offered. The arrival is the
11520
- * ceiling and the balance is only ever allowed to lower it.
11521
- * - It does not touch USDC, which moves on signed typed data and so spends
11522
- * nothing of the user's in between, nor a paymaster-configured client,
11523
- * 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.
11524
12008
  */
11525
- async settleSwapDepositAmount(chainId, token, symbol, received) {
11526
- if (symbol !== "WETH" || this.paymasterServiceUrl) {
12009
+ async settleSwapDepositAmount(chainId, token, symbol2, received) {
12010
+ if (symbol2 !== "WETH" && symbol2 !== "USDC" || this.paymasterServiceUrl || supportsSponsoredErc2612(chainId, token)) {
11527
12011
  return received.toString();
11528
12012
  }
11529
12013
  const allowance = await this.readTokenPermit2Allowance(chainId, token);
11530
- if (allowance < received) await this.approvePermit2();
12014
+ if (allowance < received) {
12015
+ await this.approvePermit2(symbol2, received, chainId);
12016
+ }
11531
12017
  const balance = await this.readTokenBalance(chainId, token);
11532
12018
  return (balance < received ? balance : received).toString();
11533
12019
  }
@@ -11654,21 +12140,30 @@ var OwneySDK = class {
11654
12140
  * Every order is attempted even if an earlier one throws: they are unrelated
11655
12141
  * escrows, and one ending badly must not abandon the next one's secrets.
11656
12142
  * `unfinished` is not a verdict — the order may still settle, so unless the
11657
- * chain has said refunded or cancelled its preimages are kept.
12143
+ * provider has confirmed full settlement or refund, its preimages are kept.
11658
12144
  */
11659
12145
  async resumePendingSwaps(options = {}) {
11660
- const pending = listOrders();
11661
- if (pending.length === 0) {
11662
- await this.resumeSameChainSwaps(options);
11663
- return [];
11664
- }
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));
11665
12159
  const runner = this.swapRunnerDeps();
11666
- const results = [];
11667
- for (const order of pending) {
12160
+ const sameChain = this.resumeSameChainSwaps(options);
12161
+ const results = await mapConcurrent(pending, async (order) => {
11668
12162
  try {
11669
12163
  await runFusionOrder(runner, {
11670
12164
  orderHash: order.orderHash,
11671
12165
  secrets: order.secrets,
12166
+ hasPartialFill: listWithdrawSwaps(Date.now(), true).some((record2) => record2.orderHash === order.orderHash && record2.status === "partially-filled"),
11672
12167
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
11673
12168
  ...options.onStage ? {
11674
12169
  onStage: (stage) => options.onStage(order.orderHash, stage)
@@ -11676,25 +12171,23 @@ var OwneySDK = class {
11676
12171
  });
11677
12172
  clearOrder(order.orderHash);
11678
12173
  updateWithdrawSwap(order.orderHash, { status: "landed" });
11679
- results.push({
11680
- orderHash: order.orderHash,
11681
- status: "completed"
11682
- });
12174
+ return { orderHash: order.orderHash, status: "completed" };
11683
12175
  } catch (error) {
11684
- 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") {
11685
12177
  clearOrder(order.orderHash);
11686
12178
  updateWithdrawSwap(order.orderHash, { status: "refunded" });
11687
12179
  } else {
11688
- 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" });
11689
12182
  }
11690
- results.push({
12183
+ return {
11691
12184
  orderHash: order.orderHash,
11692
12185
  status: "unfinished",
11693
12186
  ...error instanceof OwneyError ? { error } : {}
11694
- });
12187
+ };
11695
12188
  }
11696
- }
11697
- await this.resumeSameChainSwaps(options);
12189
+ });
12190
+ await sameChain;
11698
12191
  return results;
11699
12192
  }
11700
12193
  /**
@@ -11715,34 +12208,32 @@ var OwneySDK = class {
11715
12208
  * anything else.
11716
12209
  */
11717
12210
  async resumeSameChainSwaps(options = {}) {
11718
- const running = listWithdrawSwaps().filter(
11719
- (record) => record.status === "swapping" && record.srcChainId === record.dstChainId
11720
- );
11721
- 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) => {
11722
12213
  const runner = {
11723
12214
  // The chain bound in. That product keys its orders per chain, and the
11724
12215
  // Fusion+ endpoint answers 404 for one — which would read as an order
11725
12216
  // that vanished rather than one still in the auction.
11726
- orderStatus: (h) => this.swapApi().orderStatus(h, record.srcChainId),
12217
+ orderStatus: (h) => this.swapApi().orderStatus(h, record2.srcChainId),
11727
12218
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
11728
12219
  now: () => Date.now()
11729
12220
  };
11730
12221
  try {
11731
12222
  await runSameChainOrder(runner, {
11732
- orderHash: record.orderHash,
12223
+ orderHash: record2.orderHash,
12224
+ hasPartialFill: record2.status === "partially-filled",
11733
12225
  ...options.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs },
11734
12226
  ...options.onStage ? {
11735
- onStage: (stage) => options.onStage(record.orderHash, stage)
12227
+ onStage: (stage) => options.onStage(record2.orderHash, stage)
11736
12228
  } : {}
11737
12229
  });
11738
- updateWithdrawSwap(record.orderHash, { status: "landed" });
12230
+ updateWithdrawSwap(record2.orderHash, { status: "landed" });
11739
12231
  } catch (error) {
11740
- const code = error instanceof OwneyError ? error.code : void 0;
11741
- updateWithdrawSwap(record.orderHash, {
11742
- status: code === "SWAP_ORDER_EXPIRED" || code === "SWAP_ORDER_CANCELLED" ? "refunded" : "unknown"
11743
- });
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" });
11744
12235
  }
11745
- }
12236
+ });
11746
12237
  }
11747
12238
  /**
11748
12239
  * What the wallet actually holds, across every supported chain, restricted
@@ -11898,6 +12389,92 @@ var OwneySDK = class {
11898
12389
  );
11899
12390
  }
11900
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
+ }
11901
12478
  /**
11902
12479
  * Withdraw from an agent and swap the proceeds into whatever the user wants
11903
12480
  * to hold, delivered to their own wallet.
@@ -11926,7 +12503,14 @@ var OwneySDK = class {
11926
12503
  const state = this.requireState();
11927
12504
  const activeChainId = this.requireChainId();
11928
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;
11929
12511
  const assertCurrent = () => {
12512
+ options.assertCurrent?.();
12513
+ if (!active2) throw new OwneyError("WITHDRAW_SWAP_INCOMPLETE", "This withdrawal attempt is no longer active.", { swapSubmitted: false });
11930
12514
  if (this.state !== state || this.connectionRevision !== revision || state.chainId !== activeChainId)
11931
12515
  throw new OwneyError("NOT_CONNECTED", "Wallet changed during withdrawal and swap.");
11932
12516
  };
@@ -11968,30 +12552,95 @@ var OwneySDK = class {
11968
12552
  functionName: "balanceOf",
11969
12553
  args: [state.walletAddress]
11970
12554
  });
11971
- const baselineBlock = await srcPublic.getBlockNumber({ cacheTime: 0 });
11972
- 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);
11973
12565
  assertCurrent();
11974
12566
  debugLog("owney-sdk", "withdrawAndSwap: baseline", {
11975
12567
  asset: `${asset.symbol}@${asset.chainId}`,
11976
12568
  baseline: baseline.toString()
11977
12569
  });
11978
12570
  options.onSwapProgress?.("withdrawing");
11979
- const submittedAt = Date.now();
11980
- 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
+ };
11981
12609
  const evidence = /* @__PURE__ */ new Map();
11982
12610
  const onProgress = (next) => {
11983
12611
  if (next.chainId !== activeChainId || next.token !== asset.symbol || next.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase()) return;
11984
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
+ }
11985
12624
  notifyFunding(options.onWithdrawalProgress, next);
11986
12625
  };
11987
12626
  let withdraw;
11988
12627
  try {
11989
- withdraw = await this.withdraw({
12628
+ withdraw = saved ? { agentResult: saved.recovery.results, totalWithdrawn: null } : await bounded(() => this.withdraw({
11990
12629
  asset: asset.symbol,
11991
12630
  ...requested === void 0 ? {} : { amount: requested.toString() },
11992
12631
  ...options.agentId ? { agentId: options.agentId } : {},
11993
- onProgress
11994
- });
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
+ ));
11995
12644
  } catch (error) {
11996
12645
  const details = error instanceof OwneyError ? error.details : void 0;
11997
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));
@@ -12013,6 +12662,9 @@ var OwneySDK = class {
12013
12662
  const results = "agentResult" in withdraw ? withdraw.agentResult : {
12014
12663
  [options.agentId ?? withdraw.evidence?.agentId ?? "unknown"]: withdraw
12015
12664
  };
12665
+ record2.recovery.results = results;
12666
+ record2.recovery.phase = "verifying";
12667
+ saveWithdrawSwap(record2);
12016
12668
  const arrived = await awaitWithdrawalPayouts({
12017
12669
  results,
12018
12670
  requested,
@@ -12024,7 +12676,8 @@ var OwneySDK = class {
12024
12676
  baselineBlock,
12025
12677
  assertCurrent,
12026
12678
  readBalance: readWalletBalance,
12027
- timeoutMs: options.arrivalTimeoutMs,
12679
+ deadline: confirmationDeadline,
12680
+ verifyCompletion: (value) => this.getAgent(value.agentId).verifyWithdrawalCompletion?.(state, value) ?? Promise.resolve({ status: "unavailable" }),
12028
12681
  inspect: (value, fromHistory) => this.inspectFunding(value, "withdraw", { requireAmountMatch: fromHistory, refreshBalance: false }),
12029
12682
  resolve: (value) => this.resolveSponsoredWithdrawalEvidence(value, { submittedAt }),
12030
12683
  history: (value, fromDate, filters) => this.getFundingHistory(value, fromDate, filters),
@@ -12032,34 +12685,89 @@ var OwneySDK = class {
12032
12685
  });
12033
12686
  assertCurrent();
12034
12687
  const withdrawn = arrived.toString();
12688
+ record2.withdrawn = withdrawn;
12689
+ record2.status = "ready";
12690
+ record2.recovery.phase = "ready";
12691
+ hadDurableRecord = saveWithdrawSwap(record2) || hadDurableRecord;
12035
12692
  options.onSwapProgress?.("withdrawn");
12036
12693
  try {
12037
- const quote = await this.swapApi().quote({
12694
+ const quote = await bounded(() => this.swapApi().quote({
12038
12695
  from: { ...options.from, amount: withdrawn },
12039
12696
  to: options.to,
12040
12697
  direction: "withdraw",
12041
12698
  walletAddress: state.walletAddress
12042
- });
12699
+ }), 2e4, () => new OwneyError("SWAP_REQUEST_FAILED", "Swap pricing timed out.", { outcome: "not-submitted" }));
12043
12700
  assertCurrent();
12044
- 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
+ );
12045
12706
  assertCurrent();
12046
- 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,
12047
12720
  quote,
12048
12721
  walletAddress: state.walletAddress,
12049
12722
  direction: "withdraw",
12050
12723
  ...options.slippage === void 0 ? {} : { slippage: options.slippage },
12051
12724
  ...options.onSwapProgress ? { onStage: options.onSwapProgress } : {},
12052
- 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) => {
12053
12743
  releaseSubmission();
12054
- 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
+ });
12055
12756
  }
12056
12757
  });
12758
+ record2.status = "landed";
12759
+ record2.received = swap.received;
12760
+ record2.spent = swap.spent;
12761
+ saveWithdrawSwap(record2);
12057
12762
  return { withdraw, withdrawn, swap };
12058
12763
  } catch (error) {
12059
12764
  throw new OwneyError(
12060
12765
  "WITHDRAW_SWAP_FAILED",
12061
- `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.`,
12062
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 } : {},
12063
12771
  withdrawn,
12064
12772
  asset: asset.symbol,
12065
12773
  chainId: asset.chainId,
@@ -12070,7 +12778,25 @@ var OwneySDK = class {
12070
12778
  }
12071
12779
  );
12072
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;
12073
12798
  } finally {
12799
+ active2 = false;
12074
12800
  releaseSubmission();
12075
12801
  }
12076
12802
  }
@@ -12103,7 +12829,15 @@ var OwneySDK = class {
12103
12829
  const attempted = /* @__PURE__ */ new Set();
12104
12830
  this.withdrawalSubmissions.add(submissionKey);
12105
12831
  try {
12106
- 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
+ );
12107
12841
  } catch (error) {
12108
12842
  for (const id of ids) if (!attempted.has(id)) {
12109
12843
  notifyFunding(options.onProgress, {
@@ -12193,7 +12927,7 @@ var OwneySDK = class {
12193
12927
  progress(evidence);
12194
12928
  adapterReported = false;
12195
12929
  try {
12196
- if (options.onBeforeSubmission)
12930
+ if (options.onBeforeSubmission || options.assertCurrent)
12197
12931
  return await agent.withdraw(
12198
12932
  state,
12199
12933
  chainId,
@@ -12201,7 +12935,8 @@ var OwneySDK = class {
12201
12935
  requested2,
12202
12936
  progress,
12203
12937
  options.gasMode ?? "auto",
12204
- (next) => options.onBeforeSubmission({ ...evidence, ...next })
12938
+ options.onBeforeSubmission ? (next) => options.onBeforeSubmission({ ...evidence, ...next }) : void 0,
12939
+ options.assertCurrent
12205
12940
  );
12206
12941
  return await (options.gasMode === void 0 ? agent.withdraw(state, chainId, token, requested2, progress) : agent.withdraw(state, chainId, token, requested2, progress, options.gasMode));
12207
12942
  } catch (error) {
@@ -12325,7 +13060,7 @@ var OwneySDK = class {
12325
13060
  if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
12326
13061
  if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
12327
13062
  const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
12328
- if (!total || parseUnits3(total.amount, assetInfo.decimals) <= entry.balance) return;
13063
+ if (!total || parseUnits4(total.amount, assetInfo.decimals) <= entry.balance) return;
12329
13064
  }
12330
13065
  try {
12331
13066
  const otherAvailable = indexedAvailable - entry.balance;