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