@nodiusxyz/solana-agent-kit 0.1.0 → 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/README.md +11 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/internal/admission.d.ts +11 -0
- package/dist/internal/admission.js +96 -0
- package/dist/internal/app.js +47 -12
- package/dist/internal/journal.js +7 -1
- package/dist/internal/safety.js +24 -0
- package/dist/internal/transport.js +3 -2
- package/package.json +3 -2
- package/src/index.ts +1 -0
- package/src/internal/admission.ts +131 -0
- package/src/internal/app.ts +54 -11
- package/src/internal/journal.ts +11 -6
- package/src/internal/safety.ts +28 -0
- package/src/internal/transport.ts +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @nodiusxyz/solana-agent-kit
|
|
2
2
|
|
|
3
|
-
An opt-in **Solana Agent Kit 2.0.10** plugin for nodius.xyz recent signals, bounded monitoring, buy/sell plans, guarded execution and persistent status recovery. Node.js **
|
|
3
|
+
An opt-in **Solana Agent Kit 2.0.10** plugin for nodius.xyz recent signals, bounded monitoring, buy/sell plans, guarded execution and persistent status recovery. Node.js **20.20.2+**, ESM. This is a library plugin, not a CLI trading pack. MIT licensed.
|
|
4
4
|
|
|
5
5
|
## Install and drop in
|
|
6
6
|
|
|
@@ -32,7 +32,7 @@ console.log(await nodius.methods.nodiusRecent({ limit: 5 }));
|
|
|
32
32
|
|
|
33
33
|
All five names are registered identically in `agent.methods` and `agent.actions`:
|
|
34
34
|
|
|
35
|
-
- `nodiusRecent({ limit?: number })`: default 20, integer 1–50
|
|
35
|
+
- `nodiusRecent({ limit?: number })`: default 20, integer 1–50. Locally limits returned events even when an older server returns a larger preview; returned `count` matches those events.
|
|
36
36
|
- `nodiusMonitor({ maxFeeCredits: number, seconds?: number, maxEvents?: number, mint?: string, kinds?: SignalKind[] })`: defaults 30 seconds / 100 events; bounds 1–300 seconds / 1–1000 events. Kinds: `newPair`, `graduation`, `walletActivity`, `largeSwap`, no duplicates. Authenticated **paid** stream. Requires a nonnegative safe-integer admission budget: live `subscribe + maxEvents × notification + unsubscribe` estimate must fit **before WS opens**. For example `{ seconds: 3, maxEvents: 3, maxFeeCredits: 100 }`. Returns `{ state: "monitor-complete", events, subscription, notifications, estimated_stream_credits, note }`; closes/unsubscribes in finally. This never triggers trades. Duration is the stream deadline; HTTP pricing lookup can add up to its 10-second timeout and cleanup up to 500ms.
|
|
37
37
|
- `nodiusTrade(input)`: buy or sell; defaults a **read-only plan**, only fetching pricing. No builder, signature, funding or submission by default.
|
|
38
38
|
- `nodiusStatus({ operationId: string })`: reads the configured persistent journal and, if a signature exists, reconciles chain status. Never rebuilds, resumes or rebroadcasts. A missing signature is not permission to retry; inspect the billing ledger separately.
|
|
@@ -81,10 +81,18 @@ For executed trades, billing identity must match the wallet. The SDK uses `maxRe
|
|
|
81
81
|
|
|
82
82
|
This is a **partial policy**, not a complete on-chain proof: dynamic program state, fee-recipient governance, program upgrades, Token-2022 extensions and all CPI behavior/rent are not fully verified. `ackIncompletePolicy: true` explicitly accepts these limitations, not arbitrary instructions. Slippage arguments are forwarded and returned instruction bounds checked; they are not an independent market-price oracle. `maxSol` is not a guaranteed cap on every possible CPI effect.
|
|
83
83
|
|
|
84
|
-
Pricing is fetched fresh from `/pricing`. Buy build fee is estimated with a SOL quote no older than 15 seconds. Builder fees are charged **before** the response arrives. The estimated buy cap is **not a server-side billing cap**; the post-build check cannot undo a builder debit. Sell construction is unquoted and requires the explicit charge acknowledgement. Send failure does not automatically refund a successful build. Signals billing may include events queued beyond the locally received count; duration/event limits are **not** hard server billing caps. Keep account credit balances bounded and review ledger/transaction details.
|
|
84
|
+
Pricing is fetched fresh from `/pricing`. Buy build fee is estimated from **`amountSol`**, not `maxSol`, with a SOL quote no older than 15 seconds. `maxSol` is the aggregate safety ceiling; raising it does not raise the requested trade size or its fee estimate. Before charged buy construction (including `buildOnly`), live mint/curve and buyer-associated account data plus live rent prices admit the slippage-bound trade, known missing/growing account rent, and a conservative network/priority allowance. Token-2022 account sizes use SPL extension sizing, including metadata-extended mints; v2 zero/WSOL quote layouts reserve the known WSOL and volume-associated accounts. Missing/unsupported curve layouts fail before construction. Admission failures report required lamports and trade/rent/network components; unsigned results include an `admission` breakdown. This is a conservative partial check, not an exact rent multiplier or complete CPI-spend guarantee. Send admission uses the live landed **total** rather than adding the base fee twice. Builder fees are charged **before** the response arrives. The estimated buy cap is **not a server-side billing cap**; the post-build check cannot undo a builder debit. Sell construction is unquoted and requires the explicit charge acknowledgement. Send failure does not automatically refund a successful build. Signals billing may include events queued beyond the locally received count; duration/event limits are **not** hard server billing caps. Keep account credit balances bounded and review ledger/transaction details.
|
|
85
85
|
|
|
86
86
|
There is exactly one client `sendAndConfirm` invocation per submitted operation. The server can intentionally make bounded same-byte multi-leg resends; this is not an SDK retry. Journal logical economic intent is origin/wallet/side/mint/amount; changing slippage or priority cannot bypass an unresolved reservation (different amounts are different intents, not a global exactly-once guarantee). Operation/intent reservations precede charged construction, and signature reservations are permanent before submission. Transport failures produce `unknown`, never an automatic rebuild/rebroadcast. Preserve the journal across restarts; do not delete it or change directories to bypass guards. Prefer a private directory on durable local storage (created 0700, files 0600), never shared across unrelated wallets. Journal records store only allowlisted validated builder summaries, not arbitrary response fields or unsigned transaction payloads, and redact the current credential before writes. Journal records include intent/build details; protect them like financial records. Status reconciliation is observation, not an execution authorization. Reusing a transaction signature is always blocked, including after terminal outcomes.
|
|
87
87
|
|
|
88
|
+
### Unsigned decoder and sell rent recovery
|
|
89
|
+
|
|
90
|
+
`decodeBuilderUnsigned(unsignedTxBase64)` is exported from the package root. It accepts the canonical provider zero-signature envelope and returns a `VersionedTransaction` with an empty signing placeholder. This decoding does **not** authorize signing or validate trade instruction semantics; independently inspect/review the result. Signed-send validation remains strict and does not normalize zero signatures.
|
|
91
|
+
|
|
92
|
+
Sell requests omit `closeAccount` and honor the server default: a full-balance sell can append own-base-ATA rent recovery; partial sells leave the account open. No close override is exposed. Classic and v2 signing policies permit only the expected derived base ATA, the correct token program, and exactly three close accounts (base ATA, wallet destination, wallet authority), with the close after the sell. Arbitrary accounts/destinations/authorities and reordered closes are rejected. Inventory eligibility is server/on-chain state, not an offline client proof.
|
|
93
|
+
|
|
94
|
+
`nodiusStatus` reports a missing local ID as `Operation not found`, without filesystem paths. Malformed signed wire data reports `Malformed signed transaction` rather than parser internals. Signature-bound definitive `status: failed`/transaction errors are terminal failures; explicit `status: unknown` remains uncertain regardless of diagnostic details. Status recovery recognizes on-chain `err` as failed without sending again.
|
|
95
|
+
|
|
88
96
|
## Framework tools without hosted LLM credentials
|
|
89
97
|
|
|
90
98
|
Programmatic methods above need **no LLM API key**. If desired, convert registered actions offline using published framework helpers:
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { run } from "./internal/app.js";
|
|
3
3
|
import { endpoint as validateEndpoint } from "./internal/config.js";
|
|
4
|
+
export { decodeBuilderUnsigned } from "./internal/safety.js";
|
|
4
5
|
const integer = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
5
6
|
const decimal = z.string().regex(/^(0|[1-9]\d*)(\.\d{1,9})?$/);
|
|
6
7
|
const base = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import type { Transport } from "./transport.js";
|
|
3
|
+
/** Conservative admission, not a guarantee against dynamic CPI/fee changes. */
|
|
4
|
+
export declare function admitBuy(t: Transport, wallet: PublicKey, mint: PublicKey, amount: bigint, slippage: number, maxSol: bigint, priority: bigint): Promise<{
|
|
5
|
+
trade_bound_lamports: string;
|
|
6
|
+
account_rent_lamports: string;
|
|
7
|
+
rent_accounts: Record<string, string>;
|
|
8
|
+
network_fee_bound_lamports: string;
|
|
9
|
+
required_lamports: string;
|
|
10
|
+
note: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import { unpackMint, getAccountLenForMint, getAccountLen, getExtensionTypes, getAccountTypeOfMintType, ExtensionType, } from "@solana/spl-token";
|
|
3
|
+
const PUMP = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
|
4
|
+
const ATA = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
|
5
|
+
const TOKEN = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
6
|
+
const TOKEN22 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
|
|
7
|
+
const WSOL = new PublicKey("So11111111111111111111111111111111111111112");
|
|
8
|
+
function lamports(n) {
|
|
9
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
10
|
+
throw Error("Unsafe admission lamports");
|
|
11
|
+
return BigInt(n);
|
|
12
|
+
}
|
|
13
|
+
/** Conservative admission, not a guarantee against dynamic CPI/fee changes. */
|
|
14
|
+
export async function admitBuy(t, wallet, mint, amount, slippage, maxSol, priority) {
|
|
15
|
+
const mintInfo = await t.connection.getAccountInfo(mint);
|
|
16
|
+
if (!mintInfo ||
|
|
17
|
+
mintInfo.executable ||
|
|
18
|
+
![TOKEN, TOKEN22].some((p) => p.equals(mintInfo.owner)))
|
|
19
|
+
throw Error("Buy admission mint account unavailable/unsupported");
|
|
20
|
+
let baseSize;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = unpackMint(mint, mintInfo, mintInfo.owner);
|
|
23
|
+
if (!parsed.isInitialized)
|
|
24
|
+
throw Error("uninitialized");
|
|
25
|
+
baseSize = getAccountLenForMint(parsed);
|
|
26
|
+
if (mintInfo.owner.equals(TOKEN22))
|
|
27
|
+
baseSize = Math.max(baseSize, getAccountLen([
|
|
28
|
+
...getExtensionTypes(parsed.tlvData)
|
|
29
|
+
.map(getAccountTypeOfMintType)
|
|
30
|
+
.filter((t) => t !== ExtensionType.Uninitialized),
|
|
31
|
+
ExtensionType.ImmutableOwner,
|
|
32
|
+
]));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw Error("Buy admission mint layout/extensions unsupported before builder");
|
|
36
|
+
}
|
|
37
|
+
const ata = (owner, m, token) => PublicKey.findProgramAddressSync([owner.toBuffer(), token.toBuffer(), m.toBuffer()], ATA)[0];
|
|
38
|
+
const pda = (name, key) => PublicKey.findProgramAddressSync([Buffer.from(name), key.toBuffer()], PUMP)[0];
|
|
39
|
+
const uva = pda("user_volume_accumulator", wallet);
|
|
40
|
+
const keys = [
|
|
41
|
+
ata(wallet, mint, mintInfo.owner),
|
|
42
|
+
ata(wallet, WSOL, TOKEN),
|
|
43
|
+
uva,
|
|
44
|
+
ata(uva, WSOL, TOKEN),
|
|
45
|
+
pda("bonding-curve", mint),
|
|
46
|
+
];
|
|
47
|
+
const accounts = await t.connection.getMultipleAccountsInfo(keys);
|
|
48
|
+
const curve = accounts[4];
|
|
49
|
+
if (!curve ||
|
|
50
|
+
!curve.owner.equals(PUMP) ||
|
|
51
|
+
curve.data.length < 81 ||
|
|
52
|
+
!curve.data
|
|
53
|
+
.subarray(0, 8)
|
|
54
|
+
.equals(Buffer.from([23, 183, 248, 55, 96, 216, 172, 96])))
|
|
55
|
+
throw Error("Buy admission curve unavailable/unsupported before builder");
|
|
56
|
+
const quote = curve.data.subarray(83, 115);
|
|
57
|
+
const v2 = curve.data.length >= 115 &&
|
|
58
|
+
(quote.equals(WSOL.toBuffer()) || quote.every((b) => b === 0));
|
|
59
|
+
// Current Pump IDL UserVolumeAccumulator serialized fields: discriminator8 +
|
|
60
|
+
// user32 + bool1 + four u64/i64 + bool1 + four cashback u64 = 106 bytes.
|
|
61
|
+
// Curve uses its live allocation; these builders do not emit extendAccount.
|
|
62
|
+
const sizes = [baseSize, 165, 106, 165, curve.data.length];
|
|
63
|
+
const names = [
|
|
64
|
+
"base_ata",
|
|
65
|
+
"wsol_ata",
|
|
66
|
+
"user_volume",
|
|
67
|
+
"user_volume_wsol_ata",
|
|
68
|
+
"curve",
|
|
69
|
+
];
|
|
70
|
+
let rent = 0n;
|
|
71
|
+
const rentAccounts = {};
|
|
72
|
+
for (let i = 0; i < keys.length; i++) {
|
|
73
|
+
if (!v2 && (i === 1 || i === 3))
|
|
74
|
+
continue;
|
|
75
|
+
const info = accounts[i];
|
|
76
|
+
const size = Math.max(sizes[i], info?.data.length ?? 0);
|
|
77
|
+
const minimum = lamports(await t.connection.getMinimumBalanceForRentExemption(size));
|
|
78
|
+
const existing = info ? lamports(info.lamports) : 0n;
|
|
79
|
+
const deficit = minimum > existing ? minimum - existing : 0n;
|
|
80
|
+
rent += deficit;
|
|
81
|
+
rentAccounts[names[i]] = deficit.toString();
|
|
82
|
+
}
|
|
83
|
+
const trade = (amount * BigInt(10000 + slippage) + 9999n) / 10000n;
|
|
84
|
+
const required = trade + rent + 5000n + priority;
|
|
85
|
+
const breakdown = {
|
|
86
|
+
trade_bound_lamports: trade.toString(),
|
|
87
|
+
account_rent_lamports: rent.toString(),
|
|
88
|
+
rent_accounts: rentAccounts,
|
|
89
|
+
network_fee_bound_lamports: (5000n + priority).toString(),
|
|
90
|
+
required_lamports: required.toString(),
|
|
91
|
+
note: "Conservative pre-build admission only; dynamic fees/CPI and account races remain outside this partial policy.",
|
|
92
|
+
};
|
|
93
|
+
if (required > maxSol)
|
|
94
|
+
throw Error(`Trade + conservative network/account rent exceeds max SOL before builder; required ${required} lamports (trade ${trade}, rent ${rent}, network ${5000n + priority})`);
|
|
95
|
+
return breakdown;
|
|
96
|
+
}
|
package/dist/internal/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
|
|
2
2
|
import { required, integer, rawAmount, solLamports } from "./config.js";
|
|
3
3
|
import { Transport, estimateBuy, finite, sendFee } from "./transport.js";
|
|
4
|
+
import { admitBuy } from "./admission.js";
|
|
4
5
|
import { monitor } from "./monitor.js";
|
|
5
6
|
import { Journal, fingerprint, outcome } from "./journal.js";
|
|
6
7
|
import { decodeBase64, verifyUnsigned, checkSignature, inspectEnvelope, decodeBuilderUnsigned, } from "./safety.js";
|
|
@@ -42,7 +43,7 @@ async function submit(t, j, id, tx, emit) {
|
|
|
42
43
|
},
|
|
43
44
|
]);
|
|
44
45
|
if (result?.signature === signature) {
|
|
45
|
-
const chainError = result.chain_err ?? result.chain_error;
|
|
46
|
+
const chainError = result.transaction_error ?? result.chain_err ?? result.chain_error;
|
|
46
47
|
const landedError = Number.isSafeInteger(result.slot) &&
|
|
47
48
|
result.slot >= 0 &&
|
|
48
49
|
typeof result.landing_time_ms === "number" &&
|
|
@@ -51,20 +52,32 @@ async function submit(t, j, id, tx, emit) {
|
|
|
51
52
|
typeof result.error === "string" &&
|
|
52
53
|
result.error.length > 0 &&
|
|
53
54
|
!/timed?\s*out|timeout|without confirmation/i.test(result.error);
|
|
54
|
-
const definitive =
|
|
55
|
+
const definitive = result.status === "failed" ||
|
|
56
|
+
chainError != null ||
|
|
55
57
|
result.definitive_error === true ||
|
|
56
58
|
landedError ||
|
|
57
59
|
(typeof result.error === "string" &&
|
|
58
60
|
/^(Transaction failed on-chain:|Transaction failed:)/.test(result.error));
|
|
59
|
-
state =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
state =
|
|
62
|
+
result.status === "unknown"
|
|
63
|
+
? "unknown"
|
|
64
|
+
: definitive
|
|
65
|
+
? "failed"
|
|
66
|
+
: result.status !== "unknown" && result.confirmed === true
|
|
67
|
+
? "confirmed"
|
|
68
|
+
: "unknown";
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
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
|
+
}
|
|
68
81
|
}
|
|
69
82
|
await j.event(id, {
|
|
70
83
|
state,
|
|
@@ -124,7 +137,19 @@ export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
|
124
137
|
const limit = integer(String(a.limit ?? "20"), 50);
|
|
125
138
|
if (!limit)
|
|
126
139
|
throw new Error("Positive limit required");
|
|
127
|
-
|
|
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;
|
|
128
153
|
}
|
|
129
154
|
if (a.command === "inspect" || a.command === "migrate") {
|
|
130
155
|
requireAuth(env);
|
|
@@ -183,7 +208,13 @@ export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
|
183
208
|
const bytes = decodeBase64(required(a, "signed-tx-base64"));
|
|
184
209
|
if (bytes.length > 1232)
|
|
185
210
|
throw new Error("Signed transaction exceeds packet limit");
|
|
186
|
-
|
|
211
|
+
let tx;
|
|
212
|
+
try {
|
|
213
|
+
tx = VersionedTransaction.deserialize(bytes);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw new Error("Malformed signed transaction");
|
|
217
|
+
}
|
|
187
218
|
if (!Buffer.from(tx.serialize()).equals(bytes))
|
|
188
219
|
throw new Error("Noncanonical signed transaction");
|
|
189
220
|
const signature = checkSignature(tx);
|
|
@@ -235,7 +266,7 @@ export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
|
235
266
|
const maxPriorityLamports = BigInt(integer(String(a["max-priority-lamports"] ?? "0")));
|
|
236
267
|
const p = await t.pricing();
|
|
237
268
|
const sendCredits = sendFee(p);
|
|
238
|
-
const estimate = side === "buy" ? estimateBuy(p,
|
|
269
|
+
const estimate = side === "buy" ? estimateBuy(p, amount) : null;
|
|
239
270
|
const plan = {
|
|
240
271
|
state: "plan",
|
|
241
272
|
side,
|
|
@@ -271,6 +302,9 @@ export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
|
271
302
|
if (finite(account.balance) <
|
|
272
303
|
(estimate ?? 0) + (a["build-only"] ? 0 : sendCredits))
|
|
273
304
|
throw new Error("Insufficient billing balance");
|
|
305
|
+
const admission = side === "buy"
|
|
306
|
+
? await admitBuy(t, wallet, mint, amount, slippage, maxSol, maxPriorityLamports)
|
|
307
|
+
: undefined;
|
|
274
308
|
const id = required(a, "operation-id");
|
|
275
309
|
const intent = {
|
|
276
310
|
origin,
|
|
@@ -344,6 +378,7 @@ export async function run(a, env = process.env, emit = () => { }, bridge) {
|
|
|
344
378
|
operation_id: id,
|
|
345
379
|
fee_credits: charged,
|
|
346
380
|
unsigned_tx_base64: result.unsigned_tx_base64,
|
|
381
|
+
admission,
|
|
347
382
|
policy: envelope,
|
|
348
383
|
note: "Charged unsigned construction only. No signing/submission; instruction semantics are not authorized. No automatic resume/rebuild.",
|
|
349
384
|
};
|
package/dist/internal/journal.js
CHANGED
|
@@ -106,7 +106,13 @@ export class Journal {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
async read(id) {
|
|
109
|
-
|
|
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
|
+
}
|
|
110
116
|
let text = "";
|
|
111
117
|
try {
|
|
112
118
|
text = await readFile(join(this.directory, "events-" + fingerprint(id) + ".jsonl"), "utf8");
|
package/dist/internal/safety.js
CHANGED
|
@@ -75,6 +75,8 @@ export function verifyUnsigned(tx, wallet, p) {
|
|
|
75
75
|
["b817ee6167c5d33d", "5df6823ce7e940b2"].includes(Buffer.from(ix.data).subarray(0, 8).toString("hex")))) {
|
|
76
76
|
return verifyV2(tx, wallet, p);
|
|
77
77
|
}
|
|
78
|
+
let sellToken, sellAta;
|
|
79
|
+
let closes = 0;
|
|
78
80
|
let trades = 0, atas = 0, tradeCost = 0n;
|
|
79
81
|
const instructions = [];
|
|
80
82
|
const seenCompute = new Set();
|
|
@@ -146,6 +148,28 @@ export function verifyUnsigned(tx, wallet, p) {
|
|
|
146
148
|
}
|
|
147
149
|
else if (amount !== p.amountRaw || bound !== p.reportedCost)
|
|
148
150
|
throw new Error("Sell amount/min-output mismatch");
|
|
151
|
+
if (p.side === "sell") {
|
|
152
|
+
sellToken = ix.keys[9].pubkey;
|
|
153
|
+
sellAta = ix.keys[5].pubkey;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else if (p.side === "sell" && [TOKEN, TOKEN2022].includes(program)) {
|
|
157
|
+
const expected = PublicKey.findProgramAddressSync([wallet.toBuffer(), ix.programId.toBuffer(), p.mint.toBuffer()], new PublicKey(ATA))[0];
|
|
158
|
+
if (++closes !== 1 ||
|
|
159
|
+
trades !== 1 ||
|
|
160
|
+
ix !== msg.instructions[msg.instructions.length - 1] ||
|
|
161
|
+
data.length !== 1 ||
|
|
162
|
+
data[0] !== 9 ||
|
|
163
|
+
ix.keys.length !== 3 ||
|
|
164
|
+
!sellToken?.equals(ix.programId) ||
|
|
165
|
+
!sellAta?.equals(expected) ||
|
|
166
|
+
!ix.keys[0].pubkey.equals(expected) ||
|
|
167
|
+
!ix.keys[0].isWritable ||
|
|
168
|
+
ix.keys[0].isSigner ||
|
|
169
|
+
ix.keys
|
|
170
|
+
.slice(1)
|
|
171
|
+
.some((k) => !k.pubkey.equals(wallet) || !k.isSigner || !k.isWritable))
|
|
172
|
+
throw Error("Sell close account policy mismatch");
|
|
149
173
|
}
|
|
150
174
|
else
|
|
151
175
|
throw new Error("Unexpected instruction program: " + program);
|
|
@@ -18,7 +18,9 @@ export function estimateBuy(p, lamports) {
|
|
|
18
18
|
}
|
|
19
19
|
export function sendFee(p) {
|
|
20
20
|
const m = p.methods?.sendAndConfirm;
|
|
21
|
-
const
|
|
21
|
+
const base = finite(m?.base_credits);
|
|
22
|
+
// landed_credits is the terminal TOTAL, not a surcharge on base_credits.
|
|
23
|
+
const fee = Math.max(base, finite(m?.landed_credits));
|
|
22
24
|
if (!Number.isSafeInteger(fee) || fee <= 0)
|
|
23
25
|
throw new Error("Invalid sendAndConfirm live fee");
|
|
24
26
|
return fee;
|
|
@@ -71,7 +73,6 @@ export class Transport {
|
|
|
71
73
|
mint,
|
|
72
74
|
slippage_bps: slippage,
|
|
73
75
|
priority_fee_micro_lamports: priority,
|
|
74
|
-
closeAccount: false,
|
|
75
76
|
});
|
|
76
77
|
const body = `{"jsonrpc":"2.0","id":1,"method":"executeSell","params":[${params.slice(0, -1)},"token_amount":${amount}}]}`;
|
|
77
78
|
const r = await fetch(this.origin + "/rpc", {
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodiusxyz/solana-agent-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Opt-in bounded nodius.xyz plugin for Solana Agent Kit",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=20.20.2"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@nodiusxyz/sdk": "0.2.15",
|
|
33
33
|
"@solana/web3.js": "1.99.0",
|
|
34
|
+
"@solana/spl-token": "0.4.15",
|
|
34
35
|
"bs58": "6.0.0",
|
|
35
36
|
"tweetnacl": "1.0.3",
|
|
36
37
|
"ws": "8.22.0",
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { SolanaAgentKit, Plugin } from "solana-agent-kit";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { run } from "./internal/app.js";
|
|
4
4
|
import { endpoint as validateEndpoint, type Args } from "./internal/config.js";
|
|
5
|
+
export { decodeBuilderUnsigned } from "./internal/safety.js";
|
|
5
6
|
export interface NodiusOptions {
|
|
6
7
|
apiKey?: string;
|
|
7
8
|
endpoint?: string;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { PublicKey } from "@solana/web3.js";
|
|
2
|
+
import {
|
|
3
|
+
unpackMint,
|
|
4
|
+
getAccountLenForMint,
|
|
5
|
+
getAccountLen,
|
|
6
|
+
getExtensionTypes,
|
|
7
|
+
getAccountTypeOfMintType,
|
|
8
|
+
ExtensionType,
|
|
9
|
+
} from "@solana/spl-token";
|
|
10
|
+
import type { Transport } from "./transport.js";
|
|
11
|
+
const PUMP = new PublicKey("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
|
12
|
+
const ATA = new PublicKey("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
|
13
|
+
const TOKEN = new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
14
|
+
const TOKEN22 = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb");
|
|
15
|
+
const WSOL = new PublicKey("So11111111111111111111111111111111111111112");
|
|
16
|
+
function lamports(n: number): bigint {
|
|
17
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
18
|
+
throw Error("Unsafe admission lamports");
|
|
19
|
+
return BigInt(n);
|
|
20
|
+
}
|
|
21
|
+
/** Conservative admission, not a guarantee against dynamic CPI/fee changes. */
|
|
22
|
+
export async function admitBuy(
|
|
23
|
+
t: Transport,
|
|
24
|
+
wallet: PublicKey,
|
|
25
|
+
mint: PublicKey,
|
|
26
|
+
amount: bigint,
|
|
27
|
+
slippage: number,
|
|
28
|
+
maxSol: bigint,
|
|
29
|
+
priority: bigint,
|
|
30
|
+
) {
|
|
31
|
+
const mintInfo = await t.connection.getAccountInfo(mint);
|
|
32
|
+
if (
|
|
33
|
+
!mintInfo ||
|
|
34
|
+
mintInfo.executable ||
|
|
35
|
+
![TOKEN, TOKEN22].some((p) => p.equals(mintInfo.owner))
|
|
36
|
+
)
|
|
37
|
+
throw Error("Buy admission mint account unavailable/unsupported");
|
|
38
|
+
let baseSize: number;
|
|
39
|
+
try {
|
|
40
|
+
const parsed = unpackMint(mint, mintInfo, mintInfo.owner);
|
|
41
|
+
if (!parsed.isInitialized) throw Error("uninitialized");
|
|
42
|
+
baseSize = getAccountLenForMint(parsed);
|
|
43
|
+
if (mintInfo.owner.equals(TOKEN22))
|
|
44
|
+
baseSize = Math.max(
|
|
45
|
+
baseSize,
|
|
46
|
+
getAccountLen([
|
|
47
|
+
...getExtensionTypes(parsed.tlvData)
|
|
48
|
+
.map(getAccountTypeOfMintType)
|
|
49
|
+
.filter((t) => t !== ExtensionType.Uninitialized),
|
|
50
|
+
ExtensionType.ImmutableOwner,
|
|
51
|
+
]),
|
|
52
|
+
);
|
|
53
|
+
} catch {
|
|
54
|
+
throw Error(
|
|
55
|
+
"Buy admission mint layout/extensions unsupported before builder",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const ata = (owner: PublicKey, m: PublicKey, token: PublicKey) =>
|
|
59
|
+
PublicKey.findProgramAddressSync(
|
|
60
|
+
[owner.toBuffer(), token.toBuffer(), m.toBuffer()],
|
|
61
|
+
ATA,
|
|
62
|
+
)[0];
|
|
63
|
+
const pda = (name: string, key: PublicKey) =>
|
|
64
|
+
PublicKey.findProgramAddressSync(
|
|
65
|
+
[Buffer.from(name), key.toBuffer()],
|
|
66
|
+
PUMP,
|
|
67
|
+
)[0];
|
|
68
|
+
const uva = pda("user_volume_accumulator", wallet);
|
|
69
|
+
const keys = [
|
|
70
|
+
ata(wallet, mint, mintInfo.owner),
|
|
71
|
+
ata(wallet, WSOL, TOKEN),
|
|
72
|
+
uva,
|
|
73
|
+
ata(uva, WSOL, TOKEN),
|
|
74
|
+
pda("bonding-curve", mint),
|
|
75
|
+
];
|
|
76
|
+
const accounts = await t.connection.getMultipleAccountsInfo(keys);
|
|
77
|
+
const curve = accounts[4];
|
|
78
|
+
if (
|
|
79
|
+
!curve ||
|
|
80
|
+
!curve.owner.equals(PUMP) ||
|
|
81
|
+
curve.data.length < 81 ||
|
|
82
|
+
!curve.data
|
|
83
|
+
.subarray(0, 8)
|
|
84
|
+
.equals(Buffer.from([23, 183, 248, 55, 96, 216, 172, 96]))
|
|
85
|
+
)
|
|
86
|
+
throw Error("Buy admission curve unavailable/unsupported before builder");
|
|
87
|
+
const quote = curve.data.subarray(83, 115);
|
|
88
|
+
const v2 =
|
|
89
|
+
curve.data.length >= 115 &&
|
|
90
|
+
(quote.equals(WSOL.toBuffer()) || quote.every((b) => b === 0));
|
|
91
|
+
// Current Pump IDL UserVolumeAccumulator serialized fields: discriminator8 +
|
|
92
|
+
// user32 + bool1 + four u64/i64 + bool1 + four cashback u64 = 106 bytes.
|
|
93
|
+
// Curve uses its live allocation; these builders do not emit extendAccount.
|
|
94
|
+
const sizes = [baseSize, 165, 106, 165, curve.data.length];
|
|
95
|
+
const names = [
|
|
96
|
+
"base_ata",
|
|
97
|
+
"wsol_ata",
|
|
98
|
+
"user_volume",
|
|
99
|
+
"user_volume_wsol_ata",
|
|
100
|
+
"curve",
|
|
101
|
+
];
|
|
102
|
+
let rent = 0n;
|
|
103
|
+
const rentAccounts: Record<string, string> = {};
|
|
104
|
+
for (let i = 0; i < keys.length; i++) {
|
|
105
|
+
if (!v2 && (i === 1 || i === 3)) continue;
|
|
106
|
+
const info = accounts[i];
|
|
107
|
+
const size = Math.max(sizes[i], info?.data.length ?? 0);
|
|
108
|
+
const minimum = lamports(
|
|
109
|
+
await t.connection.getMinimumBalanceForRentExemption(size),
|
|
110
|
+
);
|
|
111
|
+
const existing = info ? lamports(info.lamports) : 0n;
|
|
112
|
+
const deficit = minimum > existing ? minimum - existing : 0n;
|
|
113
|
+
rent += deficit;
|
|
114
|
+
rentAccounts[names[i]] = deficit.toString();
|
|
115
|
+
}
|
|
116
|
+
const trade = (amount * BigInt(10000 + slippage) + 9999n) / 10000n;
|
|
117
|
+
const required = trade + rent + 5000n + priority;
|
|
118
|
+
const breakdown = {
|
|
119
|
+
trade_bound_lamports: trade.toString(),
|
|
120
|
+
account_rent_lamports: rent.toString(),
|
|
121
|
+
rent_accounts: rentAccounts,
|
|
122
|
+
network_fee_bound_lamports: (5000n + priority).toString(),
|
|
123
|
+
required_lamports: required.toString(),
|
|
124
|
+
note: "Conservative pre-build admission only; dynamic fees/CPI and account races remain outside this partial policy.",
|
|
125
|
+
};
|
|
126
|
+
if (required > maxSol)
|
|
127
|
+
throw Error(
|
|
128
|
+
`Trade + conservative network/account rent exceeds max SOL before builder; required ${required} lamports (trade ${trade}, rent ${rent}, network ${5000n + priority})`,
|
|
129
|
+
);
|
|
130
|
+
return breakdown;
|
|
131
|
+
}
|
package/src/internal/app.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PublicKey, VersionedTransaction } from "@solana/web3.js";
|
|
2
2
|
import { Args, required, integer, rawAmount, solLamports } from "./config.js";
|
|
3
3
|
import { Transport, estimateBuy, finite, sendFee } from "./transport.js";
|
|
4
|
+
import { admitBuy } from "./admission.js";
|
|
4
5
|
import { monitor } from "./monitor.js";
|
|
5
6
|
import { Journal, fingerprint, outcome } from "./journal.js";
|
|
6
7
|
import {
|
|
@@ -54,7 +55,8 @@ async function submit(
|
|
|
54
55
|
},
|
|
55
56
|
]);
|
|
56
57
|
if (result?.signature === signature) {
|
|
57
|
-
const chainError =
|
|
58
|
+
const chainError =
|
|
59
|
+
result.transaction_error ?? result.chain_err ?? result.chain_error;
|
|
58
60
|
const landedError =
|
|
59
61
|
Number.isSafeInteger(result.slot) &&
|
|
60
62
|
result.slot >= 0 &&
|
|
@@ -65,6 +67,7 @@ async function submit(
|
|
|
65
67
|
result.error.length > 0 &&
|
|
66
68
|
!/timed?\s*out|timeout|without confirmation/i.test(result.error);
|
|
67
69
|
const definitive =
|
|
70
|
+
result.status === "failed" ||
|
|
68
71
|
chainError != null ||
|
|
69
72
|
result.definitive_error === true ||
|
|
70
73
|
landedError ||
|
|
@@ -72,14 +75,25 @@ async function submit(
|
|
|
72
75
|
/^(Transaction failed on-chain:|Transaction failed:)/.test(
|
|
73
76
|
result.error,
|
|
74
77
|
));
|
|
75
|
-
state =
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
state =
|
|
79
|
+
result.status === "unknown"
|
|
80
|
+
? "unknown"
|
|
81
|
+
: definitive
|
|
82
|
+
? "failed"
|
|
83
|
+
: result.status !== "unknown" && result.confirmed === true
|
|
84
|
+
? "confirmed"
|
|
85
|
+
: "unknown";
|
|
86
|
+
}
|
|
87
|
+
} catch (error: unknown) {
|
|
88
|
+
// A signature-bound server rejection is definitive even when carried by
|
|
89
|
+
// HTTP 502. Never infer failure from a timeout/message alone, and never
|
|
90
|
+
// persist the SDK error body (it may contain transport details).
|
|
91
|
+
if (error && typeof error === "object" && "details" in error) {
|
|
92
|
+
const details: any = error.details;
|
|
93
|
+
const rejection = details?.error?.data ?? details;
|
|
94
|
+
if (rejection?.signature === signature && rejection?.status === "failed")
|
|
95
|
+
state = "failed";
|
|
80
96
|
}
|
|
81
|
-
} catch {
|
|
82
|
-
/* HTTP/RPC/timeout errors are ambiguous; no rebroadcast or rebuild. */
|
|
83
97
|
}
|
|
84
98
|
await j.event(id, {
|
|
85
99
|
state,
|
|
@@ -160,7 +174,18 @@ export async function run(
|
|
|
160
174
|
if (a.command === "recent") {
|
|
161
175
|
const limit = integer(String(a.limit ?? "20"), 50);
|
|
162
176
|
if (!limit) throw new Error("Positive limit required");
|
|
163
|
-
|
|
177
|
+
const result: any = await t.sdk.signalsRecent(limit);
|
|
178
|
+
// Some server versions return a fixed preview window despite the query.
|
|
179
|
+
if (Array.isArray(result)) return result.slice(0, limit);
|
|
180
|
+
if (Array.isArray(result?.events)) {
|
|
181
|
+
const events = result.events.slice(0, limit);
|
|
182
|
+
return {
|
|
183
|
+
...result,
|
|
184
|
+
events,
|
|
185
|
+
...(typeof result.count === "number" ? { count: events.length } : {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
164
189
|
}
|
|
165
190
|
if (a.command === "inspect" || a.command === "migrate") {
|
|
166
191
|
requireAuth(env);
|
|
@@ -222,7 +247,12 @@ export async function run(
|
|
|
222
247
|
const bytes = decodeBase64(required(a, "signed-tx-base64"));
|
|
223
248
|
if (bytes.length > 1232)
|
|
224
249
|
throw new Error("Signed transaction exceeds packet limit");
|
|
225
|
-
|
|
250
|
+
let tx: VersionedTransaction;
|
|
251
|
+
try {
|
|
252
|
+
tx = VersionedTransaction.deserialize(bytes);
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error("Malformed signed transaction");
|
|
255
|
+
}
|
|
226
256
|
if (!Buffer.from(tx.serialize()).equals(bytes))
|
|
227
257
|
throw new Error("Noncanonical signed transaction");
|
|
228
258
|
const signature = checkSignature(tx);
|
|
@@ -286,7 +316,7 @@ export async function run(
|
|
|
286
316
|
);
|
|
287
317
|
const p = await t.pricing();
|
|
288
318
|
const sendCredits = sendFee(p);
|
|
289
|
-
const estimate = side === "buy" ? estimateBuy(p,
|
|
319
|
+
const estimate = side === "buy" ? estimateBuy(p, amount) : null;
|
|
290
320
|
const plan = {
|
|
291
321
|
state: "plan",
|
|
292
322
|
side,
|
|
@@ -330,6 +360,18 @@ export async function run(
|
|
|
330
360
|
(estimate ?? 0) + (a["build-only"] ? 0 : sendCredits)
|
|
331
361
|
)
|
|
332
362
|
throw new Error("Insufficient billing balance");
|
|
363
|
+
const admission =
|
|
364
|
+
side === "buy"
|
|
365
|
+
? await admitBuy(
|
|
366
|
+
t,
|
|
367
|
+
wallet,
|
|
368
|
+
mint,
|
|
369
|
+
amount,
|
|
370
|
+
slippage,
|
|
371
|
+
maxSol,
|
|
372
|
+
maxPriorityLamports,
|
|
373
|
+
)
|
|
374
|
+
: undefined;
|
|
333
375
|
const id = required(a, "operation-id");
|
|
334
376
|
const intent = {
|
|
335
377
|
origin,
|
|
@@ -420,6 +462,7 @@ export async function run(
|
|
|
420
462
|
operation_id: id,
|
|
421
463
|
fee_credits: charged,
|
|
422
464
|
unsigned_tx_base64: result.unsigned_tx_base64,
|
|
465
|
+
admission,
|
|
423
466
|
policy: envelope,
|
|
424
467
|
note: "Charged unsigned construction only. No signing/submission; instruction semantics are not authorized. No automatic resume/rebuild.",
|
|
425
468
|
};
|
package/src/internal/journal.ts
CHANGED
|
@@ -112,12 +112,17 @@ export class Journal {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
async read(id: string): Promise<any> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
let start: any;
|
|
116
|
+
try {
|
|
117
|
+
start = JSON.parse(
|
|
118
|
+
await readFile(
|
|
119
|
+
join(this.directory, "operation-" + fingerprint(id) + ".json"),
|
|
120
|
+
"utf8",
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
} catch {
|
|
124
|
+
throw new Error("Operation not found");
|
|
125
|
+
}
|
|
121
126
|
let text = "";
|
|
122
127
|
try {
|
|
123
128
|
text = await readFile(
|
package/src/internal/safety.ts
CHANGED
|
@@ -112,6 +112,8 @@ export function verifyUnsigned(
|
|
|
112
112
|
) {
|
|
113
113
|
return verifyV2(tx, wallet, p);
|
|
114
114
|
}
|
|
115
|
+
let sellToken: PublicKey | undefined, sellAta: PublicKey | undefined;
|
|
116
|
+
let closes = 0;
|
|
115
117
|
let trades = 0,
|
|
116
118
|
atas = 0,
|
|
117
119
|
tradeCost = 0n;
|
|
@@ -196,6 +198,32 @@ export function verifyUnsigned(
|
|
|
196
198
|
);
|
|
197
199
|
} else if (amount !== p.amountRaw || bound !== p.reportedCost)
|
|
198
200
|
throw new Error("Sell amount/min-output mismatch");
|
|
201
|
+
if (p.side === "sell") {
|
|
202
|
+
sellToken = ix.keys[9].pubkey;
|
|
203
|
+
sellAta = ix.keys[5].pubkey;
|
|
204
|
+
}
|
|
205
|
+
} else if (p.side === "sell" && [TOKEN, TOKEN2022].includes(program)) {
|
|
206
|
+
const expected = PublicKey.findProgramAddressSync(
|
|
207
|
+
[wallet.toBuffer(), ix.programId.toBuffer(), p.mint.toBuffer()],
|
|
208
|
+
new PublicKey(ATA),
|
|
209
|
+
)[0];
|
|
210
|
+
if (
|
|
211
|
+
++closes !== 1 ||
|
|
212
|
+
trades !== 1 ||
|
|
213
|
+
ix !== msg.instructions[msg.instructions.length - 1] ||
|
|
214
|
+
data.length !== 1 ||
|
|
215
|
+
data[0] !== 9 ||
|
|
216
|
+
ix.keys.length !== 3 ||
|
|
217
|
+
!sellToken?.equals(ix.programId) ||
|
|
218
|
+
!sellAta?.equals(expected) ||
|
|
219
|
+
!ix.keys[0].pubkey.equals(expected) ||
|
|
220
|
+
!ix.keys[0].isWritable ||
|
|
221
|
+
ix.keys[0].isSigner ||
|
|
222
|
+
ix.keys
|
|
223
|
+
.slice(1)
|
|
224
|
+
.some((k) => !k.pubkey.equals(wallet) || !k.isSigner || !k.isWritable)
|
|
225
|
+
)
|
|
226
|
+
throw Error("Sell close account policy mismatch");
|
|
199
227
|
} else throw new Error("Unexpected instruction program: " + program);
|
|
200
228
|
}
|
|
201
229
|
if (trades !== 1) throw new Error("Exactly one trade required");
|
|
@@ -29,7 +29,9 @@ export function estimateBuy(p: Pricing, lamports: bigint): number {
|
|
|
29
29
|
}
|
|
30
30
|
export function sendFee(p: Pricing): number {
|
|
31
31
|
const m = p.methods?.sendAndConfirm;
|
|
32
|
-
const
|
|
32
|
+
const base = finite(m?.base_credits);
|
|
33
|
+
// landed_credits is the terminal TOTAL, not a surcharge on base_credits.
|
|
34
|
+
const fee = Math.max(base, finite(m?.landed_credits));
|
|
33
35
|
if (!Number.isSafeInteger(fee) || fee <= 0)
|
|
34
36
|
throw new Error("Invalid sendAndConfirm live fee");
|
|
35
37
|
return fee;
|
|
@@ -87,7 +89,6 @@ export class Transport {
|
|
|
87
89
|
mint,
|
|
88
90
|
slippage_bps: slippage,
|
|
89
91
|
priority_fee_micro_lamports: priority,
|
|
90
|
-
closeAccount: false,
|
|
91
92
|
});
|
|
92
93
|
const body = `{"jsonrpc":"2.0","id":1,"method":"executeSell","params":[${params.slice(0, -1)},"token_amount":${amount}}]}`;
|
|
93
94
|
const r = await fetch(this.origin + "/rpc", {
|