@compass-labs/widgets 0.1.41 → 0.1.43
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/compass-service-CpGcD-yK.d.mts +346 -0
- package/dist/compass-service-DilASlAA.d.ts +346 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +117 -42
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +118 -43
- package/dist/index.mjs.map +1 -1
- package/dist/server/core/index.d.mts +18 -0
- package/dist/server/core/index.d.ts +18 -0
- package/dist/server/core/index.js +1478 -0
- package/dist/server/core/index.js.map +1 -0
- package/dist/server/core/index.mjs +1471 -0
- package/dist/server/core/index.mjs.map +1 -0
- package/dist/server/index.d.mts +5 -17
- package/dist/server/index.d.ts +5 -17
- package/dist/server/index.js +1452 -1415
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1452 -1415
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/nestjs/index.d.mts +254 -0
- package/dist/server/nestjs/index.d.ts +254 -0
- package/dist/server/nestjs/index.js +1839 -0
- package/dist/server/nestjs/index.js.map +1 -0
- package/dist/server/nestjs/index.mjs +1837 -0
- package/dist/server/nestjs/index.mjs.map +1 -0
- package/dist/types-BOSq6TbU.d.mts +20 -0
- package/dist/types-BOSq6TbU.d.ts +20 -0
- package/package.json +36 -2
|
@@ -0,0 +1,1478 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chains = require('viem/chains');
|
|
4
|
+
var apiSdk = require('@compass-labs/api-sdk');
|
|
5
|
+
var viem = require('viem');
|
|
6
|
+
var accounts = require('viem/accounts');
|
|
7
|
+
|
|
8
|
+
// src/server/core/types.ts
|
|
9
|
+
var CHAIN_MAP = {
|
|
10
|
+
ethereum: chains.mainnet,
|
|
11
|
+
base: chains.base,
|
|
12
|
+
arbitrum: chains.arbitrum
|
|
13
|
+
};
|
|
14
|
+
var CREDIT_TOKENS = {
|
|
15
|
+
base: ["USDC", "WETH", "USDT", "DAI", "WBTC"],
|
|
16
|
+
ethereum: ["USDC", "WETH", "USDT", "DAI", "WBTC"],
|
|
17
|
+
arbitrum: ["USDC", "WETH", "USDT", "DAI", "WBTC"]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/server/core/utils.ts
|
|
21
|
+
function extractErrorMessage(error) {
|
|
22
|
+
if (!(error instanceof Error)) {
|
|
23
|
+
return { message: "Something went wrong. Please try again.", status: 500 };
|
|
24
|
+
}
|
|
25
|
+
const raw = error.message || "";
|
|
26
|
+
const jsonMatch = raw.match(/Body:\s*(\{[\s\S]*\})/);
|
|
27
|
+
if (jsonMatch) {
|
|
28
|
+
try {
|
|
29
|
+
const body = JSON.parse(jsonMatch[1]);
|
|
30
|
+
if (Array.isArray(body.detail)) {
|
|
31
|
+
const msgs = body.detail.map((d) => d.msg || d.message).filter(Boolean);
|
|
32
|
+
if (msgs.length > 0) return { message: msgs.join(". "), status: 422 };
|
|
33
|
+
}
|
|
34
|
+
if (typeof body.detail === "string") return { message: body.detail, status: 422 };
|
|
35
|
+
if (typeof body.error === "string") return { message: body.error, status: 500 };
|
|
36
|
+
if (typeof body.description === "string") return { message: body.description, status: 500 };
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (error.name === "SDKValidationError" || raw.startsWith("Input validation failed")) {
|
|
41
|
+
return { message: "Invalid request data. Please check your inputs and try again.", status: 400 };
|
|
42
|
+
}
|
|
43
|
+
const knownPatterns = ["Insufficient", "not deployed", "reverted", "not configured", "Unsupported chain"];
|
|
44
|
+
const lines = raw.split("\n");
|
|
45
|
+
for (const pattern of knownPatterns) {
|
|
46
|
+
const matchingLine = lines.find((line) => line.includes(pattern));
|
|
47
|
+
if (matchingLine) {
|
|
48
|
+
return { message: matchingLine.trim(), status: 500 };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { message: "Something went wrong. Please try again.", status: 500 };
|
|
52
|
+
}
|
|
53
|
+
function jsonResponse(data, status = 200) {
|
|
54
|
+
return new Response(JSON.stringify(data), {
|
|
55
|
+
status,
|
|
56
|
+
headers: { "Content-Type": "application/json" }
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
var CompassServiceError = class extends Error {
|
|
60
|
+
constructor(message, statusCode) {
|
|
61
|
+
super(message);
|
|
62
|
+
this.statusCode = statusCode;
|
|
63
|
+
this.name = "CompassServiceError";
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
var CompassCoreService = class {
|
|
67
|
+
constructor(config) {
|
|
68
|
+
this.config = config;
|
|
69
|
+
const { apiKey, serverUrl = "https://api.compasslabs.ai" } = config;
|
|
70
|
+
this.client = new apiSdk.CompassApiSDK({
|
|
71
|
+
apiKeyAuth: apiKey,
|
|
72
|
+
serverURL: serverUrl
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// --- Earn ---
|
|
76
|
+
async earnAccountCheck(params) {
|
|
77
|
+
const { owner, chain = "base" } = params;
|
|
78
|
+
if (!owner) {
|
|
79
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
80
|
+
}
|
|
81
|
+
const response = await this.client.earn.earnCreateAccount({
|
|
82
|
+
chain,
|
|
83
|
+
owner,
|
|
84
|
+
sender: owner,
|
|
85
|
+
estimateGas: false
|
|
86
|
+
});
|
|
87
|
+
const earnAccountAddress = response.earnAccountAddress;
|
|
88
|
+
const hasTransaction = !!response.transaction;
|
|
89
|
+
return {
|
|
90
|
+
earnAccountAddress,
|
|
91
|
+
isDeployed: !hasTransaction,
|
|
92
|
+
needsCreation: hasTransaction
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async earnAccountBalances(params) {
|
|
96
|
+
const { owner, chain = "base" } = params;
|
|
97
|
+
if (!owner) {
|
|
98
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
99
|
+
}
|
|
100
|
+
const response = await this.client.earn.earnBalances({
|
|
101
|
+
chain,
|
|
102
|
+
owner
|
|
103
|
+
});
|
|
104
|
+
const data = response;
|
|
105
|
+
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
106
|
+
const balances = {};
|
|
107
|
+
for (const [symbol, tokenData] of Object.entries(data.balances)) {
|
|
108
|
+
const td = tokenData;
|
|
109
|
+
const hasRealTransfers = td.transfers.some((t) => {
|
|
110
|
+
const fromAddr = (t.from_address || t.fromAddress || "").toLowerCase();
|
|
111
|
+
const toAddr = (t.to_address || t.toAddress || "").toLowerCase();
|
|
112
|
+
return fromAddr !== ZERO_ADDRESS && toAddr !== ZERO_ADDRESS;
|
|
113
|
+
});
|
|
114
|
+
const balanceFormatted = td.balance_formatted || td.balanceFormatted || "0";
|
|
115
|
+
const balanceNum = parseFloat(balanceFormatted);
|
|
116
|
+
if (balanceNum === 0 && !hasRealTransfers) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!hasRealTransfers && td.transfers.length > 0) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const usdValue = td.usd_value || td.usdValue || "0";
|
|
123
|
+
const usdValueNum = parseFloat(usdValue);
|
|
124
|
+
if (usdValueNum === 0 || isNaN(usdValueNum)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
balances[symbol] = {
|
|
128
|
+
balance: balanceFormatted,
|
|
129
|
+
usdValue
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const earnAccountAddr = data.earn_account_address || data.earnAccountAddress || "";
|
|
133
|
+
const totalUsd = data.total_usd_value || data.totalUsdValue || "0";
|
|
134
|
+
return {
|
|
135
|
+
earnAccountAddress: earnAccountAddr,
|
|
136
|
+
balances,
|
|
137
|
+
totalUsdValue: totalUsd
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async createAccount(body) {
|
|
141
|
+
const { owner, chain = "base" } = body;
|
|
142
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
143
|
+
if (!owner) {
|
|
144
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
145
|
+
}
|
|
146
|
+
if (!gasSponsorPrivateKey) {
|
|
147
|
+
throw new CompassServiceError(
|
|
148
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
149
|
+
500
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
153
|
+
if (!viemChain) {
|
|
154
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
155
|
+
}
|
|
156
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
157
|
+
if (!rpcUrl) {
|
|
158
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
159
|
+
}
|
|
160
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
161
|
+
const walletClient = viem.createWalletClient({
|
|
162
|
+
account: sponsorAccount,
|
|
163
|
+
chain: viemChain,
|
|
164
|
+
transport: viem.http(rpcUrl)
|
|
165
|
+
});
|
|
166
|
+
const publicClient = viem.createPublicClient({
|
|
167
|
+
chain: viemChain,
|
|
168
|
+
transport: viem.http(rpcUrl)
|
|
169
|
+
});
|
|
170
|
+
const response = await this.client.earn.earnCreateAccount({
|
|
171
|
+
chain,
|
|
172
|
+
owner,
|
|
173
|
+
sender: sponsorAccount.address,
|
|
174
|
+
estimateGas: false
|
|
175
|
+
});
|
|
176
|
+
const earnAccountAddress = response.earnAccountAddress;
|
|
177
|
+
if (!response.transaction) {
|
|
178
|
+
return {
|
|
179
|
+
earnAccountAddress,
|
|
180
|
+
success: true,
|
|
181
|
+
alreadyExists: true
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const transaction = response.transaction;
|
|
185
|
+
const txHash = await walletClient.sendTransaction({
|
|
186
|
+
to: transaction.to,
|
|
187
|
+
data: transaction.data,
|
|
188
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
189
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
190
|
+
});
|
|
191
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
192
|
+
hash: txHash
|
|
193
|
+
});
|
|
194
|
+
if (receipt.status === "reverted") {
|
|
195
|
+
throw new CompassServiceError("Account creation transaction reverted", 500);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
earnAccountAddress,
|
|
199
|
+
txHash,
|
|
200
|
+
success: true
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async managePrepare(body, action) {
|
|
204
|
+
const { amount, token, owner, chain, venueType, vaultAddress, marketAddress, maxSlippagePercent } = body;
|
|
205
|
+
let venue;
|
|
206
|
+
if (venueType === "VAULT" && vaultAddress) {
|
|
207
|
+
venue = {
|
|
208
|
+
type: "VAULT",
|
|
209
|
+
vaultAddress
|
|
210
|
+
};
|
|
211
|
+
} else if (venueType === "AAVE") {
|
|
212
|
+
venue = {
|
|
213
|
+
type: "AAVE",
|
|
214
|
+
token
|
|
215
|
+
};
|
|
216
|
+
} else if (venueType === "PENDLE_PT" && marketAddress) {
|
|
217
|
+
venue = {
|
|
218
|
+
type: "PENDLE_PT",
|
|
219
|
+
marketAddress,
|
|
220
|
+
token: action === "DEPOSIT" ? token : void 0,
|
|
221
|
+
maxSlippagePercent: maxSlippagePercent ?? 1
|
|
222
|
+
};
|
|
223
|
+
} else {
|
|
224
|
+
throw new CompassServiceError("Invalid venue type or missing address", 400);
|
|
225
|
+
}
|
|
226
|
+
const response = await this.client.earn.earnManage({
|
|
227
|
+
owner,
|
|
228
|
+
chain,
|
|
229
|
+
venue,
|
|
230
|
+
action,
|
|
231
|
+
amount,
|
|
232
|
+
gasSponsorship: true
|
|
233
|
+
});
|
|
234
|
+
const eip712 = response.eip712;
|
|
235
|
+
if (!eip712) {
|
|
236
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
237
|
+
}
|
|
238
|
+
const types = eip712.types;
|
|
239
|
+
const normalizedTypes = {
|
|
240
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
241
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
eip712,
|
|
245
|
+
normalizedTypes,
|
|
246
|
+
domain: eip712.domain,
|
|
247
|
+
message: eip712.message
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async execute(body) {
|
|
251
|
+
const { owner, eip712, signature, chain } = body;
|
|
252
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
253
|
+
if (!gasSponsorPrivateKey) {
|
|
254
|
+
throw new CompassServiceError(
|
|
255
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
256
|
+
500
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
260
|
+
if (!viemChain) {
|
|
261
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
262
|
+
}
|
|
263
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
264
|
+
if (!rpcUrl) {
|
|
265
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
266
|
+
}
|
|
267
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
268
|
+
const walletClient = viem.createWalletClient({
|
|
269
|
+
account: sponsorAccount,
|
|
270
|
+
chain: viemChain,
|
|
271
|
+
transport: viem.http(rpcUrl)
|
|
272
|
+
});
|
|
273
|
+
const response = await this.client.gasSponsorship.gasSponsorshipPrepare({
|
|
274
|
+
chain,
|
|
275
|
+
owner,
|
|
276
|
+
sender: sponsorAccount.address,
|
|
277
|
+
eip712,
|
|
278
|
+
signature
|
|
279
|
+
});
|
|
280
|
+
const transaction = response.transaction;
|
|
281
|
+
if (!transaction) {
|
|
282
|
+
throw new CompassServiceError(
|
|
283
|
+
"No transaction returned from gas sponsorship prepare",
|
|
284
|
+
500
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const txHash = await walletClient.sendTransaction({
|
|
288
|
+
to: transaction.to,
|
|
289
|
+
data: transaction.data,
|
|
290
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
291
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
292
|
+
});
|
|
293
|
+
return { txHash, success: true };
|
|
294
|
+
}
|
|
295
|
+
// --- Transfer ---
|
|
296
|
+
async transferApprove(body) {
|
|
297
|
+
const { owner, chain = "base", token } = body;
|
|
298
|
+
if (!owner || !token) {
|
|
299
|
+
throw new CompassServiceError("Missing owner or token parameter", 400);
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const response = await this.client.gasSponsorship.gasSponsorshipApproveTransfer({
|
|
303
|
+
owner,
|
|
304
|
+
chain,
|
|
305
|
+
token,
|
|
306
|
+
gasSponsorship: true
|
|
307
|
+
});
|
|
308
|
+
const eip712 = response.eip712 || response.eip_712;
|
|
309
|
+
const transaction = response.transaction;
|
|
310
|
+
if (!eip712 && !transaction) {
|
|
311
|
+
return {
|
|
312
|
+
approved: true,
|
|
313
|
+
message: "Token already approved for Permit2"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (eip712) {
|
|
317
|
+
const types = eip712.types;
|
|
318
|
+
const normalizedTypes = {
|
|
319
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
320
|
+
Permit: types.permit || types.Permit
|
|
321
|
+
};
|
|
322
|
+
return {
|
|
323
|
+
approved: false,
|
|
324
|
+
eip712,
|
|
325
|
+
normalizedTypes,
|
|
326
|
+
domain: eip712.domain,
|
|
327
|
+
message: eip712.message
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
approved: false,
|
|
332
|
+
transaction,
|
|
333
|
+
requiresTransaction: true
|
|
334
|
+
};
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
337
|
+
if (errorMessage.includes("already set") || errorMessage.includes("already been set")) {
|
|
338
|
+
return {
|
|
339
|
+
approved: true,
|
|
340
|
+
message: "Token allowance already set"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async transferPrepare(body) {
|
|
347
|
+
const { owner, chain = "base", token, amount, action, product } = body;
|
|
348
|
+
const { gasSponsorPrivateKey } = this.config;
|
|
349
|
+
if (!owner || !token || !amount || !action) {
|
|
350
|
+
throw new CompassServiceError("Missing required parameters", 400);
|
|
351
|
+
}
|
|
352
|
+
let spender;
|
|
353
|
+
if (action === "DEPOSIT" && gasSponsorPrivateKey) {
|
|
354
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
355
|
+
spender = sponsorAccount.address;
|
|
356
|
+
}
|
|
357
|
+
let response;
|
|
358
|
+
if (product === "credit") {
|
|
359
|
+
response = await this.client.credit.creditTransfer({
|
|
360
|
+
owner,
|
|
361
|
+
chain,
|
|
362
|
+
token,
|
|
363
|
+
amount,
|
|
364
|
+
action,
|
|
365
|
+
gasSponsorship: true,
|
|
366
|
+
...spender && { spender }
|
|
367
|
+
});
|
|
368
|
+
} else {
|
|
369
|
+
response = await this.client.earn.earnTransfer({
|
|
370
|
+
owner,
|
|
371
|
+
chain,
|
|
372
|
+
token,
|
|
373
|
+
amount,
|
|
374
|
+
action,
|
|
375
|
+
gasSponsorship: true,
|
|
376
|
+
...spender && { spender }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
const eip712 = response.eip712;
|
|
380
|
+
if (!eip712) {
|
|
381
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
382
|
+
}
|
|
383
|
+
const types = eip712.types;
|
|
384
|
+
let normalizedTypes;
|
|
385
|
+
if (action === "DEPOSIT") {
|
|
386
|
+
normalizedTypes = {
|
|
387
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
388
|
+
PermitTransferFrom: types.permitTransferFrom || types.PermitTransferFrom,
|
|
389
|
+
TokenPermissions: types.tokenPermissions || types.TokenPermissions
|
|
390
|
+
};
|
|
391
|
+
} else {
|
|
392
|
+
normalizedTypes = {
|
|
393
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
394
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
eip712,
|
|
399
|
+
normalizedTypes,
|
|
400
|
+
domain: eip712.domain,
|
|
401
|
+
message: eip712.message,
|
|
402
|
+
primaryType: eip712.primaryType
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
async transferExecute(body) {
|
|
406
|
+
const { owner, chain = "base", eip712, signature, product } = body;
|
|
407
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
408
|
+
if (!owner || !eip712 || !signature) {
|
|
409
|
+
throw new CompassServiceError("Missing required parameters", 400);
|
|
410
|
+
}
|
|
411
|
+
if (!gasSponsorPrivateKey) {
|
|
412
|
+
throw new CompassServiceError(
|
|
413
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
414
|
+
500
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
418
|
+
if (!viemChain) {
|
|
419
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
420
|
+
}
|
|
421
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
422
|
+
if (!rpcUrl) {
|
|
423
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
424
|
+
}
|
|
425
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
426
|
+
const walletClient = viem.createWalletClient({
|
|
427
|
+
account: sponsorAccount,
|
|
428
|
+
chain: viemChain,
|
|
429
|
+
transport: viem.http(rpcUrl)
|
|
430
|
+
});
|
|
431
|
+
const response = await this.client.gasSponsorship.gasSponsorshipPrepare({
|
|
432
|
+
chain,
|
|
433
|
+
owner,
|
|
434
|
+
sender: sponsorAccount.address,
|
|
435
|
+
eip712,
|
|
436
|
+
signature,
|
|
437
|
+
...product === "credit" && { product: "credit" }
|
|
438
|
+
});
|
|
439
|
+
const transaction = response.transaction;
|
|
440
|
+
if (!transaction) {
|
|
441
|
+
throw new CompassServiceError(
|
|
442
|
+
"No transaction returned from gas sponsorship prepare",
|
|
443
|
+
500
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const txHash = await walletClient.sendTransaction({
|
|
447
|
+
to: transaction.to,
|
|
448
|
+
data: transaction.data,
|
|
449
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
450
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
451
|
+
});
|
|
452
|
+
return { txHash, success: true };
|
|
453
|
+
}
|
|
454
|
+
async approvalExecute(body) {
|
|
455
|
+
const { owner, chain = "base", transaction } = body;
|
|
456
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
457
|
+
if (!owner || !transaction) {
|
|
458
|
+
throw new CompassServiceError("Missing required parameters (owner, transaction)", 400);
|
|
459
|
+
}
|
|
460
|
+
if (!gasSponsorPrivateKey) {
|
|
461
|
+
throw new CompassServiceError(
|
|
462
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
463
|
+
500
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
467
|
+
if (!viemChain) {
|
|
468
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
469
|
+
}
|
|
470
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
471
|
+
if (!rpcUrl) {
|
|
472
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
473
|
+
}
|
|
474
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
475
|
+
const walletClient = viem.createWalletClient({
|
|
476
|
+
account: sponsorAccount,
|
|
477
|
+
chain: viemChain,
|
|
478
|
+
transport: viem.http(rpcUrl)
|
|
479
|
+
});
|
|
480
|
+
const publicClient = viem.createPublicClient({
|
|
481
|
+
chain: viemChain,
|
|
482
|
+
transport: viem.http(rpcUrl)
|
|
483
|
+
});
|
|
484
|
+
const txHash = await walletClient.sendTransaction({
|
|
485
|
+
to: transaction.to,
|
|
486
|
+
data: transaction.data,
|
|
487
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
488
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
489
|
+
});
|
|
490
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
491
|
+
hash: txHash,
|
|
492
|
+
timeout: 6e4
|
|
493
|
+
});
|
|
494
|
+
if (receipt.status === "reverted") {
|
|
495
|
+
throw new CompassServiceError("Approval transaction reverted", 500);
|
|
496
|
+
}
|
|
497
|
+
return { txHash, status: "success" };
|
|
498
|
+
}
|
|
499
|
+
// --- Swap ---
|
|
500
|
+
async swapQuote(params) {
|
|
501
|
+
const { owner, chain = "base", tokenIn, tokenOut, amountIn } = params;
|
|
502
|
+
if (!owner || !tokenIn || !tokenOut || !amountIn) {
|
|
503
|
+
throw new CompassServiceError("Missing required parameters: owner, tokenIn, tokenOut, amountIn", 400);
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const response = await this.client.earn.earnSwap({
|
|
507
|
+
owner,
|
|
508
|
+
chain,
|
|
509
|
+
tokenIn,
|
|
510
|
+
tokenOut,
|
|
511
|
+
amountIn,
|
|
512
|
+
slippage: 1,
|
|
513
|
+
gasSponsorship: true
|
|
514
|
+
});
|
|
515
|
+
const estimatedAmountOut = response.estimatedAmountOut || "0";
|
|
516
|
+
return {
|
|
517
|
+
tokenIn,
|
|
518
|
+
tokenOut,
|
|
519
|
+
amountIn,
|
|
520
|
+
estimatedAmountOut: estimatedAmountOut?.toString() || "0"
|
|
521
|
+
};
|
|
522
|
+
} catch (error) {
|
|
523
|
+
let errorMessage = "Failed to get swap quote";
|
|
524
|
+
try {
|
|
525
|
+
const bodyMessage = error?.body?.message || error?.message || "";
|
|
526
|
+
if (bodyMessage.includes("{")) {
|
|
527
|
+
const jsonMatch = bodyMessage.match(/\{.*\}/s);
|
|
528
|
+
if (jsonMatch) {
|
|
529
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
530
|
+
errorMessage = parsed.description || parsed.error || parsed.message || errorMessage;
|
|
531
|
+
}
|
|
532
|
+
} else if (bodyMessage) {
|
|
533
|
+
const balanceMatch = bodyMessage.match(/Insufficient \w+ balance[^.]+/i);
|
|
534
|
+
if (balanceMatch) {
|
|
535
|
+
errorMessage = balanceMatch[0];
|
|
536
|
+
} else {
|
|
537
|
+
errorMessage = bodyMessage;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
errorMessage = error?.body?.error || error?.message || "Failed to get swap quote";
|
|
542
|
+
}
|
|
543
|
+
throw new CompassServiceError(errorMessage, 400);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async swapPrepare(body) {
|
|
547
|
+
const { owner, chain = "base", tokenIn, tokenOut, amountIn, slippage = 1 } = body;
|
|
548
|
+
if (!owner || !tokenIn || !tokenOut || !amountIn) {
|
|
549
|
+
throw new CompassServiceError("Missing required parameters: owner, tokenIn, tokenOut, amountIn", 400);
|
|
550
|
+
}
|
|
551
|
+
const response = await this.client.earn.earnSwap({
|
|
552
|
+
owner,
|
|
553
|
+
chain,
|
|
554
|
+
tokenIn,
|
|
555
|
+
tokenOut,
|
|
556
|
+
amountIn,
|
|
557
|
+
slippage,
|
|
558
|
+
gasSponsorship: true
|
|
559
|
+
});
|
|
560
|
+
const eip712 = response.eip712;
|
|
561
|
+
if (!eip712) {
|
|
562
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
563
|
+
}
|
|
564
|
+
const types = eip712.types;
|
|
565
|
+
const normalizedTypes = {
|
|
566
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
567
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
568
|
+
};
|
|
569
|
+
return {
|
|
570
|
+
eip712,
|
|
571
|
+
normalizedTypes,
|
|
572
|
+
domain: eip712.domain,
|
|
573
|
+
message: eip712.message,
|
|
574
|
+
estimatedAmountOut: response.estimatedAmountOut?.toString() || "0"
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
async swapExecute(body) {
|
|
578
|
+
const { owner, chain = "base", eip712, signature } = body;
|
|
579
|
+
if (!owner || !eip712 || !signature) {
|
|
580
|
+
throw new CompassServiceError("Missing required parameters: owner, eip712, signature", 400);
|
|
581
|
+
}
|
|
582
|
+
if (!this.config.gasSponsorPrivateKey) {
|
|
583
|
+
throw new CompassServiceError("Gas sponsor not configured", 500);
|
|
584
|
+
}
|
|
585
|
+
const chainLower = chain.toLowerCase();
|
|
586
|
+
const rpcUrl = this.config.rpcUrls?.[chainLower];
|
|
587
|
+
if (!rpcUrl) {
|
|
588
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
589
|
+
}
|
|
590
|
+
const viemChain = CHAIN_MAP[chainLower];
|
|
591
|
+
if (!viemChain) {
|
|
592
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 400);
|
|
593
|
+
}
|
|
594
|
+
const sponsorAccount = accounts.privateKeyToAccount(this.config.gasSponsorPrivateKey);
|
|
595
|
+
const walletClient = viem.createWalletClient({
|
|
596
|
+
account: sponsorAccount,
|
|
597
|
+
chain: viemChain,
|
|
598
|
+
transport: viem.http(rpcUrl)
|
|
599
|
+
});
|
|
600
|
+
const publicClient = viem.createPublicClient({
|
|
601
|
+
chain: viemChain,
|
|
602
|
+
transport: viem.http(rpcUrl)
|
|
603
|
+
});
|
|
604
|
+
const response = await this.client.gasSponsorship.gasSponsorshipPrepare({
|
|
605
|
+
chain,
|
|
606
|
+
owner,
|
|
607
|
+
sender: sponsorAccount.address,
|
|
608
|
+
eip712,
|
|
609
|
+
signature
|
|
610
|
+
});
|
|
611
|
+
const transaction = response.transaction;
|
|
612
|
+
if (!transaction) {
|
|
613
|
+
throw new CompassServiceError("No transaction returned from gas sponsorship prepare", 500);
|
|
614
|
+
}
|
|
615
|
+
const txHash = await walletClient.sendTransaction({
|
|
616
|
+
to: transaction.to,
|
|
617
|
+
data: transaction.data,
|
|
618
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
619
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
620
|
+
});
|
|
621
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
622
|
+
hash: txHash
|
|
623
|
+
});
|
|
624
|
+
if (receipt.status === "reverted") {
|
|
625
|
+
throw new CompassServiceError("Transaction reverted", 500);
|
|
626
|
+
}
|
|
627
|
+
return { txHash, success: true };
|
|
628
|
+
}
|
|
629
|
+
// --- Token ---
|
|
630
|
+
async tokenBalance(params) {
|
|
631
|
+
const { chain = "base", token, address } = params;
|
|
632
|
+
if (!token || !address) {
|
|
633
|
+
throw new CompassServiceError("Missing token or address parameter", 400);
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const response = await this.client.token.tokenBalance({
|
|
637
|
+
chain,
|
|
638
|
+
token,
|
|
639
|
+
user: address
|
|
640
|
+
});
|
|
641
|
+
return {
|
|
642
|
+
token,
|
|
643
|
+
address,
|
|
644
|
+
balance: response.amount || "0",
|
|
645
|
+
balanceRaw: response.balanceRaw || "0"
|
|
646
|
+
};
|
|
647
|
+
} catch {
|
|
648
|
+
return {
|
|
649
|
+
token,
|
|
650
|
+
address,
|
|
651
|
+
balance: "0",
|
|
652
|
+
balanceRaw: "0"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async tokenPrices(params) {
|
|
657
|
+
const { chain = "base", tokens } = params;
|
|
658
|
+
if (!tokens) {
|
|
659
|
+
throw new CompassServiceError("Missing tokens parameter", 400);
|
|
660
|
+
}
|
|
661
|
+
const tokenList = tokens.split(",").map((t) => t.trim().toUpperCase());
|
|
662
|
+
const prices = {};
|
|
663
|
+
const results = await Promise.allSettled(
|
|
664
|
+
tokenList.map(async (symbol) => {
|
|
665
|
+
const resp = await this.client.token.tokenPrice({ chain, token: symbol });
|
|
666
|
+
return { symbol, price: parseFloat(resp.price || "0") };
|
|
667
|
+
})
|
|
668
|
+
);
|
|
669
|
+
for (const result of results) {
|
|
670
|
+
if (result.status === "fulfilled" && result.value.price > 0) {
|
|
671
|
+
prices[result.value.symbol] = result.value.price;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { prices };
|
|
675
|
+
}
|
|
676
|
+
// --- Bundle ---
|
|
677
|
+
async bundlePrepare(body) {
|
|
678
|
+
const { owner, chain = "base", actions } = body;
|
|
679
|
+
if (!owner || !actions || actions.length === 0) {
|
|
680
|
+
throw new CompassServiceError("Missing owner or actions", 400);
|
|
681
|
+
}
|
|
682
|
+
const response = await this.client.earn.earnBundle({
|
|
683
|
+
owner,
|
|
684
|
+
chain,
|
|
685
|
+
gasSponsorship: true,
|
|
686
|
+
actions
|
|
687
|
+
});
|
|
688
|
+
const eip712 = response.eip712;
|
|
689
|
+
if (!eip712) {
|
|
690
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
691
|
+
}
|
|
692
|
+
const types = eip712.types;
|
|
693
|
+
const normalizedTypes = {
|
|
694
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
695
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
696
|
+
};
|
|
697
|
+
return {
|
|
698
|
+
eip712,
|
|
699
|
+
normalizedTypes,
|
|
700
|
+
domain: eip712.domain,
|
|
701
|
+
message: eip712.message,
|
|
702
|
+
actionsCount: response.actionsCount || actions.length
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
async bundleExecute(body) {
|
|
706
|
+
return this.transferExecute(body);
|
|
707
|
+
}
|
|
708
|
+
// --- Data ---
|
|
709
|
+
async vaults(params) {
|
|
710
|
+
const { chain = "base", orderBy = "apy_7d", direction = "desc", limit = "100", assetSymbol, minTvlUsd } = params;
|
|
711
|
+
try {
|
|
712
|
+
const response = await this.client.earn.earnVaults({
|
|
713
|
+
chain,
|
|
714
|
+
orderBy,
|
|
715
|
+
direction,
|
|
716
|
+
limit: parseInt(limit, 10),
|
|
717
|
+
...assetSymbol && { assetSymbol },
|
|
718
|
+
...minTvlUsd && { minTvlUsd: parseFloat(minTvlUsd) }
|
|
719
|
+
});
|
|
720
|
+
return response;
|
|
721
|
+
} catch {
|
|
722
|
+
throw new CompassServiceError("Failed to fetch vaults", 500);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async aaveMarkets(params) {
|
|
726
|
+
const { chain = "base" } = params;
|
|
727
|
+
try {
|
|
728
|
+
const response = await this.client.earn.earnAaveMarkets({
|
|
729
|
+
chain
|
|
730
|
+
});
|
|
731
|
+
return response;
|
|
732
|
+
} catch {
|
|
733
|
+
throw new CompassServiceError("Failed to fetch Aave markets", 500);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async pendleMarkets(params) {
|
|
737
|
+
const { chain = "base", orderBy = "implied_apy", direction = "desc", limit = "100", underlyingSymbol, minTvlUsd } = params;
|
|
738
|
+
try {
|
|
739
|
+
const response = await this.client.earn.earnPendleMarkets({
|
|
740
|
+
chain,
|
|
741
|
+
orderBy,
|
|
742
|
+
direction,
|
|
743
|
+
limit: parseInt(limit, 10),
|
|
744
|
+
...underlyingSymbol && { underlyingSymbol },
|
|
745
|
+
...minTvlUsd && { minTvlUsd: parseFloat(minTvlUsd) }
|
|
746
|
+
});
|
|
747
|
+
return response;
|
|
748
|
+
} catch {
|
|
749
|
+
throw new CompassServiceError("Failed to fetch Pendle markets", 500);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
async positions(params) {
|
|
753
|
+
const { chain = "base", owner } = params;
|
|
754
|
+
if (!owner) {
|
|
755
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
const positionsResponse = await this.client.earn.earnPositions({
|
|
759
|
+
chain,
|
|
760
|
+
owner
|
|
761
|
+
});
|
|
762
|
+
const raw = JSON.parse(JSON.stringify(positionsResponse));
|
|
763
|
+
const positions = [];
|
|
764
|
+
const aavePositions = raw.aave || [];
|
|
765
|
+
for (const a of aavePositions) {
|
|
766
|
+
const balance = a.balance || "0";
|
|
767
|
+
const symbol = (a.reserveSymbol || a.reserve_symbol || "UNKNOWN").toUpperCase();
|
|
768
|
+
const pnl = a.pnl;
|
|
769
|
+
positions.push({
|
|
770
|
+
protocol: "aave",
|
|
771
|
+
symbol,
|
|
772
|
+
name: `${symbol} on Aave`,
|
|
773
|
+
balance,
|
|
774
|
+
balanceUsd: a.usdValue || a.usd_value || balance,
|
|
775
|
+
apy: parseFloat(a.apy || "0"),
|
|
776
|
+
pnl: pnl ? {
|
|
777
|
+
unrealizedPnl: pnl.unrealizedPnl ?? pnl.unrealized_pnl ?? "0",
|
|
778
|
+
realizedPnl: pnl.realizedPnl ?? pnl.realized_pnl ?? "0",
|
|
779
|
+
totalPnl: pnl.totalPnl ?? pnl.total_pnl ?? "0",
|
|
780
|
+
totalDeposited: pnl.totalDeposited ?? pnl.total_deposited ?? "0"
|
|
781
|
+
} : null,
|
|
782
|
+
deposits: (a.deposits || []).map((d) => ({
|
|
783
|
+
amount: d.inputAmount || d.input_amount || d.amount || "0",
|
|
784
|
+
blockNumber: d.blockNumber || d.block_number || 0,
|
|
785
|
+
timestamp: d.blockTimestamp || d.block_timestamp || void 0,
|
|
786
|
+
txHash: d.transactionHash || d.transaction_hash || d.txHash || ""
|
|
787
|
+
})),
|
|
788
|
+
withdrawals: (a.withdrawals || []).map((w) => ({
|
|
789
|
+
amount: w.outputAmount || w.output_amount || w.amount || "0",
|
|
790
|
+
blockNumber: w.blockNumber || w.block_number || 0,
|
|
791
|
+
timestamp: w.blockTimestamp || w.block_timestamp || void 0,
|
|
792
|
+
txHash: w.transactionHash || w.transaction_hash || w.txHash || ""
|
|
793
|
+
}))
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
const vaultPositions = raw.vaults || [];
|
|
797
|
+
for (const v of vaultPositions) {
|
|
798
|
+
const balance = v.balance || "0";
|
|
799
|
+
const symbol = (v.underlyingSymbol || v.underlying_symbol || "TOKEN").toUpperCase();
|
|
800
|
+
const vaultName = v.vaultName || v.vault_name || `${symbol} Vault`;
|
|
801
|
+
const pnl = v.pnl;
|
|
802
|
+
positions.push({
|
|
803
|
+
protocol: "vaults",
|
|
804
|
+
symbol,
|
|
805
|
+
name: vaultName,
|
|
806
|
+
balance,
|
|
807
|
+
balanceUsd: v.usdValue || v.usd_value || balance,
|
|
808
|
+
apy: parseFloat(v.apy7d || v.apy_7d || "0"),
|
|
809
|
+
vaultAddress: v.vaultAddress || v.vault_address || void 0,
|
|
810
|
+
pnl: pnl ? {
|
|
811
|
+
unrealizedPnl: pnl.unrealizedPnl ?? pnl.unrealized_pnl ?? "0",
|
|
812
|
+
realizedPnl: pnl.realizedPnl ?? pnl.realized_pnl ?? "0",
|
|
813
|
+
totalPnl: pnl.totalPnl ?? pnl.total_pnl ?? "0",
|
|
814
|
+
totalDeposited: pnl.totalDeposited ?? pnl.total_deposited ?? "0"
|
|
815
|
+
} : null,
|
|
816
|
+
deposits: (v.deposits || []).map((d) => ({
|
|
817
|
+
amount: d.inputAmount || d.input_amount || d.amount || "0",
|
|
818
|
+
blockNumber: d.blockNumber || d.block_number || 0,
|
|
819
|
+
timestamp: d.blockTimestamp || d.block_timestamp || void 0,
|
|
820
|
+
txHash: d.transactionHash || d.transaction_hash || d.txHash || ""
|
|
821
|
+
})),
|
|
822
|
+
withdrawals: (v.withdrawals || []).map((w) => ({
|
|
823
|
+
amount: w.outputAmount || w.output_amount || w.amount || "0",
|
|
824
|
+
blockNumber: w.blockNumber || w.block_number || 0,
|
|
825
|
+
timestamp: w.blockTimestamp || w.block_timestamp || void 0,
|
|
826
|
+
txHash: w.transactionHash || w.transaction_hash || w.txHash || ""
|
|
827
|
+
}))
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
const pendlePositions = raw.pendlePt || raw.pendle_pt || [];
|
|
831
|
+
for (const p of pendlePositions) {
|
|
832
|
+
const balance = p.ptBalance || p.pt_balance || p.balance || "0";
|
|
833
|
+
const symbol = (p.underlyingSymbol || p.underlying_symbol || "PT").toUpperCase();
|
|
834
|
+
const pnl = p.pnl;
|
|
835
|
+
positions.push({
|
|
836
|
+
protocol: "pendle",
|
|
837
|
+
symbol,
|
|
838
|
+
name: `PT-${symbol}`,
|
|
839
|
+
balance,
|
|
840
|
+
balanceUsd: p.usdValue || p.usd_value || balance,
|
|
841
|
+
apy: parseFloat(p.impliedApy || p.implied_apy || "0"),
|
|
842
|
+
marketAddress: p.marketAddress || p.market_address || void 0,
|
|
843
|
+
pnl: pnl ? {
|
|
844
|
+
unrealizedPnl: pnl.unrealizedPnl ?? pnl.unrealized_pnl ?? "0",
|
|
845
|
+
realizedPnl: pnl.realizedPnl ?? pnl.realized_pnl ?? "0",
|
|
846
|
+
totalPnl: pnl.totalPnl ?? pnl.total_pnl ?? "0",
|
|
847
|
+
totalDeposited: pnl.totalDeposited ?? pnl.total_deposited ?? "0"
|
|
848
|
+
} : null,
|
|
849
|
+
deposits: (p.deposits || []).map((d) => ({
|
|
850
|
+
amount: d.inputAmount || d.input_amount || d.amount || "0",
|
|
851
|
+
blockNumber: d.blockNumber || d.block_number || 0,
|
|
852
|
+
timestamp: d.blockTimestamp || d.block_timestamp || void 0,
|
|
853
|
+
txHash: d.transactionHash || d.transaction_hash || d.txHash || ""
|
|
854
|
+
})),
|
|
855
|
+
withdrawals: (p.withdrawals || []).map((w) => ({
|
|
856
|
+
amount: w.outputAmount || w.output_amount || w.amount || "0",
|
|
857
|
+
blockNumber: w.blockNumber || w.block_number || 0,
|
|
858
|
+
timestamp: w.blockTimestamp || w.block_timestamp || void 0,
|
|
859
|
+
txHash: w.transactionHash || w.transaction_hash || w.txHash || ""
|
|
860
|
+
}))
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
return { positions };
|
|
864
|
+
} catch {
|
|
865
|
+
throw new CompassServiceError("Failed to fetch positions", 500);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async txReceipt(params) {
|
|
869
|
+
const { hash, chain } = params;
|
|
870
|
+
if (!hash || !chain) {
|
|
871
|
+
throw new CompassServiceError("Missing hash or chain parameter", 400);
|
|
872
|
+
}
|
|
873
|
+
const rpcUrl = this.config.rpcUrls?.[chain.toLowerCase()];
|
|
874
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
875
|
+
if (!viemChain) {
|
|
876
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 400);
|
|
877
|
+
}
|
|
878
|
+
if (!rpcUrl) {
|
|
879
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
880
|
+
}
|
|
881
|
+
const publicClient = viem.createPublicClient({
|
|
882
|
+
chain: viemChain,
|
|
883
|
+
transport: viem.http(rpcUrl)
|
|
884
|
+
});
|
|
885
|
+
try {
|
|
886
|
+
const receipt = await publicClient.getTransactionReceipt({
|
|
887
|
+
hash
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
status: receipt.status,
|
|
891
|
+
blockNumber: receipt.blockNumber.toString()
|
|
892
|
+
};
|
|
893
|
+
} catch {
|
|
894
|
+
return { status: "pending" };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
// --- Rebalance ---
|
|
898
|
+
async rebalancePreview(body) {
|
|
899
|
+
const { owner, chain = "base", targets, slippage = 0.5 } = body;
|
|
900
|
+
if (!owner) {
|
|
901
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
902
|
+
}
|
|
903
|
+
if (!targets || targets.length === 0) {
|
|
904
|
+
throw new CompassServiceError("Missing targets", 400);
|
|
905
|
+
}
|
|
906
|
+
for (const t of targets) {
|
|
907
|
+
if (t.targetPercent < 0 || t.targetPercent > 100) {
|
|
908
|
+
throw new CompassServiceError(`Invalid target percentage: ${t.targetPercent}%`, 400);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const positionsResponse = await this.client.earn.earnPositions({
|
|
913
|
+
chain,
|
|
914
|
+
owner
|
|
915
|
+
});
|
|
916
|
+
const positionsRaw = JSON.parse(JSON.stringify(positionsResponse));
|
|
917
|
+
const balancesResponse = await this.client.earn.earnBalances({
|
|
918
|
+
chain,
|
|
919
|
+
owner
|
|
920
|
+
});
|
|
921
|
+
const balancesRaw = JSON.parse(JSON.stringify(balancesResponse));
|
|
922
|
+
const currentPositions = [];
|
|
923
|
+
for (const a of positionsRaw.aave || []) {
|
|
924
|
+
const balance = a.balance || "0";
|
|
925
|
+
if (parseFloat(balance) <= 0) continue;
|
|
926
|
+
const symbol = (a.reserveSymbol || a.reserve_symbol || "UNKNOWN").toUpperCase();
|
|
927
|
+
currentPositions.push({
|
|
928
|
+
venueType: "AAVE",
|
|
929
|
+
venueAddress: symbol,
|
|
930
|
+
token: symbol,
|
|
931
|
+
usdValue: parseFloat(a.usdValue || a.usd_value || balance),
|
|
932
|
+
balance
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
for (const v of positionsRaw.vaults || []) {
|
|
936
|
+
const balance = v.balance || "0";
|
|
937
|
+
if (parseFloat(balance) <= 0) continue;
|
|
938
|
+
const symbol = (v.underlyingSymbol || v.underlying_symbol || "TOKEN").toUpperCase();
|
|
939
|
+
currentPositions.push({
|
|
940
|
+
venueType: "VAULT",
|
|
941
|
+
venueAddress: v.vaultAddress || v.vault_address || "",
|
|
942
|
+
token: symbol,
|
|
943
|
+
usdValue: parseFloat(v.usdValue || v.usd_value || balance),
|
|
944
|
+
balance
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
for (const p of positionsRaw.pendlePt || positionsRaw.pendle_pt || []) {
|
|
948
|
+
const balance = p.ptBalance || p.pt_balance || p.balance || "0";
|
|
949
|
+
if (parseFloat(balance) <= 0) continue;
|
|
950
|
+
const symbol = (p.underlyingSymbol || p.underlying_symbol || "PT").toUpperCase();
|
|
951
|
+
currentPositions.push({
|
|
952
|
+
venueType: "PENDLE_PT",
|
|
953
|
+
venueAddress: p.marketAddress || p.market_address || "",
|
|
954
|
+
token: symbol,
|
|
955
|
+
usdValue: parseFloat(p.usdValue || p.usd_value || balance),
|
|
956
|
+
balance
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
let totalIdleUsd = 0;
|
|
960
|
+
for (const [, tokenData] of Object.entries(balancesRaw.balances || {})) {
|
|
961
|
+
const td = tokenData;
|
|
962
|
+
const usdVal = parseFloat(td.usd_value || td.usdValue || "0");
|
|
963
|
+
totalIdleUsd += usdVal;
|
|
964
|
+
}
|
|
965
|
+
const totalPositionUsd = currentPositions.reduce((sum, p) => sum + p.usdValue, 0);
|
|
966
|
+
const totalUsd = totalPositionUsd + totalIdleUsd;
|
|
967
|
+
if (totalUsd <= 0) {
|
|
968
|
+
throw new CompassServiceError("No portfolio value found to rebalance", 400);
|
|
969
|
+
}
|
|
970
|
+
const allTokenSymbols = /* @__PURE__ */ new Set();
|
|
971
|
+
for (const pos of currentPositions) allTokenSymbols.add(pos.token.toUpperCase());
|
|
972
|
+
for (const t of targets) if (t.token) allTokenSymbols.add(t.token.toUpperCase());
|
|
973
|
+
for (const sym of Object.keys(balancesRaw.balances || {})) allTokenSymbols.add(sym.toUpperCase());
|
|
974
|
+
const tokenPrices = {};
|
|
975
|
+
const priceResults = await Promise.allSettled(
|
|
976
|
+
[...allTokenSymbols].map(async (symbol) => {
|
|
977
|
+
const resp = await this.client.token.tokenPrice({ chain, token: symbol });
|
|
978
|
+
return { symbol, price: parseFloat(resp.price || "0") };
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
for (const result of priceResults) {
|
|
982
|
+
if (result.status === "fulfilled" && result.value.price > 0) {
|
|
983
|
+
tokenPrices[result.value.symbol] = result.value.price;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const bundleActions = [];
|
|
987
|
+
const actionsSummary = [];
|
|
988
|
+
const warnings = [];
|
|
989
|
+
const MIN_THRESHOLD_USD = 0.01;
|
|
990
|
+
const CHANGE_THRESHOLD_PCT = 0.1;
|
|
991
|
+
const pendingDeposits = [];
|
|
992
|
+
for (const target of targets) {
|
|
993
|
+
const originalPct = target.originalPercent ?? target.targetPercent;
|
|
994
|
+
if (Math.abs(target.targetPercent - originalPct) <= CHANGE_THRESHOLD_PCT) continue;
|
|
995
|
+
const targetUsd = totalUsd * (target.targetPercent / 100);
|
|
996
|
+
const current = currentPositions.find(
|
|
997
|
+
(p) => p.venueType === target.venueType && p.venueAddress.toLowerCase() === target.venueAddress.toLowerCase()
|
|
998
|
+
);
|
|
999
|
+
const currentUsd = current?.usdValue || 0;
|
|
1000
|
+
const deltaUsd = targetUsd - currentUsd;
|
|
1001
|
+
if (Math.abs(deltaUsd) < MIN_THRESHOLD_USD) continue;
|
|
1002
|
+
if (deltaUsd < 0 && current) {
|
|
1003
|
+
const withdrawFraction = Math.abs(deltaUsd) / currentUsd;
|
|
1004
|
+
const withdrawAmount = (parseFloat(current.balance) * withdrawFraction).toString();
|
|
1005
|
+
let venue;
|
|
1006
|
+
if (target.venueType === "VAULT") {
|
|
1007
|
+
venue = { type: "VAULT", vaultAddress: target.venueAddress };
|
|
1008
|
+
} else if (target.venueType === "AAVE") {
|
|
1009
|
+
venue = { type: "AAVE", token: current.token };
|
|
1010
|
+
} else if (target.venueType === "PENDLE_PT") {
|
|
1011
|
+
venue = { type: "PENDLE_PT", marketAddress: target.venueAddress, maxSlippagePercent: slippage };
|
|
1012
|
+
warnings.push(`Withdrawing from Pendle PT - check maturity implications`);
|
|
1013
|
+
}
|
|
1014
|
+
bundleActions.push({
|
|
1015
|
+
body: {
|
|
1016
|
+
actionType: "V2_MANAGE",
|
|
1017
|
+
venue,
|
|
1018
|
+
action: "WITHDRAW",
|
|
1019
|
+
amount: withdrawAmount
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
actionsSummary.push({
|
|
1023
|
+
type: "withdraw",
|
|
1024
|
+
venue: target.venueAddress,
|
|
1025
|
+
token: current.token,
|
|
1026
|
+
amount: withdrawAmount,
|
|
1027
|
+
usdValue: Math.abs(deltaUsd)
|
|
1028
|
+
});
|
|
1029
|
+
} else if (deltaUsd > 0) {
|
|
1030
|
+
let venue;
|
|
1031
|
+
const token = target.token || current?.token || "";
|
|
1032
|
+
if (target.venueType === "VAULT") {
|
|
1033
|
+
venue = { type: "VAULT", vaultAddress: target.venueAddress };
|
|
1034
|
+
} else if (target.venueType === "AAVE") {
|
|
1035
|
+
venue = { type: "AAVE", token };
|
|
1036
|
+
} else if (target.venueType === "PENDLE_PT") {
|
|
1037
|
+
venue = { type: "PENDLE_PT", marketAddress: target.venueAddress, token, maxSlippagePercent: slippage };
|
|
1038
|
+
}
|
|
1039
|
+
pendingDeposits.push({ venue, venueAddress: target.venueAddress, token, deltaUsd });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const current of currentPositions) {
|
|
1043
|
+
const hasTarget = targets.some(
|
|
1044
|
+
(t) => t.venueType === current.venueType && t.venueAddress.toLowerCase() === current.venueAddress.toLowerCase()
|
|
1045
|
+
);
|
|
1046
|
+
if (!hasTarget && current.usdValue >= MIN_THRESHOLD_USD) {
|
|
1047
|
+
let venue;
|
|
1048
|
+
if (current.venueType === "VAULT") {
|
|
1049
|
+
venue = { type: "VAULT", vaultAddress: current.venueAddress };
|
|
1050
|
+
} else if (current.venueType === "AAVE") {
|
|
1051
|
+
venue = { type: "AAVE", token: current.token };
|
|
1052
|
+
} else if (current.venueType === "PENDLE_PT") {
|
|
1053
|
+
venue = { type: "PENDLE_PT", marketAddress: current.venueAddress, maxSlippagePercent: slippage };
|
|
1054
|
+
}
|
|
1055
|
+
bundleActions.unshift({
|
|
1056
|
+
body: {
|
|
1057
|
+
actionType: "V2_MANAGE",
|
|
1058
|
+
venue,
|
|
1059
|
+
action: "WITHDRAW",
|
|
1060
|
+
amount: current.balance
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
actionsSummary.unshift({
|
|
1064
|
+
type: "withdraw",
|
|
1065
|
+
venue: current.venueAddress,
|
|
1066
|
+
token: current.token,
|
|
1067
|
+
amount: current.balance,
|
|
1068
|
+
usdValue: current.usdValue
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const availableByToken = {};
|
|
1073
|
+
for (const action of actionsSummary) {
|
|
1074
|
+
if (action.type === "withdraw") {
|
|
1075
|
+
const key = action.token.toUpperCase();
|
|
1076
|
+
if (!availableByToken[key]) availableByToken[key] = { usd: 0, tokenAmount: 0 };
|
|
1077
|
+
availableByToken[key].usd += action.usdValue;
|
|
1078
|
+
availableByToken[key].tokenAmount += parseFloat(action.amount);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
for (const [tokenSymbol, tokenData] of Object.entries(balancesRaw.balances || {})) {
|
|
1082
|
+
const td = tokenData;
|
|
1083
|
+
const usdVal = parseFloat(td.usd_value || td.usdValue || "0");
|
|
1084
|
+
const bal = parseFloat(td.balance_formatted || td.balanceFormatted || "0");
|
|
1085
|
+
if (usdVal > MIN_THRESHOLD_USD) {
|
|
1086
|
+
const key = tokenSymbol.toUpperCase();
|
|
1087
|
+
if (!availableByToken[key]) availableByToken[key] = { usd: 0, tokenAmount: 0 };
|
|
1088
|
+
availableByToken[key].usd += usdVal;
|
|
1089
|
+
availableByToken[key].tokenAmount += bal;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const depositNeedsByToken = {};
|
|
1093
|
+
for (const dep of pendingDeposits) {
|
|
1094
|
+
const key = dep.token.toUpperCase();
|
|
1095
|
+
depositNeedsByToken[key] = (depositNeedsByToken[key] || 0) + dep.deltaUsd;
|
|
1096
|
+
}
|
|
1097
|
+
for (const [depositToken, neededUsd] of Object.entries(depositNeedsByToken)) {
|
|
1098
|
+
const availableUsd = availableByToken[depositToken]?.usd || 0;
|
|
1099
|
+
let shortfallUsd = neededUsd - availableUsd;
|
|
1100
|
+
if (shortfallUsd <= MIN_THRESHOLD_USD) continue;
|
|
1101
|
+
for (const [sourceToken, sourceData] of Object.entries(availableByToken)) {
|
|
1102
|
+
if (sourceToken === depositToken) continue;
|
|
1103
|
+
const sourceNeeded = depositNeedsByToken[sourceToken] || 0;
|
|
1104
|
+
const sourceExcess = sourceData.usd - sourceNeeded;
|
|
1105
|
+
if (sourceExcess <= MIN_THRESHOLD_USD) continue;
|
|
1106
|
+
const swapUsd = Math.min(shortfallUsd, sourceExcess);
|
|
1107
|
+
if (swapUsd < MIN_THRESHOLD_USD) continue;
|
|
1108
|
+
const tokenAmountIn = sourceData.usd > 0 ? swapUsd / sourceData.usd * sourceData.tokenAmount : tokenPrices[sourceToken] ? swapUsd / tokenPrices[sourceToken] : swapUsd;
|
|
1109
|
+
bundleActions.push({
|
|
1110
|
+
body: {
|
|
1111
|
+
actionType: "V2_SWAP",
|
|
1112
|
+
tokenIn: sourceToken,
|
|
1113
|
+
tokenOut: depositToken,
|
|
1114
|
+
amountIn: tokenAmountIn.toString(),
|
|
1115
|
+
slippage
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
actionsSummary.push({
|
|
1119
|
+
type: "swap",
|
|
1120
|
+
token: sourceToken,
|
|
1121
|
+
tokenOut: depositToken,
|
|
1122
|
+
amount: tokenAmountIn,
|
|
1123
|
+
usdValue: swapUsd
|
|
1124
|
+
});
|
|
1125
|
+
sourceData.usd -= swapUsd;
|
|
1126
|
+
sourceData.tokenAmount -= tokenAmountIn;
|
|
1127
|
+
const slippageFactor = 1 - slippage / 100;
|
|
1128
|
+
if (!availableByToken[depositToken]) availableByToken[depositToken] = { usd: 0, tokenAmount: 0 };
|
|
1129
|
+
const receivedUsd = swapUsd * slippageFactor;
|
|
1130
|
+
const existingData = availableByToken[depositToken];
|
|
1131
|
+
const impliedPrice = existingData.tokenAmount > 0 && existingData.usd > 0 ? existingData.usd / existingData.tokenAmount : tokenPrices[depositToken] || 1;
|
|
1132
|
+
availableByToken[depositToken].usd += receivedUsd;
|
|
1133
|
+
availableByToken[depositToken].tokenAmount += receivedUsd / impliedPrice;
|
|
1134
|
+
shortfallUsd -= swapUsd;
|
|
1135
|
+
warnings.push(`Swap ${sourceToken} to ${depositToken} involves slippage risk`);
|
|
1136
|
+
if (shortfallUsd <= MIN_THRESHOLD_USD) break;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
for (const dep of pendingDeposits) {
|
|
1140
|
+
const key = dep.token.toUpperCase();
|
|
1141
|
+
const available = availableByToken[key];
|
|
1142
|
+
const tokenPrice = available && available.tokenAmount > 0 && available.usd > 0 ? available.usd / available.tokenAmount : tokenPrices[key] || 1;
|
|
1143
|
+
const desiredTokens = dep.deltaUsd / tokenPrice;
|
|
1144
|
+
const maxAvailableTokens = available ? available.tokenAmount * 0.95 : 0;
|
|
1145
|
+
const maxAvailableUsd = maxAvailableTokens * tokenPrice;
|
|
1146
|
+
if (maxAvailableUsd <= MIN_THRESHOLD_USD) {
|
|
1147
|
+
warnings.push(`Skipping deposit to ${dep.token} - insufficient available balance`);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const depositTokenAmount = Math.min(desiredTokens, maxAvailableTokens);
|
|
1151
|
+
bundleActions.push({
|
|
1152
|
+
body: {
|
|
1153
|
+
actionType: "V2_MANAGE",
|
|
1154
|
+
venue: dep.venue,
|
|
1155
|
+
action: "DEPOSIT",
|
|
1156
|
+
amount: depositTokenAmount.toString()
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
const depositUsd = depositTokenAmount * tokenPrice;
|
|
1160
|
+
actionsSummary.push({
|
|
1161
|
+
type: "deposit",
|
|
1162
|
+
venue: dep.venueAddress,
|
|
1163
|
+
token: dep.token,
|
|
1164
|
+
amount: depositTokenAmount.toString(),
|
|
1165
|
+
usdValue: depositUsd
|
|
1166
|
+
});
|
|
1167
|
+
if (available) {
|
|
1168
|
+
available.usd -= depositUsd;
|
|
1169
|
+
available.tokenAmount -= depositTokenAmount;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (bundleActions.length === 0 && pendingDeposits.length === 0) {
|
|
1173
|
+
return {
|
|
1174
|
+
actions: [],
|
|
1175
|
+
actionsCount: 0,
|
|
1176
|
+
warnings: ["Portfolio is already at target allocation"]
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
bundleActions.sort((a, b) => {
|
|
1180
|
+
const getOrder = (action) => {
|
|
1181
|
+
if (action.body.action === "WITHDRAW") return 0;
|
|
1182
|
+
if (action.body.actionType === "V2_SWAP") return 1;
|
|
1183
|
+
if (action.body.action === "DEPOSIT") return 2;
|
|
1184
|
+
return 3;
|
|
1185
|
+
};
|
|
1186
|
+
return getOrder(a) - getOrder(b);
|
|
1187
|
+
});
|
|
1188
|
+
actionsSummary.sort((a, b) => {
|
|
1189
|
+
const order = { withdraw: 0, swap: 1, deposit: 2 };
|
|
1190
|
+
return (order[a.type] || 0) - (order[b.type] || 0);
|
|
1191
|
+
});
|
|
1192
|
+
if (actionsSummary.some((a) => a.type === "swap")) {
|
|
1193
|
+
warnings.push("Swap amounts are estimates - actual amounts may vary due to slippage");
|
|
1194
|
+
}
|
|
1195
|
+
const bundleResponse = await this.client.earn.earnBundle({
|
|
1196
|
+
owner,
|
|
1197
|
+
chain,
|
|
1198
|
+
gasSponsorship: true,
|
|
1199
|
+
actions: bundleActions
|
|
1200
|
+
});
|
|
1201
|
+
const eip712 = bundleResponse.eip712;
|
|
1202
|
+
if (!eip712) {
|
|
1203
|
+
throw new CompassServiceError("No EIP-712 data returned from bundle API", 500);
|
|
1204
|
+
}
|
|
1205
|
+
const types = eip712.types;
|
|
1206
|
+
const normalizedTypes = {
|
|
1207
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
1208
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
1209
|
+
};
|
|
1210
|
+
return {
|
|
1211
|
+
eip712,
|
|
1212
|
+
normalizedTypes,
|
|
1213
|
+
domain: eip712.domain,
|
|
1214
|
+
message: eip712.message,
|
|
1215
|
+
actions: actionsSummary,
|
|
1216
|
+
actionsCount: bundleActions.length,
|
|
1217
|
+
warnings
|
|
1218
|
+
};
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
if (error instanceof CompassServiceError) throw error;
|
|
1221
|
+
const message = error instanceof Error ? error.message : "Failed to compute rebalance preview";
|
|
1222
|
+
throw new CompassServiceError(message, 502);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
// --- Credit ---
|
|
1226
|
+
async creditAccountCheck(params) {
|
|
1227
|
+
const { owner, chain = "base" } = params;
|
|
1228
|
+
if (!owner) {
|
|
1229
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
1230
|
+
}
|
|
1231
|
+
const response = await this.client.credit.creditCreateAccount({
|
|
1232
|
+
chain,
|
|
1233
|
+
owner,
|
|
1234
|
+
sender: owner,
|
|
1235
|
+
estimateGas: false
|
|
1236
|
+
});
|
|
1237
|
+
const creditAccountAddress = response.creditAccountAddress;
|
|
1238
|
+
const hasTransaction = !!response.transaction;
|
|
1239
|
+
return {
|
|
1240
|
+
creditAccountAddress,
|
|
1241
|
+
isDeployed: !hasTransaction,
|
|
1242
|
+
needsCreation: hasTransaction
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
async creditCreateAccount(body) {
|
|
1246
|
+
const { owner, chain = "base" } = body;
|
|
1247
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
1248
|
+
if (!owner) {
|
|
1249
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
1250
|
+
}
|
|
1251
|
+
if (!gasSponsorPrivateKey) {
|
|
1252
|
+
throw new CompassServiceError(
|
|
1253
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
1254
|
+
500
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
1258
|
+
if (!viemChain) {
|
|
1259
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
1260
|
+
}
|
|
1261
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
1262
|
+
if (!rpcUrl) {
|
|
1263
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
1264
|
+
}
|
|
1265
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
1266
|
+
const walletClient = viem.createWalletClient({
|
|
1267
|
+
account: sponsorAccount,
|
|
1268
|
+
chain: viemChain,
|
|
1269
|
+
transport: viem.http(rpcUrl)
|
|
1270
|
+
});
|
|
1271
|
+
const publicClient = viem.createPublicClient({
|
|
1272
|
+
chain: viemChain,
|
|
1273
|
+
transport: viem.http(rpcUrl)
|
|
1274
|
+
});
|
|
1275
|
+
const response = await this.client.credit.creditCreateAccount({
|
|
1276
|
+
chain,
|
|
1277
|
+
owner,
|
|
1278
|
+
sender: sponsorAccount.address,
|
|
1279
|
+
estimateGas: false
|
|
1280
|
+
});
|
|
1281
|
+
const creditAccountAddress = response.creditAccountAddress;
|
|
1282
|
+
if (!response.transaction) {
|
|
1283
|
+
return {
|
|
1284
|
+
creditAccountAddress,
|
|
1285
|
+
success: true,
|
|
1286
|
+
alreadyExists: true
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
const transaction = response.transaction;
|
|
1290
|
+
const txHash = await walletClient.sendTransaction({
|
|
1291
|
+
to: transaction.to,
|
|
1292
|
+
data: transaction.data,
|
|
1293
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
1294
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
1295
|
+
});
|
|
1296
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
1297
|
+
hash: txHash
|
|
1298
|
+
});
|
|
1299
|
+
if (receipt.status === "reverted") {
|
|
1300
|
+
throw new CompassServiceError("Account creation transaction reverted", 500);
|
|
1301
|
+
}
|
|
1302
|
+
return {
|
|
1303
|
+
creditAccountAddress,
|
|
1304
|
+
txHash,
|
|
1305
|
+
success: true
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
async creditPositions(params) {
|
|
1309
|
+
const { owner, chain = "base" } = params;
|
|
1310
|
+
if (!owner) {
|
|
1311
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
1312
|
+
}
|
|
1313
|
+
const response = await this.client.credit.creditPositions({
|
|
1314
|
+
chain,
|
|
1315
|
+
owner
|
|
1316
|
+
});
|
|
1317
|
+
return response;
|
|
1318
|
+
}
|
|
1319
|
+
async creditBalances(params) {
|
|
1320
|
+
const { owner, chain = "base" } = params;
|
|
1321
|
+
if (!owner) {
|
|
1322
|
+
throw new CompassServiceError("Missing owner parameter", 400);
|
|
1323
|
+
}
|
|
1324
|
+
const tokens = CREDIT_TOKENS[chain.toLowerCase()] || CREDIT_TOKENS["base"];
|
|
1325
|
+
const balances = await Promise.allSettled(
|
|
1326
|
+
tokens.map(async (token) => {
|
|
1327
|
+
const response = await this.client.token.tokenBalance({
|
|
1328
|
+
chain,
|
|
1329
|
+
token,
|
|
1330
|
+
user: owner
|
|
1331
|
+
});
|
|
1332
|
+
return {
|
|
1333
|
+
tokenSymbol: token,
|
|
1334
|
+
amount: response.amount || "0",
|
|
1335
|
+
decimals: response.decimals || 18,
|
|
1336
|
+
tokenAddress: response.tokenAddress || ""
|
|
1337
|
+
};
|
|
1338
|
+
})
|
|
1339
|
+
);
|
|
1340
|
+
const result = balances.filter((b) => b.status === "fulfilled").map((b) => b.value);
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
1343
|
+
async creditBundlePrepare(body) {
|
|
1344
|
+
const { owner, chain = "base", actions } = body;
|
|
1345
|
+
if (!owner || !actions || actions.length === 0) {
|
|
1346
|
+
throw new CompassServiceError("Missing owner or actions", 400);
|
|
1347
|
+
}
|
|
1348
|
+
const wrappedActions = actions.map((action) => ({ body: action }));
|
|
1349
|
+
const response = await this.client.credit.creditBundle({
|
|
1350
|
+
owner,
|
|
1351
|
+
chain,
|
|
1352
|
+
gasSponsorship: true,
|
|
1353
|
+
actions: wrappedActions
|
|
1354
|
+
});
|
|
1355
|
+
const eip712 = response.eip712;
|
|
1356
|
+
if (!eip712) {
|
|
1357
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
1358
|
+
}
|
|
1359
|
+
const types = eip712.types;
|
|
1360
|
+
const normalizedTypes = {
|
|
1361
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain,
|
|
1362
|
+
SafeTx: types.safeTx || types.SafeTx
|
|
1363
|
+
};
|
|
1364
|
+
return {
|
|
1365
|
+
eip712,
|
|
1366
|
+
normalizedTypes,
|
|
1367
|
+
domain: eip712.domain,
|
|
1368
|
+
message: eip712.message,
|
|
1369
|
+
actionsCount: response.actionsCount || actions.length
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
async creditTransfer(body) {
|
|
1373
|
+
const { owner, chain = "base", token, amount } = body;
|
|
1374
|
+
if (!owner || !token || !amount) {
|
|
1375
|
+
throw new CompassServiceError("Missing required parameters", 400);
|
|
1376
|
+
}
|
|
1377
|
+
const response = await this.client.credit.creditTransfer({
|
|
1378
|
+
owner,
|
|
1379
|
+
chain,
|
|
1380
|
+
token,
|
|
1381
|
+
amount,
|
|
1382
|
+
action: "DEPOSIT",
|
|
1383
|
+
gasSponsorship: true
|
|
1384
|
+
});
|
|
1385
|
+
const eip712 = response.eip712;
|
|
1386
|
+
if (!eip712) {
|
|
1387
|
+
throw new CompassServiceError("No EIP-712 data returned from API", 500);
|
|
1388
|
+
}
|
|
1389
|
+
const types = eip712.types;
|
|
1390
|
+
const normalizedTypes = {
|
|
1391
|
+
EIP712Domain: types.eip712Domain || types.EIP712Domain
|
|
1392
|
+
};
|
|
1393
|
+
if (types.permitTransferFrom || types.PermitTransferFrom) {
|
|
1394
|
+
normalizedTypes.PermitTransferFrom = types.permitTransferFrom || types.PermitTransferFrom;
|
|
1395
|
+
}
|
|
1396
|
+
if (types.tokenPermissions || types.TokenPermissions) {
|
|
1397
|
+
normalizedTypes.TokenPermissions = types.tokenPermissions || types.TokenPermissions;
|
|
1398
|
+
}
|
|
1399
|
+
if (types.safeTx || types.SafeTx) {
|
|
1400
|
+
normalizedTypes.SafeTx = types.safeTx || types.SafeTx;
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
eip712,
|
|
1404
|
+
normalizedTypes,
|
|
1405
|
+
domain: eip712.domain,
|
|
1406
|
+
message: eip712.message,
|
|
1407
|
+
primaryType: eip712.primaryType
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
async creditExecute(body) {
|
|
1411
|
+
const { owner, eip712, signature, chain = "base" } = body;
|
|
1412
|
+
const { gasSponsorPrivateKey, rpcUrls } = this.config;
|
|
1413
|
+
if (!owner || !eip712 || !signature) {
|
|
1414
|
+
throw new CompassServiceError("Missing required parameters (owner, eip712, signature)", 400);
|
|
1415
|
+
}
|
|
1416
|
+
if (!gasSponsorPrivateKey) {
|
|
1417
|
+
throw new CompassServiceError(
|
|
1418
|
+
"Gas sponsor not configured. Set gasSponsorPrivateKey in handler config.",
|
|
1419
|
+
500
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
const viemChain = CHAIN_MAP[chain.toLowerCase()];
|
|
1423
|
+
if (!viemChain) {
|
|
1424
|
+
throw new CompassServiceError(`Unsupported chain: ${chain}`, 500);
|
|
1425
|
+
}
|
|
1426
|
+
const rpcUrl = rpcUrls?.[chain.toLowerCase()];
|
|
1427
|
+
if (!rpcUrl) {
|
|
1428
|
+
throw new CompassServiceError(`No RPC URL configured for chain: ${chain}`, 500);
|
|
1429
|
+
}
|
|
1430
|
+
const sponsorAccount = accounts.privateKeyToAccount(gasSponsorPrivateKey);
|
|
1431
|
+
const walletClient = viem.createWalletClient({
|
|
1432
|
+
account: sponsorAccount,
|
|
1433
|
+
chain: viemChain,
|
|
1434
|
+
transport: viem.http(rpcUrl)
|
|
1435
|
+
});
|
|
1436
|
+
const publicClient = viem.createPublicClient({
|
|
1437
|
+
chain: viemChain,
|
|
1438
|
+
transport: viem.http(rpcUrl)
|
|
1439
|
+
});
|
|
1440
|
+
const response = await this.client.gasSponsorship.gasSponsorshipPrepare({
|
|
1441
|
+
chain,
|
|
1442
|
+
owner,
|
|
1443
|
+
sender: sponsorAccount.address,
|
|
1444
|
+
eip712,
|
|
1445
|
+
signature,
|
|
1446
|
+
product: "credit"
|
|
1447
|
+
});
|
|
1448
|
+
const transaction = response.transaction;
|
|
1449
|
+
if (!transaction) {
|
|
1450
|
+
throw new CompassServiceError(
|
|
1451
|
+
"No transaction returned from gas sponsorship prepare",
|
|
1452
|
+
500
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
const txHash = await walletClient.sendTransaction({
|
|
1456
|
+
to: transaction.to,
|
|
1457
|
+
data: transaction.data,
|
|
1458
|
+
value: transaction.value ? BigInt(transaction.value) : 0n,
|
|
1459
|
+
gas: transaction.gas ? BigInt(transaction.gas) : void 0
|
|
1460
|
+
});
|
|
1461
|
+
const receipt = await publicClient.waitForTransactionReceipt({
|
|
1462
|
+
hash: txHash
|
|
1463
|
+
});
|
|
1464
|
+
if (receipt.status === "reverted") {
|
|
1465
|
+
throw new CompassServiceError("Transaction reverted", 500);
|
|
1466
|
+
}
|
|
1467
|
+
return { txHash, success: true };
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
exports.CHAIN_MAP = CHAIN_MAP;
|
|
1472
|
+
exports.CREDIT_TOKENS = CREDIT_TOKENS;
|
|
1473
|
+
exports.CompassCoreService = CompassCoreService;
|
|
1474
|
+
exports.CompassServiceError = CompassServiceError;
|
|
1475
|
+
exports.extractErrorMessage = extractErrorMessage;
|
|
1476
|
+
exports.jsonResponse = jsonResponse;
|
|
1477
|
+
//# sourceMappingURL=index.js.map
|
|
1478
|
+
//# sourceMappingURL=index.js.map
|