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