@zyfai/sdk 0.1.0

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.mjs ADDED
@@ -0,0 +1,1869 @@
1
+ // src/utils/http-client.ts
2
+ import axios from "axios";
3
+
4
+ // src/config/endpoints.ts
5
+ var API_ENDPOINTS = {
6
+ staging: "https://staging-api.zyf.ai",
7
+ production: "https://api.zyf.ai"
8
+ };
9
+ var DATA_API_ENDPOINTS = {
10
+ staging: "https://staging-defiapi.zyf.ai",
11
+ production: "https://defiapi.zyf.ai"
12
+ };
13
+ var API_VERSION = "/api/v1";
14
+ var DATA_API_VERSION = "/api/v2";
15
+ var ENDPOINTS = {
16
+ // Auth
17
+ AUTH_LOGIN: "/auth/login",
18
+ AUTH_CHALLENGE: "/auth/challenge",
19
+ // User
20
+ USER_ME: "/users/me",
21
+ USER_WITHDRAW: "/users/withdraw",
22
+ PARTIAL_WITHDRAW: "/users/partial-withdraw",
23
+ // Session Keys
24
+ SESSION_KEYS_CONFIG: "/session-keys/config",
25
+ SESSION_KEYS_ADD: "/session-keys/add",
26
+ // Protocols
27
+ PROTOCOLS: (chainId) => `/protocols?chainId=${chainId}`,
28
+ // Data (v1)
29
+ DATA_POSITION: (walletAddress) => `/data/position?walletAddress=${walletAddress}`,
30
+ DATA_HISTORY: (walletAddress, chainId) => `/data/history?walletAddress=${walletAddress}&chainId=${chainId}`,
31
+ DATA_TVL: "/data/tvl",
32
+ DATA_VOLUME: "/data/volume",
33
+ DATA_FIRST_TOPUP: (walletAddress, chainId) => `/data/first-topup?walletAddress=${walletAddress}&chainId=${chainId}`,
34
+ DATA_ACTIVE_WALLETS: (chainId) => `/data/active-wallets?chainId=${chainId}`,
35
+ DATA_BY_EOA: (address) => `/data/by-eoa?address=${address}`,
36
+ DATA_REBALANCE_FREQUENCY: (walletAddress) => `/data/rebalance-frequency?walletAddress=${walletAddress}`
37
+ };
38
+ var DATA_ENDPOINTS = {
39
+ // Earnings
40
+ ONCHAIN_EARNINGS: (walletAddress) => `/usercheck/onchain-earnings?walletAddress=${walletAddress}`,
41
+ CALCULATE_ONCHAIN_EARNINGS: "/usercheck/calculate-onchain-earnings",
42
+ DAILY_EARNINGS: (walletAddress, startDate, endDate) => {
43
+ let url = `/usercheck/daily-earnings?walletAddress=${walletAddress}`;
44
+ if (startDate) url += `&startDate=${startDate}`;
45
+ if (endDate) url += `&endDate=${endDate}`;
46
+ return url;
47
+ },
48
+ // Portfolio
49
+ DEBANK_PORTFOLIO_MULTICHAIN: (address) => `/debank/portfolio/multichain/${address}`,
50
+ // Opportunities
51
+ OPPORTUNITIES_SAFE: (chainId) => chainId ? `/opportunities/safes?chainId=${chainId}` : "/opportunities/safes",
52
+ OPPORTUNITIES_DEGEN: (chainId) => chainId ? `/opportunities/degen-strategies?chainId=${chainId}` : "/opportunities/degen-strategies",
53
+ // APY History
54
+ DAILY_APY_HISTORY_WEIGHTED: (walletAddress, days) => `/daily-apy-history/weighted/${walletAddress}${days ? `?days=${days}` : ""}`,
55
+ // Rebalance
56
+ REBALANCE_INFO: (isCrossChain) => isCrossChain !== void 0 ? `/rebalance/rebalance-info?isCrossChain=${isCrossChain}` : "/rebalance/rebalance-info"
57
+ };
58
+
59
+ // src/utils/http-client.ts
60
+ var HttpClient = class {
61
+ /**
62
+ * Create HTTP client for both Execution API and Data API
63
+ *
64
+ * @param apiKey - API key for Execution API (Utkir's backend)
65
+ * @param environment - 'staging' or 'production'
66
+ * @param dataApiKey - API key for Data API (Sunny's backend) - defaults to apiKey
67
+ */
68
+ constructor(apiKey, environment = "production", dataApiKey) {
69
+ this.authToken = null;
70
+ this.apiKey = apiKey;
71
+ this.dataApiKey = dataApiKey || apiKey;
72
+ const endpoint = API_ENDPOINTS[environment];
73
+ const parsedUrl = new URL(endpoint);
74
+ this.origin = parsedUrl.origin;
75
+ this.host = parsedUrl.host;
76
+ this.client = axios.create({
77
+ baseURL: `${endpoint}${API_VERSION}`,
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ "X-API-Key": this.apiKey,
81
+ Origin: this.origin
82
+ },
83
+ timeout: 3e4
84
+ });
85
+ const dataEndpoint = DATA_API_ENDPOINTS[environment];
86
+ this.dataClient = axios.create({
87
+ baseURL: `${dataEndpoint}${DATA_API_VERSION}`,
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "X-API-Key": this.dataApiKey
91
+ },
92
+ timeout: 3e4
93
+ });
94
+ this.setupInterceptors();
95
+ this.setupDataInterceptors();
96
+ }
97
+ setAuthToken(token) {
98
+ this.authToken = token;
99
+ }
100
+ clearAuthToken() {
101
+ this.authToken = null;
102
+ }
103
+ getOrigin() {
104
+ return this.origin;
105
+ }
106
+ getHost() {
107
+ return this.host;
108
+ }
109
+ setupInterceptors() {
110
+ this.client.interceptors.request.use(
111
+ (config) => {
112
+ config.headers["X-API-Key"] = this.apiKey;
113
+ config.headers["Origin"] = this.origin;
114
+ if (this.authToken) {
115
+ config.headers["Authorization"] = `Bearer ${this.authToken}`;
116
+ }
117
+ return config;
118
+ },
119
+ (error) => {
120
+ return Promise.reject(error);
121
+ }
122
+ );
123
+ this.client.interceptors.response.use(
124
+ (response) => response,
125
+ (error) => {
126
+ if (error.response) {
127
+ const status = error.response.status;
128
+ const data = error.response.data;
129
+ switch (status) {
130
+ case 401:
131
+ throw new Error("Unauthorized: Invalid API key");
132
+ case 403:
133
+ throw new Error("Forbidden: Access denied");
134
+ case 404:
135
+ throw new Error(
136
+ `Not found: ${data.message || "Resource not found"}`
137
+ );
138
+ case 429:
139
+ throw new Error("Rate limit exceeded. Please try again later.");
140
+ case 500:
141
+ throw new Error("Internal server error. Please try again later.");
142
+ default:
143
+ throw new Error(data.message || "An error occurred");
144
+ }
145
+ } else if (error.request) {
146
+ throw new Error("Network error: Unable to reach the server");
147
+ } else {
148
+ throw new Error(`Request error: ${error.message}`);
149
+ }
150
+ }
151
+ );
152
+ }
153
+ async get(url, config) {
154
+ const response = await this.client.get(url, config);
155
+ return response.data;
156
+ }
157
+ async post(url, data, config) {
158
+ const response = await this.client.post(url, data, config);
159
+ return response.data;
160
+ }
161
+ async patch(url, data, config) {
162
+ const response = await this.client.patch(url, data, config);
163
+ return response.data;
164
+ }
165
+ async delete(url, config) {
166
+ const response = await this.client.delete(url, config);
167
+ return response.data;
168
+ }
169
+ // Data API methods (v2)
170
+ async dataGet(url, config) {
171
+ const response = await this.dataClient.get(url, config);
172
+ return response.data;
173
+ }
174
+ async dataPost(url, data, config) {
175
+ const response = await this.dataClient.post(url, data, config);
176
+ return response.data;
177
+ }
178
+ setupDataInterceptors() {
179
+ this.dataClient.interceptors.request.use(
180
+ (config) => {
181
+ config.headers["X-API-Key"] = this.dataApiKey;
182
+ return config;
183
+ },
184
+ (error) => Promise.reject(error)
185
+ );
186
+ this.dataClient.interceptors.response.use(
187
+ (response) => response,
188
+ (error) => {
189
+ if (error.response) {
190
+ const status = error.response.status;
191
+ const data = error.response.data;
192
+ switch (status) {
193
+ case 401:
194
+ throw new Error("Unauthorized: Invalid API key");
195
+ case 403:
196
+ throw new Error("Forbidden: Access denied to data API");
197
+ case 404:
198
+ throw new Error(
199
+ `Not found: ${data.message || data.error || "Resource not found"}`
200
+ );
201
+ case 429:
202
+ throw new Error("Rate limit exceeded. Please try again later.");
203
+ case 500:
204
+ throw new Error(
205
+ data.error || "Internal server error. Please try again later."
206
+ );
207
+ default:
208
+ throw new Error(
209
+ data.message || data.error || "An error occurred"
210
+ );
211
+ }
212
+ } else if (error.request) {
213
+ throw new Error("Network error: Unable to reach the data server");
214
+ } else {
215
+ throw new Error(`Request error: ${error.message}`);
216
+ }
217
+ }
218
+ );
219
+ }
220
+ };
221
+
222
+ // src/config/abis.ts
223
+ var ERC20_ABI = [
224
+ {
225
+ name: "transfer",
226
+ type: "function",
227
+ stateMutability: "nonpayable",
228
+ inputs: [
229
+ { name: "to", type: "address" },
230
+ { name: "amount", type: "uint256" }
231
+ ],
232
+ outputs: [{ name: "", type: "bool" }]
233
+ },
234
+ {
235
+ name: "approve",
236
+ type: "function",
237
+ stateMutability: "nonpayable",
238
+ inputs: [
239
+ { name: "spender", type: "address" },
240
+ { name: "amount", type: "uint256" }
241
+ ],
242
+ outputs: [{ name: "", type: "bool" }]
243
+ },
244
+ {
245
+ name: "allowance",
246
+ type: "function",
247
+ stateMutability: "view",
248
+ inputs: [
249
+ { name: "owner", type: "address" },
250
+ { name: "spender", type: "address" }
251
+ ],
252
+ outputs: [{ name: "", type: "uint256" }]
253
+ },
254
+ {
255
+ name: "balanceOf",
256
+ type: "function",
257
+ stateMutability: "view",
258
+ inputs: [{ name: "account", type: "address" }],
259
+ outputs: [{ name: "", type: "uint256" }]
260
+ },
261
+ {
262
+ name: "decimals",
263
+ type: "function",
264
+ stateMutability: "view",
265
+ inputs: [],
266
+ outputs: [{ name: "", type: "uint8" }]
267
+ },
268
+ {
269
+ name: "symbol",
270
+ type: "function",
271
+ stateMutability: "view",
272
+ inputs: [],
273
+ outputs: [{ name: "", type: "string" }]
274
+ },
275
+ {
276
+ name: "name",
277
+ type: "function",
278
+ stateMutability: "view",
279
+ inputs: [],
280
+ outputs: [{ name: "", type: "string" }]
281
+ },
282
+ {
283
+ name: "totalSupply",
284
+ type: "function",
285
+ stateMutability: "view",
286
+ inputs: [],
287
+ outputs: [{ name: "", type: "uint256" }]
288
+ }
289
+ ];
290
+
291
+ // src/core/ZyfaiSDK.ts
292
+ import { privateKeyToAccount } from "viem/accounts";
293
+ import {
294
+ createWalletClient,
295
+ custom,
296
+ http as http3
297
+ } from "viem";
298
+
299
+ // src/config/chains.ts
300
+ import { createPublicClient, http } from "viem";
301
+ import { arbitrum, base } from "viem/chains";
302
+ import { defineChain } from "viem";
303
+ var plasma = defineChain({
304
+ id: 9745,
305
+ name: "Plasma",
306
+ nativeCurrency: {
307
+ decimals: 18,
308
+ name: "Plasma",
309
+ symbol: "PLSM"
310
+ },
311
+ rpcUrls: {
312
+ default: {
313
+ http: ["https://rpc.plasma.io"]
314
+ }
315
+ },
316
+ blockExplorers: {
317
+ default: {
318
+ name: "Plasma Explorer",
319
+ url: "https://explorer.plasma.io"
320
+ }
321
+ }
322
+ });
323
+ var DEFAULT_RPC_URLS = {
324
+ 8453: "https://mainnet.base.org",
325
+ 42161: "https://arb1.arbitrum.io/rpc",
326
+ 9745: "https://rpc.plasma.io"
327
+ };
328
+ var CHAINS = {
329
+ 8453: base,
330
+ 42161: arbitrum,
331
+ 9745: plasma
332
+ };
333
+ var getChainConfig = (chainId) => {
334
+ const chain = CHAINS[chainId];
335
+ if (!chain) {
336
+ throw new Error(`Unsupported chain ID: ${chainId}`);
337
+ }
338
+ const rpcUrl = DEFAULT_RPC_URLS[chainId];
339
+ const publicClient = createPublicClient({
340
+ chain,
341
+ transport: http(rpcUrl)
342
+ });
343
+ return {
344
+ chain,
345
+ rpcUrl,
346
+ publicClient
347
+ };
348
+ };
349
+ var isSupportedChain = (chainId) => {
350
+ return chainId in CHAINS;
351
+ };
352
+ var getSupportedChainIds = () => {
353
+ return Object.keys(CHAINS).map(Number);
354
+ };
355
+ var getBundlerUrl = (chainId, bundlerApiKey, bundlerProvider = "pimlico") => {
356
+ if (!bundlerApiKey) {
357
+ throw new Error("Bundler API key is required for Safe deployment");
358
+ }
359
+ switch (bundlerProvider) {
360
+ case "pimlico":
361
+ return `https://api.pimlico.io/v2/${chainId}/rpc?apikey=${bundlerApiKey}`;
362
+ case "custom":
363
+ return bundlerApiKey;
364
+ default:
365
+ throw new Error(`Unsupported bundler provider: ${bundlerProvider}`);
366
+ }
367
+ };
368
+
369
+ // src/utils/safe-account.ts
370
+ import {
371
+ RHINESTONE_ATTESTER_ADDRESS,
372
+ getOwnableValidator,
373
+ getAccount,
374
+ getEnableSessionDetails,
375
+ getPermissionId,
376
+ getSessionNonce
377
+ } from "@rhinestone/module-sdk";
378
+ import { createSmartAccountClient } from "permissionless";
379
+ import { getAccountNonce } from "permissionless/actions";
380
+ import { erc7579Actions } from "permissionless/actions/erc7579";
381
+ import { createPimlicoClient } from "permissionless/clients/pimlico";
382
+ import { toSafeSmartAccount } from "permissionless/accounts";
383
+ import {
384
+ http as http2,
385
+ fromHex,
386
+ pad,
387
+ toHex
388
+ } from "viem";
389
+ import {
390
+ entryPoint07Address,
391
+ getUserOperationHash
392
+ } from "viem/account-abstraction";
393
+ var SAFE_7579_ADDRESS = "0x7579EE8307284F293B1927136486880611F20002";
394
+ var ERC7579_LAUNCHPAD_ADDRESS = "0x7579011aB74c46090561ea277Ba79D510c6C00ff";
395
+ var DEFAULT_ACCOUNT_SALT = "zyfai-staging";
396
+ var getSafeAccount = async (config) => {
397
+ const {
398
+ owner,
399
+ safeOwnerAddress,
400
+ publicClient,
401
+ accountSalt = DEFAULT_ACCOUNT_SALT
402
+ } = config;
403
+ if (!owner || !owner.account) {
404
+ throw new Error("Wallet not connected. Please connect your wallet first.");
405
+ }
406
+ const actualOwnerAddress = safeOwnerAddress || owner.account.address;
407
+ const ownableValidator = getOwnableValidator({
408
+ owners: [actualOwnerAddress],
409
+ threshold: 1
410
+ });
411
+ const saltHex = fromHex(toHex(accountSalt), "bigint");
412
+ const safeAccount = await toSafeSmartAccount({
413
+ client: publicClient,
414
+ owners: [owner.account],
415
+ // Use connected wallet for signing
416
+ version: "1.4.1",
417
+ entryPoint: {
418
+ address: entryPoint07Address,
419
+ version: "0.7"
420
+ },
421
+ safe4337ModuleAddress: SAFE_7579_ADDRESS,
422
+ erc7579LaunchpadAddress: ERC7579_LAUNCHPAD_ADDRESS,
423
+ attesters: [RHINESTONE_ATTESTER_ADDRESS],
424
+ attestersThreshold: 1,
425
+ validators: [
426
+ {
427
+ address: ownableValidator.address,
428
+ context: ownableValidator.initData
429
+ }
430
+ ],
431
+ saltNonce: saltHex
432
+ });
433
+ return safeAccount;
434
+ };
435
+ var getDeterministicSafeAddress = async (config) => {
436
+ try {
437
+ const safeAccount = await getSafeAccount(config);
438
+ return await safeAccount.getAddress();
439
+ } catch (error) {
440
+ throw new Error(
441
+ `Failed to get deterministic Safe address: ${error.message}`
442
+ );
443
+ }
444
+ };
445
+ var isSafeDeployed = async (address, publicClient) => {
446
+ try {
447
+ const code = await publicClient.getCode({ address });
448
+ return !!code && code !== "0x";
449
+ } catch (error) {
450
+ console.error("Error checking if Safe is deployed:", error);
451
+ return false;
452
+ }
453
+ };
454
+ var getAccountType = async (address, publicClient) => {
455
+ try {
456
+ const code = await publicClient.getCode({ address });
457
+ if (!code || code === "0x" || code.length === 2) {
458
+ return "EOA";
459
+ }
460
+ try {
461
+ const threshold = await publicClient.readContract({
462
+ address,
463
+ abi: [
464
+ {
465
+ inputs: [],
466
+ name: "getThreshold",
467
+ outputs: [{ internalType: "uint256", name: "", type: "uint256" }],
468
+ stateMutability: "view",
469
+ type: "function"
470
+ }
471
+ ],
472
+ functionName: "getThreshold"
473
+ });
474
+ if (threshold !== void 0) {
475
+ return "Safe";
476
+ }
477
+ } catch {
478
+ }
479
+ return "Unknown";
480
+ } catch (error) {
481
+ console.error("Error checking account type:", error);
482
+ return "Unknown";
483
+ }
484
+ };
485
+ var getSmartAccountClient = async (config) => {
486
+ const { publicClient, chain, bundlerUrl } = config;
487
+ const safeAccount = await getSafeAccount(config);
488
+ const bundlerClient = createPimlicoClient({
489
+ transport: http2(bundlerUrl),
490
+ entryPoint: {
491
+ address: entryPoint07Address,
492
+ version: "0.7"
493
+ }
494
+ });
495
+ const smartAccountClient = createSmartAccountClient({
496
+ account: safeAccount,
497
+ chain,
498
+ bundlerTransport: http2(bundlerUrl),
499
+ paymaster: bundlerClient,
500
+ userOperation: {
501
+ estimateFeesPerGas: async () => {
502
+ return (await bundlerClient.getUserOperationGasPrice()).fast;
503
+ }
504
+ }
505
+ }).extend(erc7579Actions());
506
+ return smartAccountClient;
507
+ };
508
+ var deploySafeAccount = async (config) => {
509
+ try {
510
+ const { owner, publicClient } = config;
511
+ if (!owner || !owner.account) {
512
+ throw new Error(
513
+ "Wallet not connected. Please connect your wallet first."
514
+ );
515
+ }
516
+ const safeAddress = await getDeterministicSafeAddress(config);
517
+ const isDeployed = await isSafeDeployed(safeAddress, publicClient);
518
+ if (isDeployed) {
519
+ return {
520
+ safeAddress,
521
+ isDeployed: true
522
+ };
523
+ }
524
+ const smartAccountClient = await getSmartAccountClient(config);
525
+ const userOpHash = await smartAccountClient.sendUserOperation({
526
+ calls: [
527
+ {
528
+ to: safeAddress,
529
+ value: BigInt(0),
530
+ data: "0x"
531
+ }
532
+ ]
533
+ });
534
+ const receipt = await smartAccountClient.waitForUserOperationReceipt({
535
+ hash: userOpHash
536
+ });
537
+ return {
538
+ safeAddress,
539
+ txHash: receipt.receipt.transactionHash,
540
+ isDeployed: true
541
+ };
542
+ } catch (error) {
543
+ throw new Error(
544
+ `Failed to deploy Safe account: ${error.message}`
545
+ );
546
+ }
547
+ };
548
+ var signSessionKey = async (config, sessions) => {
549
+ const { owner, publicClient, chain } = config;
550
+ if (!owner || !owner.account) {
551
+ throw new Error("Wallet not connected. Please connect your wallet first.");
552
+ }
553
+ const safeAccount = await getSafeAccount(config);
554
+ const account = getAccount({
555
+ address: safeAccount.address,
556
+ type: "safe"
557
+ });
558
+ const sessionNonces = await Promise.all(
559
+ sessions.map(
560
+ (session) => getSessionNonce({
561
+ client: publicClient,
562
+ account,
563
+ permissionId: getPermissionId({
564
+ session
565
+ })
566
+ })
567
+ )
568
+ );
569
+ const sessionDetails = await getEnableSessionDetails({
570
+ sessions,
571
+ account,
572
+ clients: [publicClient],
573
+ permitGenericPolicy: true,
574
+ sessionNonces
575
+ });
576
+ const signature = await owner.signMessage({
577
+ account: owner.account,
578
+ message: {
579
+ raw: sessionDetails.permissionEnableHash
580
+ }
581
+ });
582
+ return {
583
+ signature,
584
+ sessionNonces
585
+ };
586
+ };
587
+
588
+ // src/core/ZyfaiSDK.ts
589
+ import { SiweMessage } from "siwe";
590
+ var ZyfaiSDK = class {
591
+ // TODO: The encironment should be removed. Having the same key for staging and production is not ideal, but for now it's fine.
592
+ constructor(config) {
593
+ this.signer = null;
594
+ this.walletClient = null;
595
+ this.isAuthenticated = false;
596
+ // TODO: Check with Utkir for how long the authentication token is valid for.
597
+ this.authenticatedUserId = null;
598
+ const sdkConfig = typeof config === "string" ? { apiKey: config } : config;
599
+ const { apiKey, dataApiKey, environment, bundlerApiKey } = sdkConfig;
600
+ if (!apiKey) {
601
+ throw new Error("API key is required");
602
+ }
603
+ this.environment = environment || "production";
604
+ this.httpClient = new HttpClient(apiKey, this.environment, dataApiKey);
605
+ this.bundlerApiKey = bundlerApiKey;
606
+ }
607
+ /**
608
+ * Authenticate user with SIWE (Sign-In with Ethereum) & JWT token
609
+ * This is required for accessing user-specific endpoints like session-keys/config
610
+ * Uses the connected wallet address for authentication
611
+ *
612
+ * @returns Promise that resolves when authentication is complete
613
+ */
614
+ async authenticateUser() {
615
+ try {
616
+ if (this.isAuthenticated) {
617
+ return;
618
+ }
619
+ const walletClient = this.getWalletClient();
620
+ const userAddress = walletClient.account.address;
621
+ const chainId = walletClient.chain?.id || 8453;
622
+ const challengeResponse = await this.httpClient.post(ENDPOINTS.AUTH_CHALLENGE, {});
623
+ const domain = API_ENDPOINTS[this.environment].split("//")[1];
624
+ const uri = API_ENDPOINTS[this.environment];
625
+ const messageObj = new SiweMessage({
626
+ address: userAddress,
627
+ chainId,
628
+ domain,
629
+ nonce: challengeResponse.nonce,
630
+ statement: "Sign in with Ethereum",
631
+ uri,
632
+ version: "1",
633
+ issuedAt: (/* @__PURE__ */ new Date()).toISOString()
634
+ });
635
+ const messageString = messageObj.prepareMessage();
636
+ const signature = await walletClient.signMessage({
637
+ account: walletClient.account,
638
+ message: messageString
639
+ });
640
+ const loginResponse = await this.httpClient.post(
641
+ ENDPOINTS.AUTH_LOGIN,
642
+ {
643
+ message: messageObj,
644
+ signature
645
+ },
646
+ {
647
+ headers: {
648
+ Origin: uri
649
+ }
650
+ }
651
+ );
652
+ const authToken = loginResponse.accessToken || loginResponse.token;
653
+ if (!authToken) {
654
+ throw new Error("Authentication response missing access token");
655
+ }
656
+ this.httpClient.setAuthToken(authToken);
657
+ this.authenticatedUserId = loginResponse.userId || null;
658
+ this.isAuthenticated = true;
659
+ } catch (error) {
660
+ throw new Error(
661
+ `Failed to authenticate user: ${error.message}`
662
+ );
663
+ }
664
+ }
665
+ /**
666
+ * Update user profile with Smart Wallet address and chain configuration
667
+ * This method requires SIWE authentication and is automatically called after deploySafe
668
+ *
669
+ * @param request - User profile update data
670
+ * @returns Updated user profile information
671
+ *
672
+ * @example
673
+ * ```typescript
674
+ * await sdk.updateUserProfile({
675
+ * smartWallet: "0x1396730...",
676
+ * chains: [8453, 42161],
677
+ * });
678
+ * ```
679
+ */
680
+ async updateUserProfile(request) {
681
+ try {
682
+ await this.authenticateUser();
683
+ const response = await this.httpClient.patch(
684
+ ENDPOINTS.USER_ME,
685
+ request
686
+ );
687
+ return {
688
+ success: true,
689
+ userId: response.userId || response.id,
690
+ smartWallet: response.smartWallet,
691
+ chains: response.chains
692
+ };
693
+ } catch (error) {
694
+ throw new Error(
695
+ `Failed to update user profile: ${error.message}`
696
+ );
697
+ }
698
+ }
699
+ /**
700
+ * Connect account for signing transactions
701
+ * Accepts either a private key string or a modern wallet provider
702
+ *
703
+ * @param account - Private key string or wallet provider object
704
+ * @param chainId - Target chain ID (default: 42161 - Arbitrum)
705
+ * @returns The connected EOA address
706
+ *
707
+ * @example
708
+ * // With private key
709
+ * await sdk.connectAccount('0x...');
710
+ *
711
+ * @example
712
+ * // With wallet provider (e.g., from wagmi, web3-react, etc.)
713
+ * const provider = await connector.getProvider();
714
+ * await sdk.connectAccount(provider);
715
+ */
716
+ async connectAccount(account, chainId = 42161) {
717
+ if (!isSupportedChain(chainId)) {
718
+ throw new Error(`Unsupported chain ID: ${chainId}`);
719
+ }
720
+ this.isAuthenticated = false;
721
+ this.httpClient.clearAuthToken();
722
+ const chainConfig = getChainConfig(chainId);
723
+ if (typeof account === "string") {
724
+ let privateKey = account;
725
+ if (!privateKey.startsWith("0x")) {
726
+ privateKey = `0x${privateKey}`;
727
+ }
728
+ this.signer = privateKeyToAccount(privateKey);
729
+ this.walletClient = createWalletClient({
730
+ account: this.signer,
731
+ chain: chainConfig.chain,
732
+ transport: http3(chainConfig.rpcUrl)
733
+ });
734
+ return this.signer.address;
735
+ }
736
+ const provider = account;
737
+ if (!provider) {
738
+ throw new Error(
739
+ "Invalid account parameter. Expected private key string or wallet provider."
740
+ );
741
+ }
742
+ if (provider.request) {
743
+ const accounts = await provider.request({
744
+ method: "eth_requestAccounts"
745
+ });
746
+ if (!accounts || accounts.length === 0) {
747
+ throw new Error("No accounts found in wallet provider");
748
+ }
749
+ this.walletClient = createWalletClient({
750
+ account: accounts[0],
751
+ chain: chainConfig.chain,
752
+ transport: custom(provider)
753
+ });
754
+ return accounts[0];
755
+ }
756
+ if (provider.account && provider.transport) {
757
+ this.walletClient = createWalletClient({
758
+ account: provider.account,
759
+ chain: chainConfig.chain,
760
+ transport: provider.transport
761
+ });
762
+ return provider.account.address;
763
+ }
764
+ throw new Error(
765
+ "Invalid wallet provider. Expected EIP-1193 provider or viem WalletClient."
766
+ );
767
+ }
768
+ /**
769
+ * Get wallet client (throws if not connected)
770
+ * @private
771
+ */
772
+ getWalletClient(chainId) {
773
+ if (this.signer) {
774
+ return createWalletClient({
775
+ account: this.signer,
776
+ chain: getChainConfig(chainId || 8453).chain,
777
+ transport: http3(getChainConfig(chainId || 8453).rpcUrl)
778
+ });
779
+ } else {
780
+ if (!this.walletClient) {
781
+ throw new Error("No account connected. Call connectAccount() first");
782
+ }
783
+ return this.walletClient;
784
+ }
785
+ }
786
+ /**
787
+ * Get smart wallet address for a user
788
+ * Returns the deterministic Safe address for an EOA, or the address itself if already a Safe
789
+ *
790
+ * @param userAddress - User's EOA address
791
+ * @param chainId - Target chain ID
792
+ * @returns Smart wallet information including address and deployment status
793
+ */
794
+ async getSmartWalletAddress(userAddress, chainId) {
795
+ if (!userAddress) {
796
+ throw new Error("User address is required");
797
+ }
798
+ if (!isSupportedChain(chainId)) {
799
+ throw new Error(`Unsupported chain ID: ${chainId}`);
800
+ }
801
+ const walletClient = this.getWalletClient();
802
+ const chainConfig = getChainConfig(chainId);
803
+ const safeAddress = await getDeterministicSafeAddress({
804
+ owner: walletClient,
805
+ safeOwnerAddress: userAddress,
806
+ chain: chainConfig.chain,
807
+ publicClient: chainConfig.publicClient
808
+ });
809
+ const isDeployed = await isSafeDeployed(
810
+ safeAddress,
811
+ chainConfig.publicClient
812
+ );
813
+ return {
814
+ address: safeAddress,
815
+ isDeployed
816
+ };
817
+ }
818
+ /**
819
+ * Deploy Safe Smart Wallet for a user
820
+ *
821
+ * @param userAddress - User's EOA address (the connected EOA, not the smart wallet address)
822
+ * @param chainId - Target chain ID
823
+ * @returns Deployment response with Safe address and transaction hash
824
+ */
825
+ async deploySafe(userAddress, chainId) {
826
+ try {
827
+ if (!userAddress) {
828
+ throw new Error("User address is required");
829
+ }
830
+ if (!isSupportedChain(chainId)) {
831
+ throw new Error(`Unsupported chain ID: ${chainId}`);
832
+ }
833
+ if (!this.bundlerApiKey) {
834
+ throw new Error(
835
+ "Bundler API key is required for Safe deployment. Please provide bundlerApiKey in SDK configuration."
836
+ );
837
+ }
838
+ const walletClient = this.getWalletClient(chainId);
839
+ const chainConfig = getChainConfig(chainId);
840
+ const accountType = await getAccountType(
841
+ userAddress,
842
+ chainConfig.publicClient
843
+ );
844
+ if (accountType !== "EOA") {
845
+ throw new Error(
846
+ `Address ${userAddress} is not an EOA. Only EOA addresses can deploy Safe smart wallets.`
847
+ );
848
+ }
849
+ const bundlerUrl = getBundlerUrl(chainId, this.bundlerApiKey);
850
+ const deploymentResult = await deploySafeAccount({
851
+ owner: walletClient,
852
+ safeOwnerAddress: userAddress,
853
+ chain: chainConfig.chain,
854
+ publicClient: chainConfig.publicClient,
855
+ bundlerUrl
856
+ });
857
+ try {
858
+ await this.updateUserProfile({
859
+ smartWallet: deploymentResult.safeAddress,
860
+ chains: [chainId]
861
+ });
862
+ } catch (updateError) {
863
+ console.warn(
864
+ "Failed to update user profile after Safe deployment:",
865
+ updateError.message
866
+ );
867
+ }
868
+ return {
869
+ success: true,
870
+ safeAddress: deploymentResult.safeAddress,
871
+ txHash: deploymentResult.txHash || "0x0",
872
+ status: "deployed"
873
+ };
874
+ } catch (error) {
875
+ console.error("Safe deployment failed:", error);
876
+ throw new Error(`Safe deployment failed: ${error.message}`);
877
+ }
878
+ }
879
+ /**
880
+ * Create session key with auto-fetched configuration from ZyFAI API
881
+ * This is the simplified method that automatically fetches session configuration
882
+ *
883
+ * @param userAddress - User's EOA or Safe address
884
+ * @param chainId - Target chain ID
885
+ * @returns Session key response with signature and nonces
886
+ *
887
+ * @example
888
+ * ```typescript
889
+ * // Simple usage - no need to configure sessions manually
890
+ * const result = await sdk.createSessionKey(userAddress, 8453);
891
+ * console.log("Session created:", result.signature);
892
+ * ```
893
+ */
894
+ async createSessionKey(userAddress, chainId) {
895
+ try {
896
+ await this.authenticateUser();
897
+ if (!this.authenticatedUserId) {
898
+ throw new Error(
899
+ "User ID not available. Please ensure authentication completed successfully."
900
+ );
901
+ }
902
+ const walletClient = this.getWalletClient();
903
+ const chainConfig = getChainConfig(chainId);
904
+ const safeAddress = await getDeterministicSafeAddress({
905
+ owner: walletClient,
906
+ safeOwnerAddress: userAddress,
907
+ chain: chainConfig.chain,
908
+ publicClient: chainConfig.publicClient
909
+ });
910
+ const sessionConfig = await this.httpClient.get(
911
+ ENDPOINTS.SESSION_KEYS_CONFIG
912
+ );
913
+ if (!sessionConfig || sessionConfig.length === 0) {
914
+ throw new Error("No session configuration available from API");
915
+ }
916
+ const sessions = sessionConfig.map((session) => ({
917
+ ...session,
918
+ chainId: BigInt(session.chainId)
919
+ }));
920
+ const signatureResult = await this.signSessionKey(
921
+ userAddress,
922
+ chainId,
923
+ sessions
924
+ );
925
+ const activation = await this.activateSessionKey(
926
+ signatureResult.signature,
927
+ signatureResult.sessionNonces
928
+ );
929
+ return {
930
+ ...signatureResult,
931
+ userId: this.authenticatedUserId,
932
+ sessionActivation: activation
933
+ };
934
+ } catch (error) {
935
+ throw new Error(
936
+ `Failed to create session key: ${error.message}`
937
+ );
938
+ }
939
+ }
940
+ /**
941
+ * Internal method to sign session key
942
+ * @private
943
+ */
944
+ async signSessionKey(userAddress, chainId, sessions) {
945
+ try {
946
+ if (!userAddress) {
947
+ throw new Error("User address is required");
948
+ }
949
+ if (!isSupportedChain(chainId)) {
950
+ throw new Error(`Unsupported chain ID: ${chainId}`);
951
+ }
952
+ if (!sessions || sessions.length === 0) {
953
+ throw new Error("At least one session configuration is required");
954
+ }
955
+ const walletClient = this.getWalletClient();
956
+ const chainConfig = getChainConfig(chainId);
957
+ const accountType = await getAccountType(
958
+ userAddress,
959
+ chainConfig.publicClient
960
+ );
961
+ if (accountType !== "EOA") {
962
+ throw new Error(
963
+ `Invalid account type for ${userAddress}. Must be an EOA.`
964
+ );
965
+ }
966
+ const { signature, sessionNonces } = await signSessionKey(
967
+ {
968
+ owner: walletClient,
969
+ safeOwnerAddress: userAddress,
970
+ chain: chainConfig.chain,
971
+ publicClient: chainConfig.publicClient
972
+ },
973
+ sessions
974
+ );
975
+ const safeAddress = await getDeterministicSafeAddress({
976
+ owner: walletClient,
977
+ safeOwnerAddress: userAddress,
978
+ chain: chainConfig.chain,
979
+ publicClient: chainConfig.publicClient
980
+ });
981
+ return {
982
+ success: true,
983
+ sessionKeyAddress: safeAddress,
984
+ signature,
985
+ sessionNonces
986
+ };
987
+ } catch (error) {
988
+ throw new Error(
989
+ `Failed to sign session key: ${error.message}`
990
+ );
991
+ }
992
+ }
993
+ /**
994
+ * Activate session key via ZyFAI API
995
+ */
996
+ async activateSessionKey(signature, sessionNonces) {
997
+ const nonces = this.normalizeSessionNonces(sessionNonces);
998
+ const payload = {
999
+ hash: signature,
1000
+ nonces
1001
+ };
1002
+ return await this.httpClient.post(
1003
+ ENDPOINTS.SESSION_KEYS_ADD,
1004
+ payload
1005
+ );
1006
+ }
1007
+ /**
1008
+ * Convert session nonces from bigint[] to number[]
1009
+ */
1010
+ normalizeSessionNonces(sessionNonces) {
1011
+ if (!sessionNonces || sessionNonces.length === 0) {
1012
+ throw new Error(
1013
+ "Session nonces missing from signature result. Cannot register session key."
1014
+ );
1015
+ }
1016
+ return sessionNonces.map((nonce) => {
1017
+ const value = Number(nonce);
1018
+ if (!Number.isFinite(value) || value < 0) {
1019
+ throw new Error(`Invalid session nonce value: ${nonce.toString()}`);
1020
+ }
1021
+ return value;
1022
+ });
1023
+ }
1024
+ /**
1025
+ * Deposit funds from EOA to Safe smart wallet
1026
+ * Transfers tokens from the connected wallet to the user's Safe and logs the deposit
1027
+ *
1028
+ * @param userAddress - User's address (owner of the Safe)
1029
+ * @param chainId - Target chain ID
1030
+ * @param tokenAddress - Token contract address to deposit
1031
+ * @param amount - Amount in least decimal units (e.g., "100000000" for 100 USDC with 6 decimals)
1032
+ * @returns Deposit response with transaction hash
1033
+ *
1034
+ * @example
1035
+ * ```typescript
1036
+ * // Deposit 100 USDC (6 decimals) to Safe on Arbitrum
1037
+ * const result = await sdk.depositFunds(
1038
+ * "0xUser...",
1039
+ * 42161,
1040
+ * "0xaf88d065e77c8cc2239327c5edb3a432268e5831", // USDC
1041
+ * "100000000" // 100 USDC = 100 * 10^6
1042
+ * );
1043
+ * ```
1044
+ */
1045
+ async depositFunds(userAddress, chainId, tokenAddress, amount) {
1046
+ try {
1047
+ if (!userAddress) {
1048
+ throw new Error("User address is required");
1049
+ }
1050
+ if (!isSupportedChain(chainId)) {
1051
+ throw new Error(`Unsupported chain ID: ${chainId}`);
1052
+ }
1053
+ if (!tokenAddress) {
1054
+ throw new Error("Token address is required");
1055
+ }
1056
+ if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
1057
+ throw new Error("Valid amount is required");
1058
+ }
1059
+ const walletClient = this.getWalletClient();
1060
+ const chainConfig = getChainConfig(chainId);
1061
+ const safeAddress = await getDeterministicSafeAddress({
1062
+ owner: walletClient,
1063
+ safeOwnerAddress: userAddress,
1064
+ chain: chainConfig.chain,
1065
+ publicClient: chainConfig.publicClient
1066
+ });
1067
+ const isDeployed = await isSafeDeployed(
1068
+ safeAddress,
1069
+ chainConfig.publicClient
1070
+ );
1071
+ if (!isDeployed) {
1072
+ throw new Error(
1073
+ `Safe not deployed for ${userAddress}. Please deploy the Safe first using deploySafe().`
1074
+ );
1075
+ }
1076
+ const amountBigInt = BigInt(amount);
1077
+ const txHash = await walletClient.writeContract({
1078
+ address: tokenAddress,
1079
+ abi: ERC20_ABI,
1080
+ functionName: "transfer",
1081
+ args: [safeAddress, amountBigInt],
1082
+ chain: chainConfig.chain,
1083
+ account: walletClient.account
1084
+ });
1085
+ const receipt = await chainConfig.publicClient.waitForTransactionReceipt({
1086
+ hash: txHash
1087
+ });
1088
+ if (receipt.status !== "success") {
1089
+ throw new Error("Deposit transaction failed");
1090
+ }
1091
+ return {
1092
+ success: true,
1093
+ txHash,
1094
+ smartWallet: safeAddress,
1095
+ amount: amountBigInt.toString(),
1096
+ status: "confirmed"
1097
+ };
1098
+ } catch (error) {
1099
+ throw new Error(`Deposit failed: ${error.message}`);
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Withdraw funds from Safe smart wallet
1104
+ * Triggers a withdrawal request to the ZyFAI API
1105
+ *
1106
+ * @param userAddress - User's address (owner of the Safe)
1107
+ * @param chainId - Target chain ID
1108
+ * @param amount - Optional: Amount in least decimal units to withdraw (partial withdrawal). If not specified, withdraws all funds
1109
+ * @param receiver - Optional: Receiver address. If not specified, sends to Safe owner
1110
+ * @returns Withdraw response with transaction hash
1111
+ *
1112
+ * @example
1113
+ * ```typescript
1114
+ * // Full withdrawal
1115
+ * const result = await sdk.withdrawFunds("0xUser...", 42161);
1116
+ *
1117
+ * // Partial withdrawal of 50 USDC (6 decimals)
1118
+ * const result = await sdk.withdrawFunds(
1119
+ * "0xUser...",
1120
+ * 42161,
1121
+ * "50000000", // 50 USDC = 50 * 10^6
1122
+ * "0xReceiver..."
1123
+ * );
1124
+ * ```
1125
+ */
1126
+ async withdrawFunds(userAddress, chainId, amount, receiver) {
1127
+ try {
1128
+ if (!userAddress) {
1129
+ throw new Error("User address is required");
1130
+ }
1131
+ if (!isSupportedChain(chainId)) {
1132
+ throw new Error(`Unsupported chain ID: ${chainId}`);
1133
+ }
1134
+ const walletClient = this.getWalletClient();
1135
+ const chainConfig = getChainConfig(chainId);
1136
+ const safeAddress = await getDeterministicSafeAddress({
1137
+ owner: walletClient,
1138
+ safeOwnerAddress: userAddress,
1139
+ chain: chainConfig.chain,
1140
+ publicClient: chainConfig.publicClient
1141
+ });
1142
+ const isDeployed = await isSafeDeployed(
1143
+ safeAddress,
1144
+ chainConfig.publicClient
1145
+ );
1146
+ if (!isDeployed) {
1147
+ throw new Error(
1148
+ `Safe not deployed for ${userAddress}. Please deploy the Safe first using deploySafe().`
1149
+ );
1150
+ }
1151
+ await this.authenticateUser();
1152
+ let response = {};
1153
+ if (amount) {
1154
+ response = await this.httpClient.post(ENDPOINTS.PARTIAL_WITHDRAW, {
1155
+ chainId,
1156
+ amount,
1157
+ receiver: receiver || userAddress
1158
+ });
1159
+ } else {
1160
+ response = await this.httpClient.get(ENDPOINTS.USER_WITHDRAW, {
1161
+ params: { chainId }
1162
+ });
1163
+ }
1164
+ const success = response?.success ?? true;
1165
+ return {
1166
+ success,
1167
+ txHash: response?.txHash || response?.transactionHash || "pending",
1168
+ type: amount ? "partial" : "full",
1169
+ amount: amount || "all",
1170
+ receiver: receiver || userAddress,
1171
+ status: success ? "pending" : "failed"
1172
+ };
1173
+ } catch (error) {
1174
+ throw new Error(`Withdrawal failed: ${error.message}`);
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Get available DeFi protocols and pools for a specific chain
1179
+ *
1180
+ * @param chainId - Target chain ID
1181
+ * @returns List of available protocols with their pools and APY data
1182
+ *
1183
+ * @example
1184
+ * ```typescript
1185
+ * const protocols = await sdk.getAvailableProtocols(42161);
1186
+ * protocols.forEach(protocol => {
1187
+ * console.log(`${protocol.name}: ${protocol.minApy}% - ${protocol.maxApy}% APY`);
1188
+ * });
1189
+ * ```
1190
+ */
1191
+ async getAvailableProtocols(chainId) {
1192
+ try {
1193
+ if (!isSupportedChain(chainId)) {
1194
+ throw new Error(`Unsupported chain ID: ${chainId}`);
1195
+ }
1196
+ const response = await this.httpClient.get(
1197
+ ENDPOINTS.PROTOCOLS(chainId)
1198
+ );
1199
+ return {
1200
+ success: true,
1201
+ chainId,
1202
+ protocols: response
1203
+ };
1204
+ } catch (error) {
1205
+ throw new Error(
1206
+ `Failed to get available protocols: ${error.message}`
1207
+ );
1208
+ }
1209
+ }
1210
+ /**
1211
+ * Get all active DeFi positions for a user
1212
+ *
1213
+ * @param userAddress - User's EOA address
1214
+ * @param chainId - Optional: Filter by specific chain ID
1215
+ * @returns User's positions across all protocols
1216
+ *
1217
+ * @example
1218
+ * ```typescript
1219
+ * // Get all positions across all chains
1220
+ * const positions = await sdk.getPositions(userAddress);
1221
+ *
1222
+ * // Get positions on a specific chain
1223
+ * const arbPositions = await sdk.getPositions(userAddress, 42161);
1224
+ * ```
1225
+ */
1226
+ async getPositions(userAddress, chainId) {
1227
+ try {
1228
+ if (!userAddress) {
1229
+ throw new Error("User address is required");
1230
+ }
1231
+ if (chainId && !isSupportedChain(chainId)) {
1232
+ throw new Error(`Unsupported chain ID: ${chainId}`);
1233
+ }
1234
+ const walletClient = this.getWalletClient(chainId);
1235
+ const chainConfig = getChainConfig(chainId ?? 8453);
1236
+ const safeAddress = await getDeterministicSafeAddress({
1237
+ owner: walletClient,
1238
+ safeOwnerAddress: userAddress,
1239
+ chain: chainConfig.chain,
1240
+ publicClient: chainConfig.publicClient
1241
+ });
1242
+ const response = await this.httpClient.get(
1243
+ ENDPOINTS.DATA_POSITION(safeAddress)
1244
+ );
1245
+ return {
1246
+ success: true,
1247
+ userAddress,
1248
+ totalValueUsd: 0,
1249
+ // API doesn't return this yet
1250
+ positions: response ? [response] : []
1251
+ };
1252
+ } catch (error) {
1253
+ throw new Error(`Failed to get positions: ${error.message}`);
1254
+ }
1255
+ }
1256
+ // ============================================================================
1257
+ // User Details Methods
1258
+ // ============================================================================
1259
+ /**
1260
+ * Get current authenticated user details
1261
+ * Requires SIWE authentication
1262
+ *
1263
+ * @returns User details including smart wallet, chains, protocols, etc.
1264
+ *
1265
+ * @example
1266
+ * ```typescript
1267
+ * await sdk.connectAccount(privateKey, chainId);
1268
+ * const user = await sdk.getUserDetails();
1269
+ * console.log("Smart Wallet:", user.user.smartWallet);
1270
+ * console.log("Chains:", user.user.chains);
1271
+ * ```
1272
+ */
1273
+ async getUserDetails() {
1274
+ try {
1275
+ await this.authenticateUser();
1276
+ const response = await this.httpClient.get(ENDPOINTS.USER_ME);
1277
+ return {
1278
+ success: true,
1279
+ user: {
1280
+ id: response.id,
1281
+ address: response.address,
1282
+ smartWallet: response.smartWallet,
1283
+ chains: response.chains || [],
1284
+ protocols: response.protocols || [],
1285
+ hasActiveSessionKey: response.hasActiveSessionKey || false,
1286
+ email: response.email,
1287
+ strategy: response.strategy,
1288
+ telegramId: response.telegramId,
1289
+ walletType: response.walletType,
1290
+ autoSelectProtocols: response.autoSelectProtocols || false,
1291
+ autocompounding: response.autocompounding,
1292
+ omniAccount: response.omniAccount,
1293
+ crosschainStrategy: response.crosschainStrategy,
1294
+ agentName: response.agentName,
1295
+ customization: response.customization
1296
+ }
1297
+ };
1298
+ } catch (error) {
1299
+ throw new Error(
1300
+ `Failed to get user details: ${error.message}`
1301
+ );
1302
+ }
1303
+ }
1304
+ // ============================================================================
1305
+ // TVL & Volume Methods
1306
+ // ============================================================================
1307
+ /**
1308
+ * Get total value locked (TVL) across all ZyFAI accounts
1309
+ *
1310
+ * @returns Total TVL in USD and breakdown by chain
1311
+ *
1312
+ * @example
1313
+ * ```typescript
1314
+ * const tvl = await sdk.getTVL();
1315
+ * console.log("Total TVL:", tvl.totalTvl);
1316
+ * ```
1317
+ */
1318
+ async getTVL() {
1319
+ try {
1320
+ const response = await this.httpClient.get(ENDPOINTS.DATA_TVL);
1321
+ return {
1322
+ success: true,
1323
+ totalTvl: response.totalTvl || response.tvl || 0,
1324
+ byChain: response.byChain
1325
+ };
1326
+ } catch (error) {
1327
+ throw new Error(`Failed to get TVL: ${error.message}`);
1328
+ }
1329
+ }
1330
+ /**
1331
+ * Get total volume across all ZyFAI accounts
1332
+ *
1333
+ * @returns Total volume in USD
1334
+ *
1335
+ * @example
1336
+ * ```typescript
1337
+ * const volume = await sdk.getVolume();
1338
+ * console.log("Total Volume:", volume.volumeInUSD);
1339
+ * ```
1340
+ */
1341
+ async getVolume() {
1342
+ try {
1343
+ const response = await this.httpClient.get(ENDPOINTS.DATA_VOLUME);
1344
+ return {
1345
+ success: true,
1346
+ volumeInUSD: response.volumeInUSD || "0"
1347
+ };
1348
+ } catch (error) {
1349
+ throw new Error(`Failed to get volume: ${error.message}`);
1350
+ }
1351
+ }
1352
+ // ============================================================================
1353
+ // Active Wallets Methods
1354
+ // ============================================================================
1355
+ /**
1356
+ * Get active wallets for a specific chain
1357
+ *
1358
+ * @param chainId - Chain ID to filter wallets
1359
+ * @returns List of active wallets on the specified chain
1360
+ *
1361
+ * @example
1362
+ * ```typescript
1363
+ * const wallets = await sdk.getActiveWallets(8453); // Base
1364
+ * console.log("Active wallets:", wallets.count);
1365
+ * ```
1366
+ */
1367
+ async getActiveWallets(chainId) {
1368
+ try {
1369
+ if (!chainId) {
1370
+ throw new Error("Chain ID is required");
1371
+ }
1372
+ const response = await this.httpClient.get(
1373
+ ENDPOINTS.DATA_ACTIVE_WALLETS(chainId)
1374
+ );
1375
+ const wallets = Array.isArray(response) ? response : response.wallets || [];
1376
+ return {
1377
+ success: true,
1378
+ chainId,
1379
+ wallets: wallets.map((w) => ({
1380
+ smartWallet: w.smartWallet || w,
1381
+ chains: w.chains || [chainId],
1382
+ hasBalance: w.hasBalance ?? true
1383
+ })),
1384
+ count: wallets.length
1385
+ };
1386
+ } catch (error) {
1387
+ throw new Error(
1388
+ `Failed to get active wallets: ${error.message}`
1389
+ );
1390
+ }
1391
+ }
1392
+ /**
1393
+ * Get smart wallets associated with an EOA address
1394
+ *
1395
+ * @param eoaAddress - EOA (externally owned account) address
1396
+ * @returns List of smart wallets owned by the EOA
1397
+ *
1398
+ * @example
1399
+ * ```typescript
1400
+ * const result = await sdk.getSmartWalletsByEOA("0x...");
1401
+ * console.log("Smart wallets:", result.smartWallets);
1402
+ * ```
1403
+ */
1404
+ async getSmartWalletsByEOA(eoaAddress) {
1405
+ try {
1406
+ if (!eoaAddress) {
1407
+ throw new Error("EOA address is required");
1408
+ }
1409
+ const response = await this.httpClient.get(
1410
+ ENDPOINTS.DATA_BY_EOA(eoaAddress)
1411
+ );
1412
+ const smartWallets = Array.isArray(response) ? response : response.smartWallets || [response.smartWallet].filter(Boolean);
1413
+ return {
1414
+ success: true,
1415
+ eoa: eoaAddress,
1416
+ smartWallets
1417
+ };
1418
+ } catch (error) {
1419
+ throw new Error(
1420
+ `Failed to get smart wallets by EOA: ${error.message}`
1421
+ );
1422
+ }
1423
+ }
1424
+ // ============================================================================
1425
+ // First Topup & History Methods
1426
+ // ============================================================================
1427
+ /**
1428
+ * Get the first topup (deposit) information for a wallet
1429
+ *
1430
+ * @param walletAddress - Smart wallet address
1431
+ * @param chainId - Chain ID
1432
+ * @returns First topup date and details
1433
+ *
1434
+ * @example
1435
+ * ```typescript
1436
+ * const firstTopup = await sdk.getFirstTopup("0x...", 8453);
1437
+ * console.log("First deposit date:", firstTopup.date);
1438
+ * ```
1439
+ */
1440
+ async getFirstTopup(walletAddress, chainId) {
1441
+ try {
1442
+ if (!walletAddress) {
1443
+ throw new Error("Wallet address is required");
1444
+ }
1445
+ if (!chainId) {
1446
+ throw new Error("Chain ID is required");
1447
+ }
1448
+ const response = await this.httpClient.get(
1449
+ ENDPOINTS.DATA_FIRST_TOPUP(walletAddress, chainId)
1450
+ );
1451
+ return {
1452
+ success: true,
1453
+ walletAddress,
1454
+ date: response.date || response.firstTopup?.date || "",
1455
+ amount: response.amount,
1456
+ chainId: response.chainId || chainId
1457
+ };
1458
+ } catch (error) {
1459
+ throw new Error(`Failed to get first topup: ${error.message}`);
1460
+ }
1461
+ }
1462
+ /**
1463
+ * Get transaction history for a wallet
1464
+ *
1465
+ * @param walletAddress - Smart wallet address
1466
+ * @param chainId - Chain ID
1467
+ * @param options - Optional pagination and date filters
1468
+ * @returns Transaction history
1469
+ *
1470
+ * @example
1471
+ * ```typescript
1472
+ * const history = await sdk.getHistory("0x...", 8453, { limit: 50 });
1473
+ * history.data.forEach(tx => console.log(tx.type, tx.amount));
1474
+ * ```
1475
+ */
1476
+ async getHistory(walletAddress, chainId, options) {
1477
+ try {
1478
+ if (!walletAddress) {
1479
+ throw new Error("Wallet address is required");
1480
+ }
1481
+ if (!chainId) {
1482
+ throw new Error("Chain ID is required");
1483
+ }
1484
+ let endpoint = ENDPOINTS.DATA_HISTORY(walletAddress, chainId);
1485
+ if (options?.limit) endpoint += `&limit=${options.limit}`;
1486
+ if (options?.offset) endpoint += `&offset=${options.offset}`;
1487
+ if (options?.fromDate) endpoint += `&fromDate=${options.fromDate}`;
1488
+ if (options?.toDate) endpoint += `&toDate=${options.toDate}`;
1489
+ const response = await this.httpClient.get(endpoint);
1490
+ return {
1491
+ success: true,
1492
+ walletAddress,
1493
+ data: response.data || [],
1494
+ total: response.total || 0
1495
+ };
1496
+ } catch (error) {
1497
+ throw new Error(`Failed to get history: ${error.message}`);
1498
+ }
1499
+ }
1500
+ // ============================================================================
1501
+ // Onchain Earnings Methods (Data API v2)
1502
+ // ============================================================================
1503
+ /**
1504
+ * Get onchain earnings for a wallet
1505
+ *
1506
+ * @param walletAddress - Smart wallet address
1507
+ * @returns Onchain earnings data including total, current, and lifetime
1508
+ *
1509
+ * @example
1510
+ * ```typescript
1511
+ * const earnings = await sdk.getOnchainEarnings("0x...");
1512
+ * console.log("Total earnings:", earnings.data.totalEarnings);
1513
+ * ```
1514
+ */
1515
+ async getOnchainEarnings(walletAddress) {
1516
+ try {
1517
+ if (!walletAddress) {
1518
+ throw new Error("Wallet address is required");
1519
+ }
1520
+ const response = await this.httpClient.dataGet(
1521
+ DATA_ENDPOINTS.ONCHAIN_EARNINGS(walletAddress)
1522
+ );
1523
+ return {
1524
+ success: true,
1525
+ data: {
1526
+ walletAddress,
1527
+ totalEarnings: response.total_earnings || response.totalEarnings || 0,
1528
+ currentEarnings: response.current_earnings || response.currentEarnings || 0,
1529
+ lifetimeEarnings: response.lifetime_earnings || response.lifetimeEarnings || 0,
1530
+ unrealizedEarnings: response.unrealized_earnings,
1531
+ currentEarningsByChain: response.current_earnings_by_chain,
1532
+ unrealizedEarningsByChain: response.unrealized_earnings_by_chain,
1533
+ lastCheckTimestamp: response.last_check_timestamp
1534
+ }
1535
+ };
1536
+ } catch (error) {
1537
+ throw new Error(
1538
+ `Failed to get onchain earnings: ${error.message}`
1539
+ );
1540
+ }
1541
+ }
1542
+ /**
1543
+ * Calculate/refresh onchain earnings for a wallet
1544
+ * This triggers a recalculation of earnings on the backend
1545
+ *
1546
+ * @param walletAddress - Smart wallet address
1547
+ * @returns Updated onchain earnings data
1548
+ *
1549
+ * @example
1550
+ * ```typescript
1551
+ * const earnings = await sdk.calculateOnchainEarnings("0x...");
1552
+ * console.log("Calculated earnings:", earnings.data.totalEarnings);
1553
+ * ```
1554
+ */
1555
+ async calculateOnchainEarnings(walletAddress) {
1556
+ try {
1557
+ if (!walletAddress) {
1558
+ throw new Error("Wallet address is required");
1559
+ }
1560
+ const response = await this.httpClient.dataPost(
1561
+ DATA_ENDPOINTS.CALCULATE_ONCHAIN_EARNINGS,
1562
+ { walletAddress }
1563
+ );
1564
+ const data = response.data || response;
1565
+ return {
1566
+ success: true,
1567
+ data: {
1568
+ walletAddress,
1569
+ totalEarnings: data.total_earnings || data.totalEarnings || 0,
1570
+ currentEarnings: data.current_earnings || data.currentEarnings || 0,
1571
+ lifetimeEarnings: data.lifetime_earnings || data.lifetimeEarnings || 0,
1572
+ unrealizedEarnings: data.unrealized_earnings,
1573
+ lastCheckTimestamp: data.last_check_timestamp
1574
+ }
1575
+ };
1576
+ } catch (error) {
1577
+ throw new Error(
1578
+ `Failed to calculate onchain earnings: ${error.message}`
1579
+ );
1580
+ }
1581
+ }
1582
+ /**
1583
+ * Get daily earnings for a wallet within a date range
1584
+ *
1585
+ * @param walletAddress - Smart wallet address
1586
+ * @param startDate - Start date (YYYY-MM-DD format)
1587
+ * @param endDate - End date (YYYY-MM-DD format)
1588
+ * @returns Daily earnings breakdown
1589
+ *
1590
+ * @example
1591
+ * ```typescript
1592
+ * const daily = await sdk.getDailyEarnings("0x...", "2024-01-01", "2024-01-31");
1593
+ * daily.data.forEach(d => console.log(d.date, d.earnings));
1594
+ * ```
1595
+ */
1596
+ async getDailyEarnings(walletAddress, startDate, endDate) {
1597
+ try {
1598
+ if (!walletAddress) {
1599
+ throw new Error("Wallet address is required");
1600
+ }
1601
+ const response = await this.httpClient.dataGet(
1602
+ DATA_ENDPOINTS.DAILY_EARNINGS(walletAddress, startDate, endDate)
1603
+ );
1604
+ return {
1605
+ success: true,
1606
+ walletAddress,
1607
+ data: response.data || [],
1608
+ count: response.count || 0,
1609
+ filters: {
1610
+ startDate: startDate || null,
1611
+ endDate: endDate || null
1612
+ }
1613
+ };
1614
+ } catch (error) {
1615
+ throw new Error(
1616
+ `Failed to get daily earnings: ${error.message}`
1617
+ );
1618
+ }
1619
+ }
1620
+ // ============================================================================
1621
+ // Portfolio Methods (Data API v2)
1622
+ // ============================================================================
1623
+ /**
1624
+ * Get Debank portfolio for a wallet across multiple chains
1625
+ * Note: This is a paid endpoint and may require authorization
1626
+ *
1627
+ * @param walletAddress - Smart wallet address
1628
+ * @returns Multi-chain portfolio data
1629
+ *
1630
+ * @example
1631
+ * ```typescript
1632
+ * const portfolio = await sdk.getDebankPortfolio("0x...");
1633
+ * console.log("Total value:", portfolio.totalValueUsd);
1634
+ * ```
1635
+ */
1636
+ async getDebankPortfolio(walletAddress) {
1637
+ try {
1638
+ if (!walletAddress) {
1639
+ throw new Error("Wallet address is required");
1640
+ }
1641
+ const response = await this.httpClient.dataGet(
1642
+ DATA_ENDPOINTS.DEBANK_PORTFOLIO_MULTICHAIN(walletAddress)
1643
+ );
1644
+ const data = response.data || response;
1645
+ return {
1646
+ success: true,
1647
+ walletAddress,
1648
+ totalValueUsd: data.totalValueUsd || 0,
1649
+ chains: data.chains || data
1650
+ };
1651
+ } catch (error) {
1652
+ throw new Error(
1653
+ `Failed to get Debank portfolio: ${error.message}`
1654
+ );
1655
+ }
1656
+ }
1657
+ // ============================================================================
1658
+ // Opportunities Methods (Data API v2)
1659
+ // ============================================================================
1660
+ /**
1661
+ * Get safe (low-risk) yield opportunities
1662
+ *
1663
+ * @param chainId - Optional chain ID filter
1664
+ * @returns List of safe yield opportunities
1665
+ *
1666
+ * @example
1667
+ * ```typescript
1668
+ * const opportunities = await sdk.getSafeOpportunities(8453);
1669
+ * opportunities.data.forEach(o => console.log(o.protocolName, o.apy));
1670
+ * ```
1671
+ */
1672
+ async getSafeOpportunities(chainId) {
1673
+ try {
1674
+ const response = await this.httpClient.dataGet(
1675
+ DATA_ENDPOINTS.OPPORTUNITIES_SAFE(chainId)
1676
+ );
1677
+ const data = response.data || response || [];
1678
+ return {
1679
+ success: true,
1680
+ chainId,
1681
+ strategyType: "safe",
1682
+ data: Array.isArray(data) ? data.map((o) => ({
1683
+ id: o.id,
1684
+ protocolId: o.protocol_id || o.protocolId,
1685
+ protocolName: o.protocol_name || o.protocolName,
1686
+ poolName: o.pool_name || o.poolName,
1687
+ chainId: o.chain_id || o.chainId,
1688
+ apy: o.apy || o.pool_apy || 0,
1689
+ tvl: o.tvl || o.zyfiTvl,
1690
+ asset: o.asset || o.underlying_token,
1691
+ risk: o.risk,
1692
+ strategyType: "safe",
1693
+ status: o.status
1694
+ })) : []
1695
+ };
1696
+ } catch (error) {
1697
+ throw new Error(
1698
+ `Failed to get safe opportunities: ${error.message}`
1699
+ );
1700
+ }
1701
+ }
1702
+ /**
1703
+ * Get degen (high-risk, high-reward) yield strategies
1704
+ *
1705
+ * @param chainId - Optional chain ID filter
1706
+ * @returns List of degen strategies
1707
+ *
1708
+ * @example
1709
+ * ```typescript
1710
+ * const strategies = await sdk.getDegenStrategies(8453);
1711
+ * strategies.data.forEach(s => console.log(s.protocolName, s.apy));
1712
+ * ```
1713
+ */
1714
+ async getDegenStrategies(chainId) {
1715
+ try {
1716
+ const response = await this.httpClient.dataGet(
1717
+ DATA_ENDPOINTS.OPPORTUNITIES_DEGEN(chainId)
1718
+ );
1719
+ const data = response.data || response || [];
1720
+ return {
1721
+ success: true,
1722
+ chainId,
1723
+ strategyType: "degen",
1724
+ data: Array.isArray(data) ? data.map((o) => ({
1725
+ id: o.id,
1726
+ protocolId: o.protocol_id || o.protocolId,
1727
+ protocolName: o.protocol_name || o.protocolName,
1728
+ poolName: o.pool_name || o.poolName,
1729
+ chainId: o.chain_id || o.chainId,
1730
+ apy: o.apy || o.pool_apy || 0,
1731
+ tvl: o.tvl || o.zyfiTvl,
1732
+ asset: o.asset || o.underlying_token,
1733
+ risk: o.risk,
1734
+ strategyType: "degen",
1735
+ status: o.status
1736
+ })) : []
1737
+ };
1738
+ } catch (error) {
1739
+ throw new Error(
1740
+ `Failed to get degen strategies: ${error.message}`
1741
+ );
1742
+ }
1743
+ }
1744
+ // ============================================================================
1745
+ // APY History Methods (Data API v2)
1746
+ // ============================================================================
1747
+ /**
1748
+ * Get daily APY history with weighted average for a wallet
1749
+ *
1750
+ * @param walletAddress - Smart wallet address
1751
+ * @param days - Period: "7D", "14D", or "30D" (default: "7D")
1752
+ * @returns Daily APY history with weighted averages
1753
+ *
1754
+ * @example
1755
+ * ```typescript
1756
+ * const apyHistory = await sdk.getDailyApyHistory("0x...", "30D");
1757
+ * console.log("Average APY:", apyHistory.averageWeightedApy);
1758
+ * ```
1759
+ */
1760
+ async getDailyApyHistory(walletAddress, days = "7D") {
1761
+ try {
1762
+ if (!walletAddress) {
1763
+ throw new Error("Wallet address is required");
1764
+ }
1765
+ const response = await this.httpClient.dataGet(
1766
+ DATA_ENDPOINTS.DAILY_APY_HISTORY_WEIGHTED(walletAddress, days)
1767
+ );
1768
+ const data = response.data || response;
1769
+ return {
1770
+ success: true,
1771
+ walletAddress,
1772
+ history: data.history || {},
1773
+ totalDays: data.total_days || data.totalDays || 0,
1774
+ requestedDays: data.requested_days || data.requestedDays,
1775
+ averageWeightedApy: data.average_final_weighted_apy_after_fee || data.averageWeightedApy || 0
1776
+ };
1777
+ } catch (error) {
1778
+ throw new Error(
1779
+ `Failed to get daily APY history: ${error.message}`
1780
+ );
1781
+ }
1782
+ }
1783
+ // ============================================================================
1784
+ // Rebalance Methods
1785
+ // ============================================================================
1786
+ /**
1787
+ * Get rebalance information
1788
+ * Shows yield generated by rebalancing strategies
1789
+ *
1790
+ * @param isCrossChain - Filter by cross-chain or same-chain rebalances
1791
+ * @returns List of rebalance events
1792
+ *
1793
+ * @example
1794
+ * ```typescript
1795
+ * // Get same-chain rebalance info
1796
+ * const rebalances = await sdk.getRebalanceInfo(false);
1797
+ * console.log("Rebalance count:", rebalances.count);
1798
+ * ```
1799
+ */
1800
+ async getRebalanceInfo(isCrossChain) {
1801
+ try {
1802
+ const response = await this.httpClient.dataGet(
1803
+ DATA_ENDPOINTS.REBALANCE_INFO(isCrossChain)
1804
+ );
1805
+ const data = response.data || response || [];
1806
+ return {
1807
+ success: true,
1808
+ data: Array.isArray(data) ? data.map((r) => ({
1809
+ id: r.id,
1810
+ timestamp: r.timestamp || r.created_at,
1811
+ fromProtocol: r.from_protocol || r.fromProtocol,
1812
+ toProtocol: r.to_protocol || r.toProtocol,
1813
+ fromPool: r.from_pool || r.fromPool,
1814
+ toPool: r.to_pool || r.toPool,
1815
+ amount: r.amount,
1816
+ isCrossChain: r.is_cross_chain ?? r.isCrossChain ?? false,
1817
+ fromChainId: r.from_chain_id || r.fromChainId,
1818
+ toChainId: r.to_chain_id || r.toChainId
1819
+ })) : [],
1820
+ count: data.length
1821
+ };
1822
+ } catch (error) {
1823
+ throw new Error(
1824
+ `Failed to get rebalance info: ${error.message}`
1825
+ );
1826
+ }
1827
+ }
1828
+ /**
1829
+ * Get rebalance frequency/tier for a wallet
1830
+ * Determines how often the wallet can be rebalanced based on tier
1831
+ *
1832
+ * @param walletAddress - Smart wallet address
1833
+ * @returns Rebalance frequency tier and details
1834
+ *
1835
+ * @example
1836
+ * ```typescript
1837
+ * const frequency = await sdk.getRebalanceFrequency("0x...");
1838
+ * console.log("Tier:", frequency.tier);
1839
+ * console.log("Max rebalances/day:", frequency.frequency);
1840
+ * ```
1841
+ */
1842
+ async getRebalanceFrequency(walletAddress) {
1843
+ try {
1844
+ if (!walletAddress) {
1845
+ throw new Error("Wallet address is required");
1846
+ }
1847
+ const response = await this.httpClient.get(
1848
+ ENDPOINTS.DATA_REBALANCE_FREQUENCY(walletAddress)
1849
+ );
1850
+ return {
1851
+ success: true,
1852
+ walletAddress,
1853
+ tier: response.tier || "standard",
1854
+ frequency: response.frequency || response.rebalanceFrequency || 1,
1855
+ description: response.description
1856
+ };
1857
+ } catch (error) {
1858
+ throw new Error(
1859
+ `Failed to get rebalance frequency: ${error.message}`
1860
+ );
1861
+ }
1862
+ }
1863
+ };
1864
+ export {
1865
+ ZyfaiSDK,
1866
+ getChainConfig,
1867
+ getSupportedChainIds,
1868
+ isSupportedChain
1869
+ };