@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/LICENSE +21 -0
- package/README.md +722 -0
- package/dist/index.d.mts +769 -0
- package/dist/index.d.ts +769 -0
- package/dist/index.js +1890 -0
- package/dist/index.mjs +1869 -0
- package/package.json +83 -0
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
|
+
};
|