solforge 0.2.7 → 0.2.8
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/package.json +1 -1
- package/scripts/decode-b58.ts +6 -0
- package/server/lib/instruction-parser.ts +26 -4
- package/server/lib/parsers/spl-associated-token-account.ts +44 -0
- package/server/lib/parsers/spl-token.ts +172 -0
- package/server/methods/transaction/send-transaction.ts +18 -6
- package/server/rpc-server.ts +32 -19
package/package.json
CHANGED
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
TransactionInstruction,
|
|
9
9
|
VoteInstruction,
|
|
10
10
|
} from "@solana/web3.js";
|
|
11
|
+
import { TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
12
|
+
import { tryParseSplToken } from "./parsers/spl-token";
|
|
13
|
+
import { tryParseAta } from "./parsers/spl-associated-token-account";
|
|
11
14
|
import { decodeBase58 as _decodeBase58 } from "./base58";
|
|
12
15
|
|
|
13
16
|
export type ParsedInstruction =
|
|
@@ -43,18 +46,38 @@ export function parseInstruction(
|
|
|
43
46
|
const pid = new PublicKey(programId);
|
|
44
47
|
const ix = makeIx(programId, accountKeys, accounts, dataBase58);
|
|
45
48
|
|
|
49
|
+
// SPL Token
|
|
50
|
+
if (pid.equals(TOKEN_PROGRAM_ID)) {
|
|
51
|
+
const parsed = tryParseSplToken(ix, programId, accountKeys, dataBase58);
|
|
52
|
+
if (parsed) return parsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Associated Token Account
|
|
56
|
+
if (pid.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) {
|
|
57
|
+
const parsed = tryParseAta(ix, programId);
|
|
58
|
+
if (parsed) return parsed;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
// System Program
|
|
47
62
|
if (pid.equals(SystemProgram.programId)) {
|
|
48
63
|
const t = SystemInstruction.decodeInstructionType(ix);
|
|
49
64
|
switch (t) {
|
|
50
65
|
case "Create": {
|
|
51
66
|
const p = SystemInstruction.decodeCreateAccount(ix);
|
|
67
|
+
const from = p.fromPubkey.toBase58();
|
|
68
|
+
const newAcc = p.newAccountPubkey.toBase58();
|
|
69
|
+
const owner = p.programId.toBase58();
|
|
52
70
|
return ok("system", programId, "createAccount", {
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
// Explorer-compatible field names
|
|
72
|
+
source: from,
|
|
73
|
+
newAccount: newAcc,
|
|
74
|
+
owner,
|
|
55
75
|
lamports: Number(p.lamports),
|
|
56
76
|
space: Number(p.space),
|
|
57
|
-
|
|
77
|
+
// Keep legacy aliases too
|
|
78
|
+
fromPubkey: from,
|
|
79
|
+
newAccountPubkey: newAcc,
|
|
80
|
+
programId: owner,
|
|
58
81
|
});
|
|
59
82
|
}
|
|
60
83
|
case "Transfer": {
|
|
@@ -239,4 +262,3 @@ export function parseInstruction(
|
|
|
239
262
|
};
|
|
240
263
|
}
|
|
241
264
|
}
|
|
242
|
-
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import { ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
|
|
3
|
+
|
|
4
|
+
// Keep shape compatible with instruction-parser
|
|
5
|
+
export type ParsedInstruction =
|
|
6
|
+
| { program: string; programId: string; parsed: { type: string; info: any } }
|
|
7
|
+
| { programId: string; accounts: string[]; data: string };
|
|
8
|
+
|
|
9
|
+
function ok(programId: string, type: string, info: any): ParsedInstruction {
|
|
10
|
+
return { program: "spl-associated-token-account", programId, parsed: { type, info } };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function asBase58(ix: TransactionInstruction, idx: number): string | undefined {
|
|
14
|
+
try {
|
|
15
|
+
return ix.keys[idx]?.pubkey?.toBase58();
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function tryParseAta(
|
|
22
|
+
ix: TransactionInstruction,
|
|
23
|
+
programIdStr: string,
|
|
24
|
+
): ParsedInstruction | null {
|
|
25
|
+
try {
|
|
26
|
+
if (!ix.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) return null;
|
|
27
|
+
const dataLen = ix.data?.length ?? 0;
|
|
28
|
+
// Both create (empty) and createIdempotent ([1]) map to type "create" in explorers
|
|
29
|
+
const type = "create";
|
|
30
|
+
// Expected keys: [payer, associatedToken, owner, mint, systemProgram, tokenProgram]
|
|
31
|
+
const info = {
|
|
32
|
+
source: asBase58(ix, 0),
|
|
33
|
+
account: asBase58(ix, 1),
|
|
34
|
+
wallet: asBase58(ix, 2),
|
|
35
|
+
mint: asBase58(ix, 3),
|
|
36
|
+
systemProgram: asBase58(ix, 4),
|
|
37
|
+
tokenProgram: asBase58(ix, 5),
|
|
38
|
+
};
|
|
39
|
+
return ok(programIdStr, type, info);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
|
2
|
+
import {
|
|
3
|
+
TOKEN_PROGRAM_ID,
|
|
4
|
+
decodeInstruction as splDecodeInstruction,
|
|
5
|
+
decodeMintToCheckedInstruction,
|
|
6
|
+
decodeTransferInstruction,
|
|
7
|
+
decodeTransferCheckedInstruction,
|
|
8
|
+
decodeInitializeAccount3Instruction,
|
|
9
|
+
decodeInitializeImmutableOwnerInstruction,
|
|
10
|
+
} from "@solana/spl-token";
|
|
11
|
+
import { u8 } from "@solana/buffer-layout";
|
|
12
|
+
|
|
13
|
+
// Keep shape compatible with instruction-parser
|
|
14
|
+
export type ParsedInstruction =
|
|
15
|
+
| { program: string; programId: string; parsed: { type: string; info: any } }
|
|
16
|
+
| { programId: string; accounts: string[]; data: string };
|
|
17
|
+
|
|
18
|
+
function ok(programId: string, type: string, info: any): ParsedInstruction {
|
|
19
|
+
return { program: "spl-token", programId, parsed: { type, info } };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function asBase58(pk: PublicKey | undefined): string | undefined {
|
|
23
|
+
try {
|
|
24
|
+
return pk ? pk.toBase58() : undefined;
|
|
25
|
+
} catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function tryParseSplToken(
|
|
31
|
+
ix: TransactionInstruction,
|
|
32
|
+
programIdStr: string,
|
|
33
|
+
accountKeys: string[],
|
|
34
|
+
dataBase58: string,
|
|
35
|
+
): ParsedInstruction | null {
|
|
36
|
+
try {
|
|
37
|
+
if (!ix.programId.equals(TOKEN_PROGRAM_ID)) return null;
|
|
38
|
+
|
|
39
|
+
// Probe with generic decoder (ignore result), then try specific decoders
|
|
40
|
+
try {
|
|
41
|
+
splDecodeInstruction(ix);
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
// MintToChecked
|
|
45
|
+
try {
|
|
46
|
+
const m = decodeMintToCheckedInstruction(ix);
|
|
47
|
+
const amount = m.data.amount;
|
|
48
|
+
const decimals = m.data.decimals;
|
|
49
|
+
const amtStr = typeof amount === "bigint" ? amount.toString() : String(amount);
|
|
50
|
+
const base = BigInt(10) ** BigInt(decimals);
|
|
51
|
+
const whole = BigInt(amtStr) / base;
|
|
52
|
+
const frac = BigInt(amtStr) % base;
|
|
53
|
+
const fracStr = frac.toString().padStart(decimals, "0").replace(/0+$/, "");
|
|
54
|
+
const uiStr = fracStr.length ? `${whole}.${fracStr}` : `${whole}`;
|
|
55
|
+
return ok(programIdStr, "mintToChecked", {
|
|
56
|
+
account: asBase58(m.keys.destination.pubkey),
|
|
57
|
+
mint: asBase58(m.keys.mint.pubkey),
|
|
58
|
+
mintAuthority: asBase58(m.keys.authority.pubkey),
|
|
59
|
+
tokenAmount: {
|
|
60
|
+
amount: amtStr,
|
|
61
|
+
decimals,
|
|
62
|
+
uiAmount: Number(uiStr),
|
|
63
|
+
uiAmountString: uiStr,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch {}
|
|
67
|
+
|
|
68
|
+
// Transfer / TransferChecked
|
|
69
|
+
try {
|
|
70
|
+
const t = decodeTransferInstruction(ix);
|
|
71
|
+
const amt = t.data.amount;
|
|
72
|
+
return ok(programIdStr, "transfer", {
|
|
73
|
+
amount: (typeof amt === "bigint" ? amt.toString() : String(amt)),
|
|
74
|
+
source: asBase58(t.keys.source.pubkey),
|
|
75
|
+
destination: asBase58(t.keys.destination.pubkey),
|
|
76
|
+
authority: asBase58(t.keys.owner.pubkey),
|
|
77
|
+
});
|
|
78
|
+
} catch {}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const t = decodeTransferCheckedInstruction(ix);
|
|
82
|
+
const amt = t.data.amount;
|
|
83
|
+
const decimals = t.data.decimals;
|
|
84
|
+
return ok(programIdStr, "transferChecked", {
|
|
85
|
+
tokenAmount: {
|
|
86
|
+
amount: (typeof amt === "bigint" ? amt.toString() : String(amt)),
|
|
87
|
+
decimals,
|
|
88
|
+
},
|
|
89
|
+
source: asBase58(t.keys.source.pubkey),
|
|
90
|
+
destination: asBase58(t.keys.destination.pubkey),
|
|
91
|
+
authority: asBase58(t.keys.owner.pubkey),
|
|
92
|
+
mint: asBase58(t.keys.mint.pubkey),
|
|
93
|
+
});
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
// InitializeAccount3
|
|
97
|
+
try {
|
|
98
|
+
const a = decodeInitializeAccount3Instruction(ix);
|
|
99
|
+
return ok(programIdStr, "initializeAccount3", {
|
|
100
|
+
account: asBase58(a.keys.account.pubkey),
|
|
101
|
+
mint: asBase58(a.keys.mint.pubkey),
|
|
102
|
+
owner: asBase58(a.data.owner),
|
|
103
|
+
});
|
|
104
|
+
} catch {}
|
|
105
|
+
|
|
106
|
+
// InitializeImmutableOwner
|
|
107
|
+
try {
|
|
108
|
+
const im = decodeInitializeImmutableOwnerInstruction(ix, TOKEN_PROGRAM_ID);
|
|
109
|
+
return ok(programIdStr, "initializeImmutableOwner", {
|
|
110
|
+
account: asBase58(im.keys.account.pubkey),
|
|
111
|
+
});
|
|
112
|
+
} catch {}
|
|
113
|
+
|
|
114
|
+
// GetAccountDataSize: decode extension types (u16 little-endian sequence)
|
|
115
|
+
try {
|
|
116
|
+
const bytes = Buffer.from(bs58decode(dataBase58));
|
|
117
|
+
const t = u8().decode(bytes);
|
|
118
|
+
// 21 corresponds to TokenInstruction.GetAccountDataSize
|
|
119
|
+
if (t === 21) {
|
|
120
|
+
const mint = asBase58(ix.keys[0]?.pubkey);
|
|
121
|
+
const extCodes: number[] = [];
|
|
122
|
+
for (let i = 1; i + 1 < bytes.length; i += 2) {
|
|
123
|
+
const code = bytes[i] | (bytes[i + 1] << 8);
|
|
124
|
+
extCodes.push(code);
|
|
125
|
+
}
|
|
126
|
+
const extMap: Record<number, string> = {
|
|
127
|
+
7: "immutableOwner",
|
|
128
|
+
8: "memoTransfer",
|
|
129
|
+
9: "nonTransferable",
|
|
130
|
+
12: "permanentDelegate",
|
|
131
|
+
14: "transferHook",
|
|
132
|
+
15: "transferHookAccount",
|
|
133
|
+
18: "metadataPointer",
|
|
134
|
+
19: "tokenMetadata",
|
|
135
|
+
20: "groupPointer",
|
|
136
|
+
21: "tokenGroup",
|
|
137
|
+
22: "groupMemberPointer",
|
|
138
|
+
23: "tokenGroupMember",
|
|
139
|
+
25: "scaledUiAmountConfig",
|
|
140
|
+
26: "pausableConfig",
|
|
141
|
+
27: "pausableAccount",
|
|
142
|
+
};
|
|
143
|
+
const extensionTypes = extCodes.map((c) => extMap[c] || String(c));
|
|
144
|
+
return ok(programIdStr, "getAccountDataSize", { mint, extensionTypes });
|
|
145
|
+
}
|
|
146
|
+
} catch {}
|
|
147
|
+
|
|
148
|
+
// Unknown SPL token instruction
|
|
149
|
+
return null;
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Local base58 decode to avoid importing from sibling file
|
|
156
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
157
|
+
const BASE = BigInt(ALPHABET.length);
|
|
158
|
+
function bs58decode(str: string): Uint8Array {
|
|
159
|
+
let num = 0n;
|
|
160
|
+
for (const char of str) {
|
|
161
|
+
const index = ALPHABET.indexOf(char);
|
|
162
|
+
if (index === -1) throw new Error("Invalid base58 character");
|
|
163
|
+
num = num * BASE + BigInt(index);
|
|
164
|
+
}
|
|
165
|
+
const bytes: number[] = [];
|
|
166
|
+
while (num > 0n) {
|
|
167
|
+
bytes.unshift(Number(num % 256n));
|
|
168
|
+
num = num / 256n;
|
|
169
|
+
}
|
|
170
|
+
for (let i = 0; i < str.length && str[i] === "1"; i++) bytes.unshift(0);
|
|
171
|
+
return new Uint8Array(bytes.length > 0 ? bytes : [0]);
|
|
172
|
+
}
|
|
@@ -86,14 +86,15 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
|
|
|
86
86
|
TOKEN_2022_PROGRAM_ID.toBase58(),
|
|
87
87
|
]);
|
|
88
88
|
const tokenAccountSet = new Set<string>();
|
|
89
|
+
// 1) Collect from compiled ixs (best-effort)
|
|
89
90
|
for (const ci of compiled) {
|
|
90
91
|
try {
|
|
91
|
-
const pid = staticKeys[ci.programIdIndex]?.toBase58();
|
|
92
|
+
const pid = staticKeys[(ci as any).programIdIndex]?.toBase58();
|
|
92
93
|
if (!pid || !tokenProgramIds.has(pid)) continue;
|
|
93
|
-
const accIdxs: number[] = Array.isArray(ci.accountKeyIndexes)
|
|
94
|
-
? ci.accountKeyIndexes
|
|
95
|
-
: Array.isArray(ci.accounts)
|
|
96
|
-
? ci.accounts
|
|
94
|
+
const accIdxs: number[] = Array.isArray((ci as any).accountKeyIndexes)
|
|
95
|
+
? (ci as any).accountKeyIndexes
|
|
96
|
+
: Array.isArray((ci as any).accounts)
|
|
97
|
+
? (ci as any).accounts
|
|
97
98
|
: [];
|
|
98
99
|
for (const ix of accIdxs) {
|
|
99
100
|
const addr = staticKeys[ix]?.toBase58();
|
|
@@ -101,6 +102,17 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
|
|
|
101
102
|
}
|
|
102
103
|
} catch {}
|
|
103
104
|
}
|
|
105
|
+
// 2) Also collect from all static keys that are SPL token accounts pre-send
|
|
106
|
+
for (const pk of staticKeys) {
|
|
107
|
+
try {
|
|
108
|
+
const acc = context.svm.getAccount(pk);
|
|
109
|
+
if (!acc) continue;
|
|
110
|
+
const ownerStr = new PublicKey(acc.owner).toBase58();
|
|
111
|
+
if (tokenProgramIds.has(ownerStr) && (acc.data?.length ?? 0) >= ACCOUNT_SIZE) {
|
|
112
|
+
tokenAccountSet.add(pk.toBase58());
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
104
116
|
// Pre token balances
|
|
105
117
|
const preTokenBalances: unknown[] = [];
|
|
106
118
|
const ataToInfo = new Map<
|
|
@@ -215,7 +227,7 @@ export const sendTransaction: RpcMethodHandler = (id, params, context) => {
|
|
|
215
227
|
);
|
|
216
228
|
}
|
|
217
229
|
} catch {}
|
|
218
|
-
// Post token balances
|
|
230
|
+
// Post token balances (scan token accounts among static keys)
|
|
219
231
|
const postTokenBalances: unknown[] = [];
|
|
220
232
|
for (const addr of tokenAccountSet) {
|
|
221
233
|
try {
|
package/server/rpc-server.ts
CHANGED
|
@@ -284,25 +284,38 @@ export class LiteSVMRpcServer {
|
|
|
284
284
|
.catch(() => {});
|
|
285
285
|
|
|
286
286
|
// Upsert account snapshots for static keys
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
287
|
+
const snapshots = keys
|
|
288
|
+
.map((addr) => {
|
|
289
|
+
try {
|
|
290
|
+
const acc = this.svm.getAccount(new PublicKey(addr));
|
|
291
|
+
if (!acc) return null;
|
|
292
|
+
const ownerStr = new PublicKey(acc.owner).toBase58();
|
|
293
|
+
let dataBase64: string | undefined;
|
|
294
|
+
// Store raw data for SPL Token accounts to reflect balance changes
|
|
295
|
+
try {
|
|
296
|
+
if (
|
|
297
|
+
ownerStr === "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ||
|
|
298
|
+
ownerStr === "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
299
|
+
) {
|
|
300
|
+
if (acc.data && acc.data.length > 0) {
|
|
301
|
+
dataBase64 = Buffer.from(acc.data).toString("base64");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
} catch {}
|
|
305
|
+
return {
|
|
306
|
+
address: addr,
|
|
307
|
+
lamports: Number(acc.lamports || 0n),
|
|
308
|
+
ownerProgram: ownerStr,
|
|
309
|
+
executable: !!acc.executable,
|
|
310
|
+
rentEpoch: Number(acc.rentEpoch || 0),
|
|
311
|
+
dataLen: acc.data?.length ?? 0,
|
|
312
|
+
dataBase64,
|
|
313
|
+
lastSlot: Number(this.slot),
|
|
314
|
+
};
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
})
|
|
306
319
|
.filter(Boolean) as import("../src/db/tx-store").AccountSnapshot[];
|
|
307
320
|
if (snapshots.length > 0)
|
|
308
321
|
this.store.upsertAccounts(snapshots).catch(() => {});
|