@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.
- package/README.md +23 -18
- package/lib/cjs/index.js +441 -345
- package/lib/cjs/index.js.map +1 -1
- package/lib/esm/src/index.js +347 -244
- package/lib/esm/src/index.js.map +1 -1
- package/lib/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/lib/types/src/index.d.ts +14 -12
- package/lib/types/src/index.d.ts.map +1 -1
- package/package.json +2 -2
package/lib/esm/src/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
+
return transaction.message.getAccountKeys({
|
|
35
27
|
addressLookupTableAccounts,
|
|
36
28
|
});
|
|
37
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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(...
|
|
127
|
+
.concat(...instructionProgramIds)
|
|
62
128
|
.filter(truthy);
|
|
63
129
|
const idlKeys = programKeys.map(getIdlKey);
|
|
64
|
-
const idls = (await connection
|
|
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
|
|
81
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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: "
|
|
259
|
+
severity: "critical",
|
|
116
260
|
shortMessage: "Simulation Failed",
|
|
117
261
|
message: "Transaction failed in simulation",
|
|
118
262
|
});
|
|
119
|
-
|
|
263
|
+
result = {
|
|
120
264
|
instructions,
|
|
121
265
|
error: simulatedTxn.value.err,
|
|
122
266
|
logs,
|
|
123
267
|
solFee: 0,
|
|
124
268
|
priorityFee: 0,
|
|
125
|
-
insufficientFunds:
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
482
|
-
...metadatas,
|
|
483
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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 === "
|
|
605
|
+
const tree = ix.parsed?.accounts.find((acc) => acc.name === "Merkle Tree")?.pubkey;
|
|
510
606
|
if (tree) {
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
id
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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);
|