@owney/sdk 0.7.25-beta.15 → 0.7.25-beta.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +556 -110
- package/dist/index.d.cts +24 -1
- package/dist/index.d.ts +24 -1
- package/dist/index.js +557 -110
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -404,20 +404,44 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
|
|
|
404
404
|
};
|
|
405
405
|
const notify = (outcome) => {
|
|
406
406
|
if (outcome === "rejected" && matching.length !== 1) return;
|
|
407
|
+
const evidence = matching.length === 1 ? { ...request2 } : {
|
|
408
|
+
attempted: request2.attempted,
|
|
409
|
+
method: request2.method,
|
|
410
|
+
path: request2.path,
|
|
411
|
+
...request2.elapsedMs !== void 0 ? { elapsedMs: request2.elapsedMs } : {}
|
|
412
|
+
};
|
|
407
413
|
for (const observer of matching) {
|
|
408
414
|
try {
|
|
409
|
-
observer.notify(outcome,
|
|
415
|
+
observer.notify(outcome, evidence);
|
|
410
416
|
} catch {
|
|
411
417
|
}
|
|
412
418
|
}
|
|
413
419
|
};
|
|
414
420
|
notify("attempted");
|
|
421
|
+
let callArgs = args;
|
|
422
|
+
if (withdrawal && client.client?.defaults) {
|
|
423
|
+
const configIndex = method === "get" ? 1 : 2;
|
|
424
|
+
const config = args[configIndex];
|
|
425
|
+
const transforms = config?.transformResponse !== void 0 ? config.transformResponse : client.client.defaults.transformResponse;
|
|
426
|
+
const captureStatus = (data, _headers, status) => {
|
|
427
|
+
if (typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599) request2.httpStatus = status;
|
|
428
|
+
return data;
|
|
429
|
+
};
|
|
430
|
+
callArgs = [...args];
|
|
431
|
+
callArgs[configIndex] = { ...config, transformResponse: [
|
|
432
|
+
...Array.isArray(transforms) ? transforms : typeof transforms === "function" ? [transforms] : [],
|
|
433
|
+
captureStatus
|
|
434
|
+
] };
|
|
435
|
+
}
|
|
415
436
|
try {
|
|
416
|
-
return await original.apply(this,
|
|
437
|
+
return await original.apply(this, callArgs);
|
|
417
438
|
} catch (error) {
|
|
418
439
|
const message = error instanceof Error ? error.message : "";
|
|
419
|
-
|
|
420
|
-
if (
|
|
440
|
+
const status = error?.response?.status;
|
|
441
|
+
if (request2.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
|
|
442
|
+
request2.httpStatus = status;
|
|
443
|
+
request2.failure = message === "Unauthorized: Invalid API key" ? "unauthorized" : message === "Forbidden: Access denied" ? "forbidden" : message === "Rate limit exceeded. Please try again later." ? "rate-limited" : /^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" && request2.httpStatus === 400 && message === "Partial withdrawal already in progress" ? "withdrawal-in-progress" : message === "Network error: Unable to reach the server" ? "network" : "other";
|
|
444
|
+
if (["unauthorized", "forbidden", "rate-limited", "insufficient-balance", "session-key-missing", "withdrawal-in-progress"].includes(request2.failure) && (request2.httpStatus === void 0 || request2.httpStatus >= 400 && request2.httpStatus < 500)) notify("rejected");
|
|
421
445
|
throw error;
|
|
422
446
|
} finally {
|
|
423
447
|
request2.elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
@@ -1495,6 +1519,7 @@ var VIEM_CHAIN = { 8453: import_chains.base, 42161: import_chains.arbitrum, 1: i
|
|
|
1495
1519
|
var SPLITTING_ASSETS = ["USDC", "WETH"];
|
|
1496
1520
|
var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
|
|
1497
1521
|
var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
|
|
1522
|
+
var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
|
|
1498
1523
|
var ZyfaiAgent = class _ZyfaiAgent {
|
|
1499
1524
|
id = "zyfai";
|
|
1500
1525
|
supportedChainIds = [8453, 42161, 1];
|
|
@@ -2409,9 +2434,74 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2409
2434
|
}
|
|
2410
2435
|
}
|
|
2411
2436
|
// --- IAgent: Fund operations ---
|
|
2437
|
+
/** Only after a definitive endpoint rejection, combine a new provider
|
|
2438
|
+
* snapshot with the Safe's exact idle balance. Never parse its rounded error
|
|
2439
|
+
* amount as proof of zero, or use this to skip sending a new MAX request. */
|
|
2440
|
+
async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner) {
|
|
2441
|
+
const internals = this.sdk;
|
|
2442
|
+
const generation = this.earningsGeneration;
|
|
2443
|
+
let expired = false;
|
|
2444
|
+
const unavailable = () => new OwneyError(
|
|
2445
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
2446
|
+
"Could not verify Zyfai's balance after its withdrawal rejection.",
|
|
2447
|
+
{ chainId, token },
|
|
2448
|
+
this.id
|
|
2449
|
+
);
|
|
2450
|
+
const assertScope = () => {
|
|
2451
|
+
if (expired) throw unavailable();
|
|
2452
|
+
const account = internals.walletClient?.account?.address;
|
|
2453
|
+
if (generation !== this.earningsGeneration || state.chainId !== chainId || state.walletAddress.toLowerCase() !== owner.toLowerCase() || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== chainId || account && account.toLowerCase() !== owner.toLowerCase())
|
|
2454
|
+
throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
|
|
2455
|
+
};
|
|
2456
|
+
const read2 = async () => {
|
|
2457
|
+
assertScope();
|
|
2458
|
+
const [wallet, registered, portfolio] = await Promise.all([
|
|
2459
|
+
this.sdk.getSmartWalletAddress(owner, chainId),
|
|
2460
|
+
this.sdk.getSmartWalletByEOA(owner),
|
|
2461
|
+
this.sdk.getPortfolio(owner).catch(() => void 0)
|
|
2462
|
+
]);
|
|
2463
|
+
assertScope();
|
|
2464
|
+
if (!(0, import_viem4.isAddress)(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
|
|
2465
|
+
throw new OwneyError(
|
|
2466
|
+
"AGENT_AUTH_FAILED",
|
|
2467
|
+
"Could not verify Zyfai's withdrawal wallet.",
|
|
2468
|
+
{ reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
|
|
2469
|
+
this.id
|
|
2470
|
+
);
|
|
2471
|
+
if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
|
|
2472
|
+
const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
|
|
2473
|
+
const idle = asset ? await this.getPublicClient(chainId).readContract({
|
|
2474
|
+
address: asset.address,
|
|
2475
|
+
abi: import_viem4.erc20Abi,
|
|
2476
|
+
functionName: "balanceOf",
|
|
2477
|
+
args: [wallet.address]
|
|
2478
|
+
}).catch(() => void 0) : void 0;
|
|
2479
|
+
assertScope();
|
|
2480
|
+
if (idle === 0n) throw new OwneyError(
|
|
2481
|
+
"WITHDRAW_INSUFFICIENT_BALANCE",
|
|
2482
|
+
"No funds remain in Zyfai for this asset.",
|
|
2483
|
+
{ available: "0", chainId, token },
|
|
2484
|
+
this.id
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
let timer;
|
|
2489
|
+
try {
|
|
2490
|
+
await Promise.race([read2(), new Promise((_, reject) => {
|
|
2491
|
+
timer = setTimeout(() => {
|
|
2492
|
+
expired = true;
|
|
2493
|
+
reject(unavailable());
|
|
2494
|
+
}, WITHDRAW_BALANCE_TIMEOUT_MS);
|
|
2495
|
+
})]);
|
|
2496
|
+
} finally {
|
|
2497
|
+
expired = true;
|
|
2498
|
+
if (timer) clearTimeout(timer);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2412
2501
|
/** Called only after the execution endpoint explicitly rejected a request.
|
|
2413
2502
|
* A timeout or an accepted request never authorizes repair and resubmission. */
|
|
2414
2503
|
async recoverWithdrawalSession(state, chainId, token, owner) {
|
|
2504
|
+
await this.assertNotEmptyRejectedWithdrawal(state, chainId, token, owner);
|
|
2415
2505
|
const internals = this.sdk;
|
|
2416
2506
|
const assertScope = () => {
|
|
2417
2507
|
const account = internals.walletClient?.account?.address;
|
|
@@ -2419,35 +2509,6 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2419
2509
|
throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
|
|
2420
2510
|
};
|
|
2421
2511
|
assertScope();
|
|
2422
|
-
const [wallet, registered, portfolio] = await Promise.all([
|
|
2423
|
-
this.sdk.getSmartWalletAddress(owner, chainId),
|
|
2424
|
-
this.sdk.getSmartWalletByEOA(owner),
|
|
2425
|
-
this.sdk.getPortfolio(owner).catch(() => void 0)
|
|
2426
|
-
]);
|
|
2427
|
-
assertScope();
|
|
2428
|
-
if (!(0, import_viem4.isAddress)(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
|
|
2429
|
-
throw new OwneyError(
|
|
2430
|
-
"AGENT_AUTH_FAILED",
|
|
2431
|
-
"Could not verify Zyfai's withdrawal wallet.",
|
|
2432
|
-
{ reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
|
|
2433
|
-
this.id
|
|
2434
|
-
);
|
|
2435
|
-
if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
|
|
2436
|
-
const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
|
|
2437
|
-
const idle = asset ? await this.getPublicClient(chainId).readContract({
|
|
2438
|
-
address: asset.address,
|
|
2439
|
-
abi: import_viem4.erc20Abi,
|
|
2440
|
-
functionName: "balanceOf",
|
|
2441
|
-
args: [wallet.address]
|
|
2442
|
-
}).catch(() => void 0) : void 0;
|
|
2443
|
-
assertScope();
|
|
2444
|
-
if (idle === 0n) throw new OwneyError(
|
|
2445
|
-
"WITHDRAW_INSUFFICIENT_BALANCE",
|
|
2446
|
-
"No funds remain in Zyfai for this asset.",
|
|
2447
|
-
{ available: "0", chainId, token },
|
|
2448
|
-
this.id
|
|
2449
|
-
);
|
|
2450
|
-
}
|
|
2451
2512
|
const walletState = await this.readWalletState(owner);
|
|
2452
2513
|
assertScope();
|
|
2453
2514
|
const unavailable = () => new OwneyError(
|
|
@@ -2499,6 +2560,13 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2499
2560
|
raw2 = await this.sdk.withdrawFunds(owner, validChainId, amount, token);
|
|
2500
2561
|
break;
|
|
2501
2562
|
} catch (error) {
|
|
2563
|
+
if (!amount && observation.rejected() && observation.request()?.failure === "insufficient-balance") {
|
|
2564
|
+
try {
|
|
2565
|
+
await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner);
|
|
2566
|
+
} catch (proofError) {
|
|
2567
|
+
if (proofError instanceof OwneyError && (proofError.code === "WITHDRAW_INSUFFICIENT_BALANCE" || proofError.code === "NOT_CONNECTED")) throw proofError;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2502
2570
|
if (!observation.rejected() || observation.request()?.failure !== "session-key-missing") throw error;
|
|
2503
2571
|
if (attempt === 1)
|
|
2504
2572
|
throw new OwneyError(
|
|
@@ -2592,6 +2660,67 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2592
2660
|
async getFundingWallet(state, chainId) {
|
|
2593
2661
|
return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
|
|
2594
2662
|
}
|
|
2663
|
+
/** A positive Safe balance is a withdrawal planning floor when the portfolio
|
|
2664
|
+
* index reports zero. Idle zero cannot prove that Zyfai's protocol positions
|
|
2665
|
+
* are empty: its SDK does not expose a complete contract catalog for them. */
|
|
2666
|
+
async getWithdrawableBalance(state, chainId, token) {
|
|
2667
|
+
const validChainId = isValidChainId(chainId);
|
|
2668
|
+
const asset = SupportedAssets.find((value) => value.chainId === validChainId && value.symbol === token);
|
|
2669
|
+
if (!asset) throw new OwneyError("WITHDRAW_NO_PERMITTED_TOKENS", "Asset selected is not supported", void 0, this.id);
|
|
2670
|
+
const owner = state.walletAddress;
|
|
2671
|
+
const generation = this.earningsGeneration;
|
|
2672
|
+
const internals = this.sdk;
|
|
2673
|
+
let expired = false;
|
|
2674
|
+
const unavailable = () => new OwneyError(
|
|
2675
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
2676
|
+
"Could not verify Zyfai's withdrawal balance. Its vault positions may still contain funds.",
|
|
2677
|
+
{ reason: "ZYFAI_VAULT_BALANCE_UNVERIFIED", chainId, token },
|
|
2678
|
+
this.id
|
|
2679
|
+
);
|
|
2680
|
+
const assertScope = () => {
|
|
2681
|
+
if (expired) throw unavailable();
|
|
2682
|
+
const account = internals.walletClient?.account?.address;
|
|
2683
|
+
if (generation !== this.earningsGeneration || this.connectedAddress?.toLowerCase() !== owner.toLowerCase() || this.connectedChainId !== validChainId || state.walletAddress.toLowerCase() !== owner.toLowerCase() || state.chainId !== validChainId || account && account.toLowerCase() !== owner.toLowerCase())
|
|
2684
|
+
throw new OwneyError("NOT_CONNECTED", "Wallet changed while checking Zyfai's withdrawal balance.", void 0, this.id);
|
|
2685
|
+
};
|
|
2686
|
+
let timer;
|
|
2687
|
+
const read2 = async () => {
|
|
2688
|
+
assertScope();
|
|
2689
|
+
const [wallet, registered] = await Promise.all([
|
|
2690
|
+
this.sdk.getSmartWalletAddress(owner, validChainId),
|
|
2691
|
+
this.sdk.getSmartWalletByEOA(owner)
|
|
2692
|
+
]);
|
|
2693
|
+
assertScope();
|
|
2694
|
+
if (!(0, import_viem4.isAddress)(wallet.address) || wallet.isDeployed !== true || wallet.isOwner !== true || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase()) throw unavailable();
|
|
2695
|
+
const client = this.getPublicClient(validChainId);
|
|
2696
|
+
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
|
|
2697
|
+
assertScope();
|
|
2698
|
+
const idle = await client.readContract({
|
|
2699
|
+
address: asset.address,
|
|
2700
|
+
abi: import_viem4.erc20Abi,
|
|
2701
|
+
functionName: "balanceOf",
|
|
2702
|
+
args: [wallet.address],
|
|
2703
|
+
blockNumber
|
|
2704
|
+
});
|
|
2705
|
+
assertScope();
|
|
2706
|
+
if (typeof idle !== "bigint" || idle <= 0n) throw unavailable();
|
|
2707
|
+
return idle;
|
|
2708
|
+
};
|
|
2709
|
+
try {
|
|
2710
|
+
return await Promise.race([read2(), new Promise((_, reject) => {
|
|
2711
|
+
timer = setTimeout(() => {
|
|
2712
|
+
expired = true;
|
|
2713
|
+
reject(unavailable());
|
|
2714
|
+
}, WITHDRAW_BALANCE_TIMEOUT_MS);
|
|
2715
|
+
})]);
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
if (error instanceof OwneyError) throw error;
|
|
2718
|
+
throw unavailable();
|
|
2719
|
+
} finally {
|
|
2720
|
+
expired = true;
|
|
2721
|
+
if (timer) clearTimeout(timer);
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2595
2724
|
async getBalances(state, chainId) {
|
|
2596
2725
|
const requestedAt = Date.now();
|
|
2597
2726
|
const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
|
|
@@ -4579,6 +4708,8 @@ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
|
|
|
4579
4708
|
var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
|
|
4580
4709
|
var YIELDSEEKER_MAX_WITHDRAW_SWEEPS = 3;
|
|
4581
4710
|
var YIELDSEEKER_MAX_POSITION_VERIFICATION_MS = 2e3;
|
|
4711
|
+
var YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT = 64;
|
|
4712
|
+
var YIELDSEEKER_VAULTS_PER_RPC_BATCH = 10;
|
|
4582
4713
|
function generateYieldseekerUsername() {
|
|
4583
4714
|
const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
|
|
4584
4715
|
return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
|
|
@@ -5102,17 +5233,38 @@ var YieldseekerAgent = class {
|
|
|
5102
5233
|
let settlement = null;
|
|
5103
5234
|
try {
|
|
5104
5235
|
const maxSettlementBasis = amount === void 0 ? this.snapshotMovement(state, chainId, context, 0n) : null;
|
|
5105
|
-
this.
|
|
5106
|
-
const
|
|
5236
|
+
const contextKey = this.contextKey(state, chainId, asset);
|
|
5237
|
+
const priorPositions = this.withdrawalVaultHints(state, chainId, context);
|
|
5238
|
+
this.advancePortfolioVersion(contextKey);
|
|
5239
|
+
const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5240
|
+
let timer;
|
|
5241
|
+
try {
|
|
5242
|
+
return await Promise.race([read2, new Promise((resolve) => {
|
|
5243
|
+
timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
|
|
5244
|
+
})]);
|
|
5245
|
+
} finally {
|
|
5246
|
+
if (timer) clearTimeout(timer);
|
|
5247
|
+
}
|
|
5248
|
+
};
|
|
5249
|
+
const feeRateRead = amount === void 0 ? Promise.resolve(0) : boundedVerification(
|
|
5250
|
+
(() => {
|
|
5251
|
+
try {
|
|
5252
|
+
return this.portfolioSnapshot(state, chainId, context, contextKey).read.then((value) => value.agentSnapshot.feeRateBps);
|
|
5253
|
+
} catch (error) {
|
|
5254
|
+
return Promise.reject(error);
|
|
5255
|
+
}
|
|
5256
|
+
})().catch(() => void 0)
|
|
5257
|
+
);
|
|
5258
|
+
const positionsRead = this.readWithdrawalPositions(state, chainId, context);
|
|
5107
5259
|
const metadata = YIELDSEEKER_ASSET_METADATA[asset];
|
|
5108
5260
|
const agentWallet = (0, import_viem11.getAddress)(context.wallet.walletAddress);
|
|
5109
|
-
const
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5261
|
+
const readOwnerFunds = async (blockNumber) => {
|
|
5262
|
+
const feeTracker = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
|
|
5263
|
+
address: agentWallet,
|
|
5264
|
+
abi: YIELDSEEKER_AGENT_WALLET_ABI,
|
|
5265
|
+
functionName: "feeTracker",
|
|
5266
|
+
blockNumber
|
|
5267
|
+
}), chainId, "eth_call", this.id);
|
|
5116
5268
|
this.assertSession(generation, chainId, asset);
|
|
5117
5269
|
if (!(0, import_viem11.isAddress)(feeTracker) || /^0x0{40}$/i.test(feeTracker))
|
|
5118
5270
|
throw this.invalidResponse("wallet fee tracker");
|
|
@@ -5121,61 +5273,58 @@ var YieldseekerAgent = class {
|
|
|
5121
5273
|
address: metadata.address,
|
|
5122
5274
|
abi: import_viem11.erc20Abi,
|
|
5123
5275
|
functionName: "balanceOf",
|
|
5124
|
-
args: [agentWallet]
|
|
5276
|
+
args: [agentWallet],
|
|
5277
|
+
blockNumber
|
|
5125
5278
|
}), chainId, "eth_call", this.id),
|
|
5126
5279
|
withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
|
|
5127
5280
|
address: (0, import_viem11.getAddress)(feeTracker),
|
|
5128
5281
|
abi: YIELDSEEKER_FEE_TRACKER_ABI,
|
|
5129
5282
|
functionName: "getFeesOwed",
|
|
5130
|
-
args: [agentWallet]
|
|
5283
|
+
args: [agentWallet],
|
|
5284
|
+
blockNumber
|
|
5131
5285
|
}), chainId, "eth_call", this.id)
|
|
5132
5286
|
]);
|
|
5133
5287
|
this.assertSession(generation, chainId, asset);
|
|
5134
5288
|
if (typeof idle2 !== "bigint" || idle2 < 0n || typeof fees !== "bigint" || fees < 0n)
|
|
5135
5289
|
throw this.invalidResponse("wallet withdrawal balance");
|
|
5136
|
-
return { idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
|
|
5290
|
+
return { blockNumber, idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
|
|
5137
5291
|
};
|
|
5138
|
-
const [
|
|
5292
|
+
const [funds, feeRateBps] = await Promise.all([
|
|
5293
|
+
positionsRead.then((positions2) => this.resolveWithdrawalLiquidity(
|
|
5294
|
+
state,
|
|
5295
|
+
chainId,
|
|
5296
|
+
context,
|
|
5297
|
+
positions2,
|
|
5298
|
+
priorPositions,
|
|
5299
|
+
amount === void 0 ? void 0 : BigInt(amount)
|
|
5300
|
+
)),
|
|
5301
|
+
feeRateRead
|
|
5302
|
+
]);
|
|
5303
|
+
const initialFunds = funds;
|
|
5139
5304
|
const idle = initialFunds.idle;
|
|
5140
|
-
|
|
5305
|
+
const maxLiquidity = amount === void 0 ? funds : void 0;
|
|
5306
|
+
let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()))];
|
|
5141
5307
|
let positionsStatus = "empty";
|
|
5142
|
-
const validatePositions = (values) =>
|
|
5143
|
-
|
|
5144
|
-
for (const value of values) {
|
|
5145
|
-
const address = value?.yieldOption?.address;
|
|
5146
|
-
if (typeof address !== "string" || !(0, import_viem11.isAddress)(address) || seen.has(address.toLowerCase()) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
|
|
5147
|
-
throw this.invalidResponse("yield positions");
|
|
5148
|
-
seen.add(address.toLowerCase());
|
|
5149
|
-
}
|
|
5150
|
-
return values;
|
|
5151
|
-
};
|
|
5308
|
+
const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
|
|
5309
|
+
validatePositions(positions);
|
|
5152
5310
|
const zeroValuePositions = /* @__PURE__ */ new WeakSet();
|
|
5153
5311
|
const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
|
|
5154
5312
|
const knownPositions = /* @__PURE__ */ new Map();
|
|
5155
5313
|
const rememberPositions = (values) => {
|
|
5156
5314
|
for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
|
|
5157
5315
|
};
|
|
5158
|
-
const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5159
|
-
let timer;
|
|
5160
|
-
try {
|
|
5161
|
-
return await Promise.race([read2, new Promise((resolve) => {
|
|
5162
|
-
timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
|
|
5163
|
-
})]);
|
|
5164
|
-
} finally {
|
|
5165
|
-
if (timer) clearTimeout(timer);
|
|
5166
|
-
}
|
|
5167
|
-
};
|
|
5168
5316
|
const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5169
5317
|
const verifiedPositions = [];
|
|
5170
5318
|
if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
|
|
5171
|
-
|
|
5319
|
+
let active = true;
|
|
5320
|
+
const checks = this.readVaultLiquidity(values.map((position2) => position2.yieldOption), agentWallet, blockNumber).catch(() => []);
|
|
5321
|
+
const reads = values.map(async (position2, index) => {
|
|
5172
5322
|
const address = (0, import_viem11.getAddress)(position2.yieldOption.address);
|
|
5173
5323
|
try {
|
|
5174
|
-
const
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
]);
|
|
5324
|
+
const group = (await checks).slice(index * 3, index * 3 + 3);
|
|
5325
|
+
if (!active || group.length !== 3 || group.some((value) => value.status !== "success")) return null;
|
|
5326
|
+
this.assertSession(generation, chainId, asset);
|
|
5327
|
+
const [underlying, shares, withdrawable] = group.map((value) => value.result);
|
|
5179
5328
|
if (typeof underlying !== "string" || !(0, import_viem11.isAddress)(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
|
|
5180
5329
|
const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
|
|
5181
5330
|
if (shares > 0n && withdrawable === 0n) {
|
|
@@ -5210,6 +5359,7 @@ var YieldseekerAgent = class {
|
|
|
5210
5359
|
}
|
|
5211
5360
|
});
|
|
5212
5361
|
const result = await boundedVerification(Promise.all(reads), timeoutMs);
|
|
5362
|
+
active = false;
|
|
5213
5363
|
this.assertSession(generation, chainId, asset);
|
|
5214
5364
|
return {
|
|
5215
5365
|
verified: result !== void 0 && result.every((position2) => position2 !== null),
|
|
@@ -5228,7 +5378,7 @@ var YieldseekerAgent = class {
|
|
|
5228
5378
|
return validatePositions(response.yieldPositions);
|
|
5229
5379
|
};
|
|
5230
5380
|
if (amount === void 0) {
|
|
5231
|
-
|
|
5381
|
+
rememberPositions(funds.indexedPositions);
|
|
5232
5382
|
rememberPositions(positions);
|
|
5233
5383
|
positionsStatus = hasPositions(positions) ? "remaining" : "empty";
|
|
5234
5384
|
}
|
|
@@ -5244,8 +5394,8 @@ var YieldseekerAgent = class {
|
|
|
5244
5394
|
const zeroPayoutCandidates = positions.filter((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
|
|
5245
5395
|
if (amount === void 0 && zeroPayoutCandidates.length) {
|
|
5246
5396
|
const deadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
|
|
5247
|
-
const blockNumber = await boundedVerification(
|
|
5248
|
-
Promise.resolve().then(() => this.getReceiptClient().getBlockNumber()).catch(() => void 0)
|
|
5397
|
+
const blockNumber = initialFunds.blockNumber ?? await boundedVerification(
|
|
5398
|
+
Promise.resolve().then(() => this.getReceiptClient().getBlockNumber({ cacheTime: 0 })).catch(() => void 0)
|
|
5249
5399
|
);
|
|
5250
5400
|
const proof = typeof blockNumber === "bigint" ? await verifyKnownPositions(blockNumber, zeroPayoutCandidates, deadline - Date.now()) : { verified: false, positions: [] };
|
|
5251
5401
|
positions = proof.positions;
|
|
@@ -5369,8 +5519,9 @@ var YieldseekerAgent = class {
|
|
|
5369
5519
|
remaining: remaining.toString()
|
|
5370
5520
|
});
|
|
5371
5521
|
}
|
|
5372
|
-
const
|
|
5373
|
-
const
|
|
5522
|
+
const ownerFunds = confirmedMovement ? await readOwnerFunds(latestUnwindBlock) : initialFunds;
|
|
5523
|
+
const availableForOwner = ownerFunds.available;
|
|
5524
|
+
const feeRate = feeRateBps ?? 0;
|
|
5374
5525
|
const maxFee = confirmedMovement && Number.isInteger(feeRate) && feeRate >= 0 && feeRate <= 1e3 ? (grossUnwound * BigInt(feeRate) + 9999n) / 10000n : 0n;
|
|
5375
5526
|
const shortfall = availableForOwner < requested ? requested - availableForOwner : 0n;
|
|
5376
5527
|
if (availableForOwner === 0n || amount !== void 0 && shortfall > maxFee) {
|
|
@@ -5445,7 +5596,7 @@ var YieldseekerAgent = class {
|
|
|
5445
5596
|
settlement = plannedMovement;
|
|
5446
5597
|
const evidence = receipt ? inspectFundingReceipt(receipt, { ...base6, txHash }, metadata.address, "withdraw") : { ...base6, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
|
|
5447
5598
|
notifyFunding(onProgress, evidence);
|
|
5448
|
-
let completion;
|
|
5599
|
+
let completion = maxLiquidity && !maxLiquidity.complete ? "unverified" : void 0;
|
|
5449
5600
|
if (amount === void 0) {
|
|
5450
5601
|
const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
|
|
5451
5602
|
const knownAtPayout = new Set(knownPositions.keys());
|
|
@@ -5492,10 +5643,14 @@ var YieldseekerAgent = class {
|
|
|
5492
5643
|
{ asset, available: "0", txHash },
|
|
5493
5644
|
this.id
|
|
5494
5645
|
);
|
|
5646
|
+
const confirmedAmount = evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) ? BigInt(evidence.confirmedAmount) : void 0;
|
|
5647
|
+
const feeIncrease = ownerFunds.fees - initialFunds.fees;
|
|
5648
|
+
const feeAdjustment = amount !== void 0 && receipt && initialFunds.blockNumber !== void 0 && latestUnwindBlock !== void 0 && initialFunds.blockNumber <= latestUnwindBlock && receipt.blockNumber >= latestUnwindBlock && evidence.transfer === "confirmed" && evidence.delivery === "confirmed" && confirmedAmount === payout && feeIncrease > 0n && feeIncrease <= maxFee && requested - payout === feeIncrease ? { grossAmount: requested.toString(), feeAmount: feeIncrease.toString() } : void 0;
|
|
5495
5649
|
return {
|
|
5496
5650
|
txHash,
|
|
5497
5651
|
type: amount === void 0 && !completion ? "full" : "partial",
|
|
5498
5652
|
...completion ? { completion } : {},
|
|
5653
|
+
...feeAdjustment ? { feeAdjustment } : {},
|
|
5499
5654
|
amount: evidence.confirmedAmount ?? payout.toString(),
|
|
5500
5655
|
evidence
|
|
5501
5656
|
};
|
|
@@ -5511,6 +5666,236 @@ var YieldseekerAgent = class {
|
|
|
5511
5666
|
}
|
|
5512
5667
|
}
|
|
5513
5668
|
}
|
|
5669
|
+
/** A withdrawal-only liquidity floor; display snapshots never authorize it. */
|
|
5670
|
+
async getWithdrawableBalance(state, chainId, asset, minimumAmount) {
|
|
5671
|
+
this.assertChain(chainId);
|
|
5672
|
+
this.assertAsset(asset);
|
|
5673
|
+
const generation = this.readGeneration;
|
|
5674
|
+
const context = await this.findAgent(state, chainId, asset);
|
|
5675
|
+
this.assertSession(generation, chainId, asset);
|
|
5676
|
+
if (!context) throw new OwneyError(
|
|
5677
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5678
|
+
"The Yieldseeker withdrawal wallet could not be verified.",
|
|
5679
|
+
{ asset },
|
|
5680
|
+
this.id
|
|
5681
|
+
);
|
|
5682
|
+
const hints = this.withdrawalVaultHints(state, chainId, context);
|
|
5683
|
+
const positions = await this.readWithdrawalPositions(state, chainId, context);
|
|
5684
|
+
const proof = await this.resolveWithdrawalLiquidity(state, chainId, context, positions, hints, minimumAmount);
|
|
5685
|
+
this.assertSession(generation, chainId, asset);
|
|
5686
|
+
if (proof.available === 0n && !proof.complete) throw new OwneyError(
|
|
5687
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5688
|
+
"Yieldseeker withdrawal liquidity could not be verified yet.",
|
|
5689
|
+
{ asset },
|
|
5690
|
+
this.id
|
|
5691
|
+
);
|
|
5692
|
+
return proof.available;
|
|
5693
|
+
}
|
|
5694
|
+
async readVaultLiquidity(options, wallet, blockNumber) {
|
|
5695
|
+
const owner = (0, import_viem11.getAddress)(wallet);
|
|
5696
|
+
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));
|
|
5697
|
+
const results = await Promise.all(batches.map((batch) => this.getReceiptClient().multicall({
|
|
5698
|
+
blockNumber,
|
|
5699
|
+
allowFailure: true,
|
|
5700
|
+
contracts: batch.flatMap((option) => {
|
|
5701
|
+
const address = (0, import_viem11.getAddress)(option.address);
|
|
5702
|
+
return [
|
|
5703
|
+
{ address, abi: import_viem11.erc4626Abi, functionName: "asset" },
|
|
5704
|
+
{ address, abi: import_viem11.erc20Abi, functionName: "balanceOf", args: [owner] },
|
|
5705
|
+
{ address, abi: import_viem11.erc4626Abi, functionName: "maxWithdraw", args: [owner] }
|
|
5706
|
+
];
|
|
5707
|
+
})
|
|
5708
|
+
})));
|
|
5709
|
+
return results.flat();
|
|
5710
|
+
}
|
|
5711
|
+
validateWithdrawalPositions(values, chainId, asset) {
|
|
5712
|
+
const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
|
|
5713
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5714
|
+
for (const value of values) {
|
|
5715
|
+
const option = value?.yieldOption;
|
|
5716
|
+
if (!option || typeof option.address !== "string" || !(0, import_viem11.isAddress)(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))
|
|
5717
|
+
throw this.invalidResponse("yield positions");
|
|
5718
|
+
seen.add(option.address.toLowerCase());
|
|
5719
|
+
}
|
|
5720
|
+
return values;
|
|
5721
|
+
}
|
|
5722
|
+
withdrawalVaultHints(state, chainId, context) {
|
|
5723
|
+
const key2 = this.contextKey(state, chainId, context.asset);
|
|
5724
|
+
const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
|
|
5725
|
+
const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
|
|
5726
|
+
const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
|
|
5727
|
+
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()));
|
|
5728
|
+
for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
|
|
5729
|
+
if (option.chainId !== chainId || typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== YIELDSEEKER_ASSET_METADATA[context.asset].address.toLowerCase() || !(0, import_viem11.isAddress)(option.address) || !receivedAssets.has(option.address.toLowerCase()) || hints.has(option.address.toLowerCase())) continue;
|
|
5730
|
+
hints.set(option.address.toLowerCase(), {
|
|
5731
|
+
yieldOption: option,
|
|
5732
|
+
assetsRaw: "0",
|
|
5733
|
+
assetsBase: "0",
|
|
5734
|
+
assetsUsd: 0,
|
|
5735
|
+
withdrawableAssetsRaw: "0",
|
|
5736
|
+
withdrawableAssetsBase: "0"
|
|
5737
|
+
});
|
|
5738
|
+
}
|
|
5739
|
+
return [...hints.values()];
|
|
5740
|
+
}
|
|
5741
|
+
async readWithdrawalPositions(state, chainId, context) {
|
|
5742
|
+
const generation = this.readGeneration;
|
|
5743
|
+
let timer;
|
|
5744
|
+
try {
|
|
5745
|
+
const positions = await Promise.race([
|
|
5746
|
+
this.loadActivityContext(state, chainId, context, {}).then((value) => value.positions).catch((error) => {
|
|
5747
|
+
if (!(error instanceof OwneyError) || !["AGENT_API_ERROR", "AGENT_TIMEOUT"].includes(error.code)) throw error;
|
|
5748
|
+
return void 0;
|
|
5749
|
+
}),
|
|
5750
|
+
new Promise((resolve) => {
|
|
5751
|
+
timer = setTimeout(() => resolve(void 0), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
|
|
5752
|
+
})
|
|
5753
|
+
]);
|
|
5754
|
+
this.assertSession(generation, chainId, context.asset);
|
|
5755
|
+
return positions === void 0 ? void 0 : this.validateWithdrawalPositions(positions, chainId, context.asset);
|
|
5756
|
+
} finally {
|
|
5757
|
+
if (timer) clearTimeout(timer);
|
|
5758
|
+
}
|
|
5759
|
+
}
|
|
5760
|
+
async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
|
|
5761
|
+
const generation = this.readGeneration;
|
|
5762
|
+
const contextKey = this.contextKey(state, chainId, context.asset);
|
|
5763
|
+
const version = this.portfolioVersion(contextKey);
|
|
5764
|
+
const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
|
|
5765
|
+
const unavailable = () => new OwneyError(
|
|
5766
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5767
|
+
"Yieldseeker withdrawal liquidity could not be verified yet.",
|
|
5768
|
+
{ asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
|
|
5769
|
+
this.id
|
|
5770
|
+
);
|
|
5771
|
+
let active = true;
|
|
5772
|
+
let knownProof;
|
|
5773
|
+
const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
|
|
5774
|
+
const assertReadCurrent = () => {
|
|
5775
|
+
this.assertSession(generation, chainId, context.asset);
|
|
5776
|
+
if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
|
|
5777
|
+
};
|
|
5778
|
+
const read2 = async () => {
|
|
5779
|
+
const client = this.getReceiptClient();
|
|
5780
|
+
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
|
|
5781
|
+
const wallet = (0, import_viem11.getAddress)(context.wallet.walletAddress);
|
|
5782
|
+
assertReadCurrent();
|
|
5783
|
+
if (typeof blockNumber !== "bigint") throw unavailable();
|
|
5784
|
+
const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
|
|
5785
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
5786
|
+
for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
|
|
5787
|
+
candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
|
|
5788
|
+
for (const position2 of indexedPositions) candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
|
|
5789
|
+
const options = [...candidates.values()];
|
|
5790
|
+
if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
|
|
5791
|
+
const [checks, idle, feeTracker] = await Promise.all([
|
|
5792
|
+
this.readVaultLiquidity(options, wallet, blockNumber),
|
|
5793
|
+
client.readContract({
|
|
5794
|
+
address: metadata.address,
|
|
5795
|
+
abi: import_viem11.erc20Abi,
|
|
5796
|
+
functionName: "balanceOf",
|
|
5797
|
+
args: [wallet],
|
|
5798
|
+
blockNumber
|
|
5799
|
+
}),
|
|
5800
|
+
client.readContract({
|
|
5801
|
+
address: wallet,
|
|
5802
|
+
abi: YIELDSEEKER_AGENT_WALLET_ABI,
|
|
5803
|
+
functionName: "feeTracker",
|
|
5804
|
+
blockNumber
|
|
5805
|
+
})
|
|
5806
|
+
]);
|
|
5807
|
+
if (typeof idle !== "bigint" || idle < 0n || !(0, import_viem11.isAddress)(feeTracker) || /^0x0{40}$/i.test(feeTracker)) throw unavailable();
|
|
5808
|
+
assertReadCurrent();
|
|
5809
|
+
const fees = await client.readContract({
|
|
5810
|
+
address: (0, import_viem11.getAddress)(feeTracker),
|
|
5811
|
+
abi: YIELDSEEKER_FEE_TRACKER_ABI,
|
|
5812
|
+
functionName: "getFeesOwed",
|
|
5813
|
+
args: [wallet],
|
|
5814
|
+
blockNumber
|
|
5815
|
+
});
|
|
5816
|
+
if (typeof fees !== "bigint" || fees < 0n) throw unavailable();
|
|
5817
|
+
const verified = [];
|
|
5818
|
+
const verifiedVaults = [];
|
|
5819
|
+
let deployed = 0n;
|
|
5820
|
+
const collect = (options2, results) => {
|
|
5821
|
+
let valid2 = true;
|
|
5822
|
+
options2.forEach((option, index) => {
|
|
5823
|
+
const group = results.slice(index * 3, index * 3 + 3);
|
|
5824
|
+
if (group.length !== 3 || group.some((value) => value.status !== "success")) {
|
|
5825
|
+
valid2 = false;
|
|
5826
|
+
return;
|
|
5827
|
+
}
|
|
5828
|
+
const [underlying, shares, withdrawable] = group.map((value) => value.result);
|
|
5829
|
+
if (typeof underlying !== "string" || !(0, import_viem11.isAddress)(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n || shares === 0n && withdrawable > 0n) {
|
|
5830
|
+
valid2 = false;
|
|
5831
|
+
return;
|
|
5832
|
+
}
|
|
5833
|
+
verifiedVaults.push(option.address.toLowerCase());
|
|
5834
|
+
deployed += withdrawable;
|
|
5835
|
+
verified.push({
|
|
5836
|
+
yieldOption: option,
|
|
5837
|
+
assetsRaw: shares.toString(),
|
|
5838
|
+
assetsBase: "0",
|
|
5839
|
+
assetsUsd: 0,
|
|
5840
|
+
withdrawableAssetsRaw: withdrawable.toString(),
|
|
5841
|
+
withdrawableAssetsBase: withdrawable.toString()
|
|
5842
|
+
});
|
|
5843
|
+
});
|
|
5844
|
+
return valid2;
|
|
5845
|
+
};
|
|
5846
|
+
const allKnownVerified = collect(options, checks);
|
|
5847
|
+
const proof = (complete) => ({
|
|
5848
|
+
blockNumber,
|
|
5849
|
+
idle,
|
|
5850
|
+
fees,
|
|
5851
|
+
available: idle + deployed > fees ? idle + deployed - fees : 0n,
|
|
5852
|
+
positions: [...verified],
|
|
5853
|
+
verifiedVaults: [...verifiedVaults],
|
|
5854
|
+
indexedPositions,
|
|
5855
|
+
complete
|
|
5856
|
+
});
|
|
5857
|
+
knownProof = proof(false);
|
|
5858
|
+
const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
|
|
5859
|
+
const discrepancy = verified.some((position2) => {
|
|
5860
|
+
const row = indexed.get(position2.yieldOption.address.toLowerCase());
|
|
5861
|
+
return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
|
|
5862
|
+
});
|
|
5863
|
+
const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
|
|
5864
|
+
const expand = positions === void 0 || indexedPositions.length === 0 && !typedCoveredByIdle || !allKnownVerified || discrepancy || knownProof.available === 0n || requested !== void 0 && requested > knownProof.available;
|
|
5865
|
+
if (!expand) {
|
|
5866
|
+
assertReadCurrent();
|
|
5867
|
+
return proof(true);
|
|
5868
|
+
}
|
|
5869
|
+
const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
|
|
5870
|
+
assertReadCurrent();
|
|
5871
|
+
const eligible = /* @__PURE__ */ new Map();
|
|
5872
|
+
for (const option of catalog ?? []) {
|
|
5873
|
+
if (option?.chainId === chainId && typeof option.assetAddress === "string" && option.assetAddress.toLowerCase() === metadata.address.toLowerCase() && (0, import_viem11.isAddress)(option.address))
|
|
5874
|
+
eligible.set(option.address.toLowerCase(), option);
|
|
5875
|
+
}
|
|
5876
|
+
const extra = [...eligible].filter(([address]) => !candidates.has(address)).map(([, option]) => option);
|
|
5877
|
+
if (options.length + extra.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
|
|
5878
|
+
const allExtraVerified = collect(extra, await this.readVaultLiquidity(extra, wallet, blockNumber));
|
|
5879
|
+
assertReadCurrent();
|
|
5880
|
+
const result = proof(eligible.size > 0 && allKnownVerified && allExtraVerified);
|
|
5881
|
+
if (result.available === 0n && !result.complete && !hasUnverifiedIndexedFunds(result)) throw unavailable();
|
|
5882
|
+
return result;
|
|
5883
|
+
};
|
|
5884
|
+
let timer;
|
|
5885
|
+
try {
|
|
5886
|
+
return await Promise.race([read2(), new Promise((_, reject) => {
|
|
5887
|
+
timer = setTimeout(() => reject(unavailable()), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
|
|
5888
|
+
})]);
|
|
5889
|
+
} catch (error) {
|
|
5890
|
+
assertReadCurrent();
|
|
5891
|
+
if (knownProof && (knownProof.available > 0n || hasUnverifiedIndexedFunds(knownProof)) && (!(error instanceof OwneyError) || error.code === "WITHDRAW_BALANCE_UNAVAILABLE")) return knownProof;
|
|
5892
|
+
if (error instanceof OwneyError) throw error;
|
|
5893
|
+
throw unavailable();
|
|
5894
|
+
} finally {
|
|
5895
|
+
active = false;
|
|
5896
|
+
if (timer) clearTimeout(timer);
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5514
5899
|
async getBalances(state, chainId, asset) {
|
|
5515
5900
|
this.assertChain(chainId);
|
|
5516
5901
|
if (asset !== void 0) this.assertAsset(asset);
|
|
@@ -8672,11 +9057,10 @@ var OwneySDK = class {
|
|
|
8672
9057
|
};
|
|
8673
9058
|
}
|
|
8674
9059
|
const requested = BigInt(amount);
|
|
8675
|
-
const aggregated = await this.
|
|
9060
|
+
const { balances: aggregated, failures: balanceFailures } = await this.readAggregatedBalances({ chainId, asset: token });
|
|
8676
9061
|
const unavailableAgents = eligibleAgents.filter(
|
|
8677
9062
|
(agent) => !(agent.id in aggregated.agentBalances)
|
|
8678
9063
|
);
|
|
8679
|
-
const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
|
|
8680
9064
|
const balances = projectAgentBalancesForAsset(
|
|
8681
9065
|
eligibleAgents,
|
|
8682
9066
|
aggregated.agentBalances,
|
|
@@ -8684,6 +9068,44 @@ var OwneySDK = class {
|
|
|
8684
9068
|
asset,
|
|
8685
9069
|
assetInfo.decimals
|
|
8686
9070
|
);
|
|
9071
|
+
const indexedAvailable = balances.reduce((sum, entry) => sum + entry.balance, 0n);
|
|
9072
|
+
await Promise.all(balances.map(async (entry) => {
|
|
9073
|
+
if (entry.balance !== 0n && indexedAvailable >= requested || !entry.agent.getWithdrawableBalance) return;
|
|
9074
|
+
const originalFailure = balanceFailures.get(entry.agent.id);
|
|
9075
|
+
if (originalFailure instanceof OwneyError && ![
|
|
9076
|
+
"AGENT_API_ERROR",
|
|
9077
|
+
"AGENT_TIMEOUT",
|
|
9078
|
+
"AGENT_RATE_LIMITED"
|
|
9079
|
+
].includes(originalFailure.code)) return;
|
|
9080
|
+
if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
|
|
9081
|
+
if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
|
|
9082
|
+
const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
|
|
9083
|
+
if (!total || (0, import_viem16.parseUnits)(total.amount, assetInfo.decimals) <= entry.balance) return;
|
|
9084
|
+
}
|
|
9085
|
+
try {
|
|
9086
|
+
const otherAvailable = indexedAvailable - entry.balance;
|
|
9087
|
+
const minimumAmount = requested > otherAvailable ? requested - otherAvailable : void 0;
|
|
9088
|
+
const verified = await entry.agent.getWithdrawableBalance(state, chainId, token, minimumAmount);
|
|
9089
|
+
if (typeof verified !== "bigint" || verified < 0n)
|
|
9090
|
+
throw new OwneyError("WITHDRAW_BALANCE_UNAVAILABLE", "Invalid withdrawal balance proof.", {}, entry.agent.id);
|
|
9091
|
+
if (this.state !== state || this.state.walletAddress !== state.walletAddress || this.state.chainId !== chainId)
|
|
9092
|
+
throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
|
|
9093
|
+
entry.balance = verified;
|
|
9094
|
+
const unavailableIndex = unavailableAgents.indexOf(entry.agent);
|
|
9095
|
+
if (unavailableIndex !== -1) unavailableAgents.splice(unavailableIndex, 1);
|
|
9096
|
+
if (aggregated.agentErrors) delete aggregated.agentErrors[entry.agent.id];
|
|
9097
|
+
} catch (error) {
|
|
9098
|
+
entry.balance = 0n;
|
|
9099
|
+
if (!unavailableAgents.includes(entry.agent)) unavailableAgents.push(entry.agent);
|
|
9100
|
+
aggregated.agentErrors = {
|
|
9101
|
+
...aggregated.agentErrors,
|
|
9102
|
+
[entry.agent.id]: error instanceof Error ? error.message : String(error)
|
|
9103
|
+
};
|
|
9104
|
+
}
|
|
9105
|
+
}));
|
|
9106
|
+
if (this.state !== state || this.state.chainId !== chainId)
|
|
9107
|
+
throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
|
|
9108
|
+
const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
|
|
8687
9109
|
const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
|
|
8688
9110
|
if (totalAvailable === 0n && unavailableAgents.length > 0) {
|
|
8689
9111
|
throw new OwneyError(
|
|
@@ -8768,7 +9190,18 @@ var OwneySDK = class {
|
|
|
8768
9190
|
);
|
|
8769
9191
|
}
|
|
8770
9192
|
const withdrawn = sumWithdrawnAmount(results);
|
|
8771
|
-
|
|
9193
|
+
const verifiedFeeDeductions = orderedPlans.reduce((total, plan) => {
|
|
9194
|
+
const result = results[plan.agent.id];
|
|
9195
|
+
const adjustment = result?.feeAdjustment;
|
|
9196
|
+
const evidence = result?.evidence;
|
|
9197
|
+
const submitted = submissionEvidence.get(plan.agent.id);
|
|
9198
|
+
if (plan.agent.id !== "yieldseeker" || !adjustment || !evidence || !submitted || typeof result.amount !== "string" || !/^\d+$/.test(result.amount) || typeof adjustment.grossAmount !== "string" || !/^\d+$/.test(adjustment.grossAmount) || typeof adjustment.feeAmount !== "string" || !/^\d+$/.test(adjustment.feeAmount) || evidence.agentId !== plan.agent.id || evidence.chainId !== chainId || evidence.token !== token || typeof evidence.walletAddress !== "string" || evidence.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || typeof evidence.agentWallet !== "string" || !/^0x[\da-f]{40}$/i.test(evidence.agentWallet) || typeof result.txHash !== "string" || !/^0x[\da-f]{64}$/i.test(result.txHash) || typeof evidence.txHash !== "string" || typeof submitted.txHash !== "string" || evidence.txHash?.toLowerCase() !== result.txHash.toLowerCase() || submitted.txHash?.toLowerCase() !== result.txHash.toLowerCase() || submitted.agentId !== evidence.agentId || submitted.chainId !== chainId || submitted.token !== token || typeof submitted.walletAddress !== "string" || typeof submitted.agentWallet !== "string" || submitted.walletAddress.toLowerCase() !== state.walletAddress.toLowerCase() || submitted.agentWallet?.toLowerCase() !== evidence.agentWallet.toLowerCase() || evidence.transfer !== "confirmed" || evidence.delivery !== "confirmed" || submitted.transfer !== "confirmed" || submitted.delivery !== "confirmed" || evidence.confirmedAmount !== result.amount || submitted.confirmedAmount !== result.amount || evidence.intendedAmount !== result.amount || submitted.intendedAmount !== result.amount) return total;
|
|
9199
|
+
const fee = BigInt(adjustment.feeAmount);
|
|
9200
|
+
const net = BigInt(result.amount);
|
|
9201
|
+
const gross = BigInt(adjustment.grossAmount);
|
|
9202
|
+
return fee > 0n && net > 0n && gross === plan.planned && net + fee === gross ? total + fee : total;
|
|
9203
|
+
}, 0n);
|
|
9204
|
+
if (withdrawn + verifiedFeeDeductions < requested) {
|
|
8772
9205
|
throw new OwneyError(
|
|
8773
9206
|
"WITHDRAW_PARTIAL_FAILURE",
|
|
8774
9207
|
`Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
|
|
@@ -8792,23 +9225,42 @@ var OwneySDK = class {
|
|
|
8792
9225
|
* @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
|
|
8793
9226
|
*/
|
|
8794
9227
|
async getBalances(agentId, options) {
|
|
8795
|
-
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
8796
|
-
const asset = options?.asset;
|
|
8797
|
-
const readBalance = (agent) => this.readAgent(
|
|
8798
|
-
agent,
|
|
8799
|
-
"balances",
|
|
8800
|
-
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
8801
|
-
asset ? { asset } : null
|
|
8802
|
-
);
|
|
8803
9228
|
if (agentId) {
|
|
9229
|
+
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
9230
|
+
const asset = options?.asset;
|
|
8804
9231
|
const agent = this.getAgent(agentId);
|
|
8805
|
-
const result = await
|
|
9232
|
+
const result = await this.readAgent(
|
|
9233
|
+
agent,
|
|
9234
|
+
"balances",
|
|
9235
|
+
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
9236
|
+
asset ? { asset } : null
|
|
9237
|
+
);
|
|
8806
9238
|
assertCurrent();
|
|
8807
9239
|
return {
|
|
8808
9240
|
...result,
|
|
8809
9241
|
balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
|
|
8810
9242
|
};
|
|
8811
9243
|
}
|
|
9244
|
+
const { balances, failures } = await this.readAggregatedBalances(options);
|
|
9245
|
+
if (Object.keys(balances.agentBalances).length === 0) {
|
|
9246
|
+
throw new OwneyError(
|
|
9247
|
+
"BALANCE_ALL_FAILED",
|
|
9248
|
+
"Failed to fetch balances for all active agents.",
|
|
9249
|
+
{ agentErrors: balances.agentErrors ?? {}, failures: [...failures.values()] }
|
|
9250
|
+
);
|
|
9251
|
+
}
|
|
9252
|
+
return balances;
|
|
9253
|
+
}
|
|
9254
|
+
/** Preserve failure objects for withdrawal recovery without changing public balance results. */
|
|
9255
|
+
async readAggregatedBalances(options) {
|
|
9256
|
+
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
9257
|
+
const asset = options?.asset;
|
|
9258
|
+
const readBalance = (agent) => this.readAgent(
|
|
9259
|
+
agent,
|
|
9260
|
+
"balances",
|
|
9261
|
+
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
9262
|
+
asset ? { asset } : null
|
|
9263
|
+
);
|
|
8812
9264
|
let totalBalance = 0;
|
|
8813
9265
|
const results = {};
|
|
8814
9266
|
const entries = [...this.getActiveAgents().entries()];
|
|
@@ -8825,39 +9277,33 @@ var OwneySDK = class {
|
|
|
8825
9277
|
})
|
|
8826
9278
|
);
|
|
8827
9279
|
assertCurrent();
|
|
8828
|
-
let successCount = 0;
|
|
8829
9280
|
const agentErrors = {};
|
|
8830
9281
|
const agentRetryAt = {};
|
|
8831
|
-
const agentFailures =
|
|
9282
|
+
const agentFailures = /* @__PURE__ */ new Map();
|
|
8832
9283
|
for (let i = 0; i < balanceResults.length; i++) {
|
|
8833
9284
|
const settledResult = balanceResults[i];
|
|
8834
|
-
const [
|
|
9285
|
+
const [agentId] = entries[i];
|
|
8835
9286
|
if (settledResult.status === "fulfilled") {
|
|
8836
9287
|
const [id, b] = settledResult.value;
|
|
8837
9288
|
results[id] = b;
|
|
8838
9289
|
totalBalance += Number(b.totalBalance);
|
|
8839
|
-
successCount += 1;
|
|
8840
9290
|
continue;
|
|
8841
9291
|
}
|
|
8842
9292
|
const reason = settledResult.reason;
|
|
8843
|
-
agentFailures.
|
|
9293
|
+
agentFailures.set(agentId, reason);
|
|
8844
9294
|
const retryDelay = rateLimitDelay(reason);
|
|
8845
|
-
if (retryDelay !== void 0) agentRetryAt[
|
|
8846
|
-
agentErrors[
|
|
8847
|
-
console.error(`[owney-sdk] Balance fetch failed for agent "${
|
|
8848
|
-
}
|
|
8849
|
-
if (successCount === 0) {
|
|
8850
|
-
throw new OwneyError(
|
|
8851
|
-
"BALANCE_ALL_FAILED",
|
|
8852
|
-
"Failed to fetch balances for all active agents.",
|
|
8853
|
-
{ agentErrors, failures: agentFailures }
|
|
8854
|
-
);
|
|
9295
|
+
if (retryDelay !== void 0) agentRetryAt[agentId] = Date.now() + retryDelay;
|
|
9296
|
+
agentErrors[agentId] = reason instanceof Error ? reason.message : String(reason);
|
|
9297
|
+
console.error(`[owney-sdk] Balance fetch failed for agent "${agentId}":`, reason);
|
|
8855
9298
|
}
|
|
8856
9299
|
return {
|
|
8857
|
-
|
|
8858
|
-
|
|
8859
|
-
|
|
8860
|
-
|
|
9300
|
+
balances: {
|
|
9301
|
+
totalBalance: String(totalBalance),
|
|
9302
|
+
totalBalanceAsset: "usdc",
|
|
9303
|
+
agentBalances: results,
|
|
9304
|
+
...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
|
|
9305
|
+
},
|
|
9306
|
+
failures: agentFailures
|
|
8861
9307
|
};
|
|
8862
9308
|
}
|
|
8863
9309
|
/**
|