@helium/sus 0.6.24-next.8 → 0.6.30

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.
@@ -1,4 +1,4 @@
1
- import { BorshAccountsCoder, BorshInstructionCoder, } from "@coral-xyz/anchor";
1
+ import { BN, BorshAccountsCoder, BorshInstructionCoder, } from "@coral-xyz/anchor";
2
2
  import { decodeIdlAccount } from "@coral-xyz/anchor/dist/cjs/idl";
3
3
  import { utf8 } from "@coral-xyz/anchor/dist/cjs/utils/bytes";
4
4
  import { getLeafAssetId } from "@metaplex-foundation/mpl-bubblegum";
@@ -10,15 +10,7 @@ import axios from "axios";
10
10
  import { inflate } from "pako";
11
11
  const BUBBLEGUM_PROGRAM_ID = new PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY");
12
12
  const ACCOUNT_COMPRESSION_PROGRAM_ID = new PublicKey("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK");
13
- export async function sus({ connection, wallet, serializedTransaction,
14
- /// CNFT specific params
15
- checkCNfts = false, extraSearchAssetParams, cNfts,
16
- // Cluster for explorer
17
- cluster = "mainnet-beta", }) {
18
- const warnings = [];
19
- const transaction = VersionedTransaction.deserialize(serializedTransaction);
20
- const message = transaction.message.serialize().toString("base64");
21
- const explorerLink = `https://explorer.solana.com/tx/inspector?cluster=${cluster}&message=${encodeURIComponent(message)}`;
13
+ async function getAccountKeys({ connection, transaction, }) {
22
14
  const addressLookupTableAccounts = [];
23
15
  const { addressTableLookups } = transaction.message;
24
16
  if (addressTableLookups.length > 0) {
@@ -31,37 +23,111 @@ cluster = "mainnet-beta", }) {
31
23
  }
32
24
  }
33
25
  }
34
- const accountKeys = transaction.message.getAccountKeys({
26
+ return transaction.message.getAccountKeys({
35
27
  addressLookupTableAccounts,
36
28
  });
37
- const simulationAccounts = [
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) => [
38
74
  ...new Set(accountKeys.staticAccountKeys
39
- .filter((_, index) => transaction.message.isAccountWritable(index))
75
+ .filter((_, index) => transactions[txIndex].message.isAccountWritable(index))
40
76
  .concat(accountKeys.accountKeysFromLookups
41
77
  ? // Only writable accounts will contribute to balance changes
42
78
  accountKeys.accountKeysFromLookups.writable
43
79
  : [])),
44
- ];
45
- const fetchedAccounts = await connection.getMultipleAccountsInfo(simulationAccounts);
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
+ }, {});
46
90
  const { blockhash } = await connection?.getLatestBlockhash();
47
- transaction.message.recentBlockhash = blockhash;
48
- const simulatedTxn = await connection?.simulateTransaction(transaction, {
49
- accounts: {
50
- encoding: "base64",
51
- addresses: simulationAccounts?.map((account) => account.toBase58()) || [],
52
- },
91
+ const simulatedTxs = [];
92
+ // Linearly simulate txs so as not to hit rate limits
93
+ for (const [index, transaction] of transactions.entries()) {
94
+ transaction.message.recentBlockhash = blockhash;
95
+ const simulatedTxn = await connection?.simulateTransaction(transaction, {
96
+ accounts: {
97
+ encoding: "base64",
98
+ addresses: simulationAccountsByTx[index]?.map((account) => account.toBase58()) ||
99
+ [],
100
+ },
101
+ });
102
+ simulatedTxs.push(simulatedTxn);
103
+ }
104
+ const fullAccountsByTxn = simulationAccountsByTx.map((simulationAccounts, transactionIndex) => {
105
+ const simulatedTxn = simulatedTxs[transactionIndex];
106
+ return simulationAccounts.map((account, index) => {
107
+ const post = simulatedTxn.value.accounts?.[index];
108
+ return {
109
+ address: account,
110
+ post: post
111
+ ? {
112
+ ...post,
113
+ owner: new PublicKey(post.owner),
114
+ data: Buffer.from(post.data[0], post.data[1]),
115
+ }
116
+ : undefined,
117
+ pre: fetchedAccountsByAddr[account.toBase58()],
118
+ };
119
+ });
53
120
  });
54
- const fullAccounts = simulationAccounts.map((account, index) => ({
55
- address: account,
56
- post: simulatedTxn.value.accounts?.[index],
57
- pre: fetchedAccounts[index],
58
- }));
59
- const programKeys = fullAccounts
121
+ const instructionProgramIds = transactions
122
+ .flatMap((transaction, index) => transaction.message.compiledInstructions.map((ix) => accountKeysByTx[index].get(ix.programIdIndex) || null))
123
+ .filter(truthy);
124
+ const programKeys = fullAccountsByTxn
125
+ .flat()
60
126
  .map((acc) => acc?.pre?.owner || (acc.post ? new PublicKey(acc.post.owner) : null))
61
- .concat(...transaction.message.compiledInstructions.map((ix) => accountKeys.get(ix.programIdIndex) || null))
127
+ .concat(...instructionProgramIds)
62
128
  .filter(truthy);
63
129
  const idlKeys = programKeys.map(getIdlKey);
64
- const idls = (await connection.getMultipleAccountsInfo(idlKeys))
130
+ const idls = (await getMultipleAccounts({ connection, keys: idlKeys }))
65
131
  .map((acc, index) => {
66
132
  if (acc) {
67
133
  return {
@@ -77,52 +143,130 @@ cluster = "mainnet-beta", }) {
77
143
  }
78
144
  return acc;
79
145
  }, {});
80
- const writableAccounts = await getDetailedWritableAccounts({
81
- connection,
82
- accounts: fullAccounts,
146
+ const writableAccountsByTxRaw = fullAccountsByTxn.map((accounts) => getDetailedWritableAccountsWithoutTM({
147
+ accounts,
83
148
  idls,
149
+ }));
150
+ const tokens = [
151
+ ...new Set(writableAccountsByTxRaw.flatMap((w) => w.tokens).map((t) => t.toBase58())),
152
+ ].map((t) => new PublicKey(t));
153
+ const metadatas = (await fetchMetadatas(connection, tokens)).reduce((acc, m, index) => {
154
+ if (m) {
155
+ acc[tokens[index].toBase58()] = m;
156
+ }
157
+ return acc;
158
+ }, {});
159
+ const writableAccountsByTx = writableAccountsByTxRaw.map(({ withoutMetadata }, index) => {
160
+ const writableAccounts = withoutMetadata.map((acc) => {
161
+ let name = acc.name;
162
+ let metadata;
163
+ // Attempt to take last known type
164
+ const type = (acc.pre.type !== "Unknown" && acc.pre.type) ||
165
+ (acc.post.type !== "Unknown" && acc.post.type) ||
166
+ "Unknown";
167
+ // If token, get the name based on the metadata
168
+ if (type === "Mint") {
169
+ metadata = metadatas[acc.address.toBase58()];
170
+ if (metadata) {
171
+ name = `${metadata.symbol} Mint`;
172
+ }
173
+ else {
174
+ name = `Unknown Mint`;
175
+ }
176
+ }
177
+ else if (type === "TokenAccount") {
178
+ metadata =
179
+ metadatas[(acc.pre.parsed?.mint || acc.post.parsed?.mint).toBase58()];
180
+ if (metadata) {
181
+ name = `${metadata.symbol} Token Account`;
182
+ }
183
+ else {
184
+ name = `Unknown Token Account`;
185
+ }
186
+ }
187
+ return {
188
+ ...acc,
189
+ name,
190
+ metadata,
191
+ };
192
+ });
193
+ writableAccounts.forEach((acc) => {
194
+ if (!acc.changedInSimulation) {
195
+ warningsByTx[index].push({
196
+ severity: "warning",
197
+ shortMessage: "Unchanged",
198
+ message: "Account did not change in simulation but was labeled as writable. The behavior of the transaction may differ from the simulation.",
199
+ account: acc.address,
200
+ });
201
+ }
202
+ // Catch malicious sol ownwer change
203
+ const sysProg = new PublicKey("11111111111111111111111111111111");
204
+ const postOwner = acc.post.account?.owner || sysProg;
205
+ const preOwner = acc.pre.account?.owner || sysProg;
206
+ const accountOwnerChanged = !preOwner.equals(postOwner);
207
+ if (acc.name === "Native SOL Account" && acc.owner && acc.owner.equals(wallet) && accountOwnerChanged) {
208
+ warningsByTx[index].push({
209
+ severity: "critical",
210
+ shortMessage: "Owner Changed",
211
+ message: `The owner of ${acc.name} changed to ${acc.post.parsed?.owner?.toBase58()}. This gives that wallet full custody of these tokens.`,
212
+ account: acc.address,
213
+ });
214
+ }
215
+ });
216
+ return writableAccounts;
84
217
  });
85
- const instructions = await parseInstructions({
86
- idls,
87
- instructions: transaction.message.compiledInstructions.map((ix) => ({
88
- data: Buffer.from(ix.data),
89
- programId: accountKeys.get(ix.programIdIndex),
90
- accounts: ix.accountKeyIndexes.map((ix) => ({
91
- pubkey: accountKeys.get(ix),
92
- isSigner: transaction.message.isAccountSigner(ix),
93
- isWritable: transaction.message.isAccountWritable(ix),
218
+ const instructionsByTx = await Promise.all(transactions.map(async (transaction, index) => {
219
+ const instructions = parseInstructions({
220
+ idls,
221
+ instructions: transaction.message.compiledInstructions.map((ix) => ({
222
+ data: Buffer.from(ix.data),
223
+ programId: accountKeysByTx[index].get(ix.programIdIndex),
224
+ accounts: ix.accountKeyIndexes.map((ix) => ({
225
+ pubkey: accountKeysByTx[index].get(ix),
226
+ isSigner: transaction.message.isAccountSigner(ix),
227
+ isWritable: transaction.message.isAccountWritable(ix),
228
+ })),
94
229
  })),
95
- })),
96
- });
97
- if (instructions.some((ix) => ix.parsed?.name === "ledgerTransferPositionV0")) {
98
- warnings.push({
99
- severity: "critical",
100
- shortMessage: "Theft of Locked HNT",
101
- message: "This transaction is attempting to steal your locked HNT positions",
102
- });
103
- }
104
- if ((await Promise.all(instructions.map((ix) => isBurnHotspot(connection, ix)))).some((isBurn) => isBurn)) {
105
- warnings.push({
106
- severity: "critical",
107
- shortMessage: "Hotspot Destroyed",
108
- message: "This transaction will brick your Hotspot!",
109
230
  });
110
- }
111
- const logs = simulatedTxn.value.logs;
112
- if (simulatedTxn?.value.err) {
113
- if (isInsufficientBal(simulatedTxn?.value.err)) {
231
+ if (instructions.some((ix) => ix.parsed?.name === "ledgerTransferPositionV0")) {
232
+ warningsByTx[index].push({
233
+ severity: "critical",
234
+ shortMessage: "Theft of Locked HNT",
235
+ message: "This transaction is attempting to steal your locked HNT positions",
236
+ });
237
+ }
238
+ if ((await Promise.all(instructions.map((ix) => isBurnHotspot(connection, ix, assets)))).some((isBurn) => isBurn)) {
239
+ warningsByTx[index].push({
240
+ severity: "critical",
241
+ shortMessage: "Hotspot Destroyed",
242
+ message: "This transaction will brick your Hotspot!",
243
+ });
244
+ }
245
+ return instructions;
246
+ }));
247
+ const results = [];
248
+ for (const [index, simulatedTxn] of simulatedTxs.entries()) {
249
+ const warnings = warningsByTx[index];
250
+ const instructions = instructionsByTx[index];
251
+ const writableAccounts = writableAccountsByTx[index];
252
+ const transaction = transactions[index];
253
+ const message = transaction.message.serialize().toString("base64");
254
+ const explorerLink = `https://explorer.solana.com/tx/inspector?cluster=${cluster}&message=${encodeURIComponent(message)}`;
255
+ const logs = simulatedTxn.value.logs;
256
+ let result;
257
+ if (simulatedTxn?.value.err) {
114
258
  warnings.push({
115
- severity: "warning",
259
+ severity: "critical",
116
260
  shortMessage: "Simulation Failed",
117
261
  message: "Transaction failed in simulation",
118
262
  });
119
- return {
263
+ result = {
120
264
  instructions,
121
265
  error: simulatedTxn.value.err,
122
266
  logs,
123
267
  solFee: 0,
124
268
  priorityFee: 0,
125
- insufficientFunds: true,
269
+ insufficientFunds: isInsufficientBal(simulatedTxn?.value.err),
126
270
  explorerLink,
127
271
  balanceChanges: [],
128
272
  possibleCNftChanges: [],
@@ -131,109 +275,93 @@ cluster = "mainnet-beta", }) {
131
275
  warnings,
132
276
  };
133
277
  }
134
- }
135
- let solFee = (transaction?.signatures.length || 1) * 5000;
136
- let priorityFee = 0;
137
- const fee = (await connection?.getFeeForMessage(transaction.message, "confirmed"))
138
- .value || solFee;
139
- priorityFee = fee - solFee;
140
- const balanceChanges = writableAccounts
141
- .map((acc) => {
142
- const type = acc.pre.type || acc.post.type;
143
- switch (type) {
144
- case "TokenAccount":
145
- if (acc.post.parsed?.delegate && !acc.pre.parsed?.delegate) {
146
- warnings.push({
147
- severity: "warning",
148
- shortMessage: "Withdraw Authority Given",
149
- message: `Delegation was taken on ${acc.name}. This gives permission to withdraw tokens without the owner's permission.`,
150
- account: acc.address
151
- });
152
- }
153
- if (acc.post.parsed &&
154
- acc.pre.parsed &&
155
- !acc.post.parsed.owner.equals(acc.pre.parsed.owner)) {
156
- warnings.push({
157
- severity: "warning",
158
- shortMessage: "Owner Changed",
159
- message: `The owner of ${acc.name} changed to ${acc.post.parsed?.owner}. This gives that wallet full custody of these tokens.`,
160
- account: acc.address,
161
- });
278
+ else {
279
+ let solFee = (transaction?.signatures.length || 1) * 5000;
280
+ let priorityFee = 0;
281
+ const fee = (await connection?.getFeeForMessage(transaction.message, "confirmed"))
282
+ .value || solFee;
283
+ priorityFee = fee - solFee;
284
+ const balanceChanges = writableAccounts
285
+ .map((acc) => {
286
+ const type = (acc.pre.type !== "Unknown" && acc.pre.type) ||
287
+ (acc.post.type !== "Unknown" && acc.post.type);
288
+ switch (type) {
289
+ case "TokenAccount":
290
+ if (acc.post.parsed?.delegate && !acc.pre.parsed?.delegate) {
291
+ warnings.push({
292
+ severity: "warning",
293
+ shortMessage: "Withdraw Authority Given",
294
+ message: `Delegation was taken on ${acc.name}. This gives permission to withdraw tokens without the owner's permission.`,
295
+ account: acc.address,
296
+ });
297
+ }
298
+ if (acc.post.parsed &&
299
+ acc.pre.parsed &&
300
+ !acc.post.parsed.owner.equals(acc.pre.parsed.owner)) {
301
+ warnings.push({
302
+ severity: "warning",
303
+ shortMessage: "Owner Changed",
304
+ message: `The owner of ${acc.name} changed to ${acc.post.parsed?.owner?.toBase58()}. This gives that wallet full custody of these tokens.`,
305
+ account: acc.address,
306
+ });
307
+ }
308
+ return {
309
+ owner: acc.post.parsed?.owner || acc.pre.parsed?.owner,
310
+ address: acc.address,
311
+ amount: (acc.post.parsed?.amount || BigInt(0)) -
312
+ (acc.pre.parsed?.amount || BigInt(0)),
313
+ metadata: acc.metadata,
314
+ };
315
+ case "NativeAccount":
316
+ return {
317
+ owner: acc.address,
318
+ address: acc.address,
319
+ amount: BigInt((acc.post.account?.lamports || 0) -
320
+ (acc.pre.account?.lamports || 0)),
321
+ metadata: {
322
+ mint: NATIVE_MINT,
323
+ decimals: 9,
324
+ name: "SOL",
325
+ symbol: "SOL",
326
+ },
327
+ };
328
+ default:
329
+ return null;
162
330
  }
163
- return {
164
- owner: acc.post.parsed?.owner || acc.pre.parsed?.owner,
165
- address: acc.address,
166
- amount: (acc.post.parsed?.amount || BigInt(0)) -
167
- (acc.pre.parsed?.amount || BigInt(0)),
168
- metadata: acc.metadata,
169
- };
170
- case "NativeAccount":
171
- return {
172
- owner: acc.address,
173
- address: acc.address,
174
- amount: BigInt((acc.post.account?.lamports || 0) -
175
- (acc.pre.account?.lamports || 0)),
176
- metadata: {
177
- mint: NATIVE_MINT,
178
- decimals: 9,
179
- name: "SOL",
180
- symbol: "SOL",
181
- },
182
- };
183
- default:
184
- return null;
185
- }
186
- })
187
- .filter(truthy);
188
- if (balanceChanges.filter((b) => b.owner.equals(wallet) && b.amount < BigInt(0))
189
- .length > 2) {
190
- warnings.push({
191
- severity: "warning",
192
- shortMessage: "2+ Writable Accounts",
193
- message: "More than 2 accounts with negative balance change. Is this emptying your wallet?",
194
- });
195
- }
196
- let possibleCNftChanges = [];
197
- if (checkCNfts) {
198
- let assets = cNfts;
199
- if (!assets) {
200
- const assetsResponse = await axios.post(connection.rpcEndpoint, {
201
- jsonrpc: "2.0",
202
- method: "searchAssets",
203
- id: "get-assets-op-1",
204
- params: {
205
- page: 1,
206
- // limit to checking 200 assets
207
- limit: 200,
208
- compressed: true,
209
- ...extraSearchAssetParams,
210
- },
211
- headers: {
212
- "Cache-Control": "no-cache",
213
- Pragma: "no-cache",
214
- Expires: "0",
215
- },
216
- });
217
- assets = assetsResponse.data.result?.items;
331
+ })
332
+ .filter(truthy);
333
+ // Don't count new mints being created, as this might flag on candymachine txs
334
+ if (balanceChanges.filter((b) => b.owner.equals(wallet) && fetchedAccountsByAddr[b.address.toBase58()]).length >= 3) {
335
+ warnings.push({
336
+ severity: "warning",
337
+ shortMessage: "3+ Token Accounts",
338
+ 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?",
339
+ });
340
+ }
341
+ let possibleCNftChanges = [];
342
+ if (checkCNfts) {
343
+ const possibleMerkles = new Set(writableAccounts
344
+ .filter((acc) => acc.name === "Merkle Tree")
345
+ .map((a) => a.address.toBase58()));
346
+ possibleCNftChanges = (assets || []).filter((item) => item.compression.tree && possibleMerkles.has(item.compression.tree));
347
+ }
348
+ result = {
349
+ instructions,
350
+ logs,
351
+ solFee,
352
+ priorityFee,
353
+ insufficientFunds: false,
354
+ explorerLink,
355
+ balanceChanges,
356
+ possibleCNftChanges,
357
+ writableAccounts,
358
+ rawSimulation: simulatedTxn.value,
359
+ warnings,
360
+ };
218
361
  }
219
- const possibleMerkles = new Set(writableAccounts
220
- .filter((acc) => acc.name === "Merkle Tree")
221
- .map((a) => a.address.toBase58()));
222
- possibleCNftChanges = (assets || []).filter((item) => item.compression.tree && possibleMerkles.has(item.compression.tree));
362
+ results.push(result);
223
363
  }
224
- return {
225
- instructions,
226
- logs,
227
- solFee,
228
- priorityFee,
229
- insufficientFunds: false,
230
- explorerLink,
231
- balanceChanges,
232
- possibleCNftChanges,
233
- writableAccounts,
234
- rawSimulation: simulatedTxn.value,
235
- warnings,
236
- };
364
+ return results;
237
365
  }
238
366
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
239
367
  export function isInsufficientBal(e) {
@@ -241,7 +369,7 @@ export function isInsufficientBal(e) {
241
369
  e.toString().includes('"Custom":1') ||
242
370
  e.InstructionError?.[1]?.Custom === 1);
243
371
  }
244
- async function parseInstructions({ idls, instructions, }) {
372
+ function parseInstructions({ idls, instructions, }) {
245
373
  return instructions.map((ix) => {
246
374
  const idl = idls[ix.programId.toBase58()];
247
375
  if (idl) {
@@ -290,7 +418,7 @@ function decodeIdl(account) {
290
418
  // Ignore, not a valid IDL
291
419
  }
292
420
  }
293
- export async function getDetailedWritableAccounts({ connection, accounts, idls, }) {
421
+ export function getDetailedWritableAccountsWithoutTM({ accounts, idls, }) {
294
422
  const uniqueTokens = new Set();
295
423
  const withoutMetadata = accounts.map(({ address, pre, post }) => {
296
424
  let name = "Unknown";
@@ -298,12 +426,7 @@ export async function getDetailedWritableAccounts({ connection, accounts, idls,
298
426
  let preParsed = null;
299
427
  let postParsed = null;
300
428
  let accountOwner = undefined;
301
- const postData = post &&
302
- Buffer.from(
303
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
304
- post.data[0],
305
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
306
- post.data[1]);
429
+ const postData = post && post.data;
307
430
  const postAccount = post && postData
308
431
  ? {
309
432
  executable: post.executable,
@@ -336,12 +459,11 @@ export async function getDetailedWritableAccounts({ connection, accounts, idls,
336
459
  if (owner) {
337
460
  const idl = idls[owner.toBase58()];
338
461
  if (idl) {
339
- ({ parsed: preParsed, type } = decodeIdlStruct(idl, pre) || {
340
- type: "No Published IDL",
341
- });
342
- ({ parsed: postParsed, type } = decodeIdlStruct(idl, postAccount) || {
343
- type: "No Published IDL",
344
- });
462
+ const decodedPre = decodeIdlStruct(idl, pre);
463
+ const decodedPost = decodeIdlStruct(idl, postAccount);
464
+ preParsed = decodedPre?.parsed;
465
+ postParsed = decodedPost?.parsed;
466
+ type = decodedPre?.type || decodedPost?.type || "Unknown";
345
467
  name = type;
346
468
  }
347
469
  }
@@ -383,42 +505,10 @@ export async function getDetailedWritableAccounts({ connection, accounts, idls,
383
505
  };
384
506
  });
385
507
  const tokens = [...uniqueTokens].map((t) => new PublicKey(t));
386
- const metadatas = (await fetchMetadatas(connection, tokens)).reduce((acc, m, index) => {
387
- if (m) {
388
- acc[tokens[index].toBase58()] = m;
389
- }
390
- return acc;
391
- }, {});
392
- return withoutMetadata.map((acc) => {
393
- let name = acc.name;
394
- let metadata;
395
- const type = acc.pre.type || acc.post.type;
396
- // If token, get the name based on the metadata
397
- if (type === "Mint") {
398
- metadata = metadatas[acc.address.toBase58()];
399
- if (metadata) {
400
- name = `${metadata.symbol} Mint`;
401
- }
402
- else {
403
- name = `Unknown Mint`;
404
- }
405
- }
406
- else if (type === "TokenAccount") {
407
- metadata =
408
- metadatas[(acc.pre.parsed?.mint || acc.post.parsed?.mint).toBase58()];
409
- if (metadata) {
410
- name = `${metadata.symbol} Token Account`;
411
- }
412
- else {
413
- name = `Unknown Token Account`;
414
- }
415
- }
416
- return {
417
- ...acc,
418
- name,
419
- metadata,
420
- };
421
- });
508
+ return {
509
+ withoutMetadata,
510
+ tokens,
511
+ };
422
512
  }
423
513
  function decodeIdlStruct(idl, account) {
424
514
  if (!account) {
@@ -478,51 +568,64 @@ export function getMetadataId(mint) {
478
568
  }
479
569
  export async function fetchMetadatas(connection, tokens) {
480
570
  const metadatas = tokens.map(getMetadataId);
481
- const all = await connection.getMultipleAccountsInfo([
482
- ...metadatas,
483
- ...tokens,
484
- ]);
571
+ const all = await getMultipleAccounts({
572
+ keys: [...metadatas, ...tokens],
573
+ connection,
574
+ });
485
575
  const metadataAccounts = all.slice(0, metadatas.length);
486
576
  const mintAccounts = all.slice(metadatas.length, metadatas.length * 2);
487
577
  return metadataAccounts.map((acc, index) => {
488
- const mint = unpackMint(tokens[index], mintAccounts[index]);
489
- if (acc) {
490
- const collectable = Metadata.fromAccountInfo(acc)[0];
578
+ try {
579
+ const mint = unpackMint(tokens[index], mintAccounts[index]);
580
+ if (acc) {
581
+ const collectable = Metadata.fromAccountInfo(acc)[0];
582
+ return {
583
+ name: collectable.data.name.replace(/\0/g, ""),
584
+ symbol: collectable.data.symbol.replace(/\0/g, ""),
585
+ uri: collectable.data.uri.replace(/\0/g, ""),
586
+ decimals: mint.decimals,
587
+ mint: tokens[index],
588
+ };
589
+ }
491
590
  return {
492
- name: collectable.data.name.replace(/\0/g, ""),
493
- symbol: collectable.data.symbol.replace(/\0/g, ""),
494
- uri: collectable.data.uri.replace(/\0/g, ""),
495
- decimals: mint.decimals,
496
591
  mint: tokens[index],
592
+ decimals: mint.decimals,
497
593
  };
498
594
  }
499
- return {
500
- mint: tokens[index],
501
- decimals: mint.decimals,
502
- };
595
+ catch (e) {
596
+ // Ignore, not a valid mint
597
+ }
598
+ return null;
503
599
  });
504
600
  }
505
601
  const truthy = (value) => !!value;
506
- async function isBurnHotspot(connection, ix) {
602
+ async function isBurnHotspot(connection, ix, assets) {
507
603
  if (ix.raw.programId.equals(BUBBLEGUM_PROGRAM_ID) &&
508
604
  ix.parsed?.name === "burn") {
509
- const tree = ix.parsed?.accounts.find((acc) => acc.name === "merkleTree")?.pubkey;
605
+ const tree = ix.parsed?.accounts.find((acc) => acc.name === "Merkle Tree")?.pubkey;
510
606
  if (tree) {
511
- const assetId = await getLeafAssetId(tree, ix.parsed?.data[4]);
512
- const assetResponse = await axios.post(connection.rpcEndpoint, {
513
- jsonrpc: "2.0",
514
- method: "getAsset",
515
- id: "get-asset-op-1",
516
- params: {
517
- id: assetId.toBase58(),
518
- },
519
- headers: {
520
- "Cache-Control": "no-cache",
521
- Pragma: "no-cache",
522
- Expires: "0",
523
- },
524
- });
525
- const asset = assetResponse.data.result;
607
+ const index = ix.parsed?.data?.index;
608
+ const assetId = await getLeafAssetId(tree, new BN(index));
609
+ let asset;
610
+ if (assets) {
611
+ asset = assets.find(a => a.id === assetId.toBase58());
612
+ }
613
+ else {
614
+ const assetResponse = await axios.post(connection.rpcEndpoint, {
615
+ jsonrpc: "2.0",
616
+ method: "getAsset",
617
+ id: "get-asset-op-1",
618
+ params: {
619
+ id: assetId.toBase58(),
620
+ },
621
+ headers: {
622
+ "Cache-Control": "no-cache",
623
+ Pragma: "no-cache",
624
+ Expires: "0",
625
+ },
626
+ });
627
+ asset = assetResponse.data.result;
628
+ }
526
629
  return (asset &&
527
630
  asset.creators[0]?.address == HELIUM_ENTITY_CREATOR.toBase58() &&
528
631
  asset.creators[0]?.verified);