@helium/sus 0.6.2-next.34
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 +203 -0
- package/README.md +462 -0
- package/lib/cjs/index.js +699 -0
- package/lib/cjs/index.js.map +1 -0
- package/lib/esm/src/index.js +670 -0
- package/lib/esm/src/index.js.map +1 -0
- package/lib/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/lib/types/src/index.d.ts +126 -0
- package/lib/types/src/index.d.ts.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
import { BN, BorshAccountsCoder, BorshInstructionCoder, } from "@coral-xyz/anchor";
|
|
2
|
+
import { decodeIdlAccount } from "@coral-xyz/anchor/dist/cjs/idl";
|
|
3
|
+
import { utf8 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
|
|
4
|
+
import { getLeafAssetId } from "@metaplex-foundation/mpl-bubblegum";
|
|
5
|
+
import { PROGRAM_ID as MPL_PID, Metadata, } from "@metaplex-foundation/mpl-token-metadata";
|
|
6
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
7
|
+
import { NATIVE_MINT, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, unpackAccount, unpackMint, unpackMultisig, } from "@solana/spl-token";
|
|
8
|
+
import { PublicKey, SystemProgram, VersionedTransaction, } from "@solana/web3.js";
|
|
9
|
+
import axios from "axios";
|
|
10
|
+
import { inflate } from "pako";
|
|
11
|
+
const BUBBLEGUM_PROGRAM_ID = new PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY");
|
|
12
|
+
const ACCOUNT_COMPRESSION_PROGRAM_ID = new PublicKey("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK");
|
|
13
|
+
async function getAccountKeys({ connection, transaction, }) {
|
|
14
|
+
const addressLookupTableAccounts = [];
|
|
15
|
+
const { addressTableLookups } = transaction.message;
|
|
16
|
+
if (addressTableLookups.length > 0) {
|
|
17
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
18
|
+
for (const addressTableLookup of addressTableLookups) {
|
|
19
|
+
// eslint-disable-next-line no-await-in-loop
|
|
20
|
+
const result = await connection?.getAddressLookupTable(addressTableLookup.accountKey);
|
|
21
|
+
if (result?.value) {
|
|
22
|
+
addressLookupTableAccounts.push(result?.value);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return transaction.message.getAccountKeys({
|
|
27
|
+
addressLookupTableAccounts,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async function getMultipleAccounts({ connection, keys, }) {
|
|
31
|
+
const batchSize = 100;
|
|
32
|
+
const batches = Math.ceil(keys.length / batchSize);
|
|
33
|
+
const results = [];
|
|
34
|
+
for (let i = 0; i < batches; i++) {
|
|
35
|
+
const batchKeys = keys.slice(i * batchSize, (i + 1) * batchSize);
|
|
36
|
+
const batchResults = await connection.getMultipleAccountsInfo(batchKeys);
|
|
37
|
+
results.push(...batchResults);
|
|
38
|
+
}
|
|
39
|
+
return results;
|
|
40
|
+
}
|
|
41
|
+
export async function sus({ connection, wallet, serializedTransactions,
|
|
42
|
+
/// CNFT specific params
|
|
43
|
+
checkCNfts = false, extraSearchAssetParams, cNfts,
|
|
44
|
+
// Cluster for explorer
|
|
45
|
+
cluster = "mainnet-beta", accountBlacklist, }) {
|
|
46
|
+
let assets = cNfts;
|
|
47
|
+
if (checkCNfts) {
|
|
48
|
+
if (!assets) {
|
|
49
|
+
const assetsResponse = await axios.post(connection.rpcEndpoint, {
|
|
50
|
+
jsonrpc: "2.0",
|
|
51
|
+
method: "searchAssets",
|
|
52
|
+
id: "get-assets-op-1",
|
|
53
|
+
params: {
|
|
54
|
+
page: 1,
|
|
55
|
+
// limit to checking 200 assets
|
|
56
|
+
limit: 200,
|
|
57
|
+
compressed: true,
|
|
58
|
+
ownerAddress: wallet.toBase58(),
|
|
59
|
+
...extraSearchAssetParams,
|
|
60
|
+
},
|
|
61
|
+
headers: {
|
|
62
|
+
"Cache-Control": "no-cache",
|
|
63
|
+
Pragma: "no-cache",
|
|
64
|
+
Expires: "0",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
assets = assetsResponse.data.result?.items;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const warningsByTx = serializedTransactions.map(() => []);
|
|
71
|
+
const transactions = serializedTransactions.map((t) => VersionedTransaction.deserialize(t));
|
|
72
|
+
const accountKeysByTx = await Promise.all(transactions.map((transaction) => getAccountKeys({ connection, transaction })));
|
|
73
|
+
const simulationAccountsByTx = accountKeysByTx.map((accountKeys, txIndex) => [
|
|
74
|
+
...new Set(accountKeys.staticAccountKeys
|
|
75
|
+
.filter((_, index) => transactions[txIndex].message.isAccountWritable(index))
|
|
76
|
+
.concat(accountKeys.accountKeysFromLookups
|
|
77
|
+
? // Only writable accounts will contribute to balance changes
|
|
78
|
+
accountKeys.accountKeysFromLookups.writable
|
|
79
|
+
: [])),
|
|
80
|
+
].filter((a) => !accountBlacklist?.has(a.toBase58())));
|
|
81
|
+
const allAccounts = [...new Set(simulationAccountsByTx.flat())];
|
|
82
|
+
const fetchedAccounts = await getMultipleAccounts({
|
|
83
|
+
connection,
|
|
84
|
+
keys: allAccounts,
|
|
85
|
+
});
|
|
86
|
+
const fetchedAccountsByAddr = fetchedAccounts.reduce((acc, account, index) => {
|
|
87
|
+
acc[allAccounts[index].toBase58()] = account;
|
|
88
|
+
return acc;
|
|
89
|
+
}, {});
|
|
90
|
+
let { blockhash } = await connection?.getLatestBlockhash("finalized");
|
|
91
|
+
const simulatedTxs = [];
|
|
92
|
+
// Linearly simulate txs so as not to hit rate limits
|
|
93
|
+
for (const [index, transaction] of transactions.entries()) {
|
|
94
|
+
let simulatedTxn = null;
|
|
95
|
+
let tries = 0;
|
|
96
|
+
// Retry until we stop getting blockhashNotFound
|
|
97
|
+
blockhashLoop: while (true) {
|
|
98
|
+
transaction.message.recentBlockhash = blockhash;
|
|
99
|
+
simulatedTxn = await connection.simulateTransaction(transaction, {
|
|
100
|
+
accounts: {
|
|
101
|
+
encoding: "base64",
|
|
102
|
+
addresses: simulationAccountsByTx[index]?.map((account) => account.toBase58()) || [],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
if (isBlockhashNotFound(simulatedTxn)) {
|
|
106
|
+
({ blockhash } = await connection?.getLatestBlockhash("finalized"));
|
|
107
|
+
tries++;
|
|
108
|
+
if (tries >= 5) {
|
|
109
|
+
simulatedTxs.push(simulatedTxn);
|
|
110
|
+
break blockhashLoop;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
simulatedTxs.push(simulatedTxn);
|
|
115
|
+
break blockhashLoop;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const fullAccountsByTxn = simulationAccountsByTx.map((simulationAccounts, transactionIndex) => {
|
|
120
|
+
const simulatedTxn = simulatedTxs[transactionIndex];
|
|
121
|
+
return simulationAccounts.map((account, index) => {
|
|
122
|
+
const post = simulatedTxn.value.accounts?.[index];
|
|
123
|
+
return {
|
|
124
|
+
address: account,
|
|
125
|
+
post: post
|
|
126
|
+
? {
|
|
127
|
+
...post,
|
|
128
|
+
owner: new PublicKey(post.owner),
|
|
129
|
+
data: Buffer.from(post.data[0], post.data[1]),
|
|
130
|
+
}
|
|
131
|
+
: undefined,
|
|
132
|
+
pre: fetchedAccountsByAddr[account.toBase58()],
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
const instructionProgramIds = transactions
|
|
137
|
+
.flatMap((transaction, index) => transaction.message.compiledInstructions.map((ix) => accountKeysByTx[index].get(ix.programIdIndex) || null))
|
|
138
|
+
.filter(truthy);
|
|
139
|
+
const programKeys = fullAccountsByTxn
|
|
140
|
+
.flat()
|
|
141
|
+
.map((acc) => acc?.pre?.owner || (acc.post ? new PublicKey(acc.post.owner) : null))
|
|
142
|
+
.concat(...instructionProgramIds)
|
|
143
|
+
.filter(truthy);
|
|
144
|
+
const idlKeys = programKeys.map(getIdlKey);
|
|
145
|
+
const idls = (await getMultipleAccounts({ connection, keys: idlKeys }))
|
|
146
|
+
.map((acc, index) => {
|
|
147
|
+
if (acc) {
|
|
148
|
+
return {
|
|
149
|
+
program: programKeys[index],
|
|
150
|
+
idl: decodeIdl(acc),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
.filter(truthy)
|
|
155
|
+
.reduce((acc, { program, idl }) => {
|
|
156
|
+
if (idl) {
|
|
157
|
+
acc[program.toBase58()] = idl;
|
|
158
|
+
}
|
|
159
|
+
return acc;
|
|
160
|
+
}, {});
|
|
161
|
+
const writableAccountsByTxRaw = fullAccountsByTxn.map((accounts) => getDetailedWritableAccountsWithoutTM({
|
|
162
|
+
accounts,
|
|
163
|
+
idls,
|
|
164
|
+
}));
|
|
165
|
+
const tokens = [
|
|
166
|
+
...new Set(writableAccountsByTxRaw.flatMap((w) => w.tokens).map((t) => t.toBase58())),
|
|
167
|
+
].map((t) => new PublicKey(t));
|
|
168
|
+
const metadatas = (await fetchMetadatas(connection, tokens)).reduce((acc, m, index) => {
|
|
169
|
+
if (m) {
|
|
170
|
+
acc[tokens[index].toBase58()] = m;
|
|
171
|
+
}
|
|
172
|
+
return acc;
|
|
173
|
+
}, {});
|
|
174
|
+
const writableAccountsByTx = writableAccountsByTxRaw.map(({ withoutMetadata }, index) => {
|
|
175
|
+
const writableAccounts = withoutMetadata.map((acc) => {
|
|
176
|
+
let name = acc.name;
|
|
177
|
+
let metadata;
|
|
178
|
+
// Attempt to take last known type
|
|
179
|
+
const type = (acc.pre.type !== "Unknown" && acc.pre.type) ||
|
|
180
|
+
(acc.post.type !== "Unknown" && acc.post.type) ||
|
|
181
|
+
"Unknown";
|
|
182
|
+
// If token, get the name based on the metadata
|
|
183
|
+
if (type === "Mint") {
|
|
184
|
+
metadata = metadatas[acc.address.toBase58()];
|
|
185
|
+
if (metadata) {
|
|
186
|
+
name = `${metadata.symbol} Mint`;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
name = `Unknown Mint`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (type === "TokenAccount") {
|
|
193
|
+
metadata =
|
|
194
|
+
metadatas[(acc.pre.parsed?.mint || acc.post.parsed?.mint).toBase58()];
|
|
195
|
+
if (metadata) {
|
|
196
|
+
name = `${metadata.symbol} Token Account`;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
name = `Unknown Token Account`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
...acc,
|
|
204
|
+
name,
|
|
205
|
+
metadata,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
writableAccounts.forEach((acc) => {
|
|
209
|
+
if (!acc.changedInSimulation) {
|
|
210
|
+
warningsByTx[index].push({
|
|
211
|
+
severity: "warning",
|
|
212
|
+
shortMessage: "Unchanged",
|
|
213
|
+
message: "Account did not change in simulation but was labeled as writable. The behavior of the transaction may differ from the simulation.",
|
|
214
|
+
account: acc.address,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// Catch malicious sol ownwer change
|
|
218
|
+
const sysProg = new PublicKey("11111111111111111111111111111111");
|
|
219
|
+
const postOwner = acc.post.account?.owner || sysProg;
|
|
220
|
+
const preOwner = acc.pre.account?.owner || sysProg;
|
|
221
|
+
const accountOwnerChanged = !preOwner.equals(postOwner);
|
|
222
|
+
if (acc.name === "Native SOL Account" && acc.owner && acc.owner.equals(wallet) && accountOwnerChanged) {
|
|
223
|
+
warningsByTx[index].push({
|
|
224
|
+
severity: "critical",
|
|
225
|
+
shortMessage: "Owner Changed",
|
|
226
|
+
message: `The owner of ${acc.name} changed to ${acc.post.parsed?.owner?.toBase58()}. This gives that wallet full custody of these tokens.`,
|
|
227
|
+
account: acc.address,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
return writableAccounts;
|
|
232
|
+
});
|
|
233
|
+
const instructionsByTx = await Promise.all(transactions.map(async (transaction, index) => {
|
|
234
|
+
const instructions = parseInstructions({
|
|
235
|
+
idls,
|
|
236
|
+
instructions: transaction.message.compiledInstructions.map((ix) => ({
|
|
237
|
+
data: Buffer.from(ix.data),
|
|
238
|
+
programId: accountKeysByTx[index].get(ix.programIdIndex),
|
|
239
|
+
accounts: ix.accountKeyIndexes.map((ix) => ({
|
|
240
|
+
pubkey: accountKeysByTx[index].get(ix),
|
|
241
|
+
isSigner: transaction.message.isAccountSigner(ix),
|
|
242
|
+
isWritable: transaction.message.isAccountWritable(ix),
|
|
243
|
+
})),
|
|
244
|
+
})),
|
|
245
|
+
});
|
|
246
|
+
if (instructions.some((ix) => ix.parsed?.name === "ledgerTransferPositionV0")) {
|
|
247
|
+
warningsByTx[index].push({
|
|
248
|
+
severity: "critical",
|
|
249
|
+
shortMessage: "Theft of Locked HNT",
|
|
250
|
+
message: "This transaction is attempting to steal your locked HNT positions",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (instructions.some((ix) => ix.parsed?.name === "updateDestinationV0" ||
|
|
254
|
+
ix.parsed?.name === "updateCompressionDestinationV0")) {
|
|
255
|
+
warningsByTx[index].push({
|
|
256
|
+
severity: "warning",
|
|
257
|
+
shortMessage: "Rewards Destination Changed",
|
|
258
|
+
message: "This transaction will change the destination wallet of your mining rewards",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if ((await Promise.all(instructions.map((ix) => isBurnHotspot(connection, ix, assets)))).some((isBurn) => isBurn)) {
|
|
262
|
+
warningsByTx[index].push({
|
|
263
|
+
severity: "critical",
|
|
264
|
+
shortMessage: "Hotspot Destroyed",
|
|
265
|
+
message: "This transaction will brick your Hotspot!",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return instructions;
|
|
269
|
+
}));
|
|
270
|
+
const results = [];
|
|
271
|
+
for (const [index, simulatedTxn] of simulatedTxs.entries()) {
|
|
272
|
+
const warnings = warningsByTx[index];
|
|
273
|
+
const instructions = instructionsByTx[index];
|
|
274
|
+
const writableAccounts = writableAccountsByTx[index];
|
|
275
|
+
const transaction = transactions[index];
|
|
276
|
+
const message = Buffer.from(transaction.message.serialize()).toString("base64");
|
|
277
|
+
const explorerLink = `https://explorer.solana.com/tx/inspector?cluster=${cluster}&message=${encodeURIComponent(message)}`;
|
|
278
|
+
const logs = simulatedTxn.value.logs;
|
|
279
|
+
let result;
|
|
280
|
+
if (simulatedTxn?.value.err) {
|
|
281
|
+
warnings.push({
|
|
282
|
+
severity: "critical",
|
|
283
|
+
shortMessage: "Simulation Failed",
|
|
284
|
+
message: "Transaction failed in simulation",
|
|
285
|
+
});
|
|
286
|
+
result = {
|
|
287
|
+
instructions,
|
|
288
|
+
error: simulatedTxn.value.err,
|
|
289
|
+
logs,
|
|
290
|
+
solFee: 0,
|
|
291
|
+
priorityFee: 0,
|
|
292
|
+
insufficientFunds: isInsufficientBal(simulatedTxn?.value.err),
|
|
293
|
+
explorerLink,
|
|
294
|
+
balanceChanges: [],
|
|
295
|
+
possibleCNftChanges: [],
|
|
296
|
+
writableAccounts,
|
|
297
|
+
rawSimulation: simulatedTxn.value,
|
|
298
|
+
warnings,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
let solFee = (transaction?.signatures.length || 1) * 5000;
|
|
303
|
+
let priorityFee = 0;
|
|
304
|
+
const fee = (await connection?.getFeeForMessage(transaction.message, "confirmed"))
|
|
305
|
+
.value || solFee;
|
|
306
|
+
priorityFee = fee - solFee;
|
|
307
|
+
const balanceChanges = writableAccounts
|
|
308
|
+
.map((acc) => {
|
|
309
|
+
const type = (acc.pre.type !== "Unknown" && acc.pre.type) ||
|
|
310
|
+
(acc.post.type !== "Unknown" && acc.post.type);
|
|
311
|
+
switch (type) {
|
|
312
|
+
case "TokenAccount":
|
|
313
|
+
if (acc.post.parsed?.delegate && !acc.pre.parsed?.delegate) {
|
|
314
|
+
warnings.push({
|
|
315
|
+
severity: "warning",
|
|
316
|
+
shortMessage: "Withdraw Authority Given",
|
|
317
|
+
message: `Delegation was taken on ${acc.name}. This gives permission to withdraw tokens without the owner's permission.`,
|
|
318
|
+
account: acc.address,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (acc.post.parsed &&
|
|
322
|
+
acc.pre.parsed &&
|
|
323
|
+
!acc.post.parsed.owner.equals(acc.pre.parsed.owner)) {
|
|
324
|
+
warnings.push({
|
|
325
|
+
severity: "warning",
|
|
326
|
+
shortMessage: "Owner Changed",
|
|
327
|
+
message: `The owner of ${acc.name} changed to ${acc.post.parsed?.owner?.toBase58()}. This gives that wallet full custody of these tokens.`,
|
|
328
|
+
account: acc.address,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
owner: acc.post.parsed?.owner || acc.pre.parsed?.owner,
|
|
333
|
+
address: acc.address,
|
|
334
|
+
amount: (acc.post.parsed?.amount || BigInt(0)) -
|
|
335
|
+
(acc.pre.parsed?.amount || BigInt(0)),
|
|
336
|
+
metadata: acc.metadata,
|
|
337
|
+
};
|
|
338
|
+
case "NativeAccount":
|
|
339
|
+
return {
|
|
340
|
+
owner: acc.address,
|
|
341
|
+
address: acc.address,
|
|
342
|
+
amount: BigInt((acc.post.account?.lamports || 0) -
|
|
343
|
+
(acc.pre.account?.lamports || 0)),
|
|
344
|
+
metadata: {
|
|
345
|
+
mint: NATIVE_MINT,
|
|
346
|
+
decimals: 9,
|
|
347
|
+
name: "SOL",
|
|
348
|
+
symbol: "SOL",
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
default:
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
})
|
|
355
|
+
.filter(truthy);
|
|
356
|
+
// Don't count new mints being created, as this might flag on candymachine txs
|
|
357
|
+
if (balanceChanges.filter((b) => b.owner.equals(wallet) && fetchedAccountsByAddr[b.address.toBase58()]).length >= 3) {
|
|
358
|
+
warnings.push({
|
|
359
|
+
severity: "warning",
|
|
360
|
+
shortMessage: "3+ Token Accounts",
|
|
361
|
+
message: "3 or more token accounts are impacted by this transaction. Any token account listed as writable can be emptied by the transaction, is this okay?",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
let possibleCNftChanges = [];
|
|
365
|
+
if (checkCNfts) {
|
|
366
|
+
const possibleMerkles = new Set(writableAccounts
|
|
367
|
+
.filter((acc) => acc.name === "Merkle Tree")
|
|
368
|
+
.map((a) => a.address.toBase58()));
|
|
369
|
+
possibleCNftChanges = (assets || []).filter((item) => item.compression.tree && possibleMerkles.has(item.compression.tree));
|
|
370
|
+
}
|
|
371
|
+
result = {
|
|
372
|
+
instructions,
|
|
373
|
+
logs,
|
|
374
|
+
solFee,
|
|
375
|
+
priorityFee,
|
|
376
|
+
insufficientFunds: false,
|
|
377
|
+
explorerLink,
|
|
378
|
+
balanceChanges,
|
|
379
|
+
possibleCNftChanges,
|
|
380
|
+
writableAccounts,
|
|
381
|
+
rawSimulation: simulatedTxn.value,
|
|
382
|
+
warnings,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
results.push(result);
|
|
386
|
+
}
|
|
387
|
+
return results;
|
|
388
|
+
}
|
|
389
|
+
function isBlockhashNotFound(simulatedTxn) {
|
|
390
|
+
return simulatedTxn?.value.err?.toString() === "BlockhashNotFound";
|
|
391
|
+
}
|
|
392
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
export function isInsufficientBal(e) {
|
|
394
|
+
return (e.toString().includes("Insufficient Balance") ||
|
|
395
|
+
e.toString().includes('"Custom":1') ||
|
|
396
|
+
e.InstructionError?.[1]?.Custom === 1);
|
|
397
|
+
}
|
|
398
|
+
function parseInstructions({ idls, instructions, }) {
|
|
399
|
+
return instructions.map((ix) => {
|
|
400
|
+
const idl = idls[ix.programId.toBase58()];
|
|
401
|
+
if (idl) {
|
|
402
|
+
try {
|
|
403
|
+
const coder = new BorshInstructionCoder(idl);
|
|
404
|
+
const parsed = coder.decode(ix.data, "base58");
|
|
405
|
+
if (parsed) {
|
|
406
|
+
const formatted = coder.format(parsed, ix.accounts);
|
|
407
|
+
if (formatted) {
|
|
408
|
+
return {
|
|
409
|
+
parsed: {
|
|
410
|
+
name: parsed.name,
|
|
411
|
+
programName: idl.name,
|
|
412
|
+
data: parsed.data,
|
|
413
|
+
accounts: formatted.accounts,
|
|
414
|
+
},
|
|
415
|
+
raw: ix,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch (e) {
|
|
421
|
+
// Ignore, not a valid ix
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { raw: ix };
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
function getIdlKey(programId) {
|
|
428
|
+
const base = PublicKey.findProgramAddressSync([], programId)[0];
|
|
429
|
+
const buffer = Buffer.concat([
|
|
430
|
+
base.toBuffer(),
|
|
431
|
+
Buffer.from("anchor:idl"),
|
|
432
|
+
programId.toBuffer(),
|
|
433
|
+
]);
|
|
434
|
+
const publicKeyBytes = sha256(buffer);
|
|
435
|
+
return new PublicKey(publicKeyBytes);
|
|
436
|
+
}
|
|
437
|
+
function decodeIdl(account) {
|
|
438
|
+
try {
|
|
439
|
+
const idlData = decodeIdlAccount(Buffer.from(account.data.subarray(8)));
|
|
440
|
+
const inflatedIdl = inflate(idlData.data);
|
|
441
|
+
return JSON.parse(utf8.decode(inflatedIdl));
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
// Ignore, not a valid IDL
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
export function getDetailedWritableAccountsWithoutTM({ accounts, idls, }) {
|
|
448
|
+
const uniqueTokens = new Set();
|
|
449
|
+
const withoutMetadata = accounts.map(({ address, pre, post }) => {
|
|
450
|
+
let name = "Unknown";
|
|
451
|
+
let type = "Unknown";
|
|
452
|
+
let preParsed = null;
|
|
453
|
+
let postParsed = null;
|
|
454
|
+
let accountOwner = undefined;
|
|
455
|
+
const postData = post && post.data;
|
|
456
|
+
const postAccount = post && postData
|
|
457
|
+
? {
|
|
458
|
+
executable: post.executable,
|
|
459
|
+
owner: new PublicKey(post.owner),
|
|
460
|
+
lamports: post.lamports,
|
|
461
|
+
data: postData,
|
|
462
|
+
rentEpoch: post.rentEpoch,
|
|
463
|
+
}
|
|
464
|
+
: null;
|
|
465
|
+
const owner = pre?.owner || (post ? new PublicKey(post.owner) : null);
|
|
466
|
+
switch (owner?.toBase58()) {
|
|
467
|
+
case ACCOUNT_COMPRESSION_PROGRAM_ID.toBase58():
|
|
468
|
+
name = "Merkle Tree";
|
|
469
|
+
type = "MerkleTree";
|
|
470
|
+
break;
|
|
471
|
+
case SystemProgram.programId.toBase58():
|
|
472
|
+
name = "Native SOL Account";
|
|
473
|
+
type = "NativeAccount";
|
|
474
|
+
accountOwner = address;
|
|
475
|
+
break;
|
|
476
|
+
case TOKEN_2022_PROGRAM_ID.toBase58():
|
|
477
|
+
({ parsed: preParsed, type } = decodeTokenStruct(address, pre, TOKEN_2022_PROGRAM_ID) || { type });
|
|
478
|
+
({ parsed: postParsed, type } = decodeTokenStruct(address, postAccount, TOKEN_2022_PROGRAM_ID) || { type });
|
|
479
|
+
break;
|
|
480
|
+
case TOKEN_PROGRAM_ID.toBase58():
|
|
481
|
+
({ parsed: preParsed, type } = decodeTokenStruct(address, pre, TOKEN_PROGRAM_ID) || { type });
|
|
482
|
+
({ parsed: postParsed, type } = decodeTokenStruct(address, postAccount, TOKEN_PROGRAM_ID) || { type });
|
|
483
|
+
break;
|
|
484
|
+
default:
|
|
485
|
+
if (owner) {
|
|
486
|
+
const idl = idls[owner.toBase58()];
|
|
487
|
+
if (idl) {
|
|
488
|
+
const decodedPre = decodeIdlStruct(idl, pre);
|
|
489
|
+
const decodedPost = decodeIdlStruct(idl, postAccount);
|
|
490
|
+
preParsed = decodedPre?.parsed;
|
|
491
|
+
postParsed = decodedPost?.parsed;
|
|
492
|
+
type =
|
|
493
|
+
(decodedPre?.type !== "Unknown" && decodedPre?.type) ||
|
|
494
|
+
(decodedPost?.type !== "Unknown" && decodedPost?.type) ||
|
|
495
|
+
"Unknown";
|
|
496
|
+
name = type;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
// If token, get the name based on the metadata
|
|
502
|
+
if (new Set([
|
|
503
|
+
TOKEN_2022_PROGRAM_ID.toBase58(),
|
|
504
|
+
TOKEN_PROGRAM_ID.toBase58(),
|
|
505
|
+
]).has(owner?.toBase58() || "")) {
|
|
506
|
+
if (type === "Mint") {
|
|
507
|
+
uniqueTokens.add(address.toBase58());
|
|
508
|
+
}
|
|
509
|
+
else if (type === "TokenAccount") {
|
|
510
|
+
const mint = (preParsed?.mint || postParsed?.mint).toBase58();
|
|
511
|
+
accountOwner = preParsed?.owner || postParsed?.owner;
|
|
512
|
+
uniqueTokens.add(mint);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
address,
|
|
517
|
+
name,
|
|
518
|
+
owner: accountOwner,
|
|
519
|
+
pre: {
|
|
520
|
+
type,
|
|
521
|
+
account: pre || null,
|
|
522
|
+
parsed: preParsed,
|
|
523
|
+
},
|
|
524
|
+
post: {
|
|
525
|
+
type,
|
|
526
|
+
account: post || null,
|
|
527
|
+
parsed: postParsed,
|
|
528
|
+
},
|
|
529
|
+
changedInSimulation: pre && postData
|
|
530
|
+
? !pre.data.equals(postData) ||
|
|
531
|
+
pre.lamports != post.lamports ||
|
|
532
|
+
!pre.owner.equals(new PublicKey(post.owner))
|
|
533
|
+
: true,
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
const tokens = [...uniqueTokens].map((t) => new PublicKey(t));
|
|
537
|
+
return {
|
|
538
|
+
withoutMetadata,
|
|
539
|
+
tokens,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function decodeIdlStruct(idl, account) {
|
|
543
|
+
if (!account) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const coder = new BorshAccountsCoder(idl);
|
|
548
|
+
const descriminator = account.data.slice(0, 8);
|
|
549
|
+
const type = idl.accounts?.find((account) => BorshAccountsCoder.accountDiscriminator(account.name).equals(descriminator))?.name;
|
|
550
|
+
if (type) {
|
|
551
|
+
return {
|
|
552
|
+
type,
|
|
553
|
+
parsed: coder.decode(type, account.data),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
// Ignore, not a valid account
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
function decodeTokenStruct(address, account, programId) {
|
|
563
|
+
if (!account) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
return {
|
|
568
|
+
type: "TokenAccount",
|
|
569
|
+
parsed: unpackAccount(address, account, programId),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
catch (e) {
|
|
573
|
+
// Not an account
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
return {
|
|
577
|
+
type: "Mint",
|
|
578
|
+
parsed: unpackMint(address, account, programId),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
// Not an account
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
return {
|
|
586
|
+
type: "Multisig",
|
|
587
|
+
parsed: unpackMultisig(address, account, programId),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
catch (e) {
|
|
591
|
+
// Not an account
|
|
592
|
+
}
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
export function getMetadataId(mint) {
|
|
596
|
+
return PublicKey.findProgramAddressSync([Buffer.from("metadata", "utf-8"), MPL_PID.toBuffer(), mint.toBuffer()], MPL_PID)[0];
|
|
597
|
+
}
|
|
598
|
+
export async function fetchMetadatas(connection, tokens) {
|
|
599
|
+
const metadatas = tokens.map(getMetadataId);
|
|
600
|
+
const all = await getMultipleAccounts({
|
|
601
|
+
keys: [...metadatas, ...tokens],
|
|
602
|
+
connection,
|
|
603
|
+
});
|
|
604
|
+
const metadataAccounts = all.slice(0, metadatas.length);
|
|
605
|
+
const mintAccounts = all.slice(metadatas.length, metadatas.length * 2);
|
|
606
|
+
return metadataAccounts.map((acc, index) => {
|
|
607
|
+
try {
|
|
608
|
+
const mint = unpackMint(tokens[index], mintAccounts[index]);
|
|
609
|
+
if (acc) {
|
|
610
|
+
const collectable = Metadata.fromAccountInfo(acc)[0];
|
|
611
|
+
return {
|
|
612
|
+
name: collectable.data.name.replace(/\0/g, ""),
|
|
613
|
+
symbol: collectable.data.symbol.replace(/\0/g, ""),
|
|
614
|
+
uri: collectable.data.uri.replace(/\0/g, ""),
|
|
615
|
+
decimals: mint.decimals,
|
|
616
|
+
mint: tokens[index],
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
mint: tokens[index],
|
|
621
|
+
decimals: mint.decimals,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
// Ignore, not a valid mint
|
|
626
|
+
}
|
|
627
|
+
return null;
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const truthy = (value) => !!value;
|
|
631
|
+
async function isBurnHotspot(connection, ix, assets) {
|
|
632
|
+
if (ix.raw.programId.equals(BUBBLEGUM_PROGRAM_ID) &&
|
|
633
|
+
ix.parsed?.name === "burn") {
|
|
634
|
+
const tree = ix.parsed?.accounts.find((acc) => acc.name === "Merkle Tree")?.pubkey;
|
|
635
|
+
if (tree) {
|
|
636
|
+
const index = ix.parsed?.data?.index;
|
|
637
|
+
const assetId = await getLeafAssetId(tree, new BN(index));
|
|
638
|
+
let asset;
|
|
639
|
+
if (assets) {
|
|
640
|
+
asset = assets.find(a => a.id === assetId.toBase58());
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
const assetResponse = await axios.post(connection.rpcEndpoint, {
|
|
644
|
+
jsonrpc: "2.0",
|
|
645
|
+
method: "getAsset",
|
|
646
|
+
id: "get-asset-op-1",
|
|
647
|
+
params: {
|
|
648
|
+
id: assetId.toBase58(),
|
|
649
|
+
},
|
|
650
|
+
headers: {
|
|
651
|
+
"Cache-Control": "no-cache",
|
|
652
|
+
Pragma: "no-cache",
|
|
653
|
+
Expires: "0",
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
asset = assetResponse.data.result;
|
|
657
|
+
}
|
|
658
|
+
return (asset &&
|
|
659
|
+
asset.creators[0]?.address == HELIUM_ENTITY_CREATOR.toBase58() &&
|
|
660
|
+
asset.creators[0]?.verified);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
const DAO = PublicKey.findProgramAddressSync([
|
|
666
|
+
Buffer.from("dao", "utf-8"),
|
|
667
|
+
new PublicKey("hntyVP6YFm1Hg25TN9WGLqM12b8TQmcknKrdu1oxWux").toBuffer(),
|
|
668
|
+
], new PublicKey("hdaoVTCqhfHHo75XdAMxBKdUqvq1i5bF23sisBqVgGR"))[0];
|
|
669
|
+
const HELIUM_ENTITY_CREATOR = PublicKey.findProgramAddressSync([Buffer.from("entity_creator", "utf-8"), DAO.toBuffer()], new PublicKey("hemjuPXBpNvggtaUnN1MwT3wrdhttKEfosTcc2P9Pg8"))[0];
|
|
670
|
+
//# sourceMappingURL=index.js.map
|