@nodiusxyz/solana-agent-kit 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +131 -2
- package/dist/index.d.ts +281 -0
- package/dist/index.js +277 -0
- package/dist/internal/admission.d.ts +11 -0
- package/dist/internal/admission.js +96 -0
- package/dist/internal/app.d.ts +6 -0
- package/dist/internal/app.js +422 -0
- package/dist/internal/config.d.ts +10 -0
- package/dist/internal/config.js +106 -0
- package/dist/internal/journal.d.ts +13 -0
- package/dist/internal/journal.js +135 -0
- package/dist/internal/monitor.d.ts +12 -0
- package/dist/internal/monitor.js +122 -0
- package/dist/internal/safety.d.ts +37 -0
- package/dist/internal/safety.js +199 -0
- package/dist/internal/transport.d.ts +23 -0
- package/dist/internal/transport.js +97 -0
- package/dist/internal/v2-policy.d.ts +19 -0
- package/dist/internal/v2-policy.js +168 -0
- package/examples/readonly.ts +42 -0
- package/package.json +62 -4
- package/src/index.ts +297 -0
- package/src/internal/admission.ts +131 -0
- package/src/internal/app.ts +509 -0
- package/src/internal/config.ts +107 -0
- package/src/internal/journal.ts +145 -0
- package/src/internal/monitor.ts +141 -0
- package/src/internal/safety.ts +259 -0
- package/src/internal/transport.ts +112 -0
- package/src/internal/v2-policy.ts +315 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
|
|
2
|
+
import { required, integer, rawAmount, solLamports } from "./config.js";
|
|
3
|
+
import { Transport, estimateBuy, finite, sendFee } from "./transport.js";
|
|
4
|
+
import { admitBuy } from "./admission.js";
|
|
5
|
+
import { monitor } from "./monitor.js";
|
|
6
|
+
import { Journal, fingerprint, outcome } from "./journal.js";
|
|
7
|
+
import { decodeBase64, verifyUnsigned, checkSignature, inspectEnvelope, decodeBuilderUnsigned, } from "./safety.js";
|
|
8
|
+
function safeLamports(n) {
|
|
9
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0)
|
|
10
|
+
throw new Error("Unsafe response lamports");
|
|
11
|
+
return BigInt(n);
|
|
12
|
+
}
|
|
13
|
+
function requireAuth(env) {
|
|
14
|
+
if (!env.NODIUS_API_KEY)
|
|
15
|
+
throw new Error("Set NODIUS_API_KEY in environment (not CLI)");
|
|
16
|
+
}
|
|
17
|
+
async function identity(t, wallet) {
|
|
18
|
+
const account = await t.sdk.getBillingAccount();
|
|
19
|
+
if (account.pubkey !== wallet.toBase58())
|
|
20
|
+
throw new Error("API key billing identity does not match wallet");
|
|
21
|
+
return account;
|
|
22
|
+
}
|
|
23
|
+
async function submit(t, j, id, tx, emit) {
|
|
24
|
+
const signature = checkSignature(tx);
|
|
25
|
+
await j.reserveSignature(signature);
|
|
26
|
+
await j.event(id, {
|
|
27
|
+
state: "unknown",
|
|
28
|
+
phase: "submission-intent",
|
|
29
|
+
signature,
|
|
30
|
+
});
|
|
31
|
+
emit({ state: "unknown", phase: "submission-intent", signature });
|
|
32
|
+
let state = "unknown";
|
|
33
|
+
try {
|
|
34
|
+
// Exactly one SDK call. maxRetries:0 disables upstream retries, NOT the
|
|
35
|
+
// server's intentional bounded same-byte multi-leg resends.
|
|
36
|
+
const result = await t.sdk.call("sendAndConfirm", [
|
|
37
|
+
Buffer.from(tx.serialize()).toString("base64"),
|
|
38
|
+
{
|
|
39
|
+
encoding: "base64",
|
|
40
|
+
maxRetries: 0,
|
|
41
|
+
skipPreflight: false,
|
|
42
|
+
preflightCommitment: "confirmed",
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
if (result?.signature === signature) {
|
|
46
|
+
const chainError = result.transaction_error ?? result.chain_err ?? result.chain_error;
|
|
47
|
+
const landedError = Number.isSafeInteger(result.slot) &&
|
|
48
|
+
result.slot >= 0 &&
|
|
49
|
+
typeof result.landing_time_ms === "number" &&
|
|
50
|
+
Number.isFinite(result.landing_time_ms) &&
|
|
51
|
+
result.landing_time_ms >= 0 &&
|
|
52
|
+
typeof result.error === "string" &&
|
|
53
|
+
result.error.length > 0 &&
|
|
54
|
+
!/timed?\s*out|timeout|without confirmation/i.test(result.error);
|
|
55
|
+
const definitive = result.status === "failed" ||
|
|
56
|
+
chainError != null ||
|
|
57
|
+
result.definitive_error === true ||
|
|
58
|
+
landedError ||
|
|
59
|
+
(typeof result.error === "string" &&
|
|
60
|
+
/^(Transaction failed on-chain:|Transaction failed:)/.test(result.error));
|
|
61
|
+
state =
|
|
62
|
+
result.status === "unknown"
|
|
63
|
+
? "unknown"
|
|
64
|
+
: definitive
|
|
65
|
+
? "failed"
|
|
66
|
+
: result.status !== "unknown" && result.confirmed === true
|
|
67
|
+
? "confirmed"
|
|
68
|
+
: "unknown";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
// A signature-bound server rejection is definitive even when carried by
|
|
73
|
+
// HTTP 502. Never infer failure from a timeout/message alone, and never
|
|
74
|
+
// persist the SDK error body (it may contain transport details).
|
|
75
|
+
if (error && typeof error === "object" && "details" in error) {
|
|
76
|
+
const details = error.details;
|
|
77
|
+
const rejection = details?.error?.data ?? details;
|
|
78
|
+
if (rejection?.signature === signature && rejection?.status === "failed")
|
|
79
|
+
state = "failed";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
await j.event(id, {
|
|
83
|
+
state,
|
|
84
|
+
phase: state === "unknown" ? "submission-uncertain" : "submission-result",
|
|
85
|
+
signature,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
state,
|
|
89
|
+
operation_id: id,
|
|
90
|
+
signature,
|
|
91
|
+
note: state === "unknown"
|
|
92
|
+
? "Do not rebuild/rebroadcast. Read status and account ledger; no automatic retry."
|
|
93
|
+
: undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
97
|
+
const origin = typeof a.endpoint === "string"
|
|
98
|
+
? a.endpoint
|
|
99
|
+
: (env.NODIUS_ENDPOINT ?? "https://rpc.nodius.xyz");
|
|
100
|
+
if (a.command === "help")
|
|
101
|
+
return {
|
|
102
|
+
commands: [
|
|
103
|
+
"recent",
|
|
104
|
+
"monitor",
|
|
105
|
+
"inspect",
|
|
106
|
+
"migrate",
|
|
107
|
+
"buy",
|
|
108
|
+
"sell",
|
|
109
|
+
"send",
|
|
110
|
+
"status",
|
|
111
|
+
],
|
|
112
|
+
note: "See README. Trading defaults to a read-only plan; monitoring is a paid stream.",
|
|
113
|
+
};
|
|
114
|
+
const t = new Transport(origin, env.NODIUS_API_KEY);
|
|
115
|
+
if (a.command === "monitor") {
|
|
116
|
+
requireAuth(env);
|
|
117
|
+
const filters = {};
|
|
118
|
+
if (a.mint)
|
|
119
|
+
filters.mint = new PublicKey(required(a, "mint")).toBase58();
|
|
120
|
+
if (a.kinds) {
|
|
121
|
+
const kinds = required(a, "kinds").split(",");
|
|
122
|
+
if (kinds.some((k) => !["newPair", "graduation", "walletActivity", "largeSwap"].includes(k)) ||
|
|
123
|
+
new Set(kinds).size !== kinds.length)
|
|
124
|
+
throw new Error("Unsupported/duplicate signal kinds");
|
|
125
|
+
filters.kinds = kinds;
|
|
126
|
+
}
|
|
127
|
+
return monitor(t.origin, env.NODIUS_API_KEY, {
|
|
128
|
+
seconds: integer(String(a.seconds ?? "30"), 300),
|
|
129
|
+
maxEvents: integer(String(a["max-events"] ?? "100"), 1000),
|
|
130
|
+
maxFeeCredits: a["max-fee-credits"] === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: integer(required(a, "max-fee-credits")),
|
|
133
|
+
filters,
|
|
134
|
+
}, emit);
|
|
135
|
+
}
|
|
136
|
+
if (a.command === "recent") {
|
|
137
|
+
const limit = integer(String(a.limit ?? "20"), 50);
|
|
138
|
+
if (!limit)
|
|
139
|
+
throw new Error("Positive limit required");
|
|
140
|
+
const result = await t.sdk.signalsRecent(limit);
|
|
141
|
+
// Some server versions return a fixed preview window despite the query.
|
|
142
|
+
if (Array.isArray(result))
|
|
143
|
+
return result.slice(0, limit);
|
|
144
|
+
if (Array.isArray(result?.events)) {
|
|
145
|
+
const events = result.events.slice(0, limit);
|
|
146
|
+
return {
|
|
147
|
+
...result,
|
|
148
|
+
events,
|
|
149
|
+
...(typeof result.count === "number" ? { count: events.length } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
if (a.command === "inspect" || a.command === "migrate") {
|
|
155
|
+
requireAuth(env);
|
|
156
|
+
const wallet = new PublicKey(required(a, "wallet"));
|
|
157
|
+
const [account, balance, onchain, version] = await Promise.all([
|
|
158
|
+
t.sdk.getBillingAccount(),
|
|
159
|
+
t.connection.getBalance(wallet),
|
|
160
|
+
t.connection.getAccountInfo(wallet),
|
|
161
|
+
t.connection.getVersion(),
|
|
162
|
+
]);
|
|
163
|
+
return {
|
|
164
|
+
state: "read-only",
|
|
165
|
+
wallet: wallet.toBase58(),
|
|
166
|
+
billing_identity_matches: account.pubkey === wallet.toBase58(),
|
|
167
|
+
billing_account: account,
|
|
168
|
+
balance_lamports: balance,
|
|
169
|
+
onchain_account: onchain,
|
|
170
|
+
version,
|
|
171
|
+
migration: "new Connection(origin + /rpc, {httpHeaders: {X-Api-Key: process.env.NODIUS_API_KEY}, disableRetryOnRateLimit: true})",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const j = new Journal(typeof a.journal === "string"
|
|
175
|
+
? a.journal
|
|
176
|
+
: (env.NODIUS_JOURNAL ?? ".nodius-journal"), env.NODIUS_API_KEY);
|
|
177
|
+
if (a.command === "status") {
|
|
178
|
+
requireAuth(env);
|
|
179
|
+
const record = await j.read(required(a, "operation-id"));
|
|
180
|
+
if (!record.signature)
|
|
181
|
+
return {
|
|
182
|
+
...record,
|
|
183
|
+
state: record.state === "built" ? "built" : "unknown",
|
|
184
|
+
note: "No known signature: check account ledger. No rebuild/resume performed.",
|
|
185
|
+
};
|
|
186
|
+
let chain = null;
|
|
187
|
+
try {
|
|
188
|
+
const status = (await t.sdk.getSignatureStatuses([
|
|
189
|
+
record.signature,
|
|
190
|
+
]));
|
|
191
|
+
chain = status?.value?.[0] ?? null;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
/* Read failure does not authorize another send. */
|
|
195
|
+
}
|
|
196
|
+
const observed = outcome(chain);
|
|
197
|
+
const state = ["confirmed", "failed"].includes(record.state)
|
|
198
|
+
? record.state
|
|
199
|
+
: observed;
|
|
200
|
+
await j.event(record.id, {
|
|
201
|
+
state,
|
|
202
|
+
phase: "status-reconciled",
|
|
203
|
+
signature: record.signature,
|
|
204
|
+
});
|
|
205
|
+
return { ...record, state, chain_status: chain };
|
|
206
|
+
}
|
|
207
|
+
if (a.command === "send") {
|
|
208
|
+
const bytes = decodeBase64(required(a, "signed-tx-base64"));
|
|
209
|
+
if (bytes.length > 1232)
|
|
210
|
+
throw new Error("Signed transaction exceeds packet limit");
|
|
211
|
+
let tx;
|
|
212
|
+
try {
|
|
213
|
+
tx = VersionedTransaction.deserialize(bytes);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw new Error("Malformed signed transaction");
|
|
217
|
+
}
|
|
218
|
+
if (!Buffer.from(tx.serialize()).equals(bytes))
|
|
219
|
+
throw new Error("Noncanonical signed transaction");
|
|
220
|
+
const signature = checkSignature(tx);
|
|
221
|
+
const wallet = new PublicKey(required(a, "wallet"));
|
|
222
|
+
if (tx.message.header.numRequiredSignatures !== 1 ||
|
|
223
|
+
!tx.message.staticAccountKeys[0].equals(wallet))
|
|
224
|
+
throw new Error("Signed transaction payer/signer mismatch");
|
|
225
|
+
const maxFee = integer(required(a, "max-fee-credits"));
|
|
226
|
+
const p = await t.pricing(), fee = sendFee(p);
|
|
227
|
+
if (fee > maxFee)
|
|
228
|
+
throw new Error("Send fee exceeds cap");
|
|
229
|
+
if (!a.execute)
|
|
230
|
+
return {
|
|
231
|
+
state: "plan",
|
|
232
|
+
signature,
|
|
233
|
+
send_fee_credits: fee,
|
|
234
|
+
note: "Existing signed transaction must be independently reviewed; this command does not validate trade instructions.",
|
|
235
|
+
};
|
|
236
|
+
requireAuth(env);
|
|
237
|
+
await identity(t, wallet);
|
|
238
|
+
const id = required(a, "operation-id");
|
|
239
|
+
await j.begin(id, fingerprint({ origin, signature }), {
|
|
240
|
+
command: "send",
|
|
241
|
+
wallet: wallet.toBase58(),
|
|
242
|
+
});
|
|
243
|
+
return submit(t, j, id, tx, emit);
|
|
244
|
+
}
|
|
245
|
+
if (a.command !== "buy" && a.command !== "sell")
|
|
246
|
+
throw new Error("Unsupported command");
|
|
247
|
+
if (a["build-only"] && !a.execute)
|
|
248
|
+
throw new Error("--build-only requires --execute");
|
|
249
|
+
const side = a.command;
|
|
250
|
+
const mint = new PublicKey(required(a, "mint")), wallet = new PublicKey(required(a, "wallet"));
|
|
251
|
+
const maxSol = solLamports(required(a, "max-sol"));
|
|
252
|
+
if (maxSol <= 0n || maxSol > BigInt(Number.MAX_SAFE_INTEGER))
|
|
253
|
+
throw new Error("Max SOL must be positive and safely representable in lamports");
|
|
254
|
+
const amount = side === "buy"
|
|
255
|
+
? solLamports(required(a, "amount-sol"))
|
|
256
|
+
: rawAmount(required(a, "amount-raw"));
|
|
257
|
+
if (amount <= 0n ||
|
|
258
|
+
(side === "buy" &&
|
|
259
|
+
(amount > maxSol || amount > BigInt(Number.MAX_SAFE_INTEGER))))
|
|
260
|
+
throw new Error("Amount outside SOL budget");
|
|
261
|
+
const maxFee = integer(required(a, "max-fee-credits")), slippage = integer(required(a, "slippage-bps"), 9999), priority = integer(required(a, "priority-fee")), priorityCap = integer(required(a, "max-priority-fee"));
|
|
262
|
+
if (priority > priorityCap)
|
|
263
|
+
throw new Error("Priority exceeds cap");
|
|
264
|
+
if (priority > 0 && a["max-priority-lamports"] === undefined)
|
|
265
|
+
throw new Error("Nonzero priority requires --max-priority-lamports");
|
|
266
|
+
const maxPriorityLamports = BigInt(integer(String(a["max-priority-lamports"] ?? "0")));
|
|
267
|
+
const p = await t.pricing();
|
|
268
|
+
const sendCredits = sendFee(p);
|
|
269
|
+
const estimate = side === "buy" ? estimateBuy(p, amount) : null;
|
|
270
|
+
const plan = {
|
|
271
|
+
state: "plan",
|
|
272
|
+
side,
|
|
273
|
+
wallet: wallet.toBase58(),
|
|
274
|
+
mint: mint.toBase58(),
|
|
275
|
+
amount_raw: side === "sell" ? amount.toString() : undefined,
|
|
276
|
+
amount_lamports: side === "buy" ? amount.toString() : undefined,
|
|
277
|
+
max_sol_lamports: maxSol.toString(),
|
|
278
|
+
estimated_build_fee_credits: estimate,
|
|
279
|
+
send_fee_credits: sendCredits,
|
|
280
|
+
live_pricing: p,
|
|
281
|
+
limits: {
|
|
282
|
+
max_fee_credits: maxFee,
|
|
283
|
+
slippage_bps: slippage,
|
|
284
|
+
priority_fee_micro_lamports: priority,
|
|
285
|
+
max_priority_fee_micro_lamports: priorityCap,
|
|
286
|
+
},
|
|
287
|
+
warning: "Builder is charged BEFORE returning. Buy estimate is not a server-side billing cap. Sell build is unquoted; post-build cap cannot undo its charge. Send failure does not automatically refund a successful builder.",
|
|
288
|
+
};
|
|
289
|
+
if (!a.execute)
|
|
290
|
+
return plan;
|
|
291
|
+
if (side === "sell" && !a["accept-unquoted-build-charge"])
|
|
292
|
+
throw new Error("Sell charge unquoted: explicit --accept-unquoted-build-charge required BEFORE builder");
|
|
293
|
+
if (estimate !== null &&
|
|
294
|
+
estimate + (a["build-only"] ? 0 : sendCredits) > maxFee)
|
|
295
|
+
throw new Error("Estimated builder/send credits exceed cap");
|
|
296
|
+
if (!a["build-only"] && !a["ack-incomplete-policy"])
|
|
297
|
+
throw new Error("Partial instruction policy: --ack-incomplete-policy required before signing");
|
|
298
|
+
requireAuth(env);
|
|
299
|
+
if (!a["build-only"] && (!bridge || !bridge.publicKey.equals(wallet)))
|
|
300
|
+
throw new Error("Framework wallet mismatch or missing bridge");
|
|
301
|
+
const account = await identity(t, wallet);
|
|
302
|
+
if (finite(account.balance) <
|
|
303
|
+
(estimate ?? 0) + (a["build-only"] ? 0 : sendCredits))
|
|
304
|
+
throw new Error("Insufficient billing balance");
|
|
305
|
+
const admission = side === "buy"
|
|
306
|
+
? await admitBuy(t, wallet, mint, amount, slippage, maxSol, maxPriorityLamports)
|
|
307
|
+
: undefined;
|
|
308
|
+
const id = required(a, "operation-id");
|
|
309
|
+
const intent = {
|
|
310
|
+
origin,
|
|
311
|
+
side,
|
|
312
|
+
mint: mint.toBase58(),
|
|
313
|
+
wallet: wallet.toBase58(),
|
|
314
|
+
amount: amount.toString(),
|
|
315
|
+
slippage,
|
|
316
|
+
priority,
|
|
317
|
+
};
|
|
318
|
+
await j.begin(id, fingerprint({
|
|
319
|
+
origin,
|
|
320
|
+
side,
|
|
321
|
+
mint: mint.toBase58(),
|
|
322
|
+
wallet: wallet.toBase58(),
|
|
323
|
+
amount: amount.toString(),
|
|
324
|
+
}), intent);
|
|
325
|
+
await j.event(id, { state: "unknown", phase: "builder-intent" });
|
|
326
|
+
let result;
|
|
327
|
+
try {
|
|
328
|
+
result =
|
|
329
|
+
side === "buy"
|
|
330
|
+
? await t.sdk.executeSnipe(mint.toBase58(), Number(amount) / 1e9, slippage, { priority_fee_micro_lamports: priority })
|
|
331
|
+
: await t.sell(mint.toBase58(), amount.toString(), slippage, priority);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
await j.event(id, { state: "unknown", phase: "builder-uncertain" });
|
|
335
|
+
return {
|
|
336
|
+
state: "unknown",
|
|
337
|
+
operation_id: id,
|
|
338
|
+
note: "Builder response failed. Check ledger for debit/refund; never auto-rebuild. No transaction signed/submitted.",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
await j.event(id, {
|
|
342
|
+
state: "built",
|
|
343
|
+
phase: "builder-returned",
|
|
344
|
+
build_result: {
|
|
345
|
+
mint: new PublicKey(result.mint).toBase58(),
|
|
346
|
+
fee_credits: finite(result.fee_credits),
|
|
347
|
+
...(side === "buy"
|
|
348
|
+
? {
|
|
349
|
+
tokens_out: safeLamports(result.tokens_out).toString(),
|
|
350
|
+
max_sol_cost_lamports: safeLamports(result.max_sol_cost_lamports).toString(),
|
|
351
|
+
}
|
|
352
|
+
: {
|
|
353
|
+
min_sol_output_lamports: safeLamports(result.min_sol_output_lamports).toString(),
|
|
354
|
+
}),
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
const charged = finite(result.fee_credits);
|
|
358
|
+
if (charged + (a["build-only"] ? 0 : sendCredits) > maxFee)
|
|
359
|
+
throw new Error("Already-charged builder plus send exceeds max credits; NOT signing/submitting (no automatic builder refund)");
|
|
360
|
+
if (result.mint !== mint.toBase58())
|
|
361
|
+
throw new Error("Builder mint mismatch");
|
|
362
|
+
const cost = safeLamports(side === "buy"
|
|
363
|
+
? result.max_sol_cost_lamports
|
|
364
|
+
: result.min_sol_output_lamports);
|
|
365
|
+
if (side === "buy" && cost > maxSol)
|
|
366
|
+
throw new Error("Already-charged build exceeds returned SOL cap; NOT signing/submitting");
|
|
367
|
+
const tx = decodeBuilderUnsigned(result.unsigned_tx_base64);
|
|
368
|
+
if (a["build-only"]) {
|
|
369
|
+
const envelope = inspectEnvelope(tx, wallet);
|
|
370
|
+
emit({
|
|
371
|
+
state: "built",
|
|
372
|
+
operation_id: id,
|
|
373
|
+
fee_credits: charged,
|
|
374
|
+
policy: envelope,
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
state: "built",
|
|
378
|
+
operation_id: id,
|
|
379
|
+
fee_credits: charged,
|
|
380
|
+
unsigned_tx_base64: result.unsigned_tx_base64,
|
|
381
|
+
admission,
|
|
382
|
+
policy: envelope,
|
|
383
|
+
note: "Charged unsigned construction only. No signing/submission; instruction semantics are not authorized. No automatic resume/rebuild.",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
const inspected = verifyUnsigned(tx, wallet, {
|
|
387
|
+
side,
|
|
388
|
+
mint,
|
|
389
|
+
maxSol,
|
|
390
|
+
amountRaw: side === "sell" ? amount : undefined,
|
|
391
|
+
maxPriority: BigInt(priorityCap),
|
|
392
|
+
maxPriorityLamports,
|
|
393
|
+
requestedPriority: BigInt(priority),
|
|
394
|
+
tokensOut: side === "buy" ? safeLamports(result.tokens_out) : undefined,
|
|
395
|
+
reportedCost: cost,
|
|
396
|
+
});
|
|
397
|
+
emit({
|
|
398
|
+
state: "built",
|
|
399
|
+
operation_id: id,
|
|
400
|
+
fee_credits: charged,
|
|
401
|
+
policy: inspected,
|
|
402
|
+
});
|
|
403
|
+
// Network fees are queried against this exact message; rent is conservatively
|
|
404
|
+
// reserved for a new token ATA. Pump-side CPI rent remains outside this partial policy.
|
|
405
|
+
const fee = await t.connection.getFeeForMessage(tx.message);
|
|
406
|
+
if (fee.value === null)
|
|
407
|
+
throw new Error("Message fee unavailable/expired");
|
|
408
|
+
let overhead = safeLamports(fee.value);
|
|
409
|
+
if (inspected.ataCount)
|
|
410
|
+
overhead +=
|
|
411
|
+
BigInt(inspected.ataCount) *
|
|
412
|
+
safeLamports(await t.connection.getMinimumBalanceForRentExemption(165));
|
|
413
|
+
if (inspected.tradeCost + overhead > maxSol)
|
|
414
|
+
throw new Error("Trade + known network/ATA rent exceeds max SOL; NOT signing");
|
|
415
|
+
const originalMessage = Buffer.from(tx.message.serialize());
|
|
416
|
+
const signed = await bridge.signTransaction(tx);
|
|
417
|
+
if (!signed ||
|
|
418
|
+
!Buffer.from(signed.message.serialize()).equals(originalMessage))
|
|
419
|
+
throw new Error("Wallet mutated approved transaction message");
|
|
420
|
+
checkSignature(signed);
|
|
421
|
+
return submit(t, j, id, signed, emit);
|
|
422
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Args = {
|
|
2
|
+
command: string;
|
|
3
|
+
[key: string]: string | boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function parseArgs(argv: string[]): Args;
|
|
6
|
+
export declare function required(a: Args, k: string): string;
|
|
7
|
+
export declare function integer(s: string, max?: number): number;
|
|
8
|
+
export declare function rawAmount(s: string): bigint;
|
|
9
|
+
export declare function solLamports(s: string): bigint;
|
|
10
|
+
export declare function endpoint(s: string): string;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const common = ["endpoint"];
|
|
2
|
+
const trade = [
|
|
3
|
+
"mint",
|
|
4
|
+
"wallet",
|
|
5
|
+
"keypair",
|
|
6
|
+
"max-sol",
|
|
7
|
+
"max-fee-credits",
|
|
8
|
+
"slippage-bps",
|
|
9
|
+
"priority-fee",
|
|
10
|
+
"max-priority-fee",
|
|
11
|
+
"max-priority-lamports",
|
|
12
|
+
"journal",
|
|
13
|
+
"operation-id",
|
|
14
|
+
"execute",
|
|
15
|
+
"build-only",
|
|
16
|
+
"accept-unquoted-build-charge",
|
|
17
|
+
"ack-incomplete-policy",
|
|
18
|
+
];
|
|
19
|
+
const flags = {
|
|
20
|
+
buy: [...trade, "amount-sol"],
|
|
21
|
+
sell: [...trade, "amount-raw"],
|
|
22
|
+
send: [
|
|
23
|
+
"tx-file",
|
|
24
|
+
"wallet",
|
|
25
|
+
"max-fee-credits",
|
|
26
|
+
"journal",
|
|
27
|
+
"operation-id",
|
|
28
|
+
"execute",
|
|
29
|
+
],
|
|
30
|
+
inspect: ["wallet"],
|
|
31
|
+
migrate: ["wallet"],
|
|
32
|
+
recent: ["limit"],
|
|
33
|
+
monitor: ["seconds", "max-events", "mint", "kinds"],
|
|
34
|
+
status: ["journal", "operation-id"],
|
|
35
|
+
help: [],
|
|
36
|
+
};
|
|
37
|
+
const booleans = new Set([
|
|
38
|
+
"execute",
|
|
39
|
+
"build-only",
|
|
40
|
+
"accept-unquoted-build-charge",
|
|
41
|
+
"ack-incomplete-policy",
|
|
42
|
+
]);
|
|
43
|
+
export function parseArgs(argv) {
|
|
44
|
+
const command = argv[0] ?? "help";
|
|
45
|
+
if (!(command in flags))
|
|
46
|
+
throw new Error("Unknown command");
|
|
47
|
+
const out = { command };
|
|
48
|
+
for (let i = 1; i < argv.length; i++) {
|
|
49
|
+
const flag = argv[i];
|
|
50
|
+
if (!flag.startsWith("--"))
|
|
51
|
+
throw new Error("Unexpected positional argument");
|
|
52
|
+
const k = flag.slice(2);
|
|
53
|
+
if (![...common, ...flags[command]].includes(k) || k in out)
|
|
54
|
+
throw new Error("Unknown or duplicate flag: " + flag);
|
|
55
|
+
if (booleans.has(k))
|
|
56
|
+
out[k] = true;
|
|
57
|
+
else {
|
|
58
|
+
const v = argv[++i];
|
|
59
|
+
if (!v || v.startsWith("--"))
|
|
60
|
+
throw new Error("Missing value: " + flag);
|
|
61
|
+
out[k] = v;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
export function required(a, k) {
|
|
67
|
+
const v = a[k];
|
|
68
|
+
if (typeof v !== "string" || !v)
|
|
69
|
+
throw new Error("Required --" + k);
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
export function integer(s, max = Number.MAX_SAFE_INTEGER) {
|
|
73
|
+
if (!/^(0|[1-9]\d*)$/.test(s))
|
|
74
|
+
throw new Error("Invalid integer");
|
|
75
|
+
const n = Number(s);
|
|
76
|
+
if (!Number.isSafeInteger(n) || n > max)
|
|
77
|
+
throw new Error("Integer exceeds cap");
|
|
78
|
+
return n;
|
|
79
|
+
}
|
|
80
|
+
export function rawAmount(s) {
|
|
81
|
+
if (!/^[1-9]\d*$/.test(s))
|
|
82
|
+
throw new Error("Amount must be a positive raw integer");
|
|
83
|
+
const n = BigInt(s);
|
|
84
|
+
if (n > 18446744073709551615n)
|
|
85
|
+
throw new Error("Amount exceeds u64");
|
|
86
|
+
return n;
|
|
87
|
+
}
|
|
88
|
+
export function solLamports(s) {
|
|
89
|
+
if (!/^(0|[1-9]\d*)(\.\d{1,9})?$/.test(s))
|
|
90
|
+
throw new Error("SOL requires plain decimal with <=9 places");
|
|
91
|
+
const [w, f = ""] = s.split(".");
|
|
92
|
+
return BigInt(w) * 1000000000n + BigInt(f.padEnd(9, "0"));
|
|
93
|
+
}
|
|
94
|
+
export function endpoint(s) {
|
|
95
|
+
const u = new URL(s);
|
|
96
|
+
if (u.username ||
|
|
97
|
+
u.password ||
|
|
98
|
+
u.search ||
|
|
99
|
+
u.hash ||
|
|
100
|
+
!["", "/"].includes(u.pathname) ||
|
|
101
|
+
!(u.protocol === "https:" ||
|
|
102
|
+
(u.protocol === "http:" &&
|
|
103
|
+
["127.0.0.1", "localhost", "[::1]"].includes(u.hostname))))
|
|
104
|
+
throw new Error("Use HTTPS origin (HTTP only for loopback), no URL credentials or path");
|
|
105
|
+
return u.origin;
|
|
106
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare function fingerprint(value: unknown): string;
|
|
2
|
+
export declare function outcome(status: any): "confirmed" | "failed" | "unknown";
|
|
3
|
+
export declare class Journal {
|
|
4
|
+
readonly directory: string;
|
|
5
|
+
private readonly credential?;
|
|
6
|
+
constructor(directory: string, credential?: string | undefined);
|
|
7
|
+
private encode;
|
|
8
|
+
private reserve;
|
|
9
|
+
begin(id: string, hash: string, metadata: unknown): Promise<void>;
|
|
10
|
+
reserveSignature(signature: string): Promise<void>;
|
|
11
|
+
event(id: string, event: Record<string, unknown>): Promise<void>;
|
|
12
|
+
read(id: string): Promise<any>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { mkdir, open, readFile, unlink } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
export function fingerprint(value) {
|
|
5
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
export function outcome(status) {
|
|
8
|
+
if (status?.err != null)
|
|
9
|
+
return "failed";
|
|
10
|
+
if (status?.confirmationStatus === "confirmed" ||
|
|
11
|
+
status?.confirmationStatus === "finalized")
|
|
12
|
+
return "confirmed";
|
|
13
|
+
return "unknown";
|
|
14
|
+
}
|
|
15
|
+
export class Journal {
|
|
16
|
+
directory;
|
|
17
|
+
credential;
|
|
18
|
+
constructor(directory, credential) {
|
|
19
|
+
this.directory = directory;
|
|
20
|
+
this.credential = credential;
|
|
21
|
+
}
|
|
22
|
+
encode(value) {
|
|
23
|
+
const scrub = (v) => {
|
|
24
|
+
const text = (s) => this.credential ? s.split(this.credential).join("[REDACTED]") : s;
|
|
25
|
+
if (typeof v === "string")
|
|
26
|
+
return text(v);
|
|
27
|
+
if (Array.isArray(v))
|
|
28
|
+
return v.map(scrub);
|
|
29
|
+
if (v && typeof v === "object")
|
|
30
|
+
return Object.fromEntries(Object.entries(v).map(([k, item]) => [text(k), scrub(item)]));
|
|
31
|
+
return v;
|
|
32
|
+
};
|
|
33
|
+
return JSON.stringify(scrub(value));
|
|
34
|
+
}
|
|
35
|
+
async reserve(kind, key, value) {
|
|
36
|
+
await mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
37
|
+
const path = join(this.directory, kind + "-" + fingerprint(key) + ".json");
|
|
38
|
+
const file = await open(path, "wx", 0o600);
|
|
39
|
+
try {
|
|
40
|
+
await file.writeFile(this.encode(value) + "\n");
|
|
41
|
+
await file.sync();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await file.close();
|
|
45
|
+
}
|
|
46
|
+
const dir = await open(this.directory, "r");
|
|
47
|
+
try {
|
|
48
|
+
await dir.sync();
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await dir.close();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async begin(id, hash, metadata) {
|
|
55
|
+
if (!/^[A-Za-z0-9_-]{1,80}$/.test(id))
|
|
56
|
+
throw new Error("Invalid operation id");
|
|
57
|
+
await this.reserve("operation", id, {
|
|
58
|
+
id,
|
|
59
|
+
metadata,
|
|
60
|
+
state: "unknown",
|
|
61
|
+
phase: "reserved",
|
|
62
|
+
});
|
|
63
|
+
const lockPath = join(this.directory, "intent-lock-" + fingerprint(hash));
|
|
64
|
+
const lock = await open(lockPath, "wx", 0o600);
|
|
65
|
+
try {
|
|
66
|
+
const path = join(this.directory, "fingerprint-" + fingerprint(hash) + ".json");
|
|
67
|
+
let existing;
|
|
68
|
+
try {
|
|
69
|
+
existing = JSON.parse(await readFile(path, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e.code !== "ENOENT")
|
|
73
|
+
throw e;
|
|
74
|
+
}
|
|
75
|
+
if (existing) {
|
|
76
|
+
const previous = await this.read(existing.id);
|
|
77
|
+
if (!["confirmed", "failed"].includes(previous.state) ||
|
|
78
|
+
!previous.signature)
|
|
79
|
+
throw new Error("Previous identical intent is unresolved; use status, never force/rebuild");
|
|
80
|
+
}
|
|
81
|
+
const f = await open(path, "w", 0o600);
|
|
82
|
+
try {
|
|
83
|
+
await f.writeFile(this.encode({ id }) + "\n");
|
|
84
|
+
await f.sync();
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await f.close();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await lock.close();
|
|
92
|
+
await unlink(lockPath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async reserveSignature(signature) {
|
|
96
|
+
await this.reserve("signature", signature, { signature });
|
|
97
|
+
}
|
|
98
|
+
async event(id, event) {
|
|
99
|
+
const f = await open(join(this.directory, "events-" + fingerprint(id) + ".jsonl"), "a", 0o600);
|
|
100
|
+
try {
|
|
101
|
+
await f.writeFile(this.encode({ ...event, at: new Date().toISOString() }) + "\n");
|
|
102
|
+
await f.sync();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await f.close();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async read(id) {
|
|
109
|
+
let start;
|
|
110
|
+
try {
|
|
111
|
+
start = JSON.parse(await readFile(join(this.directory, "operation-" + fingerprint(id) + ".json"), "utf8"));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new Error("Operation not found");
|
|
115
|
+
}
|
|
116
|
+
let text = "";
|
|
117
|
+
try {
|
|
118
|
+
text = await readFile(join(this.directory, "events-" + fingerprint(id) + ".jsonl"), "utf8");
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
if (e.code !== "ENOENT")
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
124
|
+
for (const line of text.split("\n").filter(Boolean)) {
|
|
125
|
+
try {
|
|
126
|
+
Object.assign(start, JSON.parse(line));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
start.state = "unknown";
|
|
130
|
+
start.phase = "corrupt-journal";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return start;
|
|
134
|
+
}
|
|
135
|
+
}
|