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