@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.js
CHANGED
|
@@ -370,20 +370,44 @@ function observeWithdrawalSubmission(sdk, chainId, token) {
|
|
|
370
370
|
};
|
|
371
371
|
const notify = (outcome) => {
|
|
372
372
|
if (outcome === "rejected" && matching.length !== 1) return;
|
|
373
|
+
const evidence = matching.length === 1 ? { ...request2 } : {
|
|
374
|
+
attempted: request2.attempted,
|
|
375
|
+
method: request2.method,
|
|
376
|
+
path: request2.path,
|
|
377
|
+
...request2.elapsedMs !== void 0 ? { elapsedMs: request2.elapsedMs } : {}
|
|
378
|
+
};
|
|
373
379
|
for (const observer of matching) {
|
|
374
380
|
try {
|
|
375
|
-
observer.notify(outcome,
|
|
381
|
+
observer.notify(outcome, evidence);
|
|
376
382
|
} catch {
|
|
377
383
|
}
|
|
378
384
|
}
|
|
379
385
|
};
|
|
380
386
|
notify("attempted");
|
|
387
|
+
let callArgs = args;
|
|
388
|
+
if (withdrawal && client.client?.defaults) {
|
|
389
|
+
const configIndex = method === "get" ? 1 : 2;
|
|
390
|
+
const config = args[configIndex];
|
|
391
|
+
const transforms = config?.transformResponse !== void 0 ? config.transformResponse : client.client.defaults.transformResponse;
|
|
392
|
+
const captureStatus = (data, _headers, status) => {
|
|
393
|
+
if (typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599) request2.httpStatus = status;
|
|
394
|
+
return data;
|
|
395
|
+
};
|
|
396
|
+
callArgs = [...args];
|
|
397
|
+
callArgs[configIndex] = { ...config, transformResponse: [
|
|
398
|
+
...Array.isArray(transforms) ? transforms : typeof transforms === "function" ? [transforms] : [],
|
|
399
|
+
captureStatus
|
|
400
|
+
] };
|
|
401
|
+
}
|
|
381
402
|
try {
|
|
382
|
-
return await original.apply(this,
|
|
403
|
+
return await original.apply(this, callArgs);
|
|
383
404
|
} catch (error) {
|
|
384
405
|
const message = error instanceof Error ? error.message : "";
|
|
385
|
-
|
|
386
|
-
if (
|
|
406
|
+
const status = error?.response?.status;
|
|
407
|
+
if (request2.httpStatus === void 0 && typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599)
|
|
408
|
+
request2.httpStatus = status;
|
|
409
|
+
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";
|
|
410
|
+
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");
|
|
387
411
|
throw error;
|
|
388
412
|
} finally {
|
|
389
413
|
request2.elapsedMs = Math.max(0, Date.now() - startedAt);
|
|
@@ -1464,6 +1488,7 @@ var VIEM_CHAIN = { 8453: base, 42161: arbitrum, 1: mainnet };
|
|
|
1464
1488
|
var SPLITTING_ASSETS = ["USDC", "WETH"];
|
|
1465
1489
|
var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
|
|
1466
1490
|
var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
|
|
1491
|
+
var WITHDRAW_BALANCE_TIMEOUT_MS = 2e3;
|
|
1467
1492
|
var ZyfaiAgent = class _ZyfaiAgent {
|
|
1468
1493
|
id = "zyfai";
|
|
1469
1494
|
supportedChainIds = [8453, 42161, 1];
|
|
@@ -2378,9 +2403,74 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2378
2403
|
}
|
|
2379
2404
|
}
|
|
2380
2405
|
// --- IAgent: Fund operations ---
|
|
2406
|
+
/** Only after a definitive endpoint rejection, combine a new provider
|
|
2407
|
+
* snapshot with the Safe's exact idle balance. Never parse its rounded error
|
|
2408
|
+
* amount as proof of zero, or use this to skip sending a new MAX request. */
|
|
2409
|
+
async assertNotEmptyRejectedWithdrawal(state, chainId, token, owner) {
|
|
2410
|
+
const internals = this.sdk;
|
|
2411
|
+
const generation = this.earningsGeneration;
|
|
2412
|
+
let expired = false;
|
|
2413
|
+
const unavailable = () => new OwneyError(
|
|
2414
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
2415
|
+
"Could not verify Zyfai's balance after its withdrawal rejection.",
|
|
2416
|
+
{ chainId, token },
|
|
2417
|
+
this.id
|
|
2418
|
+
);
|
|
2419
|
+
const assertScope = () => {
|
|
2420
|
+
if (expired) throw unavailable();
|
|
2421
|
+
const account = internals.walletClient?.account?.address;
|
|
2422
|
+
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())
|
|
2423
|
+
throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
|
|
2424
|
+
};
|
|
2425
|
+
const read2 = async () => {
|
|
2426
|
+
assertScope();
|
|
2427
|
+
const [wallet, registered, portfolio] = await Promise.all([
|
|
2428
|
+
this.sdk.getSmartWalletAddress(owner, chainId),
|
|
2429
|
+
this.sdk.getSmartWalletByEOA(owner),
|
|
2430
|
+
this.sdk.getPortfolio(owner).catch(() => void 0)
|
|
2431
|
+
]);
|
|
2432
|
+
assertScope();
|
|
2433
|
+
if (!isAddress(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
|
|
2434
|
+
throw new OwneyError(
|
|
2435
|
+
"AGENT_AUTH_FAILED",
|
|
2436
|
+
"Could not verify Zyfai's withdrawal wallet.",
|
|
2437
|
+
{ reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
|
|
2438
|
+
this.id
|
|
2439
|
+
);
|
|
2440
|
+
if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
|
|
2441
|
+
const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
|
|
2442
|
+
const idle = asset ? await this.getPublicClient(chainId).readContract({
|
|
2443
|
+
address: asset.address,
|
|
2444
|
+
abi: erc20Abi2,
|
|
2445
|
+
functionName: "balanceOf",
|
|
2446
|
+
args: [wallet.address]
|
|
2447
|
+
}).catch(() => void 0) : void 0;
|
|
2448
|
+
assertScope();
|
|
2449
|
+
if (idle === 0n) throw new OwneyError(
|
|
2450
|
+
"WITHDRAW_INSUFFICIENT_BALANCE",
|
|
2451
|
+
"No funds remain in Zyfai for this asset.",
|
|
2452
|
+
{ available: "0", chainId, token },
|
|
2453
|
+
this.id
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
let timer;
|
|
2458
|
+
try {
|
|
2459
|
+
await Promise.race([read2(), new Promise((_, reject) => {
|
|
2460
|
+
timer = setTimeout(() => {
|
|
2461
|
+
expired = true;
|
|
2462
|
+
reject(unavailable());
|
|
2463
|
+
}, WITHDRAW_BALANCE_TIMEOUT_MS);
|
|
2464
|
+
})]);
|
|
2465
|
+
} finally {
|
|
2466
|
+
expired = true;
|
|
2467
|
+
if (timer) clearTimeout(timer);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2381
2470
|
/** Called only after the execution endpoint explicitly rejected a request.
|
|
2382
2471
|
* A timeout or an accepted request never authorizes repair and resubmission. */
|
|
2383
2472
|
async recoverWithdrawalSession(state, chainId, token, owner) {
|
|
2473
|
+
await this.assertNotEmptyRejectedWithdrawal(state, chainId, token, owner);
|
|
2384
2474
|
const internals = this.sdk;
|
|
2385
2475
|
const assertScope = () => {
|
|
2386
2476
|
const account = internals.walletClient?.account?.address;
|
|
@@ -2388,35 +2478,6 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2388
2478
|
throw new OwneyError("NOT_CONNECTED", "Wallet changed while preparing Zyfai withdrawal.", void 0, this.id);
|
|
2389
2479
|
};
|
|
2390
2480
|
assertScope();
|
|
2391
|
-
const [wallet, registered, portfolio] = await Promise.all([
|
|
2392
|
-
this.sdk.getSmartWalletAddress(owner, chainId),
|
|
2393
|
-
this.sdk.getSmartWalletByEOA(owner),
|
|
2394
|
-
this.sdk.getPortfolio(owner).catch(() => void 0)
|
|
2395
|
-
]);
|
|
2396
|
-
assertScope();
|
|
2397
|
-
if (!isAddress(wallet.address) || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase())
|
|
2398
|
-
throw new OwneyError(
|
|
2399
|
-
"AGENT_AUTH_FAILED",
|
|
2400
|
-
"Could not verify Zyfai's withdrawal wallet.",
|
|
2401
|
-
{ reason: "ZYFAI_SESSION_UNAVAILABLE", phase: "prepare", chainId, token },
|
|
2402
|
-
this.id
|
|
2403
|
-
);
|
|
2404
|
-
if (hasEmptyWithdrawalSnapshot(portfolio, chainId, token)) {
|
|
2405
|
-
const asset = SupportedAssets.find((value) => value.chainId === chainId && value.symbol === token);
|
|
2406
|
-
const idle = asset ? await this.getPublicClient(chainId).readContract({
|
|
2407
|
-
address: asset.address,
|
|
2408
|
-
abi: erc20Abi2,
|
|
2409
|
-
functionName: "balanceOf",
|
|
2410
|
-
args: [wallet.address]
|
|
2411
|
-
}).catch(() => void 0) : void 0;
|
|
2412
|
-
assertScope();
|
|
2413
|
-
if (idle === 0n) throw new OwneyError(
|
|
2414
|
-
"WITHDRAW_INSUFFICIENT_BALANCE",
|
|
2415
|
-
"No funds remain in Zyfai for this asset.",
|
|
2416
|
-
{ available: "0", chainId, token },
|
|
2417
|
-
this.id
|
|
2418
|
-
);
|
|
2419
|
-
}
|
|
2420
2481
|
const walletState = await this.readWalletState(owner);
|
|
2421
2482
|
assertScope();
|
|
2422
2483
|
const unavailable = () => new OwneyError(
|
|
@@ -2468,6 +2529,13 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2468
2529
|
raw2 = await this.sdk.withdrawFunds(owner, validChainId, amount, token);
|
|
2469
2530
|
break;
|
|
2470
2531
|
} catch (error) {
|
|
2532
|
+
if (!amount && observation.rejected() && observation.request()?.failure === "insufficient-balance") {
|
|
2533
|
+
try {
|
|
2534
|
+
await this.assertNotEmptyRejectedWithdrawal(state, validChainId, token, owner);
|
|
2535
|
+
} catch (proofError) {
|
|
2536
|
+
if (proofError instanceof OwneyError && (proofError.code === "WITHDRAW_INSUFFICIENT_BALANCE" || proofError.code === "NOT_CONNECTED")) throw proofError;
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2471
2539
|
if (!observation.rejected() || observation.request()?.failure !== "session-key-missing") throw error;
|
|
2472
2540
|
if (attempt === 1)
|
|
2473
2541
|
throw new OwneyError(
|
|
@@ -2561,6 +2629,67 @@ var ZyfaiAgent = class _ZyfaiAgent {
|
|
|
2561
2629
|
async getFundingWallet(state, chainId) {
|
|
2562
2630
|
return (await this.sdk.getSmartWalletAddress(state.walletAddress, isValidChainId(chainId))).address;
|
|
2563
2631
|
}
|
|
2632
|
+
/** A positive Safe balance is a withdrawal planning floor when the portfolio
|
|
2633
|
+
* index reports zero. Idle zero cannot prove that Zyfai's protocol positions
|
|
2634
|
+
* are empty: its SDK does not expose a complete contract catalog for them. */
|
|
2635
|
+
async getWithdrawableBalance(state, chainId, token) {
|
|
2636
|
+
const validChainId = isValidChainId(chainId);
|
|
2637
|
+
const asset = SupportedAssets.find((value) => value.chainId === validChainId && value.symbol === token);
|
|
2638
|
+
if (!asset) throw new OwneyError("WITHDRAW_NO_PERMITTED_TOKENS", "Asset selected is not supported", void 0, this.id);
|
|
2639
|
+
const owner = state.walletAddress;
|
|
2640
|
+
const generation = this.earningsGeneration;
|
|
2641
|
+
const internals = this.sdk;
|
|
2642
|
+
let expired = false;
|
|
2643
|
+
const unavailable = () => new OwneyError(
|
|
2644
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
2645
|
+
"Could not verify Zyfai's withdrawal balance. Its vault positions may still contain funds.",
|
|
2646
|
+
{ reason: "ZYFAI_VAULT_BALANCE_UNVERIFIED", chainId, token },
|
|
2647
|
+
this.id
|
|
2648
|
+
);
|
|
2649
|
+
const assertScope = () => {
|
|
2650
|
+
if (expired) throw unavailable();
|
|
2651
|
+
const account = internals.walletClient?.account?.address;
|
|
2652
|
+
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())
|
|
2653
|
+
throw new OwneyError("NOT_CONNECTED", "Wallet changed while checking Zyfai's withdrawal balance.", void 0, this.id);
|
|
2654
|
+
};
|
|
2655
|
+
let timer;
|
|
2656
|
+
const read2 = async () => {
|
|
2657
|
+
assertScope();
|
|
2658
|
+
const [wallet, registered] = await Promise.all([
|
|
2659
|
+
this.sdk.getSmartWalletAddress(owner, validChainId),
|
|
2660
|
+
this.sdk.getSmartWalletByEOA(owner)
|
|
2661
|
+
]);
|
|
2662
|
+
assertScope();
|
|
2663
|
+
if (!isAddress(wallet.address) || wallet.isDeployed !== true || wallet.isOwner !== true || !registered.smartWallet || registered.smartWallet.toLowerCase() !== wallet.address.toLowerCase()) throw unavailable();
|
|
2664
|
+
const client = this.getPublicClient(validChainId);
|
|
2665
|
+
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
|
|
2666
|
+
assertScope();
|
|
2667
|
+
const idle = await client.readContract({
|
|
2668
|
+
address: asset.address,
|
|
2669
|
+
abi: erc20Abi2,
|
|
2670
|
+
functionName: "balanceOf",
|
|
2671
|
+
args: [wallet.address],
|
|
2672
|
+
blockNumber
|
|
2673
|
+
});
|
|
2674
|
+
assertScope();
|
|
2675
|
+
if (typeof idle !== "bigint" || idle <= 0n) throw unavailable();
|
|
2676
|
+
return idle;
|
|
2677
|
+
};
|
|
2678
|
+
try {
|
|
2679
|
+
return await Promise.race([read2(), new Promise((_, reject) => {
|
|
2680
|
+
timer = setTimeout(() => {
|
|
2681
|
+
expired = true;
|
|
2682
|
+
reject(unavailable());
|
|
2683
|
+
}, WITHDRAW_BALANCE_TIMEOUT_MS);
|
|
2684
|
+
})]);
|
|
2685
|
+
} catch (error) {
|
|
2686
|
+
if (error instanceof OwneyError) throw error;
|
|
2687
|
+
throw unavailable();
|
|
2688
|
+
} finally {
|
|
2689
|
+
expired = true;
|
|
2690
|
+
if (timer) clearTimeout(timer);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2564
2693
|
async getBalances(state, chainId) {
|
|
2565
2694
|
const requestedAt = Date.now();
|
|
2566
2695
|
const { smartWallet, chainId: validChainId } = await this.resolveSmartWallet(state, chainId);
|
|
@@ -4573,6 +4702,8 @@ var YIELDSEEKER_SETTLEMENT_WINDOW_MS = 5 * 6e4;
|
|
|
4573
4702
|
var YIELDSEEKER_ACTIVITY_CACHE_MS = 6e4;
|
|
4574
4703
|
var YIELDSEEKER_MAX_WITHDRAW_SWEEPS = 3;
|
|
4575
4704
|
var YIELDSEEKER_MAX_POSITION_VERIFICATION_MS = 2e3;
|
|
4705
|
+
var YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT = 64;
|
|
4706
|
+
var YIELDSEEKER_VAULTS_PER_RPC_BATCH = 10;
|
|
4576
4707
|
function generateYieldseekerUsername() {
|
|
4577
4708
|
const suffix = globalThis.crypto.randomUUID().replaceAll("-", "").slice(0, YIELDSEEKER_USERNAME_RANDOM_LENGTH).toLowerCase();
|
|
4578
4709
|
return `${YIELDSEEKER_USERNAME_PREFIX}${suffix}`;
|
|
@@ -5096,17 +5227,38 @@ var YieldseekerAgent = class {
|
|
|
5096
5227
|
let settlement = null;
|
|
5097
5228
|
try {
|
|
5098
5229
|
const maxSettlementBasis = amount === void 0 ? this.snapshotMovement(state, chainId, context, 0n) : null;
|
|
5099
|
-
this.
|
|
5100
|
-
const
|
|
5230
|
+
const contextKey = this.contextKey(state, chainId, asset);
|
|
5231
|
+
const priorPositions = this.withdrawalVaultHints(state, chainId, context);
|
|
5232
|
+
this.advancePortfolioVersion(contextKey);
|
|
5233
|
+
const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5234
|
+
let timer;
|
|
5235
|
+
try {
|
|
5236
|
+
return await Promise.race([read2, new Promise((resolve) => {
|
|
5237
|
+
timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
|
|
5238
|
+
})]);
|
|
5239
|
+
} finally {
|
|
5240
|
+
if (timer) clearTimeout(timer);
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
const feeRateRead = amount === void 0 ? Promise.resolve(0) : boundedVerification(
|
|
5244
|
+
(() => {
|
|
5245
|
+
try {
|
|
5246
|
+
return this.portfolioSnapshot(state, chainId, context, contextKey).read.then((value) => value.agentSnapshot.feeRateBps);
|
|
5247
|
+
} catch (error) {
|
|
5248
|
+
return Promise.reject(error);
|
|
5249
|
+
}
|
|
5250
|
+
})().catch(() => void 0)
|
|
5251
|
+
);
|
|
5252
|
+
const positionsRead = this.readWithdrawalPositions(state, chainId, context);
|
|
5101
5253
|
const metadata = YIELDSEEKER_ASSET_METADATA[asset];
|
|
5102
5254
|
const agentWallet = getAddress4(context.wallet.walletAddress);
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5255
|
+
const readOwnerFunds = async (blockNumber) => {
|
|
5256
|
+
const feeTracker = await withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
|
|
5257
|
+
address: agentWallet,
|
|
5258
|
+
abi: YIELDSEEKER_AGENT_WALLET_ABI,
|
|
5259
|
+
functionName: "feeTracker",
|
|
5260
|
+
blockNumber
|
|
5261
|
+
}), chainId, "eth_call", this.id);
|
|
5110
5262
|
this.assertSession(generation, chainId, asset);
|
|
5111
5263
|
if (!isAddress3(feeTracker) || /^0x0{40}$/i.test(feeTracker))
|
|
5112
5264
|
throw this.invalidResponse("wallet fee tracker");
|
|
@@ -5115,61 +5267,58 @@ var YieldseekerAgent = class {
|
|
|
5115
5267
|
address: metadata.address,
|
|
5116
5268
|
abi: erc20Abi3,
|
|
5117
5269
|
functionName: "balanceOf",
|
|
5118
|
-
args: [agentWallet]
|
|
5270
|
+
args: [agentWallet],
|
|
5271
|
+
blockNumber
|
|
5119
5272
|
}), chainId, "eth_call", this.id),
|
|
5120
5273
|
withPaidRpcDiagnostics(() => this.getReceiptClient().readContract({
|
|
5121
5274
|
address: getAddress4(feeTracker),
|
|
5122
5275
|
abi: YIELDSEEKER_FEE_TRACKER_ABI,
|
|
5123
5276
|
functionName: "getFeesOwed",
|
|
5124
|
-
args: [agentWallet]
|
|
5277
|
+
args: [agentWallet],
|
|
5278
|
+
blockNumber
|
|
5125
5279
|
}), chainId, "eth_call", this.id)
|
|
5126
5280
|
]);
|
|
5127
5281
|
this.assertSession(generation, chainId, asset);
|
|
5128
5282
|
if (typeof idle2 !== "bigint" || idle2 < 0n || typeof fees !== "bigint" || fees < 0n)
|
|
5129
5283
|
throw this.invalidResponse("wallet withdrawal balance");
|
|
5130
|
-
return { idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
|
|
5284
|
+
return { blockNumber, idle: idle2, fees, available: idle2 > fees ? idle2 - fees : 0n };
|
|
5131
5285
|
};
|
|
5132
|
-
const [
|
|
5286
|
+
const [funds, feeRateBps] = await Promise.all([
|
|
5287
|
+
positionsRead.then((positions2) => this.resolveWithdrawalLiquidity(
|
|
5288
|
+
state,
|
|
5289
|
+
chainId,
|
|
5290
|
+
context,
|
|
5291
|
+
positions2,
|
|
5292
|
+
priorPositions,
|
|
5293
|
+
amount === void 0 ? void 0 : BigInt(amount)
|
|
5294
|
+
)),
|
|
5295
|
+
feeRateRead
|
|
5296
|
+
]);
|
|
5297
|
+
const initialFunds = funds;
|
|
5133
5298
|
const idle = initialFunds.idle;
|
|
5134
|
-
|
|
5299
|
+
const maxLiquidity = amount === void 0 ? funds : void 0;
|
|
5300
|
+
let positions = [...funds.positions, ...funds.indexedPositions.filter((position2) => !funds.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()))];
|
|
5135
5301
|
let positionsStatus = "empty";
|
|
5136
|
-
const validatePositions = (values) =>
|
|
5137
|
-
|
|
5138
|
-
for (const value of values) {
|
|
5139
|
-
const address = value?.yieldOption?.address;
|
|
5140
|
-
if (typeof address !== "string" || !isAddress3(address) || seen.has(address.toLowerCase()) || typeof value.assetsRaw !== "string" || !/^\d+$/.test(value.assetsRaw) || typeof value.withdrawableAssetsRaw !== "string" || !/^\d+$/.test(value.withdrawableAssetsRaw))
|
|
5141
|
-
throw this.invalidResponse("yield positions");
|
|
5142
|
-
seen.add(address.toLowerCase());
|
|
5143
|
-
}
|
|
5144
|
-
return values;
|
|
5145
|
-
};
|
|
5302
|
+
const validatePositions = (values) => this.validateWithdrawalPositions(values, chainId, asset);
|
|
5303
|
+
validatePositions(positions);
|
|
5146
5304
|
const zeroValuePositions = /* @__PURE__ */ new WeakSet();
|
|
5147
5305
|
const hasPositions = (values) => values.some((position2) => !zeroValuePositions.has(position2) && (BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n));
|
|
5148
5306
|
const knownPositions = /* @__PURE__ */ new Map();
|
|
5149
5307
|
const rememberPositions = (values) => {
|
|
5150
5308
|
for (const position2 of values) knownPositions.set(position2.yieldOption.address.toLowerCase(), position2);
|
|
5151
5309
|
};
|
|
5152
|
-
const boundedVerification = async (read2, timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5153
|
-
let timer;
|
|
5154
|
-
try {
|
|
5155
|
-
return await Promise.race([read2, new Promise((resolve) => {
|
|
5156
|
-
timer = setTimeout(() => resolve(void 0), Math.max(0, timeoutMs));
|
|
5157
|
-
})]);
|
|
5158
|
-
} finally {
|
|
5159
|
-
if (timer) clearTimeout(timer);
|
|
5160
|
-
}
|
|
5161
|
-
};
|
|
5162
5310
|
const verifyKnownPositions = async (blockNumber, values = [...knownPositions.values()], timeoutMs = YIELDSEEKER_MAX_POSITION_VERIFICATION_MS) => {
|
|
5163
5311
|
const verifiedPositions = [];
|
|
5164
5312
|
if (timeoutMs <= 0) return { verified: false, positions: verifiedPositions };
|
|
5165
|
-
|
|
5313
|
+
let active = true;
|
|
5314
|
+
const checks = this.readVaultLiquidity(values.map((position2) => position2.yieldOption), agentWallet, blockNumber).catch(() => []);
|
|
5315
|
+
const reads = values.map(async (position2, index) => {
|
|
5166
5316
|
const address = getAddress4(position2.yieldOption.address);
|
|
5167
5317
|
try {
|
|
5168
|
-
const
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
]);
|
|
5318
|
+
const group = (await checks).slice(index * 3, index * 3 + 3);
|
|
5319
|
+
if (!active || group.length !== 3 || group.some((value) => value.status !== "success")) return null;
|
|
5320
|
+
this.assertSession(generation, chainId, asset);
|
|
5321
|
+
const [underlying, shares, withdrawable] = group.map((value) => value.result);
|
|
5173
5322
|
if (typeof underlying !== "string" || !isAddress3(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n) return null;
|
|
5174
5323
|
const verified = { ...position2, assetsRaw: shares.toString(), withdrawableAssetsRaw: withdrawable.toString() };
|
|
5175
5324
|
if (shares > 0n && withdrawable === 0n) {
|
|
@@ -5204,6 +5353,7 @@ var YieldseekerAgent = class {
|
|
|
5204
5353
|
}
|
|
5205
5354
|
});
|
|
5206
5355
|
const result = await boundedVerification(Promise.all(reads), timeoutMs);
|
|
5356
|
+
active = false;
|
|
5207
5357
|
this.assertSession(generation, chainId, asset);
|
|
5208
5358
|
return {
|
|
5209
5359
|
verified: result !== void 0 && result.every((position2) => position2 !== null),
|
|
@@ -5222,7 +5372,7 @@ var YieldseekerAgent = class {
|
|
|
5222
5372
|
return validatePositions(response.yieldPositions);
|
|
5223
5373
|
};
|
|
5224
5374
|
if (amount === void 0) {
|
|
5225
|
-
|
|
5375
|
+
rememberPositions(funds.indexedPositions);
|
|
5226
5376
|
rememberPositions(positions);
|
|
5227
5377
|
positionsStatus = hasPositions(positions) ? "remaining" : "empty";
|
|
5228
5378
|
}
|
|
@@ -5238,8 +5388,8 @@ var YieldseekerAgent = class {
|
|
|
5238
5388
|
const zeroPayoutCandidates = positions.filter((position2) => BigInt(position2.assetsRaw) > 0n || BigInt(position2.withdrawableAssetsRaw) > 0n);
|
|
5239
5389
|
if (amount === void 0 && zeroPayoutCandidates.length) {
|
|
5240
5390
|
const deadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
|
|
5241
|
-
const blockNumber = await boundedVerification(
|
|
5242
|
-
Promise.resolve().then(() => this.getReceiptClient().getBlockNumber()).catch(() => void 0)
|
|
5391
|
+
const blockNumber = initialFunds.blockNumber ?? await boundedVerification(
|
|
5392
|
+
Promise.resolve().then(() => this.getReceiptClient().getBlockNumber({ cacheTime: 0 })).catch(() => void 0)
|
|
5243
5393
|
);
|
|
5244
5394
|
const proof = typeof blockNumber === "bigint" ? await verifyKnownPositions(blockNumber, zeroPayoutCandidates, deadline - Date.now()) : { verified: false, positions: [] };
|
|
5245
5395
|
positions = proof.positions;
|
|
@@ -5363,8 +5513,9 @@ var YieldseekerAgent = class {
|
|
|
5363
5513
|
remaining: remaining.toString()
|
|
5364
5514
|
});
|
|
5365
5515
|
}
|
|
5366
|
-
const
|
|
5367
|
-
const
|
|
5516
|
+
const ownerFunds = confirmedMovement ? await readOwnerFunds(latestUnwindBlock) : initialFunds;
|
|
5517
|
+
const availableForOwner = ownerFunds.available;
|
|
5518
|
+
const feeRate = feeRateBps ?? 0;
|
|
5368
5519
|
const maxFee = confirmedMovement && Number.isInteger(feeRate) && feeRate >= 0 && feeRate <= 1e3 ? (grossUnwound * BigInt(feeRate) + 9999n) / 10000n : 0n;
|
|
5369
5520
|
const shortfall = availableForOwner < requested ? requested - availableForOwner : 0n;
|
|
5370
5521
|
if (availableForOwner === 0n || amount !== void 0 && shortfall > maxFee) {
|
|
@@ -5439,7 +5590,7 @@ var YieldseekerAgent = class {
|
|
|
5439
5590
|
settlement = plannedMovement;
|
|
5440
5591
|
const evidence = receipt ? inspectFundingReceipt(receipt, { ...base6, txHash }, metadata.address, "withdraw") : { ...base6, txHash, confirmedAmount: payout.toString(), transfer: "confirmed", delivery: "confirmed" };
|
|
5441
5592
|
notifyFunding(onProgress, evidence);
|
|
5442
|
-
let completion;
|
|
5593
|
+
let completion = maxLiquidity && !maxLiquidity.complete ? "unverified" : void 0;
|
|
5443
5594
|
if (amount === void 0) {
|
|
5444
5595
|
const verificationDeadline = Date.now() + YIELDSEEKER_MAX_POSITION_VERIFICATION_MS;
|
|
5445
5596
|
const knownAtPayout = new Set(knownPositions.keys());
|
|
@@ -5486,10 +5637,14 @@ var YieldseekerAgent = class {
|
|
|
5486
5637
|
{ asset, available: "0", txHash },
|
|
5487
5638
|
this.id
|
|
5488
5639
|
);
|
|
5640
|
+
const confirmedAmount = evidence.confirmedAmount && /^\d+$/.test(evidence.confirmedAmount) ? BigInt(evidence.confirmedAmount) : void 0;
|
|
5641
|
+
const feeIncrease = ownerFunds.fees - initialFunds.fees;
|
|
5642
|
+
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;
|
|
5489
5643
|
return {
|
|
5490
5644
|
txHash,
|
|
5491
5645
|
type: amount === void 0 && !completion ? "full" : "partial",
|
|
5492
5646
|
...completion ? { completion } : {},
|
|
5647
|
+
...feeAdjustment ? { feeAdjustment } : {},
|
|
5493
5648
|
amount: evidence.confirmedAmount ?? payout.toString(),
|
|
5494
5649
|
evidence
|
|
5495
5650
|
};
|
|
@@ -5505,6 +5660,236 @@ var YieldseekerAgent = class {
|
|
|
5505
5660
|
}
|
|
5506
5661
|
}
|
|
5507
5662
|
}
|
|
5663
|
+
/** A withdrawal-only liquidity floor; display snapshots never authorize it. */
|
|
5664
|
+
async getWithdrawableBalance(state, chainId, asset, minimumAmount) {
|
|
5665
|
+
this.assertChain(chainId);
|
|
5666
|
+
this.assertAsset(asset);
|
|
5667
|
+
const generation = this.readGeneration;
|
|
5668
|
+
const context = await this.findAgent(state, chainId, asset);
|
|
5669
|
+
this.assertSession(generation, chainId, asset);
|
|
5670
|
+
if (!context) throw new OwneyError(
|
|
5671
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5672
|
+
"The Yieldseeker withdrawal wallet could not be verified.",
|
|
5673
|
+
{ asset },
|
|
5674
|
+
this.id
|
|
5675
|
+
);
|
|
5676
|
+
const hints = this.withdrawalVaultHints(state, chainId, context);
|
|
5677
|
+
const positions = await this.readWithdrawalPositions(state, chainId, context);
|
|
5678
|
+
const proof = await this.resolveWithdrawalLiquidity(state, chainId, context, positions, hints, minimumAmount);
|
|
5679
|
+
this.assertSession(generation, chainId, asset);
|
|
5680
|
+
if (proof.available === 0n && !proof.complete) throw new OwneyError(
|
|
5681
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5682
|
+
"Yieldseeker withdrawal liquidity could not be verified yet.",
|
|
5683
|
+
{ asset },
|
|
5684
|
+
this.id
|
|
5685
|
+
);
|
|
5686
|
+
return proof.available;
|
|
5687
|
+
}
|
|
5688
|
+
async readVaultLiquidity(options, wallet, blockNumber) {
|
|
5689
|
+
const owner = getAddress4(wallet);
|
|
5690
|
+
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));
|
|
5691
|
+
const results = await Promise.all(batches.map((batch) => this.getReceiptClient().multicall({
|
|
5692
|
+
blockNumber,
|
|
5693
|
+
allowFailure: true,
|
|
5694
|
+
contracts: batch.flatMap((option) => {
|
|
5695
|
+
const address = getAddress4(option.address);
|
|
5696
|
+
return [
|
|
5697
|
+
{ address, abi: erc4626Abi, functionName: "asset" },
|
|
5698
|
+
{ address, abi: erc20Abi3, functionName: "balanceOf", args: [owner] },
|
|
5699
|
+
{ address, abi: erc4626Abi, functionName: "maxWithdraw", args: [owner] }
|
|
5700
|
+
];
|
|
5701
|
+
})
|
|
5702
|
+
})));
|
|
5703
|
+
return results.flat();
|
|
5704
|
+
}
|
|
5705
|
+
validateWithdrawalPositions(values, chainId, asset) {
|
|
5706
|
+
const underlying = YIELDSEEKER_ASSET_METADATA[asset].address.toLowerCase();
|
|
5707
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5708
|
+
for (const value of values) {
|
|
5709
|
+
const option = value?.yieldOption;
|
|
5710
|
+
if (!option || typeof option.address !== "string" || !isAddress3(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))
|
|
5711
|
+
throw this.invalidResponse("yield positions");
|
|
5712
|
+
seen.add(option.address.toLowerCase());
|
|
5713
|
+
}
|
|
5714
|
+
return values;
|
|
5715
|
+
}
|
|
5716
|
+
withdrawalVaultHints(state, chainId, context) {
|
|
5717
|
+
const key2 = this.contextKey(state, chainId, context.asset);
|
|
5718
|
+
const positions = this.readCache.get(`positions:${key2}`)?.value?.yieldPositions ?? [];
|
|
5719
|
+
const hints = new Map(this.validateWithdrawalPositions(positions, chainId, context.asset).map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
|
|
5720
|
+
const historic = this.readCache.get(`historic:${key2}`)?.value?.position;
|
|
5721
|
+
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()));
|
|
5722
|
+
for (const option of this.yieldOptions.get(context.asset)?.value ?? []) {
|
|
5723
|
+
if (option.chainId !== chainId || typeof option.assetAddress !== "string" || option.assetAddress.toLowerCase() !== YIELDSEEKER_ASSET_METADATA[context.asset].address.toLowerCase() || !isAddress3(option.address) || !receivedAssets.has(option.address.toLowerCase()) || hints.has(option.address.toLowerCase())) continue;
|
|
5724
|
+
hints.set(option.address.toLowerCase(), {
|
|
5725
|
+
yieldOption: option,
|
|
5726
|
+
assetsRaw: "0",
|
|
5727
|
+
assetsBase: "0",
|
|
5728
|
+
assetsUsd: 0,
|
|
5729
|
+
withdrawableAssetsRaw: "0",
|
|
5730
|
+
withdrawableAssetsBase: "0"
|
|
5731
|
+
});
|
|
5732
|
+
}
|
|
5733
|
+
return [...hints.values()];
|
|
5734
|
+
}
|
|
5735
|
+
async readWithdrawalPositions(state, chainId, context) {
|
|
5736
|
+
const generation = this.readGeneration;
|
|
5737
|
+
let timer;
|
|
5738
|
+
try {
|
|
5739
|
+
const positions = await Promise.race([
|
|
5740
|
+
this.loadActivityContext(state, chainId, context, {}).then((value) => value.positions).catch((error) => {
|
|
5741
|
+
if (!(error instanceof OwneyError) || !["AGENT_API_ERROR", "AGENT_TIMEOUT"].includes(error.code)) throw error;
|
|
5742
|
+
return void 0;
|
|
5743
|
+
}),
|
|
5744
|
+
new Promise((resolve) => {
|
|
5745
|
+
timer = setTimeout(() => resolve(void 0), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
|
|
5746
|
+
})
|
|
5747
|
+
]);
|
|
5748
|
+
this.assertSession(generation, chainId, context.asset);
|
|
5749
|
+
return positions === void 0 ? void 0 : this.validateWithdrawalPositions(positions, chainId, context.asset);
|
|
5750
|
+
} finally {
|
|
5751
|
+
if (timer) clearTimeout(timer);
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
async resolveWithdrawalLiquidity(state, chainId, context, positions, previousPositions = [], requested) {
|
|
5755
|
+
const generation = this.readGeneration;
|
|
5756
|
+
const contextKey = this.contextKey(state, chainId, context.asset);
|
|
5757
|
+
const version = this.portfolioVersion(contextKey);
|
|
5758
|
+
const metadata = YIELDSEEKER_ASSET_METADATA[context.asset];
|
|
5759
|
+
const unavailable = () => new OwneyError(
|
|
5760
|
+
"WITHDRAW_BALANCE_UNAVAILABLE",
|
|
5761
|
+
"Yieldseeker withdrawal liquidity could not be verified yet.",
|
|
5762
|
+
{ asset: context.asset, reason: "YIELDSEEKER_LIQUIDITY_UNVERIFIED" },
|
|
5763
|
+
this.id
|
|
5764
|
+
);
|
|
5765
|
+
let active = true;
|
|
5766
|
+
let knownProof;
|
|
5767
|
+
const hasUnverifiedIndexedFunds = (proof) => proof.indexedPositions.some((position2) => !proof.verifiedVaults.includes(position2.yieldOption.address.toLowerCase()) && BigInt(position2.withdrawableAssetsRaw) > 0n);
|
|
5768
|
+
const assertReadCurrent = () => {
|
|
5769
|
+
this.assertSession(generation, chainId, context.asset);
|
|
5770
|
+
if (!active || this.portfolioVersion(contextKey) !== version) throw unavailable();
|
|
5771
|
+
};
|
|
5772
|
+
const read2 = async () => {
|
|
5773
|
+
const client = this.getReceiptClient();
|
|
5774
|
+
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
|
|
5775
|
+
const wallet = getAddress4(context.wallet.walletAddress);
|
|
5776
|
+
assertReadCurrent();
|
|
5777
|
+
if (typeof blockNumber !== "bigint") throw unavailable();
|
|
5778
|
+
const indexedPositions = this.validateWithdrawalPositions(positions ?? [], chainId, context.asset);
|
|
5779
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
5780
|
+
for (const position2 of this.validateWithdrawalPositions([...previousPositions], chainId, context.asset))
|
|
5781
|
+
candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
|
|
5782
|
+
for (const position2 of indexedPositions) candidates.set(position2.yieldOption.address.toLowerCase(), position2.yieldOption);
|
|
5783
|
+
const options = [...candidates.values()];
|
|
5784
|
+
if (options.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
|
|
5785
|
+
const [checks, idle, feeTracker] = await Promise.all([
|
|
5786
|
+
this.readVaultLiquidity(options, wallet, blockNumber),
|
|
5787
|
+
client.readContract({
|
|
5788
|
+
address: metadata.address,
|
|
5789
|
+
abi: erc20Abi3,
|
|
5790
|
+
functionName: "balanceOf",
|
|
5791
|
+
args: [wallet],
|
|
5792
|
+
blockNumber
|
|
5793
|
+
}),
|
|
5794
|
+
client.readContract({
|
|
5795
|
+
address: wallet,
|
|
5796
|
+
abi: YIELDSEEKER_AGENT_WALLET_ABI,
|
|
5797
|
+
functionName: "feeTracker",
|
|
5798
|
+
blockNumber
|
|
5799
|
+
})
|
|
5800
|
+
]);
|
|
5801
|
+
if (typeof idle !== "bigint" || idle < 0n || !isAddress3(feeTracker) || /^0x0{40}$/i.test(feeTracker)) throw unavailable();
|
|
5802
|
+
assertReadCurrent();
|
|
5803
|
+
const fees = await client.readContract({
|
|
5804
|
+
address: getAddress4(feeTracker),
|
|
5805
|
+
abi: YIELDSEEKER_FEE_TRACKER_ABI,
|
|
5806
|
+
functionName: "getFeesOwed",
|
|
5807
|
+
args: [wallet],
|
|
5808
|
+
blockNumber
|
|
5809
|
+
});
|
|
5810
|
+
if (typeof fees !== "bigint" || fees < 0n) throw unavailable();
|
|
5811
|
+
const verified = [];
|
|
5812
|
+
const verifiedVaults = [];
|
|
5813
|
+
let deployed = 0n;
|
|
5814
|
+
const collect = (options2, results) => {
|
|
5815
|
+
let valid2 = true;
|
|
5816
|
+
options2.forEach((option, index) => {
|
|
5817
|
+
const group = results.slice(index * 3, index * 3 + 3);
|
|
5818
|
+
if (group.length !== 3 || group.some((value) => value.status !== "success")) {
|
|
5819
|
+
valid2 = false;
|
|
5820
|
+
return;
|
|
5821
|
+
}
|
|
5822
|
+
const [underlying, shares, withdrawable] = group.map((value) => value.result);
|
|
5823
|
+
if (typeof underlying !== "string" || !isAddress3(underlying) || underlying.toLowerCase() !== metadata.address.toLowerCase() || typeof shares !== "bigint" || shares < 0n || typeof withdrawable !== "bigint" || withdrawable < 0n || shares === 0n && withdrawable > 0n) {
|
|
5824
|
+
valid2 = false;
|
|
5825
|
+
return;
|
|
5826
|
+
}
|
|
5827
|
+
verifiedVaults.push(option.address.toLowerCase());
|
|
5828
|
+
deployed += withdrawable;
|
|
5829
|
+
verified.push({
|
|
5830
|
+
yieldOption: option,
|
|
5831
|
+
assetsRaw: shares.toString(),
|
|
5832
|
+
assetsBase: "0",
|
|
5833
|
+
assetsUsd: 0,
|
|
5834
|
+
withdrawableAssetsRaw: withdrawable.toString(),
|
|
5835
|
+
withdrawableAssetsBase: withdrawable.toString()
|
|
5836
|
+
});
|
|
5837
|
+
});
|
|
5838
|
+
return valid2;
|
|
5839
|
+
};
|
|
5840
|
+
const allKnownVerified = collect(options, checks);
|
|
5841
|
+
const proof = (complete) => ({
|
|
5842
|
+
blockNumber,
|
|
5843
|
+
idle,
|
|
5844
|
+
fees,
|
|
5845
|
+
available: idle + deployed > fees ? idle + deployed - fees : 0n,
|
|
5846
|
+
positions: [...verified],
|
|
5847
|
+
verifiedVaults: [...verifiedVaults],
|
|
5848
|
+
indexedPositions,
|
|
5849
|
+
complete
|
|
5850
|
+
});
|
|
5851
|
+
knownProof = proof(false);
|
|
5852
|
+
const indexed = new Map(indexedPositions.map((position2) => [position2.yieldOption.address.toLowerCase(), position2]));
|
|
5853
|
+
const discrepancy = verified.some((position2) => {
|
|
5854
|
+
const row = indexed.get(position2.yieldOption.address.toLowerCase());
|
|
5855
|
+
return row ? BigInt(position2.withdrawableAssetsRaw) < BigInt(row.withdrawableAssetsRaw) : BigInt(position2.assetsRaw) > 0n;
|
|
5856
|
+
});
|
|
5857
|
+
const typedCoveredByIdle = requested !== void 0 && idle >= requested + fees;
|
|
5858
|
+
const expand = positions === void 0 || indexedPositions.length === 0 && !typedCoveredByIdle || !allKnownVerified || discrepancy || knownProof.available === 0n || requested !== void 0 && requested > knownProof.available;
|
|
5859
|
+
if (!expand) {
|
|
5860
|
+
assertReadCurrent();
|
|
5861
|
+
return proof(true);
|
|
5862
|
+
}
|
|
5863
|
+
const catalog = await this.loadYieldOptions(context.asset).catch(() => void 0);
|
|
5864
|
+
assertReadCurrent();
|
|
5865
|
+
const eligible = /* @__PURE__ */ new Map();
|
|
5866
|
+
for (const option of catalog ?? []) {
|
|
5867
|
+
if (option?.chainId === chainId && typeof option.assetAddress === "string" && option.assetAddress.toLowerCase() === metadata.address.toLowerCase() && isAddress3(option.address))
|
|
5868
|
+
eligible.set(option.address.toLowerCase(), option);
|
|
5869
|
+
}
|
|
5870
|
+
const extra = [...eligible].filter(([address]) => !candidates.has(address)).map(([, option]) => option);
|
|
5871
|
+
if (options.length + extra.length > YIELDSEEKER_WITHDRAWAL_CANDIDATE_LIMIT) throw unavailable();
|
|
5872
|
+
const allExtraVerified = collect(extra, await this.readVaultLiquidity(extra, wallet, blockNumber));
|
|
5873
|
+
assertReadCurrent();
|
|
5874
|
+
const result = proof(eligible.size > 0 && allKnownVerified && allExtraVerified);
|
|
5875
|
+
if (result.available === 0n && !result.complete && !hasUnverifiedIndexedFunds(result)) throw unavailable();
|
|
5876
|
+
return result;
|
|
5877
|
+
};
|
|
5878
|
+
let timer;
|
|
5879
|
+
try {
|
|
5880
|
+
return await Promise.race([read2(), new Promise((_, reject) => {
|
|
5881
|
+
timer = setTimeout(() => reject(unavailable()), YIELDSEEKER_MAX_POSITION_VERIFICATION_MS);
|
|
5882
|
+
})]);
|
|
5883
|
+
} catch (error) {
|
|
5884
|
+
assertReadCurrent();
|
|
5885
|
+
if (knownProof && (knownProof.available > 0n || hasUnverifiedIndexedFunds(knownProof)) && (!(error instanceof OwneyError) || error.code === "WITHDRAW_BALANCE_UNAVAILABLE")) return knownProof;
|
|
5886
|
+
if (error instanceof OwneyError) throw error;
|
|
5887
|
+
throw unavailable();
|
|
5888
|
+
} finally {
|
|
5889
|
+
active = false;
|
|
5890
|
+
if (timer) clearTimeout(timer);
|
|
5891
|
+
}
|
|
5892
|
+
}
|
|
5508
5893
|
async getBalances(state, chainId, asset) {
|
|
5509
5894
|
this.assertChain(chainId);
|
|
5510
5895
|
if (asset !== void 0) this.assertAsset(asset);
|
|
@@ -6702,6 +7087,7 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
|
|
|
6702
7087
|
|
|
6703
7088
|
// src/client.ts
|
|
6704
7089
|
import {
|
|
7090
|
+
parseUnits as parseUnits2,
|
|
6705
7091
|
createPublicClient as createPublicClient4,
|
|
6706
7092
|
createWalletClient as createWalletClient4,
|
|
6707
7093
|
custom as custom4
|
|
@@ -8674,11 +9060,10 @@ var OwneySDK = class {
|
|
|
8674
9060
|
};
|
|
8675
9061
|
}
|
|
8676
9062
|
const requested = BigInt(amount);
|
|
8677
|
-
const aggregated = await this.
|
|
9063
|
+
const { balances: aggregated, failures: balanceFailures } = await this.readAggregatedBalances({ chainId, asset: token });
|
|
8678
9064
|
const unavailableAgents = eligibleAgents.filter(
|
|
8679
9065
|
(agent) => !(agent.id in aggregated.agentBalances)
|
|
8680
9066
|
);
|
|
8681
|
-
const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
|
|
8682
9067
|
const balances = projectAgentBalancesForAsset(
|
|
8683
9068
|
eligibleAgents,
|
|
8684
9069
|
aggregated.agentBalances,
|
|
@@ -8686,6 +9071,44 @@ var OwneySDK = class {
|
|
|
8686
9071
|
asset,
|
|
8687
9072
|
assetInfo.decimals
|
|
8688
9073
|
);
|
|
9074
|
+
const indexedAvailable = balances.reduce((sum, entry) => sum + entry.balance, 0n);
|
|
9075
|
+
await Promise.all(balances.map(async (entry) => {
|
|
9076
|
+
if (entry.balance !== 0n && indexedAvailable >= requested || !entry.agent.getWithdrawableBalance) return;
|
|
9077
|
+
const originalFailure = balanceFailures.get(entry.agent.id);
|
|
9078
|
+
if (originalFailure instanceof OwneyError && ![
|
|
9079
|
+
"AGENT_API_ERROR",
|
|
9080
|
+
"AGENT_TIMEOUT",
|
|
9081
|
+
"AGENT_RATE_LIMITED"
|
|
9082
|
+
].includes(originalFailure.code)) return;
|
|
9083
|
+
if (this.isUserRejectedWithdrawal(originalFailure)) throw originalFailure;
|
|
9084
|
+
if (entry.balance !== 0n && entry.agent.id !== "yieldseeker") {
|
|
9085
|
+
const total = aggregated.agentBalances[entry.agent.id]?.assetBalances?.find((value) => value.chainId === chainId && value.asset.toUpperCase() === token);
|
|
9086
|
+
if (!total || parseUnits2(total.amount, assetInfo.decimals) <= entry.balance) return;
|
|
9087
|
+
}
|
|
9088
|
+
try {
|
|
9089
|
+
const otherAvailable = indexedAvailable - entry.balance;
|
|
9090
|
+
const minimumAmount = requested > otherAvailable ? requested - otherAvailable : void 0;
|
|
9091
|
+
const verified = await entry.agent.getWithdrawableBalance(state, chainId, token, minimumAmount);
|
|
9092
|
+
if (typeof verified !== "bigint" || verified < 0n)
|
|
9093
|
+
throw new OwneyError("WITHDRAW_BALANCE_UNAVAILABLE", "Invalid withdrawal balance proof.", {}, entry.agent.id);
|
|
9094
|
+
if (this.state !== state || this.state.walletAddress !== state.walletAddress || this.state.chainId !== chainId)
|
|
9095
|
+
throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
|
|
9096
|
+
entry.balance = verified;
|
|
9097
|
+
const unavailableIndex = unavailableAgents.indexOf(entry.agent);
|
|
9098
|
+
if (unavailableIndex !== -1) unavailableAgents.splice(unavailableIndex, 1);
|
|
9099
|
+
if (aggregated.agentErrors) delete aggregated.agentErrors[entry.agent.id];
|
|
9100
|
+
} catch (error) {
|
|
9101
|
+
entry.balance = 0n;
|
|
9102
|
+
if (!unavailableAgents.includes(entry.agent)) unavailableAgents.push(entry.agent);
|
|
9103
|
+
aggregated.agentErrors = {
|
|
9104
|
+
...aggregated.agentErrors,
|
|
9105
|
+
[entry.agent.id]: error instanceof Error ? error.message : String(error)
|
|
9106
|
+
};
|
|
9107
|
+
}
|
|
9108
|
+
}));
|
|
9109
|
+
if (this.state !== state || this.state.chainId !== chainId)
|
|
9110
|
+
throw new OwneyError("NOT_CONNECTED", "Account changed while planning withdrawal.");
|
|
9111
|
+
const unavailableAgentIds = unavailableAgents.map((agent) => agent.id);
|
|
8689
9112
|
const totalAvailable = balances.reduce((sum, x) => sum + x.balance, 0n);
|
|
8690
9113
|
if (totalAvailable === 0n && unavailableAgents.length > 0) {
|
|
8691
9114
|
throw new OwneyError(
|
|
@@ -8770,7 +9193,18 @@ var OwneySDK = class {
|
|
|
8770
9193
|
);
|
|
8771
9194
|
}
|
|
8772
9195
|
const withdrawn = sumWithdrawnAmount(results);
|
|
8773
|
-
|
|
9196
|
+
const verifiedFeeDeductions = orderedPlans.reduce((total, plan) => {
|
|
9197
|
+
const result = results[plan.agent.id];
|
|
9198
|
+
const adjustment = result?.feeAdjustment;
|
|
9199
|
+
const evidence = result?.evidence;
|
|
9200
|
+
const submitted = submissionEvidence.get(plan.agent.id);
|
|
9201
|
+
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;
|
|
9202
|
+
const fee = BigInt(adjustment.feeAmount);
|
|
9203
|
+
const net = BigInt(result.amount);
|
|
9204
|
+
const gross = BigInt(adjustment.grossAmount);
|
|
9205
|
+
return fee > 0n && net > 0n && gross === plan.planned && net + fee === gross ? total + fee : total;
|
|
9206
|
+
}, 0n);
|
|
9207
|
+
if (withdrawn + verifiedFeeDeductions < requested) {
|
|
8774
9208
|
throw new OwneyError(
|
|
8775
9209
|
"WITHDRAW_PARTIAL_FAILURE",
|
|
8776
9210
|
`Withdrew "${withdrawn.toString()}" of requested "${amount}" of "${asset}". One or more agents failed; remaining balance could not cover the shortfall.`,
|
|
@@ -8794,23 +9228,42 @@ var OwneySDK = class {
|
|
|
8794
9228
|
* @returns {AgentBalance} for a single agent, or {OwneyBalances} with totalBalance and per-agent breakdown
|
|
8795
9229
|
*/
|
|
8796
9230
|
async getBalances(agentId, options) {
|
|
8797
|
-
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
8798
|
-
const asset = options?.asset;
|
|
8799
|
-
const readBalance = (agent) => this.readAgent(
|
|
8800
|
-
agent,
|
|
8801
|
-
"balances",
|
|
8802
|
-
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
8803
|
-
asset ? { asset } : null
|
|
8804
|
-
);
|
|
8805
9231
|
if (agentId) {
|
|
9232
|
+
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
9233
|
+
const asset = options?.asset;
|
|
8806
9234
|
const agent = this.getAgent(agentId);
|
|
8807
|
-
const result = await
|
|
9235
|
+
const result = await this.readAgent(
|
|
9236
|
+
agent,
|
|
9237
|
+
"balances",
|
|
9238
|
+
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
9239
|
+
asset ? { asset } : null
|
|
9240
|
+
);
|
|
8808
9241
|
assertCurrent();
|
|
8809
9242
|
return {
|
|
8810
9243
|
...result,
|
|
8811
9244
|
balanceComposition: agent.balanceComposition ?? "tokens-include-positions"
|
|
8812
9245
|
};
|
|
8813
9246
|
}
|
|
9247
|
+
const { balances, failures } = await this.readAggregatedBalances(options);
|
|
9248
|
+
if (Object.keys(balances.agentBalances).length === 0) {
|
|
9249
|
+
throw new OwneyError(
|
|
9250
|
+
"BALANCE_ALL_FAILED",
|
|
9251
|
+
"Failed to fetch balances for all active agents.",
|
|
9252
|
+
{ agentErrors: balances.agentErrors ?? {}, failures: [...failures.values()] }
|
|
9253
|
+
);
|
|
9254
|
+
}
|
|
9255
|
+
return balances;
|
|
9256
|
+
}
|
|
9257
|
+
/** Preserve failure objects for withdrawal recovery without changing public balance results. */
|
|
9258
|
+
async readAggregatedBalances(options) {
|
|
9259
|
+
const { state, chainId, assertCurrent } = this.captureReadScope(options?.chainId);
|
|
9260
|
+
const asset = options?.asset;
|
|
9261
|
+
const readBalance = (agent) => this.readAgent(
|
|
9262
|
+
agent,
|
|
9263
|
+
"balances",
|
|
9264
|
+
() => asset ? agent.getBalances(state, chainId, asset) : agent.getBalances(state, chainId),
|
|
9265
|
+
asset ? { asset } : null
|
|
9266
|
+
);
|
|
8814
9267
|
let totalBalance = 0;
|
|
8815
9268
|
const results = {};
|
|
8816
9269
|
const entries = [...this.getActiveAgents().entries()];
|
|
@@ -8827,39 +9280,33 @@ var OwneySDK = class {
|
|
|
8827
9280
|
})
|
|
8828
9281
|
);
|
|
8829
9282
|
assertCurrent();
|
|
8830
|
-
let successCount = 0;
|
|
8831
9283
|
const agentErrors = {};
|
|
8832
9284
|
const agentRetryAt = {};
|
|
8833
|
-
const agentFailures =
|
|
9285
|
+
const agentFailures = /* @__PURE__ */ new Map();
|
|
8834
9286
|
for (let i = 0; i < balanceResults.length; i++) {
|
|
8835
9287
|
const settledResult = balanceResults[i];
|
|
8836
|
-
const [
|
|
9288
|
+
const [agentId] = entries[i];
|
|
8837
9289
|
if (settledResult.status === "fulfilled") {
|
|
8838
9290
|
const [id, b] = settledResult.value;
|
|
8839
9291
|
results[id] = b;
|
|
8840
9292
|
totalBalance += Number(b.totalBalance);
|
|
8841
|
-
successCount += 1;
|
|
8842
9293
|
continue;
|
|
8843
9294
|
}
|
|
8844
9295
|
const reason = settledResult.reason;
|
|
8845
|
-
agentFailures.
|
|
9296
|
+
agentFailures.set(agentId, reason);
|
|
8846
9297
|
const retryDelay = rateLimitDelay(reason);
|
|
8847
|
-
if (retryDelay !== void 0) agentRetryAt[
|
|
8848
|
-
agentErrors[
|
|
8849
|
-
console.error(`[owney-sdk] Balance fetch failed for agent "${
|
|
8850
|
-
}
|
|
8851
|
-
if (successCount === 0) {
|
|
8852
|
-
throw new OwneyError(
|
|
8853
|
-
"BALANCE_ALL_FAILED",
|
|
8854
|
-
"Failed to fetch balances for all active agents.",
|
|
8855
|
-
{ agentErrors, failures: agentFailures }
|
|
8856
|
-
);
|
|
9298
|
+
if (retryDelay !== void 0) agentRetryAt[agentId] = Date.now() + retryDelay;
|
|
9299
|
+
agentErrors[agentId] = reason instanceof Error ? reason.message : String(reason);
|
|
9300
|
+
console.error(`[owney-sdk] Balance fetch failed for agent "${agentId}":`, reason);
|
|
8857
9301
|
}
|
|
8858
9302
|
return {
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
|
|
8862
|
-
|
|
9303
|
+
balances: {
|
|
9304
|
+
totalBalance: String(totalBalance),
|
|
9305
|
+
totalBalanceAsset: "usdc",
|
|
9306
|
+
agentBalances: results,
|
|
9307
|
+
...Object.keys(agentErrors).length > 0 ? { agentErrors, agentRetryAt } : {}
|
|
9308
|
+
},
|
|
9309
|
+
failures: agentFailures
|
|
8863
9310
|
};
|
|
8864
9311
|
}
|
|
8865
9312
|
/**
|