@compass-labs/widgets 0.1.42 → 0.1.44

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