@agentlayer.tech/wallet 0.1.15 → 0.1.16
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/.openclaw/extensions/agent-wallet/dist/index.js +117 -1
- package/.openclaw/extensions/agent-wallet/index.ts +117 -1
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +4 -0
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/.openclaw/extensions/pay-bridge/package.json +1 -1
- package/CHANGELOG.md +14 -0
- package/RELEASING.md +97 -0
- package/agent-wallet/.env.example +5 -0
- package/agent-wallet/README.md +24 -0
- package/agent-wallet/agent_wallet/config.py +4 -0
- package/agent-wallet/agent_wallet/openclaw_adapter.py +504 -0
- package/agent-wallet/agent_wallet/providers/flash.py +186 -0
- package/agent-wallet/agent_wallet/providers/flash_sdk_bridge.py +251 -0
- package/agent-wallet/agent_wallet/transaction_policy.py +79 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +78 -0
- package/agent-wallet/agent_wallet/wallet_layer/solana.py +623 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/flash-sdk-bridge/README.md +33 -0
- package/agent-wallet/scripts/flash-sdk-bridge/bridge.mjs +1179 -0
- package/agent-wallet/scripts/flash-sdk-bridge/package-lock.json +2377 -0
- package/agent-wallet/scripts/flash-sdk-bridge/package.json +12 -0
- package/agent-wallet/scripts/install_agent_wallet.py +46 -11
- package/agent-wallet/scripts/install_openclaw_local_config.py +4 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1179 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import util from "node:util";
|
|
4
|
+
|
|
5
|
+
const BRIDGE_NAME = "flash-sdk-bridge";
|
|
6
|
+
const USD_DECIMALS = 6;
|
|
7
|
+
const BPS_DECIMALS = 4;
|
|
8
|
+
const DEFAULT_COMPUTE_UNIT_LIMIT = 600_000;
|
|
9
|
+
// Flash docs use slippageBps = 800 with a 0.8% comment.
|
|
10
|
+
// The SDK uses BPS_DECIMALS=4, so 0.8% maps to raw 80.
|
|
11
|
+
const DEFAULT_SLIPPAGE_BPS_RAW = "80";
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
const forwardConsoleToStderr = (method) => {
|
|
15
|
+
console[method] = (...args) => {
|
|
16
|
+
const rendered = args
|
|
17
|
+
.map((value) => (typeof value === "string" ? value : util.inspect(value, { depth: 5 })))
|
|
18
|
+
.join(" ");
|
|
19
|
+
process.stderr.write(`${rendered}\n`);
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
forwardConsoleToStderr("log");
|
|
24
|
+
forwardConsoleToStderr("info");
|
|
25
|
+
forwardConsoleToStderr("warn");
|
|
26
|
+
forwardConsoleToStderr("debug");
|
|
27
|
+
|
|
28
|
+
function jsonError(message, extra = {}) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: message,
|
|
32
|
+
provider: BRIDGE_NAME,
|
|
33
|
+
...extra,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function requireString(value, fieldName) {
|
|
38
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
39
|
+
throw new Error(`${fieldName} is required`);
|
|
40
|
+
}
|
|
41
|
+
return value.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeSide(value) {
|
|
45
|
+
const side = requireString(value, "side").toLowerCase();
|
|
46
|
+
if (side !== "long" && side !== "short") {
|
|
47
|
+
throw new Error("side must be 'long' or 'short'");
|
|
48
|
+
}
|
|
49
|
+
return side;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeActionInput(payload) {
|
|
53
|
+
const action = requireString(payload.action, "action");
|
|
54
|
+
const owner =
|
|
55
|
+
typeof payload.owner === "string" && payload.owner.trim() ? payload.owner.trim() : undefined;
|
|
56
|
+
const poolName =
|
|
57
|
+
typeof payload.pool_name === "string" && payload.pool_name.trim()
|
|
58
|
+
? payload.pool_name.trim()
|
|
59
|
+
: undefined;
|
|
60
|
+
const marketSymbol =
|
|
61
|
+
typeof payload.market_symbol === "string" && payload.market_symbol.trim()
|
|
62
|
+
? payload.market_symbol.trim().toUpperCase()
|
|
63
|
+
: undefined;
|
|
64
|
+
const side = typeof payload.side === "string" && payload.side.trim() ? normalizeSide(payload.side) : undefined;
|
|
65
|
+
return {
|
|
66
|
+
action,
|
|
67
|
+
owner,
|
|
68
|
+
poolName,
|
|
69
|
+
marketSymbol,
|
|
70
|
+
side,
|
|
71
|
+
collateralSymbol:
|
|
72
|
+
typeof payload.collateral_symbol === "string"
|
|
73
|
+
? payload.collateral_symbol.trim().toUpperCase()
|
|
74
|
+
: undefined,
|
|
75
|
+
collateralAmountRaw:
|
|
76
|
+
typeof payload.collateral_amount_raw === "string"
|
|
77
|
+
? payload.collateral_amount_raw.trim()
|
|
78
|
+
: undefined,
|
|
79
|
+
leverage:
|
|
80
|
+
typeof payload.leverage === "string"
|
|
81
|
+
? payload.leverage.trim()
|
|
82
|
+
: undefined,
|
|
83
|
+
network:
|
|
84
|
+
typeof payload.network === "string" && payload.network.trim()
|
|
85
|
+
? payload.network.trim()
|
|
86
|
+
: "mainnet",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mockResponse(normalized) {
|
|
91
|
+
if (normalized.action === "get_markets") {
|
|
92
|
+
return {
|
|
93
|
+
ok: true,
|
|
94
|
+
data: {
|
|
95
|
+
bridge_mode: "mock",
|
|
96
|
+
pool_name: normalized.poolName ?? null,
|
|
97
|
+
pool_count: 1,
|
|
98
|
+
market_count: 2,
|
|
99
|
+
source: "flash-sdk-bridge",
|
|
100
|
+
markets: [
|
|
101
|
+
{
|
|
102
|
+
pool_name: normalized.poolName ?? "Crypto.1",
|
|
103
|
+
symbol: "SOL",
|
|
104
|
+
market_symbol: "SOL",
|
|
105
|
+
collateral_symbol: "SOL",
|
|
106
|
+
side: "long",
|
|
107
|
+
market_address: "MockFlashMarketLong11111111111111111111111111",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
pool_name: normalized.poolName ?? "Crypto.1",
|
|
111
|
+
symbol: "SOL",
|
|
112
|
+
market_symbol: "SOL",
|
|
113
|
+
collateral_symbol: "SOL",
|
|
114
|
+
side: "short",
|
|
115
|
+
market_address: "MockFlashMarketShort1111111111111111111111111",
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (normalized.action === "get_positions") {
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
data: {
|
|
126
|
+
bridge_mode: "mock",
|
|
127
|
+
owner: normalized.owner,
|
|
128
|
+
pool_name: normalized.poolName ?? null,
|
|
129
|
+
pool_count: 1,
|
|
130
|
+
position_count: 1,
|
|
131
|
+
source: "flash-sdk-bridge",
|
|
132
|
+
positions: [
|
|
133
|
+
{
|
|
134
|
+
pool_name: normalized.poolName ?? "Crypto.1",
|
|
135
|
+
symbol: "SOL",
|
|
136
|
+
market_symbol: "SOL",
|
|
137
|
+
collateral_symbol: "SOL",
|
|
138
|
+
side: "long",
|
|
139
|
+
is_active: true,
|
|
140
|
+
position_address: "MockFlashPosition111111111111111111111111111",
|
|
141
|
+
market_address: "MockFlashMarketLong11111111111111111111111111",
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (normalized.action === "preview_open_position_same_collateral") {
|
|
149
|
+
return {
|
|
150
|
+
ok: true,
|
|
151
|
+
preview: {
|
|
152
|
+
bridge_mode: "mock",
|
|
153
|
+
estimated_size_usd: "1250.00",
|
|
154
|
+
estimated_entry_price: "177.50",
|
|
155
|
+
estimated_liquidation_price: "161.20",
|
|
156
|
+
pool_name: normalized.poolName,
|
|
157
|
+
market_symbol: normalized.marketSymbol,
|
|
158
|
+
collateral_symbol: normalized.collateralSymbol,
|
|
159
|
+
collateral_amount_raw: normalized.collateralAmountRaw,
|
|
160
|
+
leverage: normalized.leverage,
|
|
161
|
+
side: normalized.side,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (normalized.action === "preview_close_position_same_collateral") {
|
|
167
|
+
return {
|
|
168
|
+
ok: true,
|
|
169
|
+
preview: {
|
|
170
|
+
bridge_mode: "mock",
|
|
171
|
+
position_size_usd: "1250.00",
|
|
172
|
+
close_amount_raw: "700000000",
|
|
173
|
+
pool_name: normalized.poolName,
|
|
174
|
+
market_symbol: normalized.marketSymbol,
|
|
175
|
+
side: normalized.side,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (normalized.action === "prepare_open_position_same_collateral") {
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
prepared: {
|
|
184
|
+
bridge_mode: "mock",
|
|
185
|
+
pool_name: normalized.poolName,
|
|
186
|
+
market_symbol: normalized.marketSymbol,
|
|
187
|
+
collateral_symbol: normalized.collateralSymbol,
|
|
188
|
+
collateral_amount_raw: normalized.collateralAmountRaw,
|
|
189
|
+
leverage: normalized.leverage,
|
|
190
|
+
side: normalized.side,
|
|
191
|
+
transaction_base64: "AQID",
|
|
192
|
+
transaction_encoding: "base64",
|
|
193
|
+
transaction_format: "versioned",
|
|
194
|
+
latest_blockhash: "MockFlashBlockhash111111111111111111111111111",
|
|
195
|
+
last_valid_block_height: 123456,
|
|
196
|
+
market_address: "MockFlashMarket11111111111111111111111111111",
|
|
197
|
+
position_address: "MockFlashPosition111111111111111111111111111",
|
|
198
|
+
target_custody_address: "MockFlashTargetCustody1111111111111111111111111",
|
|
199
|
+
collateral_custody_address: "MockFlashCollateralCustody1111111111111111111111",
|
|
200
|
+
collateral_mint: "So11111111111111111111111111111111111111112",
|
|
201
|
+
expected_program_ids: ["MockFlashProgram111111111111111111111111111111"],
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (normalized.action === "prepare_close_position_same_collateral") {
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
prepared: {
|
|
210
|
+
bridge_mode: "mock",
|
|
211
|
+
pool_name: normalized.poolName,
|
|
212
|
+
market_symbol: normalized.marketSymbol,
|
|
213
|
+
collateral_symbol: normalized.collateralSymbol ?? normalized.marketSymbol,
|
|
214
|
+
side: normalized.side,
|
|
215
|
+
transaction_base64: "AQID",
|
|
216
|
+
transaction_encoding: "base64",
|
|
217
|
+
transaction_format: "versioned",
|
|
218
|
+
latest_blockhash: "MockFlashBlockhash111111111111111111111111111",
|
|
219
|
+
last_valid_block_height: 123456,
|
|
220
|
+
market_address: "MockFlashMarket11111111111111111111111111111",
|
|
221
|
+
position_address: "MockFlashPosition111111111111111111111111111",
|
|
222
|
+
target_custody_address: "MockFlashTargetCustody1111111111111111111111111",
|
|
223
|
+
collateral_custody_address: "MockFlashCollateralCustody1111111111111111111111",
|
|
224
|
+
collateral_mint: "So11111111111111111111111111111111111111112",
|
|
225
|
+
expected_program_ids: ["MockFlashProgram111111111111111111111111111111"],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return jsonError(`Unsupported mock action: ${normalized.action}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function loadRealModules() {
|
|
234
|
+
if (!process.env.NEXT_PUBLIC_API_ENDPOINT && process.env.FLASH_API_ENDPOINT) {
|
|
235
|
+
process.env.NEXT_PUBLIC_API_ENDPOINT = process.env.FLASH_API_ENDPOINT;
|
|
236
|
+
}
|
|
237
|
+
const poolConfigCatalog = require("flash-sdk/dist/PoolConfig.json");
|
|
238
|
+
const [anchor, web3, flashSdk, bnModule] = await Promise.all([
|
|
239
|
+
import("@coral-xyz/anchor"),
|
|
240
|
+
import("@solana/web3.js"),
|
|
241
|
+
import("flash-sdk"),
|
|
242
|
+
import("bn.js"),
|
|
243
|
+
]);
|
|
244
|
+
return { anchor, web3, flashSdk, poolConfigCatalog, BN: bnModule.default };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function createReadOnlyWallet(web3, owner) {
|
|
248
|
+
const publicKey = new web3.PublicKey(owner);
|
|
249
|
+
return {
|
|
250
|
+
publicKey,
|
|
251
|
+
async signTransaction() {
|
|
252
|
+
throw new Error("Readonly Flash bridge wallet cannot sign transactions");
|
|
253
|
+
},
|
|
254
|
+
async signAllTransactions() {
|
|
255
|
+
throw new Error("Readonly Flash bridge wallet cannot sign transactions");
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function resolveClusterName(network) {
|
|
261
|
+
return network === "mainnet" ? "mainnet-beta" : network;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function defaultRpcUrlForNetwork(network) {
|
|
265
|
+
if (network === "mainnet") {
|
|
266
|
+
return "https://api.mainnet-beta.solana.com";
|
|
267
|
+
}
|
|
268
|
+
if (network === "devnet") {
|
|
269
|
+
return "https://api.devnet.solana.com";
|
|
270
|
+
}
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function buildRuntimeContext(normalized, poolConfigOverride = null) {
|
|
275
|
+
const rpcUrl =
|
|
276
|
+
process.env.RPC_URL?.trim() ||
|
|
277
|
+
process.env.SOLANA_RPC_URL?.trim() ||
|
|
278
|
+
defaultRpcUrlForNetwork(normalized.network);
|
|
279
|
+
if (!rpcUrl) {
|
|
280
|
+
throw new Error("RPC_URL or SOLANA_RPC_URL is required in FLASH_SDK_BRIDGE_MODE=real");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const { anchor, web3, flashSdk, BN } = await loadRealModules();
|
|
284
|
+
const wallet = createReadOnlyWallet(web3, normalized.owner);
|
|
285
|
+
const connection = new web3.Connection(rpcUrl, {
|
|
286
|
+
commitment: "confirmed",
|
|
287
|
+
});
|
|
288
|
+
const provider = new anchor.AnchorProvider(connection, wallet, {
|
|
289
|
+
commitment: "confirmed",
|
|
290
|
+
preflightCommitment: "confirmed",
|
|
291
|
+
skipPreflight: true,
|
|
292
|
+
});
|
|
293
|
+
if (!normalized.owner) {
|
|
294
|
+
throw new Error("owner is required");
|
|
295
|
+
}
|
|
296
|
+
if (!normalized.poolName) {
|
|
297
|
+
throw new Error("pool_name is required");
|
|
298
|
+
}
|
|
299
|
+
const poolConfig =
|
|
300
|
+
poolConfigOverride ||
|
|
301
|
+
flashSdk.PoolConfig.fromIdsByName(normalized.poolName, resolveClusterName(normalized.network));
|
|
302
|
+
const client = new flashSdk.PerpetualsClient(
|
|
303
|
+
provider,
|
|
304
|
+
poolConfig.programId,
|
|
305
|
+
poolConfig.perpComposibilityProgramId,
|
|
306
|
+
poolConfig.fbNftRewardProgramId,
|
|
307
|
+
poolConfig.rewardDistributionProgram.programId,
|
|
308
|
+
{
|
|
309
|
+
prioritizationFee: 0,
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
return { anchor, web3, flashSdk, BN, provider, poolConfig, client, rpcUrl };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function listPoolConfigs(flashSdk, poolConfigCatalog, network, poolName) {
|
|
316
|
+
const cluster = resolveClusterName(network);
|
|
317
|
+
const pools = Array.isArray(poolConfigCatalog?.pools) ? poolConfigCatalog.pools : [];
|
|
318
|
+
const matching = pools.filter(
|
|
319
|
+
(pool) =>
|
|
320
|
+
pool?.cluster === cluster &&
|
|
321
|
+
(!poolName || String(pool.poolName || "").trim() === poolName),
|
|
322
|
+
);
|
|
323
|
+
if (matching.length === 0) {
|
|
324
|
+
if (poolName) {
|
|
325
|
+
throw new Error(`Pool ${poolName} is not available on ${network}`);
|
|
326
|
+
}
|
|
327
|
+
throw new Error(`No Flash pools are configured for ${network}`);
|
|
328
|
+
}
|
|
329
|
+
return matching.map((pool) => flashSdk.PoolConfig.buildPoolconfigFromJson(pool));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function variantToSide(sideVariant) {
|
|
333
|
+
if (sideVariant && typeof sideVariant === "object") {
|
|
334
|
+
if ("long" in sideVariant) {
|
|
335
|
+
return "long";
|
|
336
|
+
}
|
|
337
|
+
if ("short" in sideVariant) {
|
|
338
|
+
return "short";
|
|
339
|
+
}
|
|
340
|
+
if ("none" in sideVariant) {
|
|
341
|
+
return "none";
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return String(sideVariant ?? "");
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function buildMarketSnapshot(poolConfig, marketConfig, deprecated = false) {
|
|
348
|
+
const targetToken = poolConfig.getTokenFromMintPk(marketConfig.targetMint);
|
|
349
|
+
const collateralToken = poolConfig.getTokenFromMintPk(marketConfig.collateralMint);
|
|
350
|
+
return {
|
|
351
|
+
pool_name: poolConfig.poolName,
|
|
352
|
+
symbol: targetToken.symbol,
|
|
353
|
+
market_symbol: targetToken.symbol,
|
|
354
|
+
collateral_symbol: collateralToken.symbol,
|
|
355
|
+
side: variantToSide(marketConfig.side),
|
|
356
|
+
market_id: marketConfig.marketId,
|
|
357
|
+
market_address: marketConfig.marketAccount.toBase58(),
|
|
358
|
+
target_custody_address: marketConfig.targetCustody.toBase58(),
|
|
359
|
+
collateral_custody_address: marketConfig.collateralCustody.toBase58(),
|
|
360
|
+
target_mint: marketConfig.targetMint.toBase58(),
|
|
361
|
+
collateral_mint: marketConfig.collateralMint.toBase58(),
|
|
362
|
+
max_leverage: marketConfig.maxLev,
|
|
363
|
+
degen_min_leverage: marketConfig.degenMinLev,
|
|
364
|
+
degen_max_leverage: marketConfig.degenMaxLev,
|
|
365
|
+
deprecated,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function buildPositionSnapshot(poolConfig, positionAccount) {
|
|
370
|
+
const marketConfig = poolConfig.getMarketConfigByPk(positionAccount.market);
|
|
371
|
+
const targetToken = poolConfig.getTokenFromMintPk(marketConfig.targetMint);
|
|
372
|
+
const collateralToken = poolConfig.getTokenFromMintPk(marketConfig.collateralMint);
|
|
373
|
+
return {
|
|
374
|
+
pool_name: poolConfig.poolName,
|
|
375
|
+
symbol: targetToken.symbol,
|
|
376
|
+
market_symbol: targetToken.symbol,
|
|
377
|
+
collateral_symbol: collateralToken.symbol,
|
|
378
|
+
side: variantToSide(marketConfig.side),
|
|
379
|
+
is_active: Boolean(positionAccount.isActive),
|
|
380
|
+
position_address: positionAccount.pubkey.toBase58(),
|
|
381
|
+
market_address: positionAccount.market.toBase58(),
|
|
382
|
+
entry_price: serializeOraclePrice(positionAccount.entryPrice)?.ui_price ?? null,
|
|
383
|
+
reference_price: serializeOraclePrice(positionAccount.referencePrice)?.ui_price ?? null,
|
|
384
|
+
size_amount_raw: positionAccount.sizeAmount.toString(10),
|
|
385
|
+
size_usd: integerExponentToDecimal(positionAccount.sizeUsd.toString(10), -USD_DECIMALS),
|
|
386
|
+
collateral_usd: integerExponentToDecimal(
|
|
387
|
+
positionAccount.collateralUsd.toString(10),
|
|
388
|
+
-USD_DECIMALS,
|
|
389
|
+
),
|
|
390
|
+
unsettled_value_usd: integerExponentToDecimal(
|
|
391
|
+
positionAccount.unsettledValueUsd.toString(10),
|
|
392
|
+
-USD_DECIMALS,
|
|
393
|
+
),
|
|
394
|
+
unsettled_fees_usd: integerExponentToDecimal(
|
|
395
|
+
positionAccount.unsettledFeesUsd.toString(10),
|
|
396
|
+
-USD_DECIMALS,
|
|
397
|
+
),
|
|
398
|
+
raw: serializeForJson(positionAccount),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function integerExponentToDecimal(value, exponent) {
|
|
403
|
+
const normalized = String(value);
|
|
404
|
+
if (!/^[-]?\d+$/.test(normalized)) {
|
|
405
|
+
return normalized;
|
|
406
|
+
}
|
|
407
|
+
if (!Number.isInteger(exponent)) {
|
|
408
|
+
return normalized;
|
|
409
|
+
}
|
|
410
|
+
if (exponent === 0) {
|
|
411
|
+
return normalized;
|
|
412
|
+
}
|
|
413
|
+
const negative = normalized.startsWith("-");
|
|
414
|
+
const digits = negative ? normalized.slice(1) : normalized;
|
|
415
|
+
if (exponent > 0) {
|
|
416
|
+
return `${negative ? "-" : ""}${digits}${"0".repeat(exponent)}`;
|
|
417
|
+
}
|
|
418
|
+
const places = Math.abs(exponent);
|
|
419
|
+
const padded = digits.padStart(places + 1, "0");
|
|
420
|
+
const integerPart = padded.slice(0, -places) || "0";
|
|
421
|
+
const fractionalPart = padded.slice(-places).replace(/0+$/, "");
|
|
422
|
+
const unsigned = fractionalPart ? `${integerPart}.${fractionalPart}` : integerPart;
|
|
423
|
+
return `${negative ? "-" : ""}${unsigned}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function serializeForJson(value) {
|
|
427
|
+
if (
|
|
428
|
+
value === null ||
|
|
429
|
+
value === undefined ||
|
|
430
|
+
typeof value === "string" ||
|
|
431
|
+
typeof value === "number" ||
|
|
432
|
+
typeof value === "boolean"
|
|
433
|
+
) {
|
|
434
|
+
return value;
|
|
435
|
+
}
|
|
436
|
+
if (Array.isArray(value)) {
|
|
437
|
+
return value.map((item) => serializeForJson(item));
|
|
438
|
+
}
|
|
439
|
+
if (Buffer.isBuffer(value)) {
|
|
440
|
+
return value.toString("base64");
|
|
441
|
+
}
|
|
442
|
+
if (typeof value === "object" && typeof value.toBase58 === "function") {
|
|
443
|
+
return value.toBase58();
|
|
444
|
+
}
|
|
445
|
+
if (typeof value === "object" && value.constructor?.name === "BN") {
|
|
446
|
+
return value.toString(10);
|
|
447
|
+
}
|
|
448
|
+
if (typeof value === "object") {
|
|
449
|
+
return Object.fromEntries(
|
|
450
|
+
Object.entries(value).map(([key, item]) => [key, serializeForJson(item)]),
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return String(value);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function serializeOraclePrice(oraclePrice) {
|
|
457
|
+
if (!oraclePrice || typeof oraclePrice !== "object") {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
const rawPrice = oraclePrice.price?.toString?.(10) ?? null;
|
|
461
|
+
const exponent = typeof oraclePrice.exponent === "number" ? oraclePrice.exponent : null;
|
|
462
|
+
return {
|
|
463
|
+
raw_price: rawPrice,
|
|
464
|
+
exponent,
|
|
465
|
+
ui_price:
|
|
466
|
+
rawPrice !== null && exponent !== null
|
|
467
|
+
? integerExponentToDecimal(rawPrice, exponent)
|
|
468
|
+
: null,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function toSdkOraclePrice(runtime, oraclePriceLike) {
|
|
473
|
+
if (!oraclePriceLike || typeof oraclePriceLike !== "object") {
|
|
474
|
+
throw new Error("oracle price is required");
|
|
475
|
+
}
|
|
476
|
+
if (oraclePriceLike.exponent && typeof oraclePriceLike.exponent.toNumber === "function") {
|
|
477
|
+
return oraclePriceLike;
|
|
478
|
+
}
|
|
479
|
+
const { BN } = runtime;
|
|
480
|
+
return runtime.flashSdk.OraclePrice.from({
|
|
481
|
+
price: new BN(oraclePriceLike.price?.toString?.(10) ?? String(oraclePriceLike.price ?? 0)),
|
|
482
|
+
exponent: new BN(String(oraclePriceLike.exponent ?? 0)),
|
|
483
|
+
confidence: new BN(String(oraclePriceLike.confidence ?? 0)),
|
|
484
|
+
timestamp: new BN(String(oraclePriceLike.timestamp ?? 0)),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function requireWholeNumberString(value, fieldName) {
|
|
489
|
+
const normalized = requireString(value, fieldName);
|
|
490
|
+
if (!/^\d+$/.test(normalized)) {
|
|
491
|
+
throw new Error(`${fieldName} must be a whole-number string for the current Flash bridge MVP`);
|
|
492
|
+
}
|
|
493
|
+
return normalized;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function requirePositiveDecimalString(value, fieldName) {
|
|
497
|
+
const normalized = requireString(value, fieldName);
|
|
498
|
+
if (!/^\d+(\.\d+)?$/.test(normalized)) {
|
|
499
|
+
throw new Error(`${fieldName} must be a positive decimal string`);
|
|
500
|
+
}
|
|
501
|
+
if (Number.parseFloat(normalized) <= 0) {
|
|
502
|
+
throw new Error(`${fieldName} must be greater than zero`);
|
|
503
|
+
}
|
|
504
|
+
return normalized;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function decimalToScaledIntegerString(value, decimals, fieldName) {
|
|
508
|
+
const normalized = requirePositiveDecimalString(value, fieldName);
|
|
509
|
+
const [integerPart, fractionalPart = ""] = normalized.split(".");
|
|
510
|
+
const paddedFraction = `${fractionalPart}${"0".repeat(decimals)}`.slice(0, decimals);
|
|
511
|
+
const combined = `${integerPart}${paddedFraction}`.replace(/^0+(?=\d)/, "");
|
|
512
|
+
if (!/^\d+$/.test(combined)) {
|
|
513
|
+
throw new Error(`${fieldName} could not be converted to scaled integer form`);
|
|
514
|
+
}
|
|
515
|
+
return combined || "0";
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function slippageTolerancePercent(slippageBpsRaw) {
|
|
519
|
+
return integerExponentToDecimal(String(slippageBpsRaw), -BPS_DECIMALS + 2);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getSideVariant(flashSdk, side) {
|
|
523
|
+
return side === "long" ? flashSdk.Side.Long : flashSdk.Side.Short;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function getCustodyConfigBySymbol(poolConfig, symbol) {
|
|
527
|
+
const allCustodies = [...(poolConfig.custodies || []), ...(poolConfig.custodiesDeprecated || [])];
|
|
528
|
+
const custodyConfig = allCustodies.find(
|
|
529
|
+
(custody) => String(custody.symbol || "").trim().toUpperCase() === symbol,
|
|
530
|
+
);
|
|
531
|
+
if (!custodyConfig) {
|
|
532
|
+
throw new Error(`Custody ${symbol} is not available in pool ${poolConfig.poolName}`);
|
|
533
|
+
}
|
|
534
|
+
return custodyConfig;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function getMarketContext(runtime, normalized) {
|
|
538
|
+
const sideVariant = getSideVariant(runtime.flashSdk, normalized.side);
|
|
539
|
+
const marketToken = runtime.poolConfig.getTokenFromSymbol(normalized.marketSymbol);
|
|
540
|
+
const collateralToken = runtime.poolConfig.getTokenFromSymbol(normalized.collateralSymbol);
|
|
541
|
+
const targetCustodyConfig = getCustodyConfigBySymbol(runtime.poolConfig, normalized.marketSymbol);
|
|
542
|
+
const collateralCustodyConfig = getCustodyConfigBySymbol(
|
|
543
|
+
runtime.poolConfig,
|
|
544
|
+
normalized.collateralSymbol,
|
|
545
|
+
);
|
|
546
|
+
const marketConfig = runtime.poolConfig.getMarketConfig(
|
|
547
|
+
targetCustodyConfig.custodyAccount,
|
|
548
|
+
collateralCustodyConfig.custodyAccount,
|
|
549
|
+
sideVariant,
|
|
550
|
+
);
|
|
551
|
+
if (!marketConfig) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`Market ${normalized.marketSymbol}/${normalized.collateralSymbol}/${normalized.side} is not available in pool ${normalized.poolName}`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return { sideVariant, marketToken, collateralToken, marketConfig };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function getComputeUnitLimit() {
|
|
560
|
+
const raw = process.env.FLASH_SDK_BRIDGE_COMPUTE_UNIT_LIMIT?.trim();
|
|
561
|
+
if (!raw) {
|
|
562
|
+
return DEFAULT_COMPUTE_UNIT_LIMIT;
|
|
563
|
+
}
|
|
564
|
+
const value = Number.parseInt(raw, 10);
|
|
565
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
566
|
+
throw new Error("FLASH_SDK_BRIDGE_COMPUTE_UNIT_LIMIT must be a positive integer");
|
|
567
|
+
}
|
|
568
|
+
return value;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function buildVersionedTransaction(runtime, instructions, additionalSigners = []) {
|
|
572
|
+
const [lookupTableState, latestBlockhash] = await Promise.all([
|
|
573
|
+
runtime.client.getOrLoadAddressLookupTable(runtime.poolConfig),
|
|
574
|
+
runtime.provider.connection.getLatestBlockhash("finalized"),
|
|
575
|
+
]);
|
|
576
|
+
const message = runtime.web3.MessageV0.compile({
|
|
577
|
+
payerKey: runtime.provider.wallet.publicKey,
|
|
578
|
+
instructions,
|
|
579
|
+
recentBlockhash: latestBlockhash.blockhash,
|
|
580
|
+
addressLookupTableAccounts: lookupTableState.addressLookupTables,
|
|
581
|
+
});
|
|
582
|
+
const versionedTransaction = new runtime.web3.VersionedTransaction(message);
|
|
583
|
+
if (additionalSigners.length > 0) {
|
|
584
|
+
versionedTransaction.sign(additionalSigners);
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
transactionBase64: Buffer.from(versionedTransaction.serialize()).toString("base64"),
|
|
588
|
+
latestBlockhash: latestBlockhash.blockhash,
|
|
589
|
+
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function getOpenPositionPreview(runtime, normalized) {
|
|
594
|
+
if (!normalized.owner) {
|
|
595
|
+
throw new Error("owner is required");
|
|
596
|
+
}
|
|
597
|
+
if (!normalized.poolName) {
|
|
598
|
+
throw new Error("pool_name is required");
|
|
599
|
+
}
|
|
600
|
+
if (!normalized.marketSymbol) {
|
|
601
|
+
throw new Error("market_symbol is required");
|
|
602
|
+
}
|
|
603
|
+
if (!normalized.side) {
|
|
604
|
+
throw new Error("side is required");
|
|
605
|
+
}
|
|
606
|
+
if (!normalized.collateralSymbol || !normalized.collateralAmountRaw || !normalized.leverage) {
|
|
607
|
+
throw new Error(
|
|
608
|
+
"collateral_symbol, collateral_amount_raw, and leverage are required for open preview",
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
if (normalized.collateralSymbol !== normalized.marketSymbol) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
"Current bridge MVP supports only same-collateral opens where collateral_symbol matches market_symbol",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const { BN } = runtime;
|
|
618
|
+
const privilege = runtime.flashSdk.Privilege.None;
|
|
619
|
+
const ownerPublicKey = runtime.provider.wallet.publicKey;
|
|
620
|
+
const { marketConfig, sideVariant } = getMarketContext(runtime, normalized);
|
|
621
|
+
const leverage = requirePositiveDecimalString(normalized.leverage, "leverage");
|
|
622
|
+
const leverageRaw = decimalToScaledIntegerString(leverage, BPS_DECIMALS, "leverage");
|
|
623
|
+
const slippageBpsRaw = DEFAULT_SLIPPAGE_BPS_RAW;
|
|
624
|
+
const slippageBps = new BN(slippageBpsRaw);
|
|
625
|
+
const quote = await runtime.client.getOpenPositionQuote(
|
|
626
|
+
new BN(normalized.collateralAmountRaw),
|
|
627
|
+
new BN(leverageRaw),
|
|
628
|
+
marketConfig,
|
|
629
|
+
runtime.poolConfig,
|
|
630
|
+
privilege,
|
|
631
|
+
undefined,
|
|
632
|
+
undefined,
|
|
633
|
+
null,
|
|
634
|
+
null,
|
|
635
|
+
ownerPublicKey,
|
|
636
|
+
);
|
|
637
|
+
const priceAfterSlippage = runtime.client.getPriceAfterSlippage(
|
|
638
|
+
true,
|
|
639
|
+
slippageBps,
|
|
640
|
+
toSdkOraclePrice(runtime, quote.entryPrice),
|
|
641
|
+
sideVariant,
|
|
642
|
+
);
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
ok: true,
|
|
646
|
+
preview: {
|
|
647
|
+
bridge_mode: "real",
|
|
648
|
+
requested_leverage: leverage,
|
|
649
|
+
requested_leverage_raw: leverageRaw,
|
|
650
|
+
pool_name: normalized.poolName,
|
|
651
|
+
market_symbol: normalized.marketSymbol,
|
|
652
|
+
collateral_symbol: normalized.collateralSymbol,
|
|
653
|
+
collateral_amount_raw: normalized.collateralAmountRaw,
|
|
654
|
+
side: normalized.side,
|
|
655
|
+
slippage_bps_raw: slippageBpsRaw,
|
|
656
|
+
slippage_tolerance_percent: slippageTolerancePercent(slippageBpsRaw),
|
|
657
|
+
estimated_size_usd: integerExponentToDecimal(quote.sizeUsd.toString(10), -USD_DECIMALS),
|
|
658
|
+
estimated_size_amount_raw: quote.sizeAmount.toString(10),
|
|
659
|
+
estimated_collateral_usd: integerExponentToDecimal(
|
|
660
|
+
quote.collateralUsd.toString(10),
|
|
661
|
+
-USD_DECIMALS,
|
|
662
|
+
),
|
|
663
|
+
estimated_collateral_amount_raw: quote.collateralAmount.toString(10),
|
|
664
|
+
estimated_entry_price: serializeOraclePrice(quote.entryPrice)?.ui_price ?? null,
|
|
665
|
+
estimated_liquidation_price: serializeOraclePrice(quote.liquidationPrice)?.ui_price ?? null,
|
|
666
|
+
estimated_entry_fee_usd: integerExponentToDecimal(
|
|
667
|
+
quote.entryFeeUsd.toString(10),
|
|
668
|
+
-USD_DECIMALS,
|
|
669
|
+
),
|
|
670
|
+
estimated_total_fee_usd: integerExponentToDecimal(
|
|
671
|
+
quote.totalFeeUsd.toString(10),
|
|
672
|
+
-USD_DECIMALS,
|
|
673
|
+
),
|
|
674
|
+
estimated_entry_price_with_slippage: serializeOraclePrice(priceAfterSlippage)?.ui_price ?? null,
|
|
675
|
+
estimated_fee_rate_bps: quote.feeRate.toString(10),
|
|
676
|
+
estimated_available_liquidity_usd: integerExponentToDecimal(
|
|
677
|
+
quote.availableLiquidityUsd.toString(10),
|
|
678
|
+
-USD_DECIMALS,
|
|
679
|
+
),
|
|
680
|
+
estimated_borrow_fee_rate: quote.borrowFeeRate.toString(10),
|
|
681
|
+
quote: serializeForJson(quote),
|
|
682
|
+
},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function getClosePositionPreview(runtime, normalized) {
|
|
687
|
+
if (!normalized.owner) {
|
|
688
|
+
throw new Error("owner is required");
|
|
689
|
+
}
|
|
690
|
+
if (!normalized.poolName) {
|
|
691
|
+
throw new Error("pool_name is required");
|
|
692
|
+
}
|
|
693
|
+
if (!normalized.marketSymbol) {
|
|
694
|
+
throw new Error("market_symbol is required");
|
|
695
|
+
}
|
|
696
|
+
if (!normalized.side) {
|
|
697
|
+
throw new Error("side is required");
|
|
698
|
+
}
|
|
699
|
+
const { BN } = runtime;
|
|
700
|
+
const privilege = runtime.flashSdk.Privilege.None;
|
|
701
|
+
const ownerPublicKey = runtime.provider.wallet.publicKey;
|
|
702
|
+
const sameCollateralNormalized = {
|
|
703
|
+
...normalized,
|
|
704
|
+
collateralSymbol: normalized.collateralSymbol ?? normalized.marketSymbol,
|
|
705
|
+
};
|
|
706
|
+
const { marketConfig, sideVariant } = getMarketContext(runtime, sameCollateralNormalized);
|
|
707
|
+
const slippageBpsRaw = DEFAULT_SLIPPAGE_BPS_RAW;
|
|
708
|
+
const slippageBps = new BN(slippageBpsRaw);
|
|
709
|
+
const positionPk = runtime.poolConfig.getPositionFromCustodyPk(
|
|
710
|
+
ownerPublicKey,
|
|
711
|
+
marketConfig.targetCustody,
|
|
712
|
+
marketConfig.collateralCustody,
|
|
713
|
+
sideVariant,
|
|
714
|
+
);
|
|
715
|
+
const positionAccount = await runtime.client.getPosition(positionPk);
|
|
716
|
+
if (!positionAccount?.isActive) {
|
|
717
|
+
throw new Error(
|
|
718
|
+
`No active Flash position found for ${normalized.marketSymbol}/${normalized.side} in pool ${normalized.poolName}`,
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const quote = await runtime.client.getClosePositionQuote(
|
|
722
|
+
positionPk,
|
|
723
|
+
positionAccount,
|
|
724
|
+
runtime.poolConfig,
|
|
725
|
+
new BN(0),
|
|
726
|
+
privilege,
|
|
727
|
+
undefined,
|
|
728
|
+
null,
|
|
729
|
+
null,
|
|
730
|
+
ownerPublicKey,
|
|
731
|
+
);
|
|
732
|
+
const priceAfterSlippage = runtime.client.getPriceAfterSlippage(
|
|
733
|
+
false,
|
|
734
|
+
slippageBps,
|
|
735
|
+
toSdkOraclePrice(runtime, quote.markPrice),
|
|
736
|
+
sideVariant,
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
ok: true,
|
|
741
|
+
preview: {
|
|
742
|
+
bridge_mode: "real",
|
|
743
|
+
pool_name: normalized.poolName,
|
|
744
|
+
market_symbol: normalized.marketSymbol,
|
|
745
|
+
collateral_symbol: sameCollateralNormalized.collateralSymbol,
|
|
746
|
+
side: normalized.side,
|
|
747
|
+
slippage_bps_raw: slippageBpsRaw,
|
|
748
|
+
slippage_tolerance_percent: slippageTolerancePercent(slippageBpsRaw),
|
|
749
|
+
position_pubkey: positionPk.toBase58(),
|
|
750
|
+
position_size_usd: integerExponentToDecimal(
|
|
751
|
+
positionAccount.sizeUsd.toString(10),
|
|
752
|
+
-USD_DECIMALS,
|
|
753
|
+
),
|
|
754
|
+
position_size_amount_raw: quote.existingSize.toString(10),
|
|
755
|
+
close_amount_raw: quote.receiveTokenAmount.toString(10),
|
|
756
|
+
estimated_receive_amount_usd: integerExponentToDecimal(
|
|
757
|
+
quote.receiveTokenAmountUsd.toString(10),
|
|
758
|
+
-USD_DECIMALS,
|
|
759
|
+
),
|
|
760
|
+
estimated_mark_price: serializeOraclePrice(quote.markPrice)?.ui_price ?? null,
|
|
761
|
+
estimated_entry_price: serializeOraclePrice(quote.entryPrice)?.ui_price ?? null,
|
|
762
|
+
estimated_existing_liquidation_price:
|
|
763
|
+
serializeOraclePrice(quote.existingLiquidationPrice)?.ui_price ?? null,
|
|
764
|
+
estimated_new_liquidation_price:
|
|
765
|
+
serializeOraclePrice(quote.newLiquidationPrice)?.ui_price ?? null,
|
|
766
|
+
estimated_profit_usd: integerExponentToDecimal(quote.profitUsd.toString(10), -USD_DECIMALS),
|
|
767
|
+
estimated_loss_usd: integerExponentToDecimal(quote.lossUsd.toString(10), -USD_DECIMALS),
|
|
768
|
+
estimated_settled_pnl_usd: integerExponentToDecimal(
|
|
769
|
+
quote.settledPnlUsd.toString(10),
|
|
770
|
+
-USD_DECIMALS,
|
|
771
|
+
),
|
|
772
|
+
estimated_exit_price_with_slippage: serializeOraclePrice(priceAfterSlippage)?.ui_price ?? null,
|
|
773
|
+
estimated_exit_fee_usd: integerExponentToDecimal(
|
|
774
|
+
quote.exitFeeUsd.toString(10),
|
|
775
|
+
-USD_DECIMALS,
|
|
776
|
+
),
|
|
777
|
+
estimated_total_fees_usd: integerExponentToDecimal(quote.fees.toString(10), -USD_DECIMALS),
|
|
778
|
+
estimated_existing_leverage: integerExponentToDecimal(
|
|
779
|
+
quote.existingLeverage.toString(10),
|
|
780
|
+
-BPS_DECIMALS,
|
|
781
|
+
),
|
|
782
|
+
estimated_new_leverage: integerExponentToDecimal(
|
|
783
|
+
quote.newLeverage.toString(10),
|
|
784
|
+
-BPS_DECIMALS,
|
|
785
|
+
),
|
|
786
|
+
is_profitable: Boolean(quote.isProfitable),
|
|
787
|
+
is_solvent: Boolean(quote.isSolvent),
|
|
788
|
+
is_partial_close: Boolean(quote.isPartialClose),
|
|
789
|
+
quote: serializeForJson(quote),
|
|
790
|
+
position: serializeForJson(positionAccount),
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function prepareOpenPosition(runtime, normalized) {
|
|
796
|
+
if (!normalized.owner) {
|
|
797
|
+
throw new Error("owner is required");
|
|
798
|
+
}
|
|
799
|
+
if (!normalized.poolName) {
|
|
800
|
+
throw new Error("pool_name is required");
|
|
801
|
+
}
|
|
802
|
+
if (!normalized.marketSymbol) {
|
|
803
|
+
throw new Error("market_symbol is required");
|
|
804
|
+
}
|
|
805
|
+
if (!normalized.side) {
|
|
806
|
+
throw new Error("side is required");
|
|
807
|
+
}
|
|
808
|
+
if (!normalized.collateralSymbol || !normalized.collateralAmountRaw || !normalized.leverage) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
"collateral_symbol, collateral_amount_raw, and leverage are required for open prepare",
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
if (normalized.collateralSymbol !== normalized.marketSymbol) {
|
|
814
|
+
throw new Error(
|
|
815
|
+
"Current bridge MVP supports only same-collateral opens where collateral_symbol matches market_symbol",
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const { BN } = runtime;
|
|
820
|
+
const privilege = runtime.flashSdk.Privilege.None;
|
|
821
|
+
const ownerPublicKey = runtime.provider.wallet.publicKey;
|
|
822
|
+
const { marketConfig, collateralToken, sideVariant } = getMarketContext(runtime, normalized);
|
|
823
|
+
const leverage = requirePositiveDecimalString(normalized.leverage, "leverage");
|
|
824
|
+
const leverageRaw = decimalToScaledIntegerString(leverage, BPS_DECIMALS, "leverage");
|
|
825
|
+
const slippageBpsRaw = DEFAULT_SLIPPAGE_BPS_RAW;
|
|
826
|
+
const slippageBps = new BN(slippageBpsRaw);
|
|
827
|
+
const quote = await runtime.client.getOpenPositionQuote(
|
|
828
|
+
new BN(normalized.collateralAmountRaw),
|
|
829
|
+
new BN(leverageRaw),
|
|
830
|
+
marketConfig,
|
|
831
|
+
runtime.poolConfig,
|
|
832
|
+
privilege,
|
|
833
|
+
undefined,
|
|
834
|
+
undefined,
|
|
835
|
+
null,
|
|
836
|
+
null,
|
|
837
|
+
ownerPublicKey,
|
|
838
|
+
);
|
|
839
|
+
const priceAfterSlippage = runtime.client.getPriceAfterSlippage(
|
|
840
|
+
true,
|
|
841
|
+
slippageBps,
|
|
842
|
+
toSdkOraclePrice(runtime, quote.entryPrice),
|
|
843
|
+
sideVariant,
|
|
844
|
+
);
|
|
845
|
+
const backupOracleInstructions = await runtime.flashSdk.createBackupOracleInstruction(
|
|
846
|
+
runtime.poolConfig.poolAddress.toBase58(),
|
|
847
|
+
);
|
|
848
|
+
const computeBudgetIx = runtime.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
|
849
|
+
units: getComputeUnitLimit(),
|
|
850
|
+
});
|
|
851
|
+
const { instructions, additionalSigners } = await runtime.client.openPosition(
|
|
852
|
+
normalized.marketSymbol,
|
|
853
|
+
normalized.collateralSymbol,
|
|
854
|
+
priceAfterSlippage,
|
|
855
|
+
new BN(normalized.collateralAmountRaw),
|
|
856
|
+
quote.sizeAmount,
|
|
857
|
+
sideVariant,
|
|
858
|
+
runtime.poolConfig,
|
|
859
|
+
privilege,
|
|
860
|
+
);
|
|
861
|
+
const fullInstructions = [computeBudgetIx, ...backupOracleInstructions, ...instructions];
|
|
862
|
+
const builtTransaction = await buildVersionedTransaction(
|
|
863
|
+
runtime,
|
|
864
|
+
fullInstructions,
|
|
865
|
+
additionalSigners,
|
|
866
|
+
);
|
|
867
|
+
return {
|
|
868
|
+
ok: true,
|
|
869
|
+
prepared: {
|
|
870
|
+
bridge_mode: "real",
|
|
871
|
+
pool_name: normalized.poolName,
|
|
872
|
+
market_symbol: normalized.marketSymbol,
|
|
873
|
+
collateral_symbol: normalized.collateralSymbol,
|
|
874
|
+
collateral_amount_raw: normalized.collateralAmountRaw,
|
|
875
|
+
leverage,
|
|
876
|
+
leverage_raw: leverageRaw,
|
|
877
|
+
side: normalized.side,
|
|
878
|
+
slippage_bps_raw: slippageBpsRaw,
|
|
879
|
+
slippage_tolerance_percent: slippageTolerancePercent(slippageBpsRaw),
|
|
880
|
+
estimated_size_usd: integerExponentToDecimal(quote.sizeUsd.toString(10), -USD_DECIMALS),
|
|
881
|
+
estimated_size_amount_raw: quote.sizeAmount.toString(10),
|
|
882
|
+
estimated_entry_price: serializeOraclePrice(quote.entryPrice)?.ui_price ?? null,
|
|
883
|
+
estimated_liquidation_price: serializeOraclePrice(quote.liquidationPrice)?.ui_price ?? null,
|
|
884
|
+
estimated_entry_price_with_slippage: serializeOraclePrice(priceAfterSlippage)?.ui_price ?? null,
|
|
885
|
+
transaction_base64: builtTransaction.transactionBase64,
|
|
886
|
+
transaction_encoding: "base64",
|
|
887
|
+
transaction_format: "versioned",
|
|
888
|
+
latest_blockhash: builtTransaction.latestBlockhash,
|
|
889
|
+
last_valid_block_height: builtTransaction.lastValidBlockHeight,
|
|
890
|
+
market_address: marketConfig.marketAccount.toBase58(),
|
|
891
|
+
position_address: runtime.poolConfig
|
|
892
|
+
.getPositionFromMarketPk(ownerPublicKey, marketConfig.marketAccount)
|
|
893
|
+
.toBase58(),
|
|
894
|
+
target_custody_address: marketConfig.targetCustody.toBase58(),
|
|
895
|
+
collateral_custody_address: marketConfig.collateralCustody.toBase58(),
|
|
896
|
+
collateral_mint: collateralToken.mintKey.toBase58(),
|
|
897
|
+
expected_program_ids: Array.from(
|
|
898
|
+
new Set([
|
|
899
|
+
runtime.poolConfig.programId.toBase58(),
|
|
900
|
+
runtime.poolConfig.perpComposibilityProgramId.toBase58(),
|
|
901
|
+
...fullInstructions.map((instruction) => instruction.programId.toBase58()),
|
|
902
|
+
]),
|
|
903
|
+
),
|
|
904
|
+
quote: serializeForJson(quote),
|
|
905
|
+
instruction_count: fullInstructions.length,
|
|
906
|
+
additional_signer_count: additionalSigners.length,
|
|
907
|
+
},
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function prepareClosePosition(runtime, normalized) {
|
|
912
|
+
if (!normalized.owner) {
|
|
913
|
+
throw new Error("owner is required");
|
|
914
|
+
}
|
|
915
|
+
if (!normalized.poolName) {
|
|
916
|
+
throw new Error("pool_name is required");
|
|
917
|
+
}
|
|
918
|
+
if (!normalized.marketSymbol) {
|
|
919
|
+
throw new Error("market_symbol is required");
|
|
920
|
+
}
|
|
921
|
+
if (!normalized.side) {
|
|
922
|
+
throw new Error("side is required");
|
|
923
|
+
}
|
|
924
|
+
const { BN } = runtime;
|
|
925
|
+
const privilege = runtime.flashSdk.Privilege.None;
|
|
926
|
+
const ownerPublicKey = runtime.provider.wallet.publicKey;
|
|
927
|
+
const sameCollateralNormalized = {
|
|
928
|
+
...normalized,
|
|
929
|
+
collateralSymbol: normalized.collateralSymbol ?? normalized.marketSymbol,
|
|
930
|
+
};
|
|
931
|
+
const { marketConfig, collateralToken, sideVariant } = getMarketContext(runtime, sameCollateralNormalized);
|
|
932
|
+
const slippageBpsRaw = DEFAULT_SLIPPAGE_BPS_RAW;
|
|
933
|
+
const slippageBps = new BN(slippageBpsRaw);
|
|
934
|
+
const positionPk = runtime.poolConfig.getPositionFromCustodyPk(
|
|
935
|
+
ownerPublicKey,
|
|
936
|
+
marketConfig.targetCustody,
|
|
937
|
+
marketConfig.collateralCustody,
|
|
938
|
+
sideVariant,
|
|
939
|
+
);
|
|
940
|
+
const positionAccount = await runtime.client.getPosition(positionPk);
|
|
941
|
+
if (!positionAccount?.isActive) {
|
|
942
|
+
throw new Error(
|
|
943
|
+
`No active Flash position found for ${normalized.marketSymbol}/${normalized.side} in pool ${normalized.poolName}`,
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
const quote = await runtime.client.getClosePositionQuote(
|
|
947
|
+
positionPk,
|
|
948
|
+
positionAccount,
|
|
949
|
+
runtime.poolConfig,
|
|
950
|
+
new BN(0),
|
|
951
|
+
privilege,
|
|
952
|
+
undefined,
|
|
953
|
+
null,
|
|
954
|
+
null,
|
|
955
|
+
ownerPublicKey,
|
|
956
|
+
);
|
|
957
|
+
const priceAfterSlippage = runtime.client.getPriceAfterSlippage(
|
|
958
|
+
false,
|
|
959
|
+
slippageBps,
|
|
960
|
+
toSdkOraclePrice(runtime, quote.markPrice),
|
|
961
|
+
sideVariant,
|
|
962
|
+
);
|
|
963
|
+
const backupOracleInstructions = await runtime.flashSdk.createBackupOracleInstruction(
|
|
964
|
+
runtime.poolConfig.poolAddress.toBase58(),
|
|
965
|
+
);
|
|
966
|
+
const computeBudgetIx = runtime.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
|
967
|
+
units: getComputeUnitLimit(),
|
|
968
|
+
});
|
|
969
|
+
const { instructions, additionalSigners } = await runtime.client.closePosition(
|
|
970
|
+
normalized.marketSymbol,
|
|
971
|
+
sameCollateralNormalized.collateralSymbol,
|
|
972
|
+
priceAfterSlippage,
|
|
973
|
+
sideVariant,
|
|
974
|
+
runtime.poolConfig,
|
|
975
|
+
privilege,
|
|
976
|
+
);
|
|
977
|
+
const fullInstructions = [computeBudgetIx, ...backupOracleInstructions, ...instructions];
|
|
978
|
+
const builtTransaction = await buildVersionedTransaction(
|
|
979
|
+
runtime,
|
|
980
|
+
fullInstructions,
|
|
981
|
+
additionalSigners,
|
|
982
|
+
);
|
|
983
|
+
return {
|
|
984
|
+
ok: true,
|
|
985
|
+
prepared: {
|
|
986
|
+
bridge_mode: "real",
|
|
987
|
+
pool_name: normalized.poolName,
|
|
988
|
+
market_symbol: normalized.marketSymbol,
|
|
989
|
+
collateral_symbol: sameCollateralNormalized.collateralSymbol,
|
|
990
|
+
side: normalized.side,
|
|
991
|
+
slippage_bps_raw: slippageBpsRaw,
|
|
992
|
+
slippage_tolerance_percent: slippageTolerancePercent(slippageBpsRaw),
|
|
993
|
+
position_pubkey: positionPk.toBase58(),
|
|
994
|
+
position_size_usd: integerExponentToDecimal(
|
|
995
|
+
positionAccount.sizeUsd.toString(10),
|
|
996
|
+
-USD_DECIMALS,
|
|
997
|
+
),
|
|
998
|
+
close_amount_raw: quote.receiveTokenAmount.toString(10),
|
|
999
|
+
estimated_receive_amount_usd: integerExponentToDecimal(
|
|
1000
|
+
quote.receiveTokenAmountUsd.toString(10),
|
|
1001
|
+
-USD_DECIMALS,
|
|
1002
|
+
),
|
|
1003
|
+
estimated_mark_price: serializeOraclePrice(quote.markPrice)?.ui_price ?? null,
|
|
1004
|
+
estimated_exit_price_with_slippage: serializeOraclePrice(priceAfterSlippage)?.ui_price ?? null,
|
|
1005
|
+
estimated_existing_liquidation_price:
|
|
1006
|
+
serializeOraclePrice(quote.existingLiquidationPrice)?.ui_price ?? null,
|
|
1007
|
+
transaction_base64: builtTransaction.transactionBase64,
|
|
1008
|
+
transaction_encoding: "base64",
|
|
1009
|
+
transaction_format: "versioned",
|
|
1010
|
+
latest_blockhash: builtTransaction.latestBlockhash,
|
|
1011
|
+
last_valid_block_height: builtTransaction.lastValidBlockHeight,
|
|
1012
|
+
market_address: marketConfig.marketAccount.toBase58(),
|
|
1013
|
+
position_address: positionPk.toBase58(),
|
|
1014
|
+
target_custody_address: marketConfig.targetCustody.toBase58(),
|
|
1015
|
+
collateral_custody_address: marketConfig.collateralCustody.toBase58(),
|
|
1016
|
+
collateral_mint: collateralToken.mintKey.toBase58(),
|
|
1017
|
+
expected_program_ids: Array.from(
|
|
1018
|
+
new Set([
|
|
1019
|
+
runtime.poolConfig.programId.toBase58(),
|
|
1020
|
+
runtime.poolConfig.perpComposibilityProgramId.toBase58(),
|
|
1021
|
+
...fullInstructions.map((instruction) => instruction.programId.toBase58()),
|
|
1022
|
+
]),
|
|
1023
|
+
),
|
|
1024
|
+
quote: serializeForJson(quote),
|
|
1025
|
+
position: serializeForJson(positionAccount),
|
|
1026
|
+
instruction_count: fullInstructions.length,
|
|
1027
|
+
additional_signer_count: additionalSigners.length,
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function getMarketsReal(normalized) {
|
|
1033
|
+
const { flashSdk, poolConfigCatalog } = await loadRealModules();
|
|
1034
|
+
const poolConfigs = listPoolConfigs(
|
|
1035
|
+
flashSdk,
|
|
1036
|
+
poolConfigCatalog,
|
|
1037
|
+
normalized.network,
|
|
1038
|
+
normalized.poolName,
|
|
1039
|
+
);
|
|
1040
|
+
const markets = [];
|
|
1041
|
+
for (const poolConfig of poolConfigs) {
|
|
1042
|
+
for (const marketConfig of poolConfig.markets || []) {
|
|
1043
|
+
markets.push(buildMarketSnapshot(poolConfig, marketConfig, false));
|
|
1044
|
+
}
|
|
1045
|
+
for (const marketConfig of poolConfig.marketsDeprecated || []) {
|
|
1046
|
+
markets.push(buildMarketSnapshot(poolConfig, marketConfig, true));
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
data: {
|
|
1052
|
+
bridge_mode: "real",
|
|
1053
|
+
pool_name: normalized.poolName ?? null,
|
|
1054
|
+
pool_count: poolConfigs.length,
|
|
1055
|
+
market_count: markets.length,
|
|
1056
|
+
source: "flash-sdk-bridge",
|
|
1057
|
+
markets,
|
|
1058
|
+
},
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
async function getPositionsReal(normalized) {
|
|
1063
|
+
if (!normalized.owner) {
|
|
1064
|
+
throw new Error("owner is required");
|
|
1065
|
+
}
|
|
1066
|
+
const modules = await loadRealModules();
|
|
1067
|
+
const poolConfigs = listPoolConfigs(
|
|
1068
|
+
modules.flashSdk,
|
|
1069
|
+
modules.poolConfigCatalog,
|
|
1070
|
+
normalized.network,
|
|
1071
|
+
normalized.poolName,
|
|
1072
|
+
);
|
|
1073
|
+
const positions = [];
|
|
1074
|
+
for (const poolConfig of poolConfigs) {
|
|
1075
|
+
const runtime = await buildRuntimeContext(
|
|
1076
|
+
{
|
|
1077
|
+
...normalized,
|
|
1078
|
+
owner: normalized.owner,
|
|
1079
|
+
poolName: poolConfig.poolName,
|
|
1080
|
+
},
|
|
1081
|
+
poolConfig,
|
|
1082
|
+
);
|
|
1083
|
+
const poolPositions = await runtime.client.getUserPositions(
|
|
1084
|
+
runtime.provider.wallet.publicKey,
|
|
1085
|
+
poolConfig,
|
|
1086
|
+
);
|
|
1087
|
+
for (const positionAccount of poolPositions || []) {
|
|
1088
|
+
if (!positionAccount?.isActive) {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
positions.push(buildPositionSnapshot(poolConfig, positionAccount));
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return {
|
|
1095
|
+
ok: true,
|
|
1096
|
+
data: {
|
|
1097
|
+
bridge_mode: "real",
|
|
1098
|
+
owner: normalized.owner,
|
|
1099
|
+
pool_name: normalized.poolName ?? null,
|
|
1100
|
+
pool_count: poolConfigs.length,
|
|
1101
|
+
position_count: positions.length,
|
|
1102
|
+
source: "flash-sdk-bridge",
|
|
1103
|
+
positions,
|
|
1104
|
+
},
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function realResponse(normalized) {
|
|
1109
|
+
if (normalized.action === "get_markets") {
|
|
1110
|
+
return getMarketsReal(normalized);
|
|
1111
|
+
}
|
|
1112
|
+
if (normalized.action === "get_positions") {
|
|
1113
|
+
return getPositionsReal(normalized);
|
|
1114
|
+
}
|
|
1115
|
+
const runtime = await buildRuntimeContext(normalized);
|
|
1116
|
+
if (normalized.action === "preview_open_position_same_collateral") {
|
|
1117
|
+
return getOpenPositionPreview(runtime, normalized);
|
|
1118
|
+
}
|
|
1119
|
+
if (normalized.action === "preview_close_position_same_collateral") {
|
|
1120
|
+
return getClosePositionPreview(runtime, normalized);
|
|
1121
|
+
}
|
|
1122
|
+
if (normalized.action === "prepare_open_position_same_collateral") {
|
|
1123
|
+
return prepareOpenPosition(runtime, normalized);
|
|
1124
|
+
}
|
|
1125
|
+
if (normalized.action === "prepare_close_position_same_collateral") {
|
|
1126
|
+
return prepareClosePosition(runtime, normalized);
|
|
1127
|
+
}
|
|
1128
|
+
return jsonError(`Unsupported real action: ${normalized.action}`, {
|
|
1129
|
+
detail: {
|
|
1130
|
+
bridge_mode: "real",
|
|
1131
|
+
rpc_url: runtime.rpcUrl,
|
|
1132
|
+
pool_name: normalized.poolName,
|
|
1133
|
+
market_symbol: normalized.marketSymbol,
|
|
1134
|
+
network: normalized.network,
|
|
1135
|
+
pool_address:
|
|
1136
|
+
runtime.poolConfig.poolAddress && typeof runtime.poolConfig.poolAddress.toBase58 === "function"
|
|
1137
|
+
? runtime.poolConfig.poolAddress.toBase58()
|
|
1138
|
+
: null,
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async function readStdinJson() {
|
|
1144
|
+
const chunks = [];
|
|
1145
|
+
for await (const chunk of process.stdin) {
|
|
1146
|
+
chunks.push(chunk);
|
|
1147
|
+
}
|
|
1148
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
1149
|
+
if (!raw) {
|
|
1150
|
+
return {};
|
|
1151
|
+
}
|
|
1152
|
+
return JSON.parse(raw);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function main() {
|
|
1156
|
+
try {
|
|
1157
|
+
const payload = await readStdinJson();
|
|
1158
|
+
const normalized = normalizeActionInput(payload);
|
|
1159
|
+
const mode = (process.env.FLASH_SDK_BRIDGE_MODE || "mock").trim().toLowerCase();
|
|
1160
|
+
|
|
1161
|
+
let response;
|
|
1162
|
+
if (mode === "mock") {
|
|
1163
|
+
response = mockResponse(normalized);
|
|
1164
|
+
} else if (mode === "real") {
|
|
1165
|
+
response = await realResponse(normalized);
|
|
1166
|
+
} else {
|
|
1167
|
+
response = jsonError(`Unsupported FLASH_SDK_BRIDGE_MODE: ${mode}`);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
1171
|
+
process.exit(response.ok === false ? 1 : 0);
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1174
|
+
process.stdout.write(`${JSON.stringify(jsonError(message))}\n`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
await main();
|