@owney/sdk 0.7.7-beta.6 → 0.7.8

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/README.md CHANGED
@@ -263,6 +263,25 @@ console.log(allEarnings.agentEarnings.zyfai.lifetimeEarnings); // 42.5
263
263
  console.log(allEarnings.agentEarnings.zyfai.tokens); // per-chain, per-asset breakdown
264
264
  ```
265
265
 
266
+ ### `refreshEarnings(agentId?)`
267
+
268
+ Refresh earnings for agents whose earnings data may be outdated. If the latest
269
+ snapshot is less than one hour old, the SDK returns it without recalculating.
270
+
271
+ | Param | Type | Description |
272
+ | --------- | -------------------- | ------------------------------------ |
273
+ | `agentId` | `"zyfai"` (optional) | Agent to refresh. Omit for all agents. |
274
+
275
+ Returns: `AgentEarnings` (single agent) or `OwneyEarnings` (all agents)
276
+
277
+ ```typescript
278
+ // Single agent
279
+ const refreshed = await sdk.refreshEarnings("zyfai");
280
+
281
+ // All agents
282
+ const allRefreshed = await sdk.refreshEarnings();
283
+ ```
284
+
266
285
  ### `getAccountApy({ agentId?, days })`
267
286
 
268
287
  Get the weighted APY for the user's account. When querying all agents, the total APY is a balance-weighted average.
package/dist/index.cjs CHANGED
@@ -871,6 +871,7 @@ var WETH_ADDRESS_BY_CHAIN = {
871
871
  var VIEM_CHAIN = { 8453: import_chains.base, 42161: import_chains.arbitrum, 1: import_chains.mainnet };
872
872
  var SPLITTING_ASSETS = ["USDC", "WETH"];
873
873
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
874
+ var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
874
875
  var ZyfaiAgent = class {
875
876
  id = "zyfai";
876
877
  supportedChainIds = [8453, 42161, 1];
@@ -922,6 +923,7 @@ var ZyfaiAgent = class {
922
923
  // successful decode is cached forever; failures are NOT cached so a transient
923
924
  // RPC error retries on the next fetch.
924
925
  withdrawAmountCache = /* @__PURE__ */ new Map();
926
+ earningsRefreshInFlight = null;
925
927
  constructor(apiKey, rpcUrls, referralSource) {
926
928
  this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
927
929
  this.sdk = new import_sdk.ZyfaiSDK({
@@ -1719,6 +1721,22 @@ var ZyfaiAgent = class {
1719
1721
  const raw = await this.sdk.getOnchainEarnings(smartWallet);
1720
1722
  return mapEarnings(raw, smartWallet);
1721
1723
  }
1724
+ async refreshEarnings(state, chainId) {
1725
+ if (this.earningsRefreshInFlight) return this.earningsRefreshInFlight;
1726
+ this.earningsRefreshInFlight = (async () => {
1727
+ const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1728
+ const current = await this.sdk.getOnchainEarnings(smartWallet);
1729
+ const lastCheck = current.data.lastCheckTimestamp ? Date.parse(current.data.lastCheckTimestamp) : Number.NaN;
1730
+ const isFresh = Number.isFinite(lastCheck) && Date.now() - lastCheck < EARNINGS_REFRESH_COOLDOWN_MS;
1731
+ const earnings = isFresh ? current : await this.sdk.calculateOnchainEarnings(smartWallet);
1732
+ return mapEarnings(earnings, smartWallet);
1733
+ })();
1734
+ try {
1735
+ return await this.earningsRefreshInFlight;
1736
+ } finally {
1737
+ this.earningsRefreshInFlight = null;
1738
+ }
1739
+ }
1722
1740
  async getAccountApy(state, chainId, days, tokenSymbol) {
1723
1741
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1724
1742
  const raw = await this.sdk.getDailyApyHistory(smartWallet, days);
@@ -2025,7 +2043,7 @@ function aggregateApyByChainAndAsset(agentApys, agentBalances) {
2025
2043
  }
2026
2044
 
2027
2045
  // src/client.ts
2028
- var import_viem5 = require("viem");
2046
+ var import_viem6 = require("viem");
2029
2047
  var import_chains2 = require("viem/chains");
2030
2048
 
2031
2049
  // src/lib/transfer-auth.ts
@@ -2455,6 +2473,97 @@ function makeSponsoredWethCallback(deps) {
2455
2473
  };
2456
2474
  }
2457
2475
 
2476
+ // src/lib/sponsored-calls-deposit.ts
2477
+ var import_viem5 = require("viem");
2478
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
2479
+ var DEFAULT_MAX_POLLS = 30;
2480
+ async function paymasterSupported(provider, owner, chainId) {
2481
+ const caps = await provider.request({
2482
+ method: "wallet_getCapabilities",
2483
+ params: [owner]
2484
+ });
2485
+ const forChain = caps?.[(0, import_viem5.toHex)(chainId)] ?? caps?.[String(chainId)];
2486
+ return Boolean(forChain?.paymasterService?.supported);
2487
+ }
2488
+ function makeSponsoredCallsCallback(deps) {
2489
+ const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2490
+ const maxPolls = deps.maxPolls ?? DEFAULT_MAX_POLLS;
2491
+ const absolutePaymasterUrl = () => {
2492
+ const configured = deps.paymasterServiceUrl;
2493
+ if (/^https?:\/\//i.test(configured)) return configured;
2494
+ const origin = globalThis.location?.origin;
2495
+ if (!origin) {
2496
+ throw new OwneyError(
2497
+ "SPONSORED_CALLS_UNSUPPORTED",
2498
+ `paymasterServiceUrl "${configured}" is relative and no page origin is available to resolve it.`
2499
+ );
2500
+ }
2501
+ return new URL(configured, origin).toString();
2502
+ };
2503
+ return async (smartWallet, chainId, amount) => {
2504
+ const cid = chainId;
2505
+ const token = deps.tokenAddressByChain[cid];
2506
+ if (!token) {
2507
+ throw new OwneyError(
2508
+ "CHAIN_UNSUPPORTED",
2509
+ `No sponsored token configured for chain ${chainId}`
2510
+ );
2511
+ }
2512
+ if (!await paymasterSupported(deps.provider, deps.ownerAddress, chainId)) {
2513
+ throw new OwneyError(
2514
+ "SPONSORED_CALLS_UNSUPPORTED",
2515
+ `Wallet does not advertise paymasterService on chain ${chainId}.`,
2516
+ { chainId }
2517
+ );
2518
+ }
2519
+ const data = (0, import_viem5.encodeFunctionData)({
2520
+ abi: import_viem5.erc20Abi,
2521
+ functionName: "transfer",
2522
+ args: [smartWallet, BigInt(amount)]
2523
+ });
2524
+ const sendResult = await deps.provider.request({
2525
+ method: "wallet_sendCalls",
2526
+ params: [
2527
+ {
2528
+ version: "2.0.0",
2529
+ from: deps.ownerAddress,
2530
+ chainId: (0, import_viem5.toHex)(chainId),
2531
+ atomicRequired: false,
2532
+ calls: [{ to: token, value: "0x0", data }],
2533
+ capabilities: {
2534
+ paymasterService: { url: absolutePaymasterUrl() }
2535
+ }
2536
+ }
2537
+ ]
2538
+ });
2539
+ deps.onApproved?.();
2540
+ const callsId = typeof sendResult === "string" ? sendResult : sendResult?.id;
2541
+ if (!callsId) {
2542
+ throw new OwneyError(
2543
+ "SPONSORED_CALLS_NO_ID",
2544
+ "Wallet accepted wallet_sendCalls but returned no calls id.",
2545
+ { chainId }
2546
+ );
2547
+ }
2548
+ for (let attempt = 0; attempt < maxPolls; attempt += 1) {
2549
+ const status = await deps.provider.request({
2550
+ method: "wallet_getCallsStatus",
2551
+ params: [callsId]
2552
+ });
2553
+ const txHash = status?.receipts?.[0]?.transactionHash;
2554
+ if (txHash) return txHash;
2555
+ if (pollIntervalMs > 0) {
2556
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2557
+ }
2558
+ }
2559
+ throw new OwneyError(
2560
+ "SPONSORED_CALLS_NO_RECEIPT",
2561
+ `No receipt for calls ${callsId} after ${maxPolls} polls; the deposit may still settle.`,
2562
+ { chainId, callsId }
2563
+ );
2564
+ };
2565
+ }
2566
+
2458
2567
  // src/client.ts
2459
2568
  function encodeMultiAgentCursor(map) {
2460
2569
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
@@ -2516,12 +2625,15 @@ var OwneySDK = class {
2516
2625
  referralSource;
2517
2626
  cachedSponsoredCallback = null;
2518
2627
  cachedWethSponsoredCallback = null;
2628
+ paymasterServiceUrl;
2629
+ cachedSponsoredCallsCallbacks = /* @__PURE__ */ new Map();
2519
2630
  initializingAgentsPromise = null;
2520
2631
  constructor(config) {
2521
2632
  this.apiKey = config.apiKey;
2522
2633
  if (config.debug) setOwneyDebug(true);
2523
2634
  this.zyfaiRpcUrls = config.zyfaiRpcUrls;
2524
2635
  this.routingApiBaseUrl = config.routingApiBaseUrl;
2636
+ this.paymasterServiceUrl = config.paymasterServiceUrl;
2525
2637
  this.referralSource = config.referralSource;
2526
2638
  }
2527
2639
  /**
@@ -2544,6 +2656,7 @@ var OwneySDK = class {
2544
2656
  this.state = { provider, walletAddress, chainId: null };
2545
2657
  this.cachedSponsoredCallback = null;
2546
2658
  this.cachedWethSponsoredCallback = null;
2659
+ this.cachedSponsoredCallsCallbacks.clear();
2547
2660
  }
2548
2661
  /**
2549
2662
  * Disconnect from all agents and clear connection state.
@@ -2558,6 +2671,7 @@ var OwneySDK = class {
2558
2671
  this.state = null;
2559
2672
  this.cachedSponsoredCallback = null;
2560
2673
  this.cachedWethSponsoredCallback = null;
2674
+ this.cachedSponsoredCallsCallbacks.clear();
2561
2675
  }
2562
2676
  isAgentDisabled(agentId) {
2563
2677
  return this.disabledAgents.has(agentId);
@@ -2620,14 +2734,14 @@ var OwneySDK = class {
2620
2734
  // Casts work around viem's chain-narrowed Client vs the generic
2621
2735
  // PublicClient/WalletClient param types — structurally identical at
2622
2736
  // runtime, but the two share a name TS treats as unrelated.
2623
- getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
2737
+ getPublicClient: (cid) => (0, import_viem6.createPublicClient)({
2624
2738
  chain: VIEM_CHAIN2[cid],
2625
- transport: (0, import_viem5.custom)(provider)
2739
+ transport: (0, import_viem6.custom)(provider)
2626
2740
  }),
2627
- getWalletClient: (cid) => (0, import_viem5.createWalletClient)({
2741
+ getWalletClient: (cid) => (0, import_viem6.createWalletClient)({
2628
2742
  account: owner,
2629
2743
  chain: VIEM_CHAIN2[cid],
2630
- transport: (0, import_viem5.custom)(provider)
2744
+ transport: (0, import_viem6.custom)(provider)
2631
2745
  })
2632
2746
  });
2633
2747
  if (!onApproved) this.cachedSponsoredCallback = callback;
@@ -2639,6 +2753,26 @@ var OwneySDK = class {
2639
2753
  * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2640
2754
  * `PermitTransferFrom` instead of an EIP-3009 authorization.
2641
2755
  */
2756
+ getDefaultSponsoredCallsCallback(asset, onApproved) {
2757
+ const cached = this.cachedSponsoredCallsCallbacks.get(asset);
2758
+ if (!onApproved && cached) return cached;
2759
+ const provider = this.requireConnectedProvider();
2760
+ const callback = makeSponsoredCallsCallback({
2761
+ provider,
2762
+ ownerAddress: this.state.walletAddress,
2763
+ paymasterServiceUrl: this.paymasterServiceUrl,
2764
+ onApproved,
2765
+ tokenAddressByChain: asset === "WETH" ? SPONSORED_WETH_BY_CHAIN : SPONSORED_USDC_BY_CHAIN
2766
+ });
2767
+ if (!onApproved) this.cachedSponsoredCallsCallbacks.set(asset, callback);
2768
+ return callback;
2769
+ }
2770
+ /**
2771
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2772
+ * callback used when the caller omits `depositCallback` for a WETH deposit.
2773
+ * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2774
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
2775
+ */
2642
2776
  getDefaultWethSponsoredCallback(onApproved) {
2643
2777
  if (!onApproved && this.cachedWethSponsoredCallback)
2644
2778
  return this.cachedWethSponsoredCallback;
@@ -2653,14 +2787,14 @@ var OwneySDK = class {
2653
2787
  // Casts work around viem's chain-narrowed Client vs the generic
2654
2788
  // PublicClient/WalletClient param types — structurally identical at
2655
2789
  // runtime, but the two share a name TS treats as unrelated.
2656
- getPublicClient: (cid) => (0, import_viem5.createPublicClient)({
2790
+ getPublicClient: (cid) => (0, import_viem6.createPublicClient)({
2657
2791
  chain: VIEM_CHAIN2[cid],
2658
- transport: (0, import_viem5.custom)(provider)
2792
+ transport: (0, import_viem6.custom)(provider)
2659
2793
  }),
2660
- getWalletClient: (cid) => (0, import_viem5.createWalletClient)({
2794
+ getWalletClient: (cid) => (0, import_viem6.createWalletClient)({
2661
2795
  account: owner,
2662
2796
  chain: VIEM_CHAIN2[cid],
2663
- transport: (0, import_viem5.custom)(provider)
2797
+ transport: (0, import_viem6.custom)(provider)
2664
2798
  })
2665
2799
  });
2666
2800
  if (!onApproved) this.cachedWethSponsoredCallback = callback;
@@ -2861,7 +2995,7 @@ var OwneySDK = class {
2861
2995
  }
2862
2996
  const state = this.requireState();
2863
2997
  const chainId = this.requireChainId();
2864
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2998
+ const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2865
2999
  if (agentId) {
2866
3000
  if (this.isAgentDisabled(agentId)) {
2867
3001
  throw new OwneyError(
@@ -3303,6 +3437,28 @@ var OwneySDK = class {
3303
3437
  agentEarnings: results
3304
3438
  };
3305
3439
  }
3440
+ /**
3441
+ * Return refreshed earnings while letting each agent own its refresh policy.
3442
+ * Agents without a refresh capability fall back to their current snapshot.
3443
+ */
3444
+ async refreshEarnings(agentId) {
3445
+ const state = this.requireState();
3446
+ const chainId = this.requireChainId();
3447
+ const refreshAgent = (agent) => agent.refreshEarnings?.(state, chainId) ?? agent.getEarnings(state, chainId);
3448
+ if (agentId) return refreshAgent(this.getAgent(agentId));
3449
+ let totalEarnings = 0;
3450
+ const agentEarnings = {};
3451
+ const refreshed = await Promise.all(
3452
+ [...this.getActiveAgents().entries()].map(
3453
+ async ([id, agent]) => [id, await refreshAgent(agent)]
3454
+ )
3455
+ );
3456
+ for (const [id, earnings] of refreshed) {
3457
+ agentEarnings[id] = earnings;
3458
+ totalEarnings += Number(earnings.lifetimeEarnings);
3459
+ }
3460
+ return { totalEarnings: String(totalEarnings), agentEarnings };
3461
+ }
3306
3462
  /**
3307
3463
  * Get the weighted APY for the user's account over a time period.
3308
3464
  * When querying all agents, the total APY is a balance-weighted average.
@@ -3504,10 +3660,10 @@ var OwneySDK = class {
3504
3660
  );
3505
3661
  }
3506
3662
  const provider = this.requireConnectedProvider();
3507
- const wallet = (0, import_viem5.createWalletClient)({
3663
+ const wallet = (0, import_viem6.createWalletClient)({
3508
3664
  account: state.walletAddress,
3509
3665
  chain: VIEM_CHAIN2[chainId],
3510
- transport: (0, import_viem5.custom)(provider)
3666
+ transport: (0, import_viem6.custom)(provider)
3511
3667
  });
3512
3668
  const hash = await wallet.writeContract({
3513
3669
  address: token,
@@ -3517,9 +3673,9 @@ var OwneySDK = class {
3517
3673
  account: state.walletAddress,
3518
3674
  chain: VIEM_CHAIN2[chainId]
3519
3675
  });
3520
- const publicClient = (0, import_viem5.createPublicClient)({
3676
+ const publicClient = (0, import_viem6.createPublicClient)({
3521
3677
  chain: VIEM_CHAIN2[chainId],
3522
- transport: (0, import_viem5.custom)(provider)
3678
+ transport: (0, import_viem6.custom)(provider)
3523
3679
  });
3524
3680
  const receipt = await publicClient.waitForTransactionReceipt({
3525
3681
  hash,
@@ -3628,7 +3784,7 @@ var OwneySDK = class {
3628
3784
  };
3629
3785
 
3630
3786
  // src/agents/zyfai/zyfai.siwx.ts
3631
- var import_viem6 = require("viem");
3787
+ var import_viem7 = require("viem");
3632
3788
  var import_siwe = require("siwe");
3633
3789
  var import_sdk2 = require("@zyfai/sdk");
3634
3790
 
@@ -3755,7 +3911,7 @@ function buildSIWXConfig(deps) {
3755
3911
  issuedAt,
3756
3912
  toString() {
3757
3913
  return new import_siwe.SiweMessage({
3758
- address: (0, import_viem6.getAddress)(accountAddress),
3914
+ address: (0, import_viem7.getAddress)(accountAddress),
3759
3915
  chainId: numericChainId(chainId),
3760
3916
  domain,
3761
3917
  uri,
package/dist/index.d.cts CHANGED
@@ -30,6 +30,17 @@ interface OwneySDKConfig {
30
30
  * already-deployed app: set `globalThis.__OWNEY_DEBUG__ = true` in devtools.
31
31
  */
32
32
  debug?: boolean;
33
+ /**
34
+ * Absolute URL of an ERC-7677 paymaster service. When set, deposits go out as
35
+ * an EIP-5792 `wallet_sendCalls` with `capabilities.paymasterService`, so a
36
+ * wallet that advertises sponsorship pays no native gas — the case EIP-3009
37
+ * cannot serve, because USDC verifies it with `ecrecover` and a smart account
38
+ * signs via ERC-1271.
39
+ *
40
+ * MUST be absolute: the wallet fetches this URL itself, so a relative path is
41
+ * unresolvable. Omit it and every rail behaves exactly as before.
42
+ */
43
+ paymasterServiceUrl?: string;
33
44
  }
34
45
  interface ConnectionState {
35
46
  provider: any;
@@ -372,6 +383,11 @@ interface IAgent {
372
383
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
373
384
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
374
385
  getEarnings(state: ConnectionState, chainId: number): Promise<AgentEarnings>;
386
+ /**
387
+ * Refresh the agent's earnings snapshot when its backend supports doing so.
388
+ * Implementations own freshness and request-deduplication policy.
389
+ */
390
+ refreshEarnings?(state: ConnectionState, chainId: number): Promise<AgentEarnings>;
375
391
  getAccountApy(state: ConnectionState, chainId: number, days: DailyApyDays,
376
392
  /**
377
393
  * Optional asset symbol ("USDC" / "WETH") scoping the daily series to a
@@ -418,6 +434,8 @@ declare class OwneySDK {
418
434
  private referralSource?;
419
435
  private cachedSponsoredCallback;
420
436
  private cachedWethSponsoredCallback;
437
+ private paymasterServiceUrl?;
438
+ private cachedSponsoredCallsCallbacks;
421
439
  private initializingAgentsPromise;
422
440
  constructor(config: OwneySDKConfig);
423
441
  /**
@@ -458,6 +476,13 @@ declare class OwneySDK {
458
476
  * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
459
477
  * `PermitTransferFrom` instead of an EIP-3009 authorization.
460
478
  */
479
+ private getDefaultSponsoredCallsCallback;
480
+ /**
481
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
482
+ * callback used when the caller omits `depositCallback` for a WETH deposit.
483
+ * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
484
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
485
+ */
461
486
  private getDefaultWethSponsoredCallback;
462
487
  private getAgent;
463
488
  private getActiveAgents;
@@ -561,6 +586,11 @@ declare class OwneySDK {
561
586
  * @returns {AgentEarnings} for a single agent, or {OwneyEarnings} with totalEarnings and per-agent breakdown
562
587
  */
563
588
  getEarnings(agentId?: AgentId): Promise<OwneyEarnings | AgentEarnings>;
589
+ /**
590
+ * Return refreshed earnings while letting each agent own its refresh policy.
591
+ * Agents without a refresh capability fall back to their current snapshot.
592
+ */
593
+ refreshEarnings(agentId?: AgentId): Promise<OwneyEarnings | AgentEarnings>;
564
594
  /**
565
595
  * Get the weighted APY for the user's account over a time period.
566
596
  * When querying all agents, the total APY is a balance-weighted average.
@@ -634,7 +664,7 @@ declare class OwneySDK {
634
664
  getAllocationApy({ agentId, }?: AllocationApyOptions): Promise<OwneyAllocationApy>;
635
665
  }
636
666
 
637
- type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
667
+ type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
638
668
  declare class OwneyError extends Error {
639
669
  readonly code: OwneyErrorCode;
640
670
  readonly details?: Record<string, unknown>;
package/dist/index.d.ts CHANGED
@@ -30,6 +30,17 @@ interface OwneySDKConfig {
30
30
  * already-deployed app: set `globalThis.__OWNEY_DEBUG__ = true` in devtools.
31
31
  */
32
32
  debug?: boolean;
33
+ /**
34
+ * Absolute URL of an ERC-7677 paymaster service. When set, deposits go out as
35
+ * an EIP-5792 `wallet_sendCalls` with `capabilities.paymasterService`, so a
36
+ * wallet that advertises sponsorship pays no native gas — the case EIP-3009
37
+ * cannot serve, because USDC verifies it with `ecrecover` and a smart account
38
+ * signs via ERC-1271.
39
+ *
40
+ * MUST be absolute: the wallet fetches this URL itself, so a relative path is
41
+ * unresolvable. Omit it and every rail behaves exactly as before.
42
+ */
43
+ paymasterServiceUrl?: string;
33
44
  }
34
45
  interface ConnectionState {
35
46
  provider: any;
@@ -372,6 +383,11 @@ interface IAgent {
372
383
  withdraw(state: ConnectionState, chainId: number, token: OwneySupportedTokens, amount?: string): Promise<AgentWithdrawResult>;
373
384
  getBalances(state: ConnectionState, chainId: number): Promise<AgentBalance>;
374
385
  getEarnings(state: ConnectionState, chainId: number): Promise<AgentEarnings>;
386
+ /**
387
+ * Refresh the agent's earnings snapshot when its backend supports doing so.
388
+ * Implementations own freshness and request-deduplication policy.
389
+ */
390
+ refreshEarnings?(state: ConnectionState, chainId: number): Promise<AgentEarnings>;
375
391
  getAccountApy(state: ConnectionState, chainId: number, days: DailyApyDays,
376
392
  /**
377
393
  * Optional asset symbol ("USDC" / "WETH") scoping the daily series to a
@@ -418,6 +434,8 @@ declare class OwneySDK {
418
434
  private referralSource?;
419
435
  private cachedSponsoredCallback;
420
436
  private cachedWethSponsoredCallback;
437
+ private paymasterServiceUrl?;
438
+ private cachedSponsoredCallsCallbacks;
421
439
  private initializingAgentsPromise;
422
440
  constructor(config: OwneySDKConfig);
423
441
  /**
@@ -458,6 +476,13 @@ declare class OwneySDK {
458
476
  * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
459
477
  * `PermitTransferFrom` instead of an EIP-3009 authorization.
460
478
  */
479
+ private getDefaultSponsoredCallsCallback;
480
+ /**
481
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
482
+ * callback used when the caller omits `depositCallback` for a WETH deposit.
483
+ * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
484
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
485
+ */
461
486
  private getDefaultWethSponsoredCallback;
462
487
  private getAgent;
463
488
  private getActiveAgents;
@@ -561,6 +586,11 @@ declare class OwneySDK {
561
586
  * @returns {AgentEarnings} for a single agent, or {OwneyEarnings} with totalEarnings and per-agent breakdown
562
587
  */
563
588
  getEarnings(agentId?: AgentId): Promise<OwneyEarnings | AgentEarnings>;
589
+ /**
590
+ * Return refreshed earnings while letting each agent own its refresh policy.
591
+ * Agents without a refresh capability fall back to their current snapshot.
592
+ */
593
+ refreshEarnings(agentId?: AgentId): Promise<OwneyEarnings | AgentEarnings>;
564
594
  /**
565
595
  * Get the weighted APY for the user's account over a time period.
566
596
  * When querying all agents, the total APY is a balance-weighted average.
@@ -634,7 +664,7 @@ declare class OwneySDK {
634
664
  getAllocationApy({ agentId, }?: AllocationApyOptions): Promise<OwneyAllocationApy>;
635
665
  }
636
666
 
637
- type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
667
+ type OwneyErrorCode = "NOT_CONNECTED" | "NO_ACTIVE_CHAIN" | "WALLET_NO_ACCOUNTS" | "WALLET_ADDRESS_REQUIRED" | "WALLET_NOT_DEPLOYED" | "AGENT_NOT_FOUND" | "AGENT_CHAIN_INCOMPATIBLE" | "AGENT_EMPTY_LIST" | "AGENT_DISABLED" | "CHAIN_UNSUPPORTED" | "CHAIN_NO_COMPATIBLE_AGENTS" | "CHAIN_MISMATCH" | "ASSET_UNSUPPORTED" | "ASSET_NO_COMPATIBLE_AGENTS" | "DEPOSIT_AMOUNT_BELOW_MINIMUM" | "DEPOSIT_CALLBACK_REQUIRED" | "DEPOSIT_CALLBACK_INVALID" | "DEPOSIT_NO_PERMITTED_TOKENS" | "DEPOSIT_INSUFFICIENT_BALANCE" | "WITHDRAW_NO_PERMITTED_TOKENS" | "WITHDRAW_INSUFFICIENT_BALANCE" | "WITHDRAW_ALL_FAILED" | "WITHDRAW_PARTIAL_FAILURE" | "WITHDRAW_FAILED" | "API_ROUTING_ERROR" | "API_ROUTING_FAILED" | "API_SAIL_ERROR" | "API_SAIL_TIMEOUT" | "API_NO_AGENTS" | "SPONSOR_REQUEST_FAILED" | "PERMIT2_APPROVAL_REQUIRED" | "SPONSORED_CALLS_UNSUPPORTED" | "SPONSORED_CALLS_NO_ID" | "SPONSORED_CALLS_NO_RECEIPT" | "BALANCE_ALL_FAILED" | "ALLOCATION_ALL_FAILED" | "VALIDATION_INVALID_DAYS";
638
668
  declare class OwneyError extends Error {
639
669
  readonly code: OwneyErrorCode;
640
670
  readonly details?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -838,6 +838,7 @@ var WETH_ADDRESS_BY_CHAIN = {
838
838
  var VIEM_CHAIN = { 8453: base, 42161: arbitrum, 1: mainnet };
839
839
  var SPLITTING_ASSETS = ["USDC", "WETH"];
840
840
  var POOL_POLICY_CHAIN_IDS = [8453, 42161, 1];
841
+ var EARNINGS_REFRESH_COOLDOWN_MS = 60 * 60 * 1e3;
841
842
  var ZyfaiAgent = class {
842
843
  id = "zyfai";
843
844
  supportedChainIds = [8453, 42161, 1];
@@ -889,6 +890,7 @@ var ZyfaiAgent = class {
889
890
  // successful decode is cached forever; failures are NOT cached so a transient
890
891
  // RPC error retries on the next fetch.
891
892
  withdrawAmountCache = /* @__PURE__ */ new Map();
893
+ earningsRefreshInFlight = null;
892
894
  constructor(apiKey, rpcUrls, referralSource) {
893
895
  this.rpcUrls = rpcUrls ?? DEFAULT_ZYFAI_RPC_URLS;
894
896
  this.sdk = new ZyfaiSDK({
@@ -1686,6 +1688,22 @@ var ZyfaiAgent = class {
1686
1688
  const raw = await this.sdk.getOnchainEarnings(smartWallet);
1687
1689
  return mapEarnings(raw, smartWallet);
1688
1690
  }
1691
+ async refreshEarnings(state, chainId) {
1692
+ if (this.earningsRefreshInFlight) return this.earningsRefreshInFlight;
1693
+ this.earningsRefreshInFlight = (async () => {
1694
+ const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1695
+ const current = await this.sdk.getOnchainEarnings(smartWallet);
1696
+ const lastCheck = current.data.lastCheckTimestamp ? Date.parse(current.data.lastCheckTimestamp) : Number.NaN;
1697
+ const isFresh = Number.isFinite(lastCheck) && Date.now() - lastCheck < EARNINGS_REFRESH_COOLDOWN_MS;
1698
+ const earnings = isFresh ? current : await this.sdk.calculateOnchainEarnings(smartWallet);
1699
+ return mapEarnings(earnings, smartWallet);
1700
+ })();
1701
+ try {
1702
+ return await this.earningsRefreshInFlight;
1703
+ } finally {
1704
+ this.earningsRefreshInFlight = null;
1705
+ }
1706
+ }
1689
1707
  async getAccountApy(state, chainId, days, tokenSymbol) {
1690
1708
  const { smartWallet } = await this.resolveSmartWallet(state, chainId);
1691
1709
  const raw = await this.sdk.getDailyApyHistory(smartWallet, days);
@@ -2426,6 +2444,97 @@ function makeSponsoredWethCallback(deps) {
2426
2444
  };
2427
2445
  }
2428
2446
 
2447
+ // src/lib/sponsored-calls-deposit.ts
2448
+ import { encodeFunctionData, erc20Abi, toHex } from "viem";
2449
+ var DEFAULT_POLL_INTERVAL_MS = 1500;
2450
+ var DEFAULT_MAX_POLLS = 30;
2451
+ async function paymasterSupported(provider, owner, chainId) {
2452
+ const caps = await provider.request({
2453
+ method: "wallet_getCapabilities",
2454
+ params: [owner]
2455
+ });
2456
+ const forChain = caps?.[toHex(chainId)] ?? caps?.[String(chainId)];
2457
+ return Boolean(forChain?.paymasterService?.supported);
2458
+ }
2459
+ function makeSponsoredCallsCallback(deps) {
2460
+ const pollIntervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
2461
+ const maxPolls = deps.maxPolls ?? DEFAULT_MAX_POLLS;
2462
+ const absolutePaymasterUrl = () => {
2463
+ const configured = deps.paymasterServiceUrl;
2464
+ if (/^https?:\/\//i.test(configured)) return configured;
2465
+ const origin = globalThis.location?.origin;
2466
+ if (!origin) {
2467
+ throw new OwneyError(
2468
+ "SPONSORED_CALLS_UNSUPPORTED",
2469
+ `paymasterServiceUrl "${configured}" is relative and no page origin is available to resolve it.`
2470
+ );
2471
+ }
2472
+ return new URL(configured, origin).toString();
2473
+ };
2474
+ return async (smartWallet, chainId, amount) => {
2475
+ const cid = chainId;
2476
+ const token = deps.tokenAddressByChain[cid];
2477
+ if (!token) {
2478
+ throw new OwneyError(
2479
+ "CHAIN_UNSUPPORTED",
2480
+ `No sponsored token configured for chain ${chainId}`
2481
+ );
2482
+ }
2483
+ if (!await paymasterSupported(deps.provider, deps.ownerAddress, chainId)) {
2484
+ throw new OwneyError(
2485
+ "SPONSORED_CALLS_UNSUPPORTED",
2486
+ `Wallet does not advertise paymasterService on chain ${chainId}.`,
2487
+ { chainId }
2488
+ );
2489
+ }
2490
+ const data = encodeFunctionData({
2491
+ abi: erc20Abi,
2492
+ functionName: "transfer",
2493
+ args: [smartWallet, BigInt(amount)]
2494
+ });
2495
+ const sendResult = await deps.provider.request({
2496
+ method: "wallet_sendCalls",
2497
+ params: [
2498
+ {
2499
+ version: "2.0.0",
2500
+ from: deps.ownerAddress,
2501
+ chainId: toHex(chainId),
2502
+ atomicRequired: false,
2503
+ calls: [{ to: token, value: "0x0", data }],
2504
+ capabilities: {
2505
+ paymasterService: { url: absolutePaymasterUrl() }
2506
+ }
2507
+ }
2508
+ ]
2509
+ });
2510
+ deps.onApproved?.();
2511
+ const callsId = typeof sendResult === "string" ? sendResult : sendResult?.id;
2512
+ if (!callsId) {
2513
+ throw new OwneyError(
2514
+ "SPONSORED_CALLS_NO_ID",
2515
+ "Wallet accepted wallet_sendCalls but returned no calls id.",
2516
+ { chainId }
2517
+ );
2518
+ }
2519
+ for (let attempt = 0; attempt < maxPolls; attempt += 1) {
2520
+ const status = await deps.provider.request({
2521
+ method: "wallet_getCallsStatus",
2522
+ params: [callsId]
2523
+ });
2524
+ const txHash = status?.receipts?.[0]?.transactionHash;
2525
+ if (txHash) return txHash;
2526
+ if (pollIntervalMs > 0) {
2527
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
2528
+ }
2529
+ }
2530
+ throw new OwneyError(
2531
+ "SPONSORED_CALLS_NO_RECEIPT",
2532
+ `No receipt for calls ${callsId} after ${maxPolls} polls; the deposit may still settle.`,
2533
+ { chainId, callsId }
2534
+ );
2535
+ };
2536
+ }
2537
+
2429
2538
  // src/client.ts
2430
2539
  function encodeMultiAgentCursor(map) {
2431
2540
  return Buffer.from(JSON.stringify(map), "utf8").toString("base64");
@@ -2487,12 +2596,15 @@ var OwneySDK = class {
2487
2596
  referralSource;
2488
2597
  cachedSponsoredCallback = null;
2489
2598
  cachedWethSponsoredCallback = null;
2599
+ paymasterServiceUrl;
2600
+ cachedSponsoredCallsCallbacks = /* @__PURE__ */ new Map();
2490
2601
  initializingAgentsPromise = null;
2491
2602
  constructor(config) {
2492
2603
  this.apiKey = config.apiKey;
2493
2604
  if (config.debug) setOwneyDebug(true);
2494
2605
  this.zyfaiRpcUrls = config.zyfaiRpcUrls;
2495
2606
  this.routingApiBaseUrl = config.routingApiBaseUrl;
2607
+ this.paymasterServiceUrl = config.paymasterServiceUrl;
2496
2608
  this.referralSource = config.referralSource;
2497
2609
  }
2498
2610
  /**
@@ -2515,6 +2627,7 @@ var OwneySDK = class {
2515
2627
  this.state = { provider, walletAddress, chainId: null };
2516
2628
  this.cachedSponsoredCallback = null;
2517
2629
  this.cachedWethSponsoredCallback = null;
2630
+ this.cachedSponsoredCallsCallbacks.clear();
2518
2631
  }
2519
2632
  /**
2520
2633
  * Disconnect from all agents and clear connection state.
@@ -2529,6 +2642,7 @@ var OwneySDK = class {
2529
2642
  this.state = null;
2530
2643
  this.cachedSponsoredCallback = null;
2531
2644
  this.cachedWethSponsoredCallback = null;
2645
+ this.cachedSponsoredCallsCallbacks.clear();
2532
2646
  }
2533
2647
  isAgentDisabled(agentId) {
2534
2648
  return this.disabledAgents.has(agentId);
@@ -2610,6 +2724,26 @@ var OwneySDK = class {
2610
2724
  * deposit. Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2611
2725
  * `PermitTransferFrom` instead of an EIP-3009 authorization.
2612
2726
  */
2727
+ getDefaultSponsoredCallsCallback(asset, onApproved) {
2728
+ const cached = this.cachedSponsoredCallsCallbacks.get(asset);
2729
+ if (!onApproved && cached) return cached;
2730
+ const provider = this.requireConnectedProvider();
2731
+ const callback = makeSponsoredCallsCallback({
2732
+ provider,
2733
+ ownerAddress: this.state.walletAddress,
2734
+ paymasterServiceUrl: this.paymasterServiceUrl,
2735
+ onApproved,
2736
+ tokenAddressByChain: asset === "WETH" ? SPONSORED_WETH_BY_CHAIN : SPONSORED_USDC_BY_CHAIN
2737
+ });
2738
+ if (!onApproved) this.cachedSponsoredCallsCallbacks.set(asset, callback);
2739
+ return callback;
2740
+ }
2741
+ /**
2742
+ * Lazily builds (and caches) the default Permit2 sponsored WETH deposit
2743
+ * callback used when the caller omits `depositCallback` for a WETH deposit.
2744
+ * Mirrors `getDefaultSponsoredCallback()` but signs a Permit2
2745
+ * `PermitTransferFrom` instead of an EIP-3009 authorization.
2746
+ */
2613
2747
  getDefaultWethSponsoredCallback(onApproved) {
2614
2748
  if (!onApproved && this.cachedWethSponsoredCallback)
2615
2749
  return this.cachedWethSponsoredCallback;
@@ -2832,7 +2966,7 @@ var OwneySDK = class {
2832
2966
  }
2833
2967
  const state = this.requireState();
2834
2968
  const chainId = this.requireChainId();
2835
- const effectiveCallback = depositCallback ?? (asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2969
+ const effectiveCallback = depositCallback ?? (this.paymasterServiceUrl ? this.getDefaultSponsoredCallsCallback(asset, onApproved) : asset === "WETH" ? this.getDefaultWethSponsoredCallback(onApproved) : this.getDefaultSponsoredCallback(onApproved));
2836
2970
  if (agentId) {
2837
2971
  if (this.isAgentDisabled(agentId)) {
2838
2972
  throw new OwneyError(
@@ -3274,6 +3408,28 @@ var OwneySDK = class {
3274
3408
  agentEarnings: results
3275
3409
  };
3276
3410
  }
3411
+ /**
3412
+ * Return refreshed earnings while letting each agent own its refresh policy.
3413
+ * Agents without a refresh capability fall back to their current snapshot.
3414
+ */
3415
+ async refreshEarnings(agentId) {
3416
+ const state = this.requireState();
3417
+ const chainId = this.requireChainId();
3418
+ const refreshAgent = (agent) => agent.refreshEarnings?.(state, chainId) ?? agent.getEarnings(state, chainId);
3419
+ if (agentId) return refreshAgent(this.getAgent(agentId));
3420
+ let totalEarnings = 0;
3421
+ const agentEarnings = {};
3422
+ const refreshed = await Promise.all(
3423
+ [...this.getActiveAgents().entries()].map(
3424
+ async ([id, agent]) => [id, await refreshAgent(agent)]
3425
+ )
3426
+ );
3427
+ for (const [id, earnings] of refreshed) {
3428
+ agentEarnings[id] = earnings;
3429
+ totalEarnings += Number(earnings.lifetimeEarnings);
3430
+ }
3431
+ return { totalEarnings: String(totalEarnings), agentEarnings };
3432
+ }
3277
3433
  /**
3278
3434
  * Get the weighted APY for the user's account over a time period.
3279
3435
  * When querying all agents, the total APY is a balance-weighted average.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owney/sdk",
3
- "version": "0.7.7-beta.6",
3
+ "version": "0.7.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.js",