@owney/sdk 0.7.7-beta.1 → 0.7.7-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +146 -117
- package/dist/index.d.cts +45 -19
- package/dist/index.d.ts +45 -19
- package/dist/index.js +146 -117
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1336,43 +1336,7 @@ var ZyfaiAgent = class {
|
|
|
1336
1336
|
* recoverable (ensureWalletDeployed tries the other deploy call), but
|
|
1337
1337
|
* failing the deposit outright is not.
|
|
1338
1338
|
*/
|
|
1339
|
-
/**
|
|
1340
|
-
* Zyfai's own answer to "what kind of wallet is this, and can the agent act on
|
|
1341
|
-
* it", read from the positions payload (`GET /data/position`, which
|
|
1342
|
-
* `getPositions` wraps).
|
|
1343
|
-
*
|
|
1344
|
-
* `predeployed` and `hasActiveSessionKey` are both returned at runtime but
|
|
1345
|
-
* declared on neither `Portfolio` nor their docs, hence the cast. Verified
|
|
1346
|
-
* live: an old wallet reports `{predeployed: false, hasActiveSessionKey:
|
|
1347
|
-
* false}` while a pool wallet reports `{true, true}`.
|
|
1348
|
-
*
|
|
1349
|
-
* Preferred over the SDK's private `isPredeployed`, which is cached from the
|
|
1350
|
-
* login response and can go stale mid-session. Returns an empty object on any
|
|
1351
|
-
* failure so every caller degrades to its own fallback.
|
|
1352
|
-
*/
|
|
1353
|
-
async readWalletState(ownerAddress) {
|
|
1354
|
-
try {
|
|
1355
|
-
const { portfolio } = await this.sdk.getPositions(ownerAddress);
|
|
1356
|
-
const raw = portfolio;
|
|
1357
|
-
return {
|
|
1358
|
-
predeployed: raw?.predeployed,
|
|
1359
|
-
hasActiveSessionKey: raw?.hasActiveSessionKey
|
|
1360
|
-
};
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
console.warn(
|
|
1363
|
-
"[zyfai] getPositions wallet-state read failed (non-fatal):",
|
|
1364
|
-
error instanceof Error ? error.message : String(error)
|
|
1365
|
-
);
|
|
1366
|
-
return {};
|
|
1367
|
-
}
|
|
1368
|
-
}
|
|
1369
1339
|
async isPredeployedPoolWallet(ownerAddress, smartWallet) {
|
|
1370
|
-
const { predeployed } = await this.readWalletState(ownerAddress);
|
|
1371
|
-
if (typeof predeployed === "boolean") return predeployed;
|
|
1372
|
-
const internals = this.sdk;
|
|
1373
|
-
if (typeof internals.isPredeployed === "boolean") {
|
|
1374
|
-
return internals.isPredeployed;
|
|
1375
|
-
}
|
|
1376
1340
|
try {
|
|
1377
1341
|
const byEoa = await this.sdk.getSmartWalletByEOA(ownerAddress);
|
|
1378
1342
|
return typeof byEoa?.smartWallet === "string" && byEoa.smartWallet.toLowerCase() === smartWallet.toLowerCase();
|
|
@@ -1414,13 +1378,7 @@ var ZyfaiAgent = class {
|
|
|
1414
1378
|
chainId
|
|
1415
1379
|
);
|
|
1416
1380
|
if (!wallet.isDeployed) return;
|
|
1417
|
-
|
|
1418
|
-
await this.ensureSessionKey(
|
|
1419
|
-
ownerAddress,
|
|
1420
|
-
chainId,
|
|
1421
|
-
wallet.address,
|
|
1422
|
-
hasActiveSessionKey
|
|
1423
|
-
);
|
|
1381
|
+
await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
|
|
1424
1382
|
} catch (error) {
|
|
1425
1383
|
console.warn(
|
|
1426
1384
|
"[zyfai] session-key check at activation failed (non-fatal):",
|
|
@@ -1428,19 +1386,15 @@ var ZyfaiAgent = class {
|
|
|
1428
1386
|
);
|
|
1429
1387
|
}
|
|
1430
1388
|
}
|
|
1431
|
-
async ensureSessionKey(ownerAddress, chainId, smartWallet
|
|
1389
|
+
async ensureSessionKey(ownerAddress, chainId, smartWallet) {
|
|
1432
1390
|
try {
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
}
|
|
1440
|
-
const installed = await this.readSmartSessionsInstalled(
|
|
1441
|
-
chainId,
|
|
1442
|
-
smartWallet
|
|
1443
|
-
);
|
|
1391
|
+
const installed = await this.getPublicClient(chainId).readContract({
|
|
1392
|
+
address: smartWallet,
|
|
1393
|
+
abi: ERC7579_IS_MODULE_INSTALLED_ABI,
|
|
1394
|
+
functionName: "isModuleInstalled",
|
|
1395
|
+
// moduleTypeId 1 = validator (ERC-7579)
|
|
1396
|
+
args: [1n, import_sdk.SMART_SESSIONS_VALIDATOR, "0x"]
|
|
1397
|
+
});
|
|
1444
1398
|
if (installed) return;
|
|
1445
1399
|
console.warn(
|
|
1446
1400
|
`[zyfai] Safe ${smartWallet} has no smart-sessions validator on chain ${chainId}; creating a session key so the agent can allocate.`
|
|
@@ -1453,16 +1407,6 @@ var ZyfaiAgent = class {
|
|
|
1453
1407
|
);
|
|
1454
1408
|
}
|
|
1455
1409
|
}
|
|
1456
|
-
/** Ground truth for "can Zyfai's automation act on this Safe, on this chain". */
|
|
1457
|
-
async readSmartSessionsInstalled(chainId, smartWallet) {
|
|
1458
|
-
return this.getPublicClient(chainId).readContract({
|
|
1459
|
-
address: smartWallet,
|
|
1460
|
-
abi: ERC7579_IS_MODULE_INSTALLED_ABI,
|
|
1461
|
-
functionName: "isModuleInstalled",
|
|
1462
|
-
// moduleTypeId 1 = validator (ERC-7579)
|
|
1463
|
-
args: [1n, import_sdk.SMART_SESSIONS_VALIDATOR, "0x"]
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
1410
|
/**
|
|
1467
1411
|
* Guarantee the user's Safe has code on `chainId` before anything transfers
|
|
1468
1412
|
* funds into it, and return its address.
|
|
@@ -1484,13 +1428,7 @@ var ZyfaiAgent = class {
|
|
|
1484
1428
|
async ensureWalletDeployed(ownerAddress, chainId) {
|
|
1485
1429
|
const wallet = await this.sdk.getSmartWalletAddress(ownerAddress, chainId);
|
|
1486
1430
|
if (wallet.isDeployed) {
|
|
1487
|
-
|
|
1488
|
-
await this.ensureSessionKey(
|
|
1489
|
-
ownerAddress,
|
|
1490
|
-
chainId,
|
|
1491
|
-
wallet.address,
|
|
1492
|
-
hasActiveSessionKey
|
|
1493
|
-
);
|
|
1431
|
+
await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
|
|
1494
1432
|
return wallet.address;
|
|
1495
1433
|
}
|
|
1496
1434
|
const poolFirst = await this.isPredeployedPoolWallet(
|
|
@@ -1974,9 +1912,12 @@ async function postSponsorTransferAuth(input) {
|
|
|
1974
1912
|
{
|
|
1975
1913
|
statusCode: res.status,
|
|
1976
1914
|
responseBody: text.slice(0, 500),
|
|
1977
|
-
//
|
|
1978
|
-
//
|
|
1979
|
-
|
|
1915
|
+
// A 4xx is an explicit refusal before execution (DTO validation,
|
|
1916
|
+
// signature rejection, simulation) — nothing was broadcast, so a
|
|
1917
|
+
// user-paid retry cannot double-spend. 503 SPONSOR_UNAVAILABLE means
|
|
1918
|
+
// the relayer was out of gas, also pre-broadcast. Any other 5xx stays
|
|
1919
|
+
// ambiguous: the transfer may already be in flight.
|
|
1920
|
+
safeToFallback: res.status >= 400 && res.status < 500 || res.status === 503
|
|
1980
1921
|
}
|
|
1981
1922
|
);
|
|
1982
1923
|
}
|
|
@@ -2248,9 +2189,9 @@ function makeSponsoredDepositCallback(deps) {
|
|
|
2248
2189
|
};
|
|
2249
2190
|
}
|
|
2250
2191
|
|
|
2251
|
-
// src/lib/sponsored-
|
|
2192
|
+
// src/lib/sponsored-permit2-deposit.ts
|
|
2252
2193
|
var PERMIT_WINDOW_SECONDS = 15 * 60;
|
|
2253
|
-
function
|
|
2194
|
+
function makeSponsoredPermit2Callback(deps) {
|
|
2254
2195
|
const get = deps.httpGet ?? getSponsorRelayerAddress;
|
|
2255
2196
|
const post = deps.httpPost ?? postSponsorPermit2Transfer;
|
|
2256
2197
|
return async (smartWallet, chainId, amount) => {
|
|
@@ -2259,7 +2200,7 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2259
2200
|
if (!token) {
|
|
2260
2201
|
throw new OwneyError(
|
|
2261
2202
|
"CHAIN_UNSUPPORTED",
|
|
2262
|
-
`No sponsored
|
|
2203
|
+
`No sponsored ${deps.assetLabel} configured for chain ${chainId}`
|
|
2263
2204
|
);
|
|
2264
2205
|
}
|
|
2265
2206
|
const amountWei = BigInt(amount);
|
|
@@ -2271,14 +2212,14 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2271
2212
|
if (balance < amountWei) {
|
|
2272
2213
|
throw new OwneyError(
|
|
2273
2214
|
"DEPOSIT_INSUFFICIENT_BALANCE",
|
|
2274
|
-
|
|
2215
|
+
`Insufficient ${deps.assetLabel} balance for this deposit.`,
|
|
2275
2216
|
{ token, chainId: cid, balance: balance.toString(), amount }
|
|
2276
2217
|
);
|
|
2277
2218
|
}
|
|
2278
2219
|
} catch (err) {
|
|
2279
2220
|
if (err instanceof OwneyError) throw err;
|
|
2280
2221
|
console.warn(
|
|
2281
|
-
|
|
2222
|
+
`[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
|
|
2282
2223
|
err instanceof Error ? err.message : String(err)
|
|
2283
2224
|
);
|
|
2284
2225
|
}
|
|
@@ -2286,7 +2227,7 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2286
2227
|
if (allowance < amountWei) {
|
|
2287
2228
|
throw new OwneyError(
|
|
2288
2229
|
"PERMIT2_APPROVAL_REQUIRED",
|
|
2289
|
-
|
|
2230
|
+
`${deps.assetLabel} gasless deposits need a one-time Permit2 approval; call approvePermit2("${deps.assetLabel}") first`,
|
|
2290
2231
|
{ token, chainId: cid, allowance: allowance.toString(), amount }
|
|
2291
2232
|
);
|
|
2292
2233
|
}
|
|
@@ -2367,6 +2308,8 @@ var SPONSORED_WETH_BY_CHAIN = {
|
|
|
2367
2308
|
42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
2368
2309
|
1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
|
2369
2310
|
};
|
|
2311
|
+
var PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS = 10;
|
|
2312
|
+
var PERMIT2_ALLOWANCE_POLL_INTERVAL_MS = 1500;
|
|
2370
2313
|
function shouldFallbackToUserPaid(error, asset, appCallback) {
|
|
2371
2314
|
return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
|
|
2372
2315
|
}
|
|
@@ -2391,7 +2334,7 @@ var OwneySDK = class {
|
|
|
2391
2334
|
routingApiBaseUrl;
|
|
2392
2335
|
referralSource;
|
|
2393
2336
|
cachedSponsoredCallback = null;
|
|
2394
|
-
|
|
2337
|
+
cachedPermit2Callbacks = /* @__PURE__ */ new Map();
|
|
2395
2338
|
initializingAgentsPromise = null;
|
|
2396
2339
|
constructor(config) {
|
|
2397
2340
|
this.apiKey = config.apiKey;
|
|
@@ -2419,7 +2362,7 @@ var OwneySDK = class {
|
|
|
2419
2362
|
}
|
|
2420
2363
|
this.state = { provider, walletAddress, chainId: null };
|
|
2421
2364
|
this.cachedSponsoredCallback = null;
|
|
2422
|
-
this.
|
|
2365
|
+
this.cachedPermit2Callbacks.clear();
|
|
2423
2366
|
}
|
|
2424
2367
|
/**
|
|
2425
2368
|
* Disconnect from all agents and clear connection state.
|
|
@@ -2433,7 +2376,7 @@ var OwneySDK = class {
|
|
|
2433
2376
|
this.disabledAgents.clear();
|
|
2434
2377
|
this.state = null;
|
|
2435
2378
|
this.cachedSponsoredCallback = null;
|
|
2436
|
-
this.
|
|
2379
|
+
this.cachedPermit2Callbacks.clear();
|
|
2437
2380
|
}
|
|
2438
2381
|
isAgentDisabled(agentId) {
|
|
2439
2382
|
return this.disabledAgents.has(agentId);
|
|
@@ -2510,25 +2453,23 @@ var OwneySDK = class {
|
|
|
2510
2453
|
return callback;
|
|
2511
2454
|
}
|
|
2512
2455
|
/**
|
|
2513
|
-
* Lazily builds (and caches) the
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2516
|
-
*
|
|
2456
|
+
* Lazily builds (and caches) the Permit2 sponsored callback for one asset.
|
|
2457
|
+
* Used for WETH always, and for USDC when the connected owner is a smart
|
|
2458
|
+
* account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
|
|
2459
|
+
* contract signature, while Permit2 supports ERC-1271.
|
|
2517
2460
|
*/
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2461
|
+
getDefaultPermit2Callback(asset, onApproved) {
|
|
2462
|
+
const cached = this.cachedPermit2Callbacks.get(asset);
|
|
2463
|
+
if (!onApproved && cached) return cached;
|
|
2521
2464
|
const provider = this.requireConnectedProvider();
|
|
2522
2465
|
const owner = this.state.walletAddress;
|
|
2523
|
-
const callback =
|
|
2466
|
+
const callback = makeSponsoredPermit2Callback({
|
|
2524
2467
|
apiKey: this.apiKey,
|
|
2525
2468
|
baseUrl: this.routingApiBaseUrl,
|
|
2526
2469
|
ownerAddress: owner,
|
|
2470
|
+
assetLabel: asset,
|
|
2527
2471
|
onApproved,
|
|
2528
|
-
tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
|
|
2529
|
-
// Casts work around viem's chain-narrowed Client vs the generic
|
|
2530
|
-
// PublicClient/WalletClient param types — structurally identical at
|
|
2531
|
-
// runtime, but the two share a name TS treats as unrelated.
|
|
2472
|
+
tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
|
|
2532
2473
|
getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
|
|
2533
2474
|
chain: VIEM_CHAIN2[cid],
|
|
2534
2475
|
transport: (0, import_viem5.custom)(provider)
|
|
@@ -2539,9 +2480,34 @@ var OwneySDK = class {
|
|
|
2539
2480
|
transport: (0, import_viem5.custom)(provider)
|
|
2540
2481
|
})
|
|
2541
2482
|
});
|
|
2542
|
-
if (!onApproved) this.
|
|
2483
|
+
if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
|
|
2543
2484
|
return callback;
|
|
2544
2485
|
}
|
|
2486
|
+
/**
|
|
2487
|
+
* True when the connected owner has bytecode — a deployed smart wallet, or an
|
|
2488
|
+
* EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
|
|
2489
|
+
* Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
|
|
2490
|
+
*
|
|
2491
|
+
* Fails OPEN: an unreadable code check returns false so a working EOA deposit
|
|
2492
|
+
* is never blocked by a flaky provider.
|
|
2493
|
+
*/
|
|
2494
|
+
async ownerHasCode() {
|
|
2495
|
+
try {
|
|
2496
|
+
const provider = this.requireConnectedProvider();
|
|
2497
|
+
const code = await provider.request({
|
|
2498
|
+
method: "eth_getCode",
|
|
2499
|
+
params: [this.state.walletAddress, "latest"]
|
|
2500
|
+
});
|
|
2501
|
+
return Boolean(code) && code !== "0x";
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
debugLog(
|
|
2504
|
+
"deposit",
|
|
2505
|
+
"owner code check failed; assuming EOA",
|
|
2506
|
+
error instanceof Error ? error.message : String(error)
|
|
2507
|
+
);
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2545
2511
|
getAgent(agentId) {
|
|
2546
2512
|
const agent = this.agents.get(agentId);
|
|
2547
2513
|
if (!agent) {
|
|
@@ -2737,7 +2703,11 @@ var OwneySDK = class {
|
|
|
2737
2703
|
}
|
|
2738
2704
|
const state = this.requireState();
|
|
2739
2705
|
const chainId = this.requireChainId();
|
|
2740
|
-
const
|
|
2706
|
+
const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
|
|
2707
|
+
const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
|
|
2708
|
+
asset === "WETH" ? "WETH" : "USDC",
|
|
2709
|
+
onApproved
|
|
2710
|
+
) : this.getDefaultSponsoredCallback(onApproved));
|
|
2741
2711
|
if (agentId) {
|
|
2742
2712
|
if (this.isAgentDisabled(agentId)) {
|
|
2743
2713
|
throw new OwneyError(
|
|
@@ -2806,13 +2776,16 @@ var OwneySDK = class {
|
|
|
2806
2776
|
* 1. Missing Permit2 allowance: when the app did not supply its own
|
|
2807
2777
|
* callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
|
|
2808
2778
|
* WETH deposit, this is the wallet's first gasless WETH deposit. We send
|
|
2809
|
-
* the one-time (user-paid) Permit2 approval via `approvePermit2()
|
|
2810
|
-
*
|
|
2811
|
-
*
|
|
2812
|
-
*
|
|
2813
|
-
*
|
|
2814
|
-
*
|
|
2815
|
-
*
|
|
2779
|
+
* the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
|
|
2780
|
+
* for the new allowance to actually be readable via
|
|
2781
|
+
* `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
|
|
2782
|
+
* mined tx), then retry the SAME sponsored attempt once. Bounded to one
|
|
2783
|
+
* approval attempt per call so a wallet/agent that keeps reporting the
|
|
2784
|
+
* allowance as missing can't loop forever. If `approvePermit2()` itself
|
|
2785
|
+
* throws (e.g. the user rejects the wallet prompt), that error
|
|
2786
|
+
* propagates as-is — the user said no to a transaction, so we must not
|
|
2787
|
+
* turn around and ask them to pay for a different one via the fallback
|
|
2788
|
+
* below.
|
|
2816
2789
|
* 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
|
|
2817
2790
|
* the infrastructure layer in a provably safe-to-retry way, so we warn
|
|
2818
2791
|
* and retry once with `undefined`, falling through to a user-paid
|
|
@@ -2824,6 +2797,49 @@ var OwneySDK = class {
|
|
|
2824
2797
|
* propagates unchanged. Scoped per agent invocation so one agent's failure
|
|
2825
2798
|
* doesn't force a retry of sibling agents in a multi-agent split.
|
|
2826
2799
|
*/
|
|
2800
|
+
/**
|
|
2801
|
+
* Polls the owner's Permit2 allowance for `asset` until it covers `amount`
|
|
2802
|
+
* or the attempt budget runs out. Called right after `approvePermit2()`
|
|
2803
|
+
* resolves: the approval tx is mined, but the wallet's own RPC can still
|
|
2804
|
+
* serve a stale (zero) allowance on the very next read (replica lag), which
|
|
2805
|
+
* would otherwise make the immediate retry fail with the same
|
|
2806
|
+
* PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
|
|
2807
|
+
* throws — if the allowance still hasn't caught up after the budget, the
|
|
2808
|
+
* caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
|
|
2809
|
+
* rather than this helper inventing a different one.
|
|
2810
|
+
*/
|
|
2811
|
+
async waitForPermit2Allowance(asset, amount) {
|
|
2812
|
+
const state = this.requireState();
|
|
2813
|
+
const chainId = this.requireChainId();
|
|
2814
|
+
const provider = this.requireConnectedProvider();
|
|
2815
|
+
const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
|
|
2816
|
+
if (!token) return;
|
|
2817
|
+
const publicClient = (0, import_viem5.createPublicClient)({
|
|
2818
|
+
chain: VIEM_CHAIN2[chainId],
|
|
2819
|
+
transport: (0, import_viem5.custom)(provider)
|
|
2820
|
+
});
|
|
2821
|
+
for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS; attempt++) {
|
|
2822
|
+
if (attempt > 0) {
|
|
2823
|
+
await new Promise(
|
|
2824
|
+
(resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_POLL_INTERVAL_MS)
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
try {
|
|
2828
|
+
const allowance = await readPermit2Allowance(
|
|
2829
|
+
publicClient,
|
|
2830
|
+
token,
|
|
2831
|
+
state.walletAddress
|
|
2832
|
+
);
|
|
2833
|
+
if (allowance >= amount) return;
|
|
2834
|
+
} catch (error) {
|
|
2835
|
+
debugLog(
|
|
2836
|
+
"deposit",
|
|
2837
|
+
"Permit2 allowance poll read failed; retrying",
|
|
2838
|
+
error instanceof Error ? error.message : String(error)
|
|
2839
|
+
);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2827
2843
|
async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
|
|
2828
2844
|
const attempt = (callback) => withFailureReporting(
|
|
2829
2845
|
this.apiKey,
|
|
@@ -2841,12 +2857,16 @@ var OwneySDK = class {
|
|
|
2841
2857
|
try {
|
|
2842
2858
|
return await attempt(effectiveCallback);
|
|
2843
2859
|
} catch (error) {
|
|
2844
|
-
if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
|
|
2860
|
+
if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
|
|
2845
2861
|
approvalAttempted = true;
|
|
2846
2862
|
console.warn(
|
|
2847
|
-
|
|
2863
|
+
`[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
|
|
2864
|
+
);
|
|
2865
|
+
await this.approvePermit2(asset);
|
|
2866
|
+
await this.waitForPermit2Allowance(
|
|
2867
|
+
asset,
|
|
2868
|
+
BigInt(amount)
|
|
2848
2869
|
);
|
|
2849
|
-
await this.approvePermit2();
|
|
2850
2870
|
continue;
|
|
2851
2871
|
}
|
|
2852
2872
|
if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
|
|
@@ -3360,23 +3380,32 @@ var OwneySDK = class {
|
|
|
3360
3380
|
return agent.ensureAutoSelectProtocols(state, chainId, asset);
|
|
3361
3381
|
}
|
|
3362
3382
|
/**
|
|
3363
|
-
* One-time, user-paid approval of Permit2 on the sponsored
|
|
3364
|
-
* the active chain. Required once per wallet per chain before
|
|
3365
|
-
*
|
|
3366
|
-
*
|
|
3367
|
-
*
|
|
3383
|
+
* One-time, user-paid approval of Permit2 on the given sponsored token for
|
|
3384
|
+
* the active chain. Required once per wallet per chain per token before a
|
|
3385
|
+
* gasless deposit of that asset over Permit2; afterwards deposit() is
|
|
3386
|
+
* signature-only for it. This call itself is a real transaction the user
|
|
3387
|
+
* pays gas for. Resolves only after the approval transaction is mined (1
|
|
3388
|
+
* confirmation), so a subsequent deposit() will see the new allowance;
|
|
3389
|
+
* throws if the transaction reverted.
|
|
3368
3390
|
* @returns the approval transaction hash.
|
|
3369
3391
|
*/
|
|
3370
3392
|
async approvePermit2(asset = "WETH") {
|
|
3371
|
-
void asset;
|
|
3372
3393
|
const state = this.requireState();
|
|
3373
3394
|
const chainId = this.requireChainId();
|
|
3374
|
-
this.
|
|
3375
|
-
|
|
3395
|
+
let guard = this.agents.get("zyfai");
|
|
3396
|
+
if (!guard) {
|
|
3397
|
+
try {
|
|
3398
|
+
guard = this.getEligibleAgents(chainId, asset)[0];
|
|
3399
|
+
} catch {
|
|
3400
|
+
guard = void 0;
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
if (guard) this.validateAssetSupport(guard, chainId, asset);
|
|
3404
|
+
const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
|
|
3376
3405
|
if (!token) {
|
|
3377
3406
|
throw new OwneyError(
|
|
3378
3407
|
"CHAIN_UNSUPPORTED",
|
|
3379
|
-
`No sponsored
|
|
3408
|
+
`No sponsored ${asset} on chain ${chainId}`
|
|
3380
3409
|
);
|
|
3381
3410
|
}
|
|
3382
3411
|
const provider = this.requireConnectedProvider();
|
package/dist/index.d.cts
CHANGED
|
@@ -392,7 +392,7 @@ declare class OwneySDK {
|
|
|
392
392
|
private routingApiBaseUrl?;
|
|
393
393
|
private referralSource?;
|
|
394
394
|
private cachedSponsoredCallback;
|
|
395
|
-
private
|
|
395
|
+
private cachedPermit2Callbacks;
|
|
396
396
|
private initializingAgentsPromise;
|
|
397
397
|
constructor(config: OwneySDKConfig);
|
|
398
398
|
/**
|
|
@@ -428,12 +428,21 @@ declare class OwneySDK {
|
|
|
428
428
|
*/
|
|
429
429
|
private getDefaultSponsoredCallback;
|
|
430
430
|
/**
|
|
431
|
-
* Lazily builds (and caches) the
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
431
|
+
* Lazily builds (and caches) the Permit2 sponsored callback for one asset.
|
|
432
|
+
* Used for WETH always, and for USDC when the connected owner is a smart
|
|
433
|
+
* account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
|
|
434
|
+
* contract signature, while Permit2 supports ERC-1271.
|
|
435
435
|
*/
|
|
436
|
-
private
|
|
436
|
+
private getDefaultPermit2Callback;
|
|
437
|
+
/**
|
|
438
|
+
* True when the connected owner has bytecode — a deployed smart wallet, or an
|
|
439
|
+
* EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
|
|
440
|
+
* Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
|
|
441
|
+
*
|
|
442
|
+
* Fails OPEN: an unreadable code check returns false so a working EOA deposit
|
|
443
|
+
* is never blocked by a flaky provider.
|
|
444
|
+
*/
|
|
445
|
+
private ownerHasCode;
|
|
437
446
|
private getAgent;
|
|
438
447
|
private getActiveAgents;
|
|
439
448
|
/**
|
|
@@ -483,13 +492,16 @@ declare class OwneySDK {
|
|
|
483
492
|
* 1. Missing Permit2 allowance: when the app did not supply its own
|
|
484
493
|
* callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
|
|
485
494
|
* WETH deposit, this is the wallet's first gasless WETH deposit. We send
|
|
486
|
-
* the one-time (user-paid) Permit2 approval via `approvePermit2()
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
495
|
+
* the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
|
|
496
|
+
* for the new allowance to actually be readable via
|
|
497
|
+
* `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
|
|
498
|
+
* mined tx), then retry the SAME sponsored attempt once. Bounded to one
|
|
499
|
+
* approval attempt per call so a wallet/agent that keeps reporting the
|
|
500
|
+
* allowance as missing can't loop forever. If `approvePermit2()` itself
|
|
501
|
+
* throws (e.g. the user rejects the wallet prompt), that error
|
|
502
|
+
* propagates as-is — the user said no to a transaction, so we must not
|
|
503
|
+
* turn around and ask them to pay for a different one via the fallback
|
|
504
|
+
* below.
|
|
493
505
|
* 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
|
|
494
506
|
* the infrastructure layer in a provably safe-to-retry way, so we warn
|
|
495
507
|
* and retry once with `undefined`, falling through to a user-paid
|
|
@@ -501,6 +513,18 @@ declare class OwneySDK {
|
|
|
501
513
|
* propagates unchanged. Scoped per agent invocation so one agent's failure
|
|
502
514
|
* doesn't force a retry of sibling agents in a multi-agent split.
|
|
503
515
|
*/
|
|
516
|
+
/**
|
|
517
|
+
* Polls the owner's Permit2 allowance for `asset` until it covers `amount`
|
|
518
|
+
* or the attempt budget runs out. Called right after `approvePermit2()`
|
|
519
|
+
* resolves: the approval tx is mined, but the wallet's own RPC can still
|
|
520
|
+
* serve a stale (zero) allowance on the very next read (replica lag), which
|
|
521
|
+
* would otherwise make the immediate retry fail with the same
|
|
522
|
+
* PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
|
|
523
|
+
* throws — if the allowance still hasn't caught up after the budget, the
|
|
524
|
+
* caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
|
|
525
|
+
* rather than this helper inventing a different one.
|
|
526
|
+
*/
|
|
527
|
+
private waitForPermit2Allowance;
|
|
504
528
|
private depositWithFallback;
|
|
505
529
|
private getMinDepositAmount;
|
|
506
530
|
private splitDepositAmount;
|
|
@@ -578,14 +602,16 @@ declare class OwneySDK {
|
|
|
578
602
|
*/
|
|
579
603
|
ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
|
|
580
604
|
/**
|
|
581
|
-
* One-time, user-paid approval of Permit2 on the sponsored
|
|
582
|
-
* the active chain. Required once per wallet per chain before
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
605
|
+
* One-time, user-paid approval of Permit2 on the given sponsored token for
|
|
606
|
+
* the active chain. Required once per wallet per chain per token before a
|
|
607
|
+
* gasless deposit of that asset over Permit2; afterwards deposit() is
|
|
608
|
+
* signature-only for it. This call itself is a real transaction the user
|
|
609
|
+
* pays gas for. Resolves only after the approval transaction is mined (1
|
|
610
|
+
* confirmation), so a subsequent deposit() will see the new allowance;
|
|
611
|
+
* throws if the transaction reverted.
|
|
586
612
|
* @returns the approval transaction hash.
|
|
587
613
|
*/
|
|
588
|
-
approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
|
|
614
|
+
approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
|
|
589
615
|
/**
|
|
590
616
|
* Get the agent's average APY performance over a time period. Does not require a wallet connection.
|
|
591
617
|
* @param options - Contains agentId (optional) and days ("7D", "14D", or "30D")
|
package/dist/index.d.ts
CHANGED
|
@@ -392,7 +392,7 @@ declare class OwneySDK {
|
|
|
392
392
|
private routingApiBaseUrl?;
|
|
393
393
|
private referralSource?;
|
|
394
394
|
private cachedSponsoredCallback;
|
|
395
|
-
private
|
|
395
|
+
private cachedPermit2Callbacks;
|
|
396
396
|
private initializingAgentsPromise;
|
|
397
397
|
constructor(config: OwneySDKConfig);
|
|
398
398
|
/**
|
|
@@ -428,12 +428,21 @@ declare class OwneySDK {
|
|
|
428
428
|
*/
|
|
429
429
|
private getDefaultSponsoredCallback;
|
|
430
430
|
/**
|
|
431
|
-
* Lazily builds (and caches) the
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
431
|
+
* Lazily builds (and caches) the Permit2 sponsored callback for one asset.
|
|
432
|
+
* Used for WETH always, and for USDC when the connected owner is a smart
|
|
433
|
+
* account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
|
|
434
|
+
* contract signature, while Permit2 supports ERC-1271.
|
|
435
435
|
*/
|
|
436
|
-
private
|
|
436
|
+
private getDefaultPermit2Callback;
|
|
437
|
+
/**
|
|
438
|
+
* True when the connected owner has bytecode — a deployed smart wallet, or an
|
|
439
|
+
* EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
|
|
440
|
+
* Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
|
|
441
|
+
*
|
|
442
|
+
* Fails OPEN: an unreadable code check returns false so a working EOA deposit
|
|
443
|
+
* is never blocked by a flaky provider.
|
|
444
|
+
*/
|
|
445
|
+
private ownerHasCode;
|
|
437
446
|
private getAgent;
|
|
438
447
|
private getActiveAgents;
|
|
439
448
|
/**
|
|
@@ -483,13 +492,16 @@ declare class OwneySDK {
|
|
|
483
492
|
* 1. Missing Permit2 allowance: when the app did not supply its own
|
|
484
493
|
* callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
|
|
485
494
|
* WETH deposit, this is the wallet's first gasless WETH deposit. We send
|
|
486
|
-
* the one-time (user-paid) Permit2 approval via `approvePermit2()
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
*
|
|
491
|
-
*
|
|
492
|
-
*
|
|
495
|
+
* the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
|
|
496
|
+
* for the new allowance to actually be readable via
|
|
497
|
+
* `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
|
|
498
|
+
* mined tx), then retry the SAME sponsored attempt once. Bounded to one
|
|
499
|
+
* approval attempt per call so a wallet/agent that keeps reporting the
|
|
500
|
+
* allowance as missing can't loop forever. If `approvePermit2()` itself
|
|
501
|
+
* throws (e.g. the user rejects the wallet prompt), that error
|
|
502
|
+
* propagates as-is — the user said no to a transaction, so we must not
|
|
503
|
+
* turn around and ask them to pay for a different one via the fallback
|
|
504
|
+
* below.
|
|
493
505
|
* 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
|
|
494
506
|
* the infrastructure layer in a provably safe-to-retry way, so we warn
|
|
495
507
|
* and retry once with `undefined`, falling through to a user-paid
|
|
@@ -501,6 +513,18 @@ declare class OwneySDK {
|
|
|
501
513
|
* propagates unchanged. Scoped per agent invocation so one agent's failure
|
|
502
514
|
* doesn't force a retry of sibling agents in a multi-agent split.
|
|
503
515
|
*/
|
|
516
|
+
/**
|
|
517
|
+
* Polls the owner's Permit2 allowance for `asset` until it covers `amount`
|
|
518
|
+
* or the attempt budget runs out. Called right after `approvePermit2()`
|
|
519
|
+
* resolves: the approval tx is mined, but the wallet's own RPC can still
|
|
520
|
+
* serve a stale (zero) allowance on the very next read (replica lag), which
|
|
521
|
+
* would otherwise make the immediate retry fail with the same
|
|
522
|
+
* PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
|
|
523
|
+
* throws — if the allowance still hasn't caught up after the budget, the
|
|
524
|
+
* caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
|
|
525
|
+
* rather than this helper inventing a different one.
|
|
526
|
+
*/
|
|
527
|
+
private waitForPermit2Allowance;
|
|
504
528
|
private depositWithFallback;
|
|
505
529
|
private getMinDepositAmount;
|
|
506
530
|
private splitDepositAmount;
|
|
@@ -578,14 +602,16 @@ declare class OwneySDK {
|
|
|
578
602
|
*/
|
|
579
603
|
ensureAutoSelectProtocols(asset: "USDC" | "WETH", agentId?: AgentId): Promise<boolean>;
|
|
580
604
|
/**
|
|
581
|
-
* One-time, user-paid approval of Permit2 on the sponsored
|
|
582
|
-
* the active chain. Required once per wallet per chain before
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
605
|
+
* One-time, user-paid approval of Permit2 on the given sponsored token for
|
|
606
|
+
* the active chain. Required once per wallet per chain per token before a
|
|
607
|
+
* gasless deposit of that asset over Permit2; afterwards deposit() is
|
|
608
|
+
* signature-only for it. This call itself is a real transaction the user
|
|
609
|
+
* pays gas for. Resolves only after the approval transaction is mined (1
|
|
610
|
+
* confirmation), so a subsequent deposit() will see the new allowance;
|
|
611
|
+
* throws if the transaction reverted.
|
|
586
612
|
* @returns the approval transaction hash.
|
|
587
613
|
*/
|
|
588
|
-
approvePermit2(asset?: "WETH"): Promise<`0x${string}`>;
|
|
614
|
+
approvePermit2(asset?: "WETH" | "USDC"): Promise<`0x${string}`>;
|
|
589
615
|
/**
|
|
590
616
|
* Get the agent's average APY performance over a time period. Does not require a wallet connection.
|
|
591
617
|
* @param options - Contains agentId (optional) and days ("7D", "14D", or "30D")
|
package/dist/index.js
CHANGED
|
@@ -1303,43 +1303,7 @@ var ZyfaiAgent = class {
|
|
|
1303
1303
|
* recoverable (ensureWalletDeployed tries the other deploy call), but
|
|
1304
1304
|
* failing the deposit outright is not.
|
|
1305
1305
|
*/
|
|
1306
|
-
/**
|
|
1307
|
-
* Zyfai's own answer to "what kind of wallet is this, and can the agent act on
|
|
1308
|
-
* it", read from the positions payload (`GET /data/position`, which
|
|
1309
|
-
* `getPositions` wraps).
|
|
1310
|
-
*
|
|
1311
|
-
* `predeployed` and `hasActiveSessionKey` are both returned at runtime but
|
|
1312
|
-
* declared on neither `Portfolio` nor their docs, hence the cast. Verified
|
|
1313
|
-
* live: an old wallet reports `{predeployed: false, hasActiveSessionKey:
|
|
1314
|
-
* false}` while a pool wallet reports `{true, true}`.
|
|
1315
|
-
*
|
|
1316
|
-
* Preferred over the SDK's private `isPredeployed`, which is cached from the
|
|
1317
|
-
* login response and can go stale mid-session. Returns an empty object on any
|
|
1318
|
-
* failure so every caller degrades to its own fallback.
|
|
1319
|
-
*/
|
|
1320
|
-
async readWalletState(ownerAddress) {
|
|
1321
|
-
try {
|
|
1322
|
-
const { portfolio } = await this.sdk.getPositions(ownerAddress);
|
|
1323
|
-
const raw = portfolio;
|
|
1324
|
-
return {
|
|
1325
|
-
predeployed: raw?.predeployed,
|
|
1326
|
-
hasActiveSessionKey: raw?.hasActiveSessionKey
|
|
1327
|
-
};
|
|
1328
|
-
} catch (error) {
|
|
1329
|
-
console.warn(
|
|
1330
|
-
"[zyfai] getPositions wallet-state read failed (non-fatal):",
|
|
1331
|
-
error instanceof Error ? error.message : String(error)
|
|
1332
|
-
);
|
|
1333
|
-
return {};
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
1306
|
async isPredeployedPoolWallet(ownerAddress, smartWallet) {
|
|
1337
|
-
const { predeployed } = await this.readWalletState(ownerAddress);
|
|
1338
|
-
if (typeof predeployed === "boolean") return predeployed;
|
|
1339
|
-
const internals = this.sdk;
|
|
1340
|
-
if (typeof internals.isPredeployed === "boolean") {
|
|
1341
|
-
return internals.isPredeployed;
|
|
1342
|
-
}
|
|
1343
1307
|
try {
|
|
1344
1308
|
const byEoa = await this.sdk.getSmartWalletByEOA(ownerAddress);
|
|
1345
1309
|
return typeof byEoa?.smartWallet === "string" && byEoa.smartWallet.toLowerCase() === smartWallet.toLowerCase();
|
|
@@ -1381,13 +1345,7 @@ var ZyfaiAgent = class {
|
|
|
1381
1345
|
chainId
|
|
1382
1346
|
);
|
|
1383
1347
|
if (!wallet.isDeployed) return;
|
|
1384
|
-
|
|
1385
|
-
await this.ensureSessionKey(
|
|
1386
|
-
ownerAddress,
|
|
1387
|
-
chainId,
|
|
1388
|
-
wallet.address,
|
|
1389
|
-
hasActiveSessionKey
|
|
1390
|
-
);
|
|
1348
|
+
await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
|
|
1391
1349
|
} catch (error) {
|
|
1392
1350
|
console.warn(
|
|
1393
1351
|
"[zyfai] session-key check at activation failed (non-fatal):",
|
|
@@ -1395,19 +1353,15 @@ var ZyfaiAgent = class {
|
|
|
1395
1353
|
);
|
|
1396
1354
|
}
|
|
1397
1355
|
}
|
|
1398
|
-
async ensureSessionKey(ownerAddress, chainId, smartWallet
|
|
1356
|
+
async ensureSessionKey(ownerAddress, chainId, smartWallet) {
|
|
1399
1357
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
}
|
|
1407
|
-
const installed = await this.readSmartSessionsInstalled(
|
|
1408
|
-
chainId,
|
|
1409
|
-
smartWallet
|
|
1410
|
-
);
|
|
1358
|
+
const installed = await this.getPublicClient(chainId).readContract({
|
|
1359
|
+
address: smartWallet,
|
|
1360
|
+
abi: ERC7579_IS_MODULE_INSTALLED_ABI,
|
|
1361
|
+
functionName: "isModuleInstalled",
|
|
1362
|
+
// moduleTypeId 1 = validator (ERC-7579)
|
|
1363
|
+
args: [1n, SMART_SESSIONS_VALIDATOR, "0x"]
|
|
1364
|
+
});
|
|
1411
1365
|
if (installed) return;
|
|
1412
1366
|
console.warn(
|
|
1413
1367
|
`[zyfai] Safe ${smartWallet} has no smart-sessions validator on chain ${chainId}; creating a session key so the agent can allocate.`
|
|
@@ -1420,16 +1374,6 @@ var ZyfaiAgent = class {
|
|
|
1420
1374
|
);
|
|
1421
1375
|
}
|
|
1422
1376
|
}
|
|
1423
|
-
/** Ground truth for "can Zyfai's automation act on this Safe, on this chain". */
|
|
1424
|
-
async readSmartSessionsInstalled(chainId, smartWallet) {
|
|
1425
|
-
return this.getPublicClient(chainId).readContract({
|
|
1426
|
-
address: smartWallet,
|
|
1427
|
-
abi: ERC7579_IS_MODULE_INSTALLED_ABI,
|
|
1428
|
-
functionName: "isModuleInstalled",
|
|
1429
|
-
// moduleTypeId 1 = validator (ERC-7579)
|
|
1430
|
-
args: [1n, SMART_SESSIONS_VALIDATOR, "0x"]
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
1377
|
/**
|
|
1434
1378
|
* Guarantee the user's Safe has code on `chainId` before anything transfers
|
|
1435
1379
|
* funds into it, and return its address.
|
|
@@ -1451,13 +1395,7 @@ var ZyfaiAgent = class {
|
|
|
1451
1395
|
async ensureWalletDeployed(ownerAddress, chainId) {
|
|
1452
1396
|
const wallet = await this.sdk.getSmartWalletAddress(ownerAddress, chainId);
|
|
1453
1397
|
if (wallet.isDeployed) {
|
|
1454
|
-
|
|
1455
|
-
await this.ensureSessionKey(
|
|
1456
|
-
ownerAddress,
|
|
1457
|
-
chainId,
|
|
1458
|
-
wallet.address,
|
|
1459
|
-
hasActiveSessionKey
|
|
1460
|
-
);
|
|
1398
|
+
await this.ensureSessionKey(ownerAddress, chainId, wallet.address);
|
|
1461
1399
|
return wallet.address;
|
|
1462
1400
|
}
|
|
1463
1401
|
const poolFirst = await this.isPredeployedPoolWallet(
|
|
@@ -1945,9 +1883,12 @@ async function postSponsorTransferAuth(input) {
|
|
|
1945
1883
|
{
|
|
1946
1884
|
statusCode: res.status,
|
|
1947
1885
|
responseBody: text.slice(0, 500),
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
1950
|
-
|
|
1886
|
+
// A 4xx is an explicit refusal before execution (DTO validation,
|
|
1887
|
+
// signature rejection, simulation) — nothing was broadcast, so a
|
|
1888
|
+
// user-paid retry cannot double-spend. 503 SPONSOR_UNAVAILABLE means
|
|
1889
|
+
// the relayer was out of gas, also pre-broadcast. Any other 5xx stays
|
|
1890
|
+
// ambiguous: the transfer may already be in flight.
|
|
1891
|
+
safeToFallback: res.status >= 400 && res.status < 500 || res.status === 503
|
|
1951
1892
|
}
|
|
1952
1893
|
);
|
|
1953
1894
|
}
|
|
@@ -2219,9 +2160,9 @@ function makeSponsoredDepositCallback(deps) {
|
|
|
2219
2160
|
};
|
|
2220
2161
|
}
|
|
2221
2162
|
|
|
2222
|
-
// src/lib/sponsored-
|
|
2163
|
+
// src/lib/sponsored-permit2-deposit.ts
|
|
2223
2164
|
var PERMIT_WINDOW_SECONDS = 15 * 60;
|
|
2224
|
-
function
|
|
2165
|
+
function makeSponsoredPermit2Callback(deps) {
|
|
2225
2166
|
const get = deps.httpGet ?? getSponsorRelayerAddress;
|
|
2226
2167
|
const post = deps.httpPost ?? postSponsorPermit2Transfer;
|
|
2227
2168
|
return async (smartWallet, chainId, amount) => {
|
|
@@ -2230,7 +2171,7 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2230
2171
|
if (!token) {
|
|
2231
2172
|
throw new OwneyError(
|
|
2232
2173
|
"CHAIN_UNSUPPORTED",
|
|
2233
|
-
`No sponsored
|
|
2174
|
+
`No sponsored ${deps.assetLabel} configured for chain ${chainId}`
|
|
2234
2175
|
);
|
|
2235
2176
|
}
|
|
2236
2177
|
const amountWei = BigInt(amount);
|
|
@@ -2242,14 +2183,14 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2242
2183
|
if (balance < amountWei) {
|
|
2243
2184
|
throw new OwneyError(
|
|
2244
2185
|
"DEPOSIT_INSUFFICIENT_BALANCE",
|
|
2245
|
-
|
|
2186
|
+
`Insufficient ${deps.assetLabel} balance for this deposit.`,
|
|
2246
2187
|
{ token, chainId: cid, balance: balance.toString(), amount }
|
|
2247
2188
|
);
|
|
2248
2189
|
}
|
|
2249
2190
|
} catch (err) {
|
|
2250
2191
|
if (err instanceof OwneyError) throw err;
|
|
2251
2192
|
console.warn(
|
|
2252
|
-
|
|
2193
|
+
`[owney-sdk] ${deps.assetLabel} balance pre-check failed (non-fatal):`,
|
|
2253
2194
|
err instanceof Error ? err.message : String(err)
|
|
2254
2195
|
);
|
|
2255
2196
|
}
|
|
@@ -2257,7 +2198,7 @@ function makeSponsoredWethCallback(deps) {
|
|
|
2257
2198
|
if (allowance < amountWei) {
|
|
2258
2199
|
throw new OwneyError(
|
|
2259
2200
|
"PERMIT2_APPROVAL_REQUIRED",
|
|
2260
|
-
|
|
2201
|
+
`${deps.assetLabel} gasless deposits need a one-time Permit2 approval; call approvePermit2("${deps.assetLabel}") first`,
|
|
2261
2202
|
{ token, chainId: cid, allowance: allowance.toString(), amount }
|
|
2262
2203
|
);
|
|
2263
2204
|
}
|
|
@@ -2338,6 +2279,8 @@ var SPONSORED_WETH_BY_CHAIN = {
|
|
|
2338
2279
|
42161: "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
|
|
2339
2280
|
1: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
|
2340
2281
|
};
|
|
2282
|
+
var PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS = 10;
|
|
2283
|
+
var PERMIT2_ALLOWANCE_POLL_INTERVAL_MS = 1500;
|
|
2341
2284
|
function shouldFallbackToUserPaid(error, asset, appCallback) {
|
|
2342
2285
|
return (asset === "WETH" || asset === "USDC") && appCallback === void 0 && error instanceof OwneyError && error.code === "SPONSOR_REQUEST_FAILED" && error.details?.safeToFallback === true;
|
|
2343
2286
|
}
|
|
@@ -2362,7 +2305,7 @@ var OwneySDK = class {
|
|
|
2362
2305
|
routingApiBaseUrl;
|
|
2363
2306
|
referralSource;
|
|
2364
2307
|
cachedSponsoredCallback = null;
|
|
2365
|
-
|
|
2308
|
+
cachedPermit2Callbacks = /* @__PURE__ */ new Map();
|
|
2366
2309
|
initializingAgentsPromise = null;
|
|
2367
2310
|
constructor(config) {
|
|
2368
2311
|
this.apiKey = config.apiKey;
|
|
@@ -2390,7 +2333,7 @@ var OwneySDK = class {
|
|
|
2390
2333
|
}
|
|
2391
2334
|
this.state = { provider, walletAddress, chainId: null };
|
|
2392
2335
|
this.cachedSponsoredCallback = null;
|
|
2393
|
-
this.
|
|
2336
|
+
this.cachedPermit2Callbacks.clear();
|
|
2394
2337
|
}
|
|
2395
2338
|
/**
|
|
2396
2339
|
* Disconnect from all agents and clear connection state.
|
|
@@ -2404,7 +2347,7 @@ var OwneySDK = class {
|
|
|
2404
2347
|
this.disabledAgents.clear();
|
|
2405
2348
|
this.state = null;
|
|
2406
2349
|
this.cachedSponsoredCallback = null;
|
|
2407
|
-
this.
|
|
2350
|
+
this.cachedPermit2Callbacks.clear();
|
|
2408
2351
|
}
|
|
2409
2352
|
isAgentDisabled(agentId) {
|
|
2410
2353
|
return this.disabledAgents.has(agentId);
|
|
@@ -2481,25 +2424,23 @@ var OwneySDK = class {
|
|
|
2481
2424
|
return callback;
|
|
2482
2425
|
}
|
|
2483
2426
|
/**
|
|
2484
|
-
* Lazily builds (and caches) the
|
|
2485
|
-
*
|
|
2486
|
-
*
|
|
2487
|
-
*
|
|
2427
|
+
* Lazily builds (and caches) the Permit2 sponsored callback for one asset.
|
|
2428
|
+
* Used for WETH always, and for USDC when the connected owner is a smart
|
|
2429
|
+
* account — EIP-3009 verifies with ecrecover inside USDC and cannot accept a
|
|
2430
|
+
* contract signature, while Permit2 supports ERC-1271.
|
|
2488
2431
|
*/
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2432
|
+
getDefaultPermit2Callback(asset, onApproved) {
|
|
2433
|
+
const cached = this.cachedPermit2Callbacks.get(asset);
|
|
2434
|
+
if (!onApproved && cached) return cached;
|
|
2492
2435
|
const provider = this.requireConnectedProvider();
|
|
2493
2436
|
const owner = this.state.walletAddress;
|
|
2494
|
-
const callback =
|
|
2437
|
+
const callback = makeSponsoredPermit2Callback({
|
|
2495
2438
|
apiKey: this.apiKey,
|
|
2496
2439
|
baseUrl: this.routingApiBaseUrl,
|
|
2497
2440
|
ownerAddress: owner,
|
|
2441
|
+
assetLabel: asset,
|
|
2498
2442
|
onApproved,
|
|
2499
|
-
tokenAddressByChain: SPONSORED_WETH_BY_CHAIN,
|
|
2500
|
-
// Casts work around viem's chain-narrowed Client vs the generic
|
|
2501
|
-
// PublicClient/WalletClient param types — structurally identical at
|
|
2502
|
-
// runtime, but the two share a name TS treats as unrelated.
|
|
2443
|
+
tokenAddressByChain: asset === "USDC" ? SPONSORED_USDC_BY_CHAIN : SPONSORED_WETH_BY_CHAIN,
|
|
2503
2444
|
getPublicClient: (cid) => createPublicClient2({
|
|
2504
2445
|
chain: VIEM_CHAIN2[cid],
|
|
2505
2446
|
transport: custom(provider)
|
|
@@ -2510,9 +2451,34 @@ var OwneySDK = class {
|
|
|
2510
2451
|
transport: custom(provider)
|
|
2511
2452
|
})
|
|
2512
2453
|
});
|
|
2513
|
-
if (!onApproved) this.
|
|
2454
|
+
if (!onApproved) this.cachedPermit2Callbacks.set(asset, callback);
|
|
2514
2455
|
return callback;
|
|
2515
2456
|
}
|
|
2457
|
+
/**
|
|
2458
|
+
* True when the connected owner has bytecode — a deployed smart wallet, or an
|
|
2459
|
+
* EOA carrying an EIP-7702 delegation (the Base app's "Upgrade your wallet").
|
|
2460
|
+
* Such wallets sign via ERC-1271, which EIP-3009 cannot verify.
|
|
2461
|
+
*
|
|
2462
|
+
* Fails OPEN: an unreadable code check returns false so a working EOA deposit
|
|
2463
|
+
* is never blocked by a flaky provider.
|
|
2464
|
+
*/
|
|
2465
|
+
async ownerHasCode() {
|
|
2466
|
+
try {
|
|
2467
|
+
const provider = this.requireConnectedProvider();
|
|
2468
|
+
const code = await provider.request({
|
|
2469
|
+
method: "eth_getCode",
|
|
2470
|
+
params: [this.state.walletAddress, "latest"]
|
|
2471
|
+
});
|
|
2472
|
+
return Boolean(code) && code !== "0x";
|
|
2473
|
+
} catch (error) {
|
|
2474
|
+
debugLog(
|
|
2475
|
+
"deposit",
|
|
2476
|
+
"owner code check failed; assuming EOA",
|
|
2477
|
+
error instanceof Error ? error.message : String(error)
|
|
2478
|
+
);
|
|
2479
|
+
return false;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2516
2482
|
getAgent(agentId) {
|
|
2517
2483
|
const agent = this.agents.get(agentId);
|
|
2518
2484
|
if (!agent) {
|
|
@@ -2708,7 +2674,11 @@ var OwneySDK = class {
|
|
|
2708
2674
|
}
|
|
2709
2675
|
const state = this.requireState();
|
|
2710
2676
|
const chainId = this.requireChainId();
|
|
2711
|
-
const
|
|
2677
|
+
const ownerIsSmartAccount = depositCallback === void 0 && asset !== "WETH" ? await this.ownerHasCode() : false;
|
|
2678
|
+
const effectiveCallback = depositCallback ?? (asset === "WETH" || ownerIsSmartAccount ? this.getDefaultPermit2Callback(
|
|
2679
|
+
asset === "WETH" ? "WETH" : "USDC",
|
|
2680
|
+
onApproved
|
|
2681
|
+
) : this.getDefaultSponsoredCallback(onApproved));
|
|
2712
2682
|
if (agentId) {
|
|
2713
2683
|
if (this.isAgentDisabled(agentId)) {
|
|
2714
2684
|
throw new OwneyError(
|
|
@@ -2777,13 +2747,16 @@ var OwneySDK = class {
|
|
|
2777
2747
|
* 1. Missing Permit2 allowance: when the app did not supply its own
|
|
2778
2748
|
* callback and the attempt fails with `PERMIT2_APPROVAL_REQUIRED` on a
|
|
2779
2749
|
* WETH deposit, this is the wallet's first gasless WETH deposit. We send
|
|
2780
|
-
* the one-time (user-paid) Permit2 approval via `approvePermit2()
|
|
2781
|
-
*
|
|
2782
|
-
*
|
|
2783
|
-
*
|
|
2784
|
-
*
|
|
2785
|
-
*
|
|
2786
|
-
*
|
|
2750
|
+
* the one-time (user-paid) Permit2 approval via `approvePermit2()`, wait
|
|
2751
|
+
* for the new allowance to actually be readable via
|
|
2752
|
+
* `waitForPermit2Allowance()` (the wallet's own RPC can lag a freshly
|
|
2753
|
+
* mined tx), then retry the SAME sponsored attempt once. Bounded to one
|
|
2754
|
+
* approval attempt per call so a wallet/agent that keeps reporting the
|
|
2755
|
+
* allowance as missing can't loop forever. If `approvePermit2()` itself
|
|
2756
|
+
* throws (e.g. the user rejects the wallet prompt), that error
|
|
2757
|
+
* propagates as-is — the user said no to a transaction, so we must not
|
|
2758
|
+
* turn around and ask them to pay for a different one via the fallback
|
|
2759
|
+
* below.
|
|
2787
2760
|
* 2. `shouldFallbackToUserPaid`: the SDK's own WETH sponsor path failed at
|
|
2788
2761
|
* the infrastructure layer in a provably safe-to-retry way, so we warn
|
|
2789
2762
|
* and retry once with `undefined`, falling through to a user-paid
|
|
@@ -2795,6 +2768,49 @@ var OwneySDK = class {
|
|
|
2795
2768
|
* propagates unchanged. Scoped per agent invocation so one agent's failure
|
|
2796
2769
|
* doesn't force a retry of sibling agents in a multi-agent split.
|
|
2797
2770
|
*/
|
|
2771
|
+
/**
|
|
2772
|
+
* Polls the owner's Permit2 allowance for `asset` until it covers `amount`
|
|
2773
|
+
* or the attempt budget runs out. Called right after `approvePermit2()`
|
|
2774
|
+
* resolves: the approval tx is mined, but the wallet's own RPC can still
|
|
2775
|
+
* serve a stale (zero) allowance on the very next read (replica lag), which
|
|
2776
|
+
* would otherwise make the immediate retry fail with the same
|
|
2777
|
+
* PERMIT2_APPROVAL_REQUIRED error the user just approved past. Never
|
|
2778
|
+
* throws — if the allowance still hasn't caught up after the budget, the
|
|
2779
|
+
* caller's retry will surface the honest PERMIT2_APPROVAL_REQUIRED error
|
|
2780
|
+
* rather than this helper inventing a different one.
|
|
2781
|
+
*/
|
|
2782
|
+
async waitForPermit2Allowance(asset, amount) {
|
|
2783
|
+
const state = this.requireState();
|
|
2784
|
+
const chainId = this.requireChainId();
|
|
2785
|
+
const provider = this.requireConnectedProvider();
|
|
2786
|
+
const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
|
|
2787
|
+
if (!token) return;
|
|
2788
|
+
const publicClient = createPublicClient2({
|
|
2789
|
+
chain: VIEM_CHAIN2[chainId],
|
|
2790
|
+
transport: custom(provider)
|
|
2791
|
+
});
|
|
2792
|
+
for (let attempt = 0; attempt < PERMIT2_ALLOWANCE_POLL_MAX_ATTEMPTS; attempt++) {
|
|
2793
|
+
if (attempt > 0) {
|
|
2794
|
+
await new Promise(
|
|
2795
|
+
(resolve) => setTimeout(resolve, PERMIT2_ALLOWANCE_POLL_INTERVAL_MS)
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
try {
|
|
2799
|
+
const allowance = await readPermit2Allowance(
|
|
2800
|
+
publicClient,
|
|
2801
|
+
token,
|
|
2802
|
+
state.walletAddress
|
|
2803
|
+
);
|
|
2804
|
+
if (allowance >= amount) return;
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
debugLog(
|
|
2807
|
+
"deposit",
|
|
2808
|
+
"Permit2 allowance poll read failed; retrying",
|
|
2809
|
+
error instanceof Error ? error.message : String(error)
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2798
2814
|
async depositWithFallback(agent, state, chainId, amount, asset, effectiveCallback, appCallback) {
|
|
2799
2815
|
const attempt = (callback) => withFailureReporting(
|
|
2800
2816
|
this.apiKey,
|
|
@@ -2812,12 +2828,16 @@ var OwneySDK = class {
|
|
|
2812
2828
|
try {
|
|
2813
2829
|
return await attempt(effectiveCallback);
|
|
2814
2830
|
} catch (error) {
|
|
2815
|
-
if (!approvalAttempted && appCallback === void 0 && asset === "WETH" && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
|
|
2831
|
+
if (!approvalAttempted && appCallback === void 0 && (asset === "WETH" || asset === "USDC") && error instanceof OwneyError && error.code === "PERMIT2_APPROVAL_REQUIRED") {
|
|
2816
2832
|
approvalAttempted = true;
|
|
2817
2833
|
console.warn(
|
|
2818
|
-
|
|
2834
|
+
`[owney-sdk] First ${asset} gasless deposit: sending one-time Permit2 approval...`
|
|
2835
|
+
);
|
|
2836
|
+
await this.approvePermit2(asset);
|
|
2837
|
+
await this.waitForPermit2Allowance(
|
|
2838
|
+
asset,
|
|
2839
|
+
BigInt(amount)
|
|
2819
2840
|
);
|
|
2820
|
-
await this.approvePermit2();
|
|
2821
2841
|
continue;
|
|
2822
2842
|
}
|
|
2823
2843
|
if (!shouldFallbackToUserPaid(error, asset, appCallback)) throw error;
|
|
@@ -3331,23 +3351,32 @@ var OwneySDK = class {
|
|
|
3331
3351
|
return agent.ensureAutoSelectProtocols(state, chainId, asset);
|
|
3332
3352
|
}
|
|
3333
3353
|
/**
|
|
3334
|
-
* One-time, user-paid approval of Permit2 on the sponsored
|
|
3335
|
-
* the active chain. Required once per wallet per chain before
|
|
3336
|
-
*
|
|
3337
|
-
*
|
|
3338
|
-
*
|
|
3354
|
+
* One-time, user-paid approval of Permit2 on the given sponsored token for
|
|
3355
|
+
* the active chain. Required once per wallet per chain per token before a
|
|
3356
|
+
* gasless deposit of that asset over Permit2; afterwards deposit() is
|
|
3357
|
+
* signature-only for it. This call itself is a real transaction the user
|
|
3358
|
+
* pays gas for. Resolves only after the approval transaction is mined (1
|
|
3359
|
+
* confirmation), so a subsequent deposit() will see the new allowance;
|
|
3360
|
+
* throws if the transaction reverted.
|
|
3339
3361
|
* @returns the approval transaction hash.
|
|
3340
3362
|
*/
|
|
3341
3363
|
async approvePermit2(asset = "WETH") {
|
|
3342
|
-
void asset;
|
|
3343
3364
|
const state = this.requireState();
|
|
3344
3365
|
const chainId = this.requireChainId();
|
|
3345
|
-
this.
|
|
3346
|
-
|
|
3366
|
+
let guard = this.agents.get("zyfai");
|
|
3367
|
+
if (!guard) {
|
|
3368
|
+
try {
|
|
3369
|
+
guard = this.getEligibleAgents(chainId, asset)[0];
|
|
3370
|
+
} catch {
|
|
3371
|
+
guard = void 0;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (guard) this.validateAssetSupport(guard, chainId, asset);
|
|
3375
|
+
const token = asset === "USDC" ? SPONSORED_USDC_BY_CHAIN[chainId] : SPONSORED_WETH_BY_CHAIN[chainId];
|
|
3347
3376
|
if (!token) {
|
|
3348
3377
|
throw new OwneyError(
|
|
3349
3378
|
"CHAIN_UNSUPPORTED",
|
|
3350
|
-
`No sponsored
|
|
3379
|
+
`No sponsored ${asset} on chain ${chainId}`
|
|
3351
3380
|
);
|
|
3352
3381
|
}
|
|
3353
3382
|
const provider = this.requireConnectedProvider();
|