explorer: Token mint histories (token balance and token instructions tabs) (#15861)
* feat: refactor and introduce balances and instructions tabs for mints * fix: refactor instruction utilities into common file * refactor: move transaction history components into one spot * chore: minor cleanup * fix: show only token instructions * fix: use better naming for slugs and paths * feat: refactor and work on transaction status * feat: show token transfer details * fix: format code and remove some extra spaces * fix: exclude non-mint transfers * feat: introduce react-moment and reorganize history tables * feat: reintroduce status columns and reorganize columns * fix: remove ts-ignore * feat: refactor history card components * fix: remove detailed history provider * fix: filter instructions and inner instructions based on mint * fix: use better key and report parse error * fix: remove double spacing * feat: batch transaction map batches * fix: remove debug code * fix: pass proper signatureInfo and reduce batch size to 10
This commit is contained in:
parent
d92721aab9
commit
39eddfd161
|
@ -2470,8 +2470,8 @@
|
||||||
"superstruct": "^0.8.3",
|
"superstruct": "^0.8.3",
|
||||||
"tweetnacl": "^1.0.0",
|
"tweetnacl": "^1.0.0",
|
||||||
"ws": "^7.0.0"
|
"ws": "^7.0.0"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
"superstruct": {
|
"superstruct": {
|
||||||
"version": "0.8.4",
|
"version": "0.8.4",
|
||||||
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz",
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.8.4.tgz",
|
||||||
|
@ -2483,6 +2483,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"superstruct": {
|
||||||
|
"version": "0.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz",
|
||||||
|
"integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@react-hook/debounce": {
|
"@react-hook/debounce": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-hook/debounce/-/debounce-3.0.0.tgz",
|
||||||
|
@ -3080,9 +3087,9 @@
|
||||||
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
|
"integrity": "sha512-2koNhpWm3DgWRp5tpkiJ8JGc1xTn2q0l+jUNUE7oMKXUf5NpI9AIdC4kbjGNFBdHtcxBD18LAksoudAVhFKCjw=="
|
||||||
},
|
},
|
||||||
"@types/connect": {
|
"@types/connect": {
|
||||||
"version": "3.4.33",
|
"version": "3.4.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
|
||||||
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
|
"integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
|
@ -3102,9 +3109,9 @@
|
||||||
"integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg=="
|
"integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg=="
|
||||||
},
|
},
|
||||||
"@types/express-serve-static-core": {
|
"@types/express-serve-static-core": {
|
||||||
"version": "4.17.13",
|
"version": "4.17.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.19.tgz",
|
||||||
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
|
"integrity": "sha512-DJOSHzX7pCiSElWaGR8kCprwibCB/3yW6vcT8VG3P0SJjnv19gnWG/AZMfM60Xj/YJIp/YCaDHyvzsFVeniARA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/qs": "*",
|
"@types/qs": "*",
|
||||||
|
@ -3179,9 +3186,9 @@
|
||||||
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
|
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4="
|
||||||
},
|
},
|
||||||
"@types/lodash": {
|
"@types/lodash": {
|
||||||
"version": "4.14.164",
|
"version": "4.14.168",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.164.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz",
|
||||||
"integrity": "sha512-fXCEmONnrtbYUc5014avwBeMdhHHO8YJCkOBflUL9EoJBSKZ1dei+VO74fA7JkTHZ1GvZack2TyIw5U+1lT8jg=="
|
"integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q=="
|
||||||
},
|
},
|
||||||
"@types/minimatch": {
|
"@types/minimatch": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
@ -3219,9 +3226,9 @@
|
||||||
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
|
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug=="
|
||||||
},
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.9.5",
|
"version": "6.9.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz",
|
||||||
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ=="
|
"integrity": "sha512-0/HnwIfW4ki2D8L8c9GVcG5I72s9jP5GSLVF0VIXDW00kmIpA6O33G7a8n59Tmh7Nz0WUC3rSb7PTY/sdW2JzA=="
|
||||||
},
|
},
|
||||||
"@types/range-parser": {
|
"@types/range-parser": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
|
@ -4993,20 +5000,12 @@
|
||||||
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
|
"integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
|
||||||
},
|
},
|
||||||
"bufferutil": {
|
"bufferutil": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz",
|
||||||
"integrity": "sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA==",
|
"integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-gyp-build": "~3.7.0"
|
"node-gyp-build": "^4.2.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"node-gyp-build": {
|
|
||||||
"version": "3.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz",
|
|
||||||
"integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==",
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"builtin-modules": {
|
"builtin-modules": {
|
||||||
|
@ -9792,9 +9791,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "12.19.16",
|
"version": "12.20.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.7.tgz",
|
||||||
"integrity": "sha512-7xHmXm/QJ7cbK2laF+YYD7gb5MggHIIQwqyjin3bpEGiSuvScMQ5JZZXPvRipi1MwckTQbJZROMns/JxdnIL1Q=="
|
"integrity": "sha512-gWL8VUkg8VRaCAUgG9WmhefMqHmMblxe2rVpMF86nZY/+ZysU+BkAp+3cz03AixWDSSz0ks5WX59yAhv/cDwFA=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -15770,6 +15769,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"react-moment": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-moment/-/react-moment-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WjwvxBSnmLMRcU33do0KixDB+9vP3e84eCse+rd+HNklAMNWyRgZTDEQlay/qK6lcXFPRuEIASJTpEt6pyK7Ww=="
|
||||||
|
},
|
||||||
"react-refresh": {
|
"react-refresh": {
|
||||||
"version": "0.8.3",
|
"version": "0.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.8.3.tgz",
|
||||||
|
@ -16522,9 +16526,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rpc-websockets": {
|
"rpc-websockets": {
|
||||||
"version": "7.4.6",
|
"version": "7.4.9",
|
||||||
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.4.9.tgz",
|
||||||
"integrity": "sha512-vDGdyJv858O5ZIc7glov8pQDdFztOqujA7iNyrfPxw87ajHT5s8WQU4MLNEG8pTR/xzqOn06dYH7kef2hijInw==",
|
"integrity": "sha512-5MsJlPDzJkt3eqlUeYHg66A7mxXSSYRE11lKGfNmAXgcMBw4F3a7CLgviwqf6rb850qP3Q1BP8ygp+V+DDq1qQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@babel/runtime": "^7.11.2",
|
"@babel/runtime": "^7.11.2",
|
||||||
"assert-args": "^1.2.1",
|
"assert-args": "^1.2.1",
|
||||||
|
@ -16537,9 +16541,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uuid": {
|
"uuid": {
|
||||||
"version": "8.3.1",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
"integrity": "sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg=="
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -18618,20 +18622,12 @@
|
||||||
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
"integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ=="
|
||||||
},
|
},
|
||||||
"utf-8-validate": {
|
"utf-8-validate": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz",
|
||||||
"integrity": "sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw==",
|
"integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"node-gyp-build": "~3.7.0"
|
"node-gyp-build": "^4.2.0"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"node-gyp-build": {
|
|
||||||
"version": "3.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.7.0.tgz",
|
|
||||||
"integrity": "sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w==",
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"util": {
|
"util": {
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"react-chartjs-2": "^2.11.1",
|
"react-chartjs-2": "^2.11.1",
|
||||||
"react-countup": "^4.3.3",
|
"react-countup": "^4.3.3",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-moment": "^1.1.1",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
"react-select": "^4.3.0",
|
"react-select": "^4.3.0",
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ConfirmedSignatureInfo, TransactionError } from "@solana/web3.js";
|
||||||
|
|
||||||
|
export type TransactionRow = {
|
||||||
|
slot: number;
|
||||||
|
signature: string;
|
||||||
|
err: TransactionError | null;
|
||||||
|
blockTime: number | null | undefined;
|
||||||
|
statusClass: string;
|
||||||
|
statusText: string;
|
||||||
|
signatureInfo: ConfirmedSignatureInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function HistoryCardHeader({
|
||||||
|
title,
|
||||||
|
refresh,
|
||||||
|
fetching,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
refresh: Function;
|
||||||
|
fetching: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="card-header align-items-center">
|
||||||
|
<h3 className="card-header-title">{title}</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-white btn-sm"
|
||||||
|
disabled={fetching}
|
||||||
|
onClick={() => refresh()}
|
||||||
|
>
|
||||||
|
{fetching ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="fe fe-refresh-cw mr-2"></span>
|
||||||
|
Refresh
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HistoryCardFooter({
|
||||||
|
fetching,
|
||||||
|
foundOldest,
|
||||||
|
loadMore,
|
||||||
|
}: {
|
||||||
|
fetching: boolean;
|
||||||
|
foundOldest: boolean;
|
||||||
|
loadMore: Function;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="card-footer">
|
||||||
|
{foundOldest ? (
|
||||||
|
<div className="text-muted text-center">Fetched full history</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
className="btn btn-primary w-100"
|
||||||
|
onClick={() => loadMore()}
|
||||||
|
disabled={fetching}
|
||||||
|
>
|
||||||
|
{fetching ? (
|
||||||
|
<>
|
||||||
|
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
||||||
|
Loading
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Load More"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTransactionRows(
|
||||||
|
transactions: ConfirmedSignatureInfo[]
|
||||||
|
): TransactionRow[] {
|
||||||
|
const transactionRows: TransactionRow[] = [];
|
||||||
|
for (var i = 0; i < transactions.length; i++) {
|
||||||
|
const slot = transactions[i].slot;
|
||||||
|
const slotTransactions = [transactions[i]];
|
||||||
|
while (i + 1 < transactions.length) {
|
||||||
|
const nextSlot = transactions[i + 1].slot;
|
||||||
|
if (nextSlot !== slot) break;
|
||||||
|
slotTransactions.push(transactions[++i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let slotTransaction of slotTransactions) {
|
||||||
|
let statusText;
|
||||||
|
let statusClass;
|
||||||
|
if (slotTransaction.err) {
|
||||||
|
statusClass = "warning";
|
||||||
|
statusText = "Failed";
|
||||||
|
} else {
|
||||||
|
statusClass = "success";
|
||||||
|
statusText = "Success";
|
||||||
|
}
|
||||||
|
transactionRows.push({
|
||||||
|
slot,
|
||||||
|
signature: slotTransaction.signature,
|
||||||
|
err: slotTransaction.err,
|
||||||
|
blockTime: slotTransaction.blockTime,
|
||||||
|
statusClass,
|
||||||
|
statusText,
|
||||||
|
signatureInfo: slotTransaction,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionRows;
|
||||||
|
}
|
|
@ -25,12 +25,6 @@ import {
|
||||||
useFetchTransactionDetails,
|
useFetchTransactionDetails,
|
||||||
useTransactionDetailsCache,
|
useTransactionDetailsCache,
|
||||||
} from "providers/transactions/details";
|
} from "providers/transactions/details";
|
||||||
import { create } from "superstruct";
|
|
||||||
import { ParsedInfo } from "validators";
|
|
||||||
import {
|
|
||||||
TokenInstructionType,
|
|
||||||
IX_TITLES,
|
|
||||||
} from "components/instruction/token/types";
|
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
import { intoTransactionInstruction, displayAddress } from "utils/tx";
|
import { intoTransactionInstruction, displayAddress } from "utils/tx";
|
||||||
import {
|
import {
|
||||||
|
@ -52,6 +46,7 @@ import { Location } from "history";
|
||||||
import { useQuery } from "utils/url";
|
import { useQuery } from "utils/url";
|
||||||
import { TokenInfoMap } from "@solana/spl-token-registry";
|
import { TokenInfoMap } from "@solana/spl-token-registry";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
|
import { getTokenProgramInstructionName } from "utils/instruction";
|
||||||
|
|
||||||
const TRUNCATE_TOKEN_LENGTH = 10;
|
const TRUNCATE_TOKEN_LENGTH = 10;
|
||||||
const ALL_TOKENS = "";
|
const ALL_TOKENS = "";
|
||||||
|
@ -363,21 +358,6 @@ const FilterDropdown = ({ filter, toggle, show, tokens }: FilterProps) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function instructionTypeName(
|
|
||||||
ix: ParsedInstruction,
|
|
||||||
tx: ConfirmedSignatureInfo
|
|
||||||
): string {
|
|
||||||
try {
|
|
||||||
const parsed = create(ix.parsed, ParsedInfo);
|
|
||||||
const { type: rawType } = parsed;
|
|
||||||
const type = create(rawType, TokenInstructionType);
|
|
||||||
return IX_TITLES[type];
|
|
||||||
} catch (err) {
|
|
||||||
reportError(err, { signature: tx.signature });
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const TokenTransactionRow = React.memo(
|
const TokenTransactionRow = React.memo(
|
||||||
({
|
({
|
||||||
mint,
|
mint,
|
||||||
|
@ -474,7 +454,7 @@ const TokenTransactionRow = React.memo(
|
||||||
|
|
||||||
if ("parsed" in ix) {
|
if ("parsed" in ix) {
|
||||||
if (ix.program === "spl-token") {
|
if (ix.program === "spl-token") {
|
||||||
name = instructionTypeName(ix, tx);
|
name = getTokenProgramInstructionName(ix, tx);
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -521,8 +501,8 @@ const TokenTransactionRow = React.memo(
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: name,
|
name,
|
||||||
innerInstructions: innerInstructions,
|
innerInstructions,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((name) => name !== undefined) as InstructionType[];
|
.filter((name) => name !== undefined) as InstructionType[];
|
||||||
|
@ -574,7 +554,7 @@ function InstructionDetails({
|
||||||
let instructionTypes = instructionType.innerInstructions
|
let instructionTypes = instructionType.innerInstructions
|
||||||
.map((ix) => {
|
.map((ix) => {
|
||||||
if ("parsed" in ix && ix.program === "spl-token") {
|
if ("parsed" in ix && ix.program === "spl-token") {
|
||||||
return instructionTypeName(ix, tx);
|
return getTokenProgramInstructionName(ix, tx);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { PublicKey } from "@solana/web3.js";
|
|
||||||
import { FetchStatus } from "providers/cache";
|
|
||||||
import { useAccountInfo, useAccountHistory } from "providers/accounts";
|
|
||||||
import { useFetchAccountHistory } from "providers/accounts/history";
|
|
||||||
import { Signature } from "components/common/Signature";
|
|
||||||
import { ErrorCard } from "components/common/ErrorCard";
|
|
||||||
import { LoadingCard } from "components/common/LoadingCard";
|
|
||||||
import { Slot } from "components/common/Slot";
|
|
||||||
import { displayTimestamp } from "utils/date";
|
|
||||||
|
|
||||||
export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
|
||||||
const address = pubkey.toBase58();
|
|
||||||
const info = useAccountInfo(address);
|
|
||||||
const history = useAccountHistory(address);
|
|
||||||
const fetchAccountHistory = useFetchAccountHistory();
|
|
||||||
const refresh = () => fetchAccountHistory(pubkey, true);
|
|
||||||
const loadMore = () => fetchAccountHistory(pubkey);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!history) refresh();
|
|
||||||
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
if (!history || info?.data === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (history?.data === undefined) {
|
|
||||||
if (history.status === FetchStatus.Fetching) {
|
|
||||||
return <LoadingCard message="Loading history" />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transactions = history.data.fetched;
|
|
||||||
if (transactions.length === 0) {
|
|
||||||
if (history.status === FetchStatus.Fetching) {
|
|
||||||
return <LoadingCard message="Loading history" />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ErrorCard
|
|
||||||
retry={loadMore}
|
|
||||||
retryText="Try again"
|
|
||||||
text="No transaction history found"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasTimestamps = !!transactions.find((element) => !!element.blockTime);
|
|
||||||
const detailsList: React.ReactNode[] = [];
|
|
||||||
for (var i = 0; i < transactions.length; i++) {
|
|
||||||
const slot = transactions[i].slot;
|
|
||||||
const slotTransactions = [transactions[i]];
|
|
||||||
while (i + 1 < transactions.length) {
|
|
||||||
const nextSlot = transactions[i + 1].slot;
|
|
||||||
if (nextSlot !== slot) break;
|
|
||||||
slotTransactions.push(transactions[++i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
slotTransactions.forEach(({ signature, err, blockTime }) => {
|
|
||||||
let statusText;
|
|
||||||
let statusClass;
|
|
||||||
if (err) {
|
|
||||||
statusClass = "warning";
|
|
||||||
statusText = "Failed";
|
|
||||||
} else {
|
|
||||||
statusClass = "success";
|
|
||||||
statusText = "Success";
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsList.push(
|
|
||||||
<tr key={signature}>
|
|
||||||
<td className="w-1">
|
|
||||||
<Slot slot={slot} link />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
{hasTimestamps && (
|
|
||||||
<td className="text-muted">
|
|
||||||
{blockTime ? displayTimestamp(blockTime * 1000, true) : "---"}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<span className={`badge badge-soft-${statusClass}`}>
|
|
||||||
{statusText}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td>
|
|
||||||
<Signature signature={signature} link />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetching = history.status === FetchStatus.Fetching;
|
|
||||||
return (
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-header align-items-center">
|
|
||||||
<h3 className="card-header-title">Transaction History</h3>
|
|
||||||
<button
|
|
||||||
className="btn btn-white btn-sm"
|
|
||||||
disabled={fetching}
|
|
||||||
onClick={refresh}
|
|
||||||
>
|
|
||||||
{fetching ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="fe fe-refresh-cw mr-2"></span>
|
|
||||||
Refresh
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="table-responsive mb-0">
|
|
||||||
<table className="table table-sm table-nowrap card-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-muted w-1">Slot</th>
|
|
||||||
{hasTimestamps && <th className="text-muted">Timestamp</th>}
|
|
||||||
<th className="text-muted">Result</th>
|
|
||||||
<th className="text-muted">Transaction Signature</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="list">{detailsList}</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card-footer">
|
|
||||||
{history.data.foundOldest ? (
|
|
||||||
<div className="text-muted text-center">Fetched full history</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className="btn btn-primary w-100"
|
|
||||||
onClick={loadMore}
|
|
||||||
disabled={fetching}
|
|
||||||
>
|
|
||||||
{fetching ? (
|
|
||||||
<>
|
|
||||||
<span className="spinner-grow spinner-grow-sm mr-2"></span>
|
|
||||||
Loading
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Load More"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,195 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
ParsedConfirmedTransaction,
|
||||||
|
ParsedInstruction,
|
||||||
|
PartiallyDecodedInstruction,
|
||||||
|
PublicKey,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { useAccountHistory } from "providers/accounts";
|
||||||
|
import { Signature } from "components/common/Signature";
|
||||||
|
import {
|
||||||
|
getTokenInstructionName,
|
||||||
|
InstructionContainer,
|
||||||
|
} from "utils/instruction";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { FetchStatus } from "providers/cache";
|
||||||
|
import { useFetchAccountHistory } from "providers/accounts/history";
|
||||||
|
import {
|
||||||
|
getTransactionRows,
|
||||||
|
HistoryCardFooter,
|
||||||
|
HistoryCardHeader,
|
||||||
|
} from "../HistoryCardComponents";
|
||||||
|
import { extractMintDetails, MintDetails } from "./common";
|
||||||
|
import Moment from "react-moment";
|
||||||
|
|
||||||
|
export function TokenInstructionsCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const history = useAccountHistory(address);
|
||||||
|
const fetchAccountHistory = useFetchAccountHistory();
|
||||||
|
const refresh = () => fetchAccountHistory(pubkey, true, true);
|
||||||
|
const loadMore = () => fetchAccountHistory(pubkey, true);
|
||||||
|
|
||||||
|
const transactionRows = React.useMemo(() => {
|
||||||
|
if (history?.data?.fetched) {
|
||||||
|
return getTransactionRows(history.data.fetched);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!history || !history.data?.transactionMap?.size) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const { hasTimestamps, detailsList } = React.useMemo(() => {
|
||||||
|
const detailedHistoryMap =
|
||||||
|
history?.data?.transactionMap ||
|
||||||
|
new Map<string, ParsedConfirmedTransaction>();
|
||||||
|
const hasTimestamps = transactionRows.some((element) => element.blockTime);
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
const mintMap = new Map<string, MintDetails>();
|
||||||
|
|
||||||
|
transactionRows.forEach(
|
||||||
|
({ signatureInfo, signature, blockTime, statusClass, statusText }) => {
|
||||||
|
const parsed = detailedHistoryMap.get(signature);
|
||||||
|
if (!parsed) return;
|
||||||
|
|
||||||
|
extractMintDetails(parsed, mintMap);
|
||||||
|
|
||||||
|
let instructions: (
|
||||||
|
| ParsedInstruction
|
||||||
|
| PartiallyDecodedInstruction
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
InstructionContainer.create(parsed).instructions.forEach(
|
||||||
|
({ instruction, inner }, index) => {
|
||||||
|
if (isRelevantInstruction(pubkey, address, mintMap, instruction)) {
|
||||||
|
instructions.push(instruction);
|
||||||
|
}
|
||||||
|
instructions.push(
|
||||||
|
...inner.filter((instruction) =>
|
||||||
|
isRelevantInstruction(pubkey, address, mintMap, instruction)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
instructions.forEach((ix, index) => {
|
||||||
|
const programId = ix.programId;
|
||||||
|
|
||||||
|
const instructionName = getTokenInstructionName(
|
||||||
|
parsed,
|
||||||
|
ix,
|
||||||
|
signatureInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (instructionName) {
|
||||||
|
detailsList.push(
|
||||||
|
<tr key={signature + index}>
|
||||||
|
<td>
|
||||||
|
<Signature signature={signature} link truncateChars={48} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{hasTimestamps && (
|
||||||
|
<td className="text-muted">
|
||||||
|
{blockTime && <Moment date={blockTime * 1000} fromNow />}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<td>{instructionName}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Address
|
||||||
|
pubkey={programId}
|
||||||
|
link
|
||||||
|
truncate
|
||||||
|
truncateChars={16}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasTimestamps,
|
||||||
|
detailsList,
|
||||||
|
};
|
||||||
|
}, [history, transactionRows, address, pubkey]);
|
||||||
|
|
||||||
|
if (!history) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history?.data === undefined) {
|
||||||
|
if (history.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard message="Loading token instructions" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorCard retry={refresh} text="Failed to fetch token instructions" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetching = history.status === FetchStatus.Fetching;
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<HistoryCardHeader
|
||||||
|
fetching={fetching}
|
||||||
|
refresh={() => refresh()}
|
||||||
|
title="Token Instructions"
|
||||||
|
/>
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted w-1">Transaction Signature</th>
|
||||||
|
{hasTimestamps && <th className="text-muted">Age</th>}
|
||||||
|
<th className="text-muted">Instruction</th>
|
||||||
|
<th className="text-muted">Program</th>
|
||||||
|
<th className="text-muted">Result</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{detailsList}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<HistoryCardFooter
|
||||||
|
fetching={fetching}
|
||||||
|
foundOldest={history.data.foundOldest}
|
||||||
|
loadMore={() => loadMore()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRelevantInstruction(
|
||||||
|
pubkey: PublicKey,
|
||||||
|
address: string,
|
||||||
|
mintMap: Map<string, MintDetails>,
|
||||||
|
instruction: ParsedInstruction | PartiallyDecodedInstruction
|
||||||
|
) {
|
||||||
|
if ("accounts" in instruction) {
|
||||||
|
return instruction.accounts.some(
|
||||||
|
(account) =>
|
||||||
|
account.equals(pubkey) ||
|
||||||
|
mintMap.get(account.toBase58())?.mint === address
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Object.entries(instruction.parsed.info).some(
|
||||||
|
([key, value]) =>
|
||||||
|
value === address ||
|
||||||
|
(typeof value === "string" && mintMap.get(value)?.mint === address)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,272 @@
|
||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
ParsedConfirmedTransaction,
|
||||||
|
ParsedInstruction,
|
||||||
|
PartiallyDecodedInstruction,
|
||||||
|
PublicKey,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { useAccountHistory } from "providers/accounts";
|
||||||
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
|
import { create } from "superstruct";
|
||||||
|
import {
|
||||||
|
TokenInstructionType,
|
||||||
|
Transfer,
|
||||||
|
TransferChecked,
|
||||||
|
} from "components/instruction/token/types";
|
||||||
|
import { InstructionContainer } from "utils/instruction";
|
||||||
|
import { Signature } from "components/common/Signature";
|
||||||
|
import { Address } from "components/common/Address";
|
||||||
|
import { normalizeTokenAmount } from "utils";
|
||||||
|
import {
|
||||||
|
getTransactionRows,
|
||||||
|
HistoryCardFooter,
|
||||||
|
HistoryCardHeader,
|
||||||
|
} from "../HistoryCardComponents";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { useFetchAccountHistory } from "providers/accounts/history";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
import { FetchStatus } from "providers/cache";
|
||||||
|
import Moment from "react-moment";
|
||||||
|
import { extractMintDetails, MintDetails } from "./common";
|
||||||
|
import { Cluster, useCluster } from "providers/cluster";
|
||||||
|
import { reportError } from "utils/sentry";
|
||||||
|
|
||||||
|
type IndexedTransfer = {
|
||||||
|
index: number;
|
||||||
|
childIndex?: number;
|
||||||
|
transfer: Transfer | TransferChecked;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function TokenTransfersCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const { cluster } = useCluster();
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const history = useAccountHistory(address);
|
||||||
|
const fetchAccountHistory = useFetchAccountHistory();
|
||||||
|
const refresh = () => fetchAccountHistory(pubkey, true, true);
|
||||||
|
const loadMore = () => fetchAccountHistory(pubkey, true);
|
||||||
|
|
||||||
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
|
|
||||||
|
const mintDetails = React.useMemo(() => tokenRegistry.get(address), [
|
||||||
|
address,
|
||||||
|
tokenRegistry,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const transactionRows = React.useMemo(() => {
|
||||||
|
if (history?.data?.fetched) {
|
||||||
|
return getTransactionRows(history.data.fetched);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!history || !history.data?.transactionMap?.size) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const { hasTimestamps, detailsList } = React.useMemo(() => {
|
||||||
|
const detailedHistoryMap =
|
||||||
|
history?.data?.transactionMap ||
|
||||||
|
new Map<string, ParsedConfirmedTransaction>();
|
||||||
|
const hasTimestamps = transactionRows.some((element) => element.blockTime);
|
||||||
|
const detailsList: React.ReactNode[] = [];
|
||||||
|
const mintMap = new Map<string, MintDetails>();
|
||||||
|
|
||||||
|
transactionRows.forEach(
|
||||||
|
({ signature, blockTime, statusText, statusClass }) => {
|
||||||
|
const parsed = detailedHistoryMap.get(signature);
|
||||||
|
if (!parsed) return;
|
||||||
|
|
||||||
|
// Extract mint information from token deltas
|
||||||
|
// (used to filter out non-checked tokens transfers not belonging to this mint)
|
||||||
|
extractMintDetails(parsed, mintMap);
|
||||||
|
|
||||||
|
// Extract all transfers from transaction
|
||||||
|
let transfers: IndexedTransfer[] = [];
|
||||||
|
InstructionContainer.create(parsed).instructions.forEach(
|
||||||
|
({ instruction, inner }, index) => {
|
||||||
|
const transfer = getTransfer(instruction, cluster, signature);
|
||||||
|
if (transfer) {
|
||||||
|
transfers.push({
|
||||||
|
transfer,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
inner.forEach((instruction, childIndex) => {
|
||||||
|
const transfer = getTransfer(instruction, cluster, signature);
|
||||||
|
if (transfer) {
|
||||||
|
transfers.push({
|
||||||
|
transfer,
|
||||||
|
index,
|
||||||
|
childIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter out transfers not belonging to this mint
|
||||||
|
transfers = transfers.filter(({ transfer }) => {
|
||||||
|
const sourceKey = transfer.source.toBase58();
|
||||||
|
const destinationKey = transfer.destination.toBase58();
|
||||||
|
|
||||||
|
if ("tokenAmount" in transfer && transfer.mint.equals(pubkey)) {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
mintMap.has(sourceKey) &&
|
||||||
|
mintMap.get(sourceKey)?.mint === address
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
mintMap.has(destinationKey) &&
|
||||||
|
mintMap.get(destinationKey)?.mint === address
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
transfers.forEach(({ transfer, index, childIndex }) => {
|
||||||
|
let units = "Tokens";
|
||||||
|
let amountString = "";
|
||||||
|
|
||||||
|
if (mintDetails?.symbol) {
|
||||||
|
units = mintDetails.symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("tokenAmount" in transfer) {
|
||||||
|
amountString = transfer.tokenAmount.uiAmountString;
|
||||||
|
} else {
|
||||||
|
let decimals = 0;
|
||||||
|
|
||||||
|
if (mintDetails?.decimals) {
|
||||||
|
decimals = mintDetails.decimals;
|
||||||
|
} else if (mintMap.has(transfer.source.toBase58())) {
|
||||||
|
decimals = mintMap.get(transfer.source.toBase58())?.decimals || 0;
|
||||||
|
} else if (mintMap.has(transfer.destination.toBase58())) {
|
||||||
|
decimals =
|
||||||
|
mintMap.get(transfer.destination.toBase58())?.decimals || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
amountString = new Intl.NumberFormat("en-US", {
|
||||||
|
minimumFractionDigits: decimals,
|
||||||
|
maximumFractionDigits: decimals,
|
||||||
|
}).format(normalizeTokenAmount(transfer.amount, decimals));
|
||||||
|
}
|
||||||
|
|
||||||
|
detailsList.push(
|
||||||
|
<tr key={signature + index + (childIndex || "")}>
|
||||||
|
<td>
|
||||||
|
<Signature signature={signature} link truncateChars={24} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{hasTimestamps && (
|
||||||
|
<td className="text-muted">
|
||||||
|
{blockTime && <Moment date={blockTime * 1000} fromNow />}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Address pubkey={transfer.source} link truncateChars={16} />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<Address
|
||||||
|
pubkey={transfer.destination}
|
||||||
|
link
|
||||||
|
truncateChars={16}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{amountString} {units}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasTimestamps,
|
||||||
|
detailsList,
|
||||||
|
};
|
||||||
|
}, [history, transactionRows, mintDetails, pubkey, address, cluster]);
|
||||||
|
|
||||||
|
if (!history) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history?.data === undefined) {
|
||||||
|
if (history.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard message="Loading token transfers" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <ErrorCard retry={refresh} text="Failed to fetch token transfers" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetching = history.status === FetchStatus.Fetching;
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<HistoryCardHeader
|
||||||
|
fetching={fetching}
|
||||||
|
refresh={() => refresh()}
|
||||||
|
title="Token Transfers"
|
||||||
|
/>
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted">Transaction Signature</th>
|
||||||
|
{hasTimestamps && <th className="text-muted">Age</th>}
|
||||||
|
<th className="text-muted">Source</th>
|
||||||
|
<th className="text-muted">Destination</th>
|
||||||
|
<th className="text-muted">Amount</th>
|
||||||
|
<th className="text-muted">Result</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{detailsList}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<HistoryCardFooter
|
||||||
|
fetching={fetching}
|
||||||
|
foundOldest={history.data.foundOldest}
|
||||||
|
loadMore={() => loadMore()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTransfer(
|
||||||
|
instruction: ParsedInstruction | PartiallyDecodedInstruction,
|
||||||
|
cluster: Cluster,
|
||||||
|
signature: string
|
||||||
|
): Transfer | TransferChecked | undefined {
|
||||||
|
if ("parsed" in instruction && instruction.program === "spl-token") {
|
||||||
|
try {
|
||||||
|
const { type: rawType } = instruction.parsed;
|
||||||
|
const type = create(rawType, TokenInstructionType);
|
||||||
|
|
||||||
|
if (type === "transferChecked") {
|
||||||
|
return create(instruction.parsed.info, TransferChecked);
|
||||||
|
} else if (type === "transfer") {
|
||||||
|
return create(instruction.parsed.info, Transfer);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster === Cluster.MainnetBeta) {
|
||||||
|
reportError(error, {
|
||||||
|
signature,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Signature } from "components/common/Signature";
|
||||||
|
import { Slot } from "components/common/Slot";
|
||||||
|
import Moment from "react-moment";
|
||||||
|
import { PublicKey } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
useAccountHistory,
|
||||||
|
useFetchAccountHistory,
|
||||||
|
} from "providers/accounts/history";
|
||||||
|
import {
|
||||||
|
getTransactionRows,
|
||||||
|
HistoryCardFooter,
|
||||||
|
HistoryCardHeader,
|
||||||
|
} from "../HistoryCardComponents";
|
||||||
|
import { FetchStatus } from "providers/cache";
|
||||||
|
import { LoadingCard } from "components/common/LoadingCard";
|
||||||
|
import { ErrorCard } from "components/common/ErrorCard";
|
||||||
|
|
||||||
|
export function TransactionHistoryCard({ pubkey }: { pubkey: PublicKey }) {
|
||||||
|
const address = pubkey.toBase58();
|
||||||
|
const history = useAccountHistory(address);
|
||||||
|
const fetchAccountHistory = useFetchAccountHistory();
|
||||||
|
const refresh = () => fetchAccountHistory(pubkey, false, true);
|
||||||
|
const loadMore = () => fetchAccountHistory(pubkey, false);
|
||||||
|
|
||||||
|
const transactionRows = React.useMemo(() => {
|
||||||
|
if (history?.data?.fetched) {
|
||||||
|
return getTransactionRows(history.data.fetched);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [history]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!history) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}, [address]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (!history) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (history?.data === undefined) {
|
||||||
|
if (history.status === FetchStatus.Fetching) {
|
||||||
|
return <LoadingCard message="Loading history" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorCard retry={refresh} text="Failed to fetch transaction history" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTimestamps = transactionRows.some((element) => element.blockTime);
|
||||||
|
const detailsList: React.ReactNode[] = transactionRows.map(
|
||||||
|
({ slot, signature, blockTime, statusClass, statusText }) => {
|
||||||
|
return (
|
||||||
|
<tr key={signature}>
|
||||||
|
<td>
|
||||||
|
<Signature signature={signature} link truncate />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="w-1">
|
||||||
|
<Slot slot={slot} link />
|
||||||
|
</td>
|
||||||
|
|
||||||
|
{hasTimestamps && (
|
||||||
|
<td className="text-muted">
|
||||||
|
{blockTime ? <Moment date={blockTime * 1000} fromNow /> : "---"}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span className={`badge badge-soft-${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetching = history.status === FetchStatus.Fetching;
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<HistoryCardHeader
|
||||||
|
fetching={fetching}
|
||||||
|
refresh={() => refresh()}
|
||||||
|
title="Transaction History"
|
||||||
|
/>
|
||||||
|
<div className="table-responsive mb-0">
|
||||||
|
<table className="table table-sm table-nowrap card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-muted w-1">Transaction Signature</th>
|
||||||
|
<th className="text-muted w-1">Slot</th>
|
||||||
|
{hasTimestamps && <th className="text-muted w-1">Age</th>}
|
||||||
|
<th className="text-muted">Result</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="list">{detailsList}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<HistoryCardFooter
|
||||||
|
fetching={fetching}
|
||||||
|
foundOldest={history.data.foundOldest}
|
||||||
|
loadMore={() => loadMore()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { ParsedConfirmedTransaction } from "@solana/web3.js";
|
||||||
|
|
||||||
|
export type MintDetails = {
|
||||||
|
decimals: number;
|
||||||
|
mint: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function extractMintDetails(
|
||||||
|
parsedTransaction: ParsedConfirmedTransaction,
|
||||||
|
mintMap: Map<string, MintDetails>
|
||||||
|
) {
|
||||||
|
if (parsedTransaction.meta?.preTokenBalances) {
|
||||||
|
parsedTransaction.meta.preTokenBalances.forEach((balance) => {
|
||||||
|
const account =
|
||||||
|
parsedTransaction.transaction.message.accountKeys[balance.accountIndex];
|
||||||
|
mintMap.set(account.pubkey.toBase58(), {
|
||||||
|
decimals: balance.uiTokenAmount.decimals,
|
||||||
|
mint: balance.mint,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedTransaction.meta?.postTokenBalances) {
|
||||||
|
parsedTransaction.meta.postTokenBalances.forEach((balance) => {
|
||||||
|
const account =
|
||||||
|
parsedTransaction.transaction.message.accountKeys[balance.accountIndex];
|
||||||
|
mintMap.set(account.pubkey.toBase58(), {
|
||||||
|
decimals: balance.uiTokenAmount.decimals,
|
||||||
|
mint: balance.mint,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ type Props = {
|
||||||
raw?: boolean;
|
raw?: boolean;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
truncateUnknown?: boolean;
|
truncateUnknown?: boolean;
|
||||||
|
truncateChars?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Address({
|
export function Address({
|
||||||
|
@ -23,6 +24,7 @@ export function Address({
|
||||||
raw,
|
raw,
|
||||||
truncate,
|
truncate,
|
||||||
truncateUnknown,
|
truncateUnknown,
|
||||||
|
truncateChars,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const address = pubkey.toBase58();
|
const address = pubkey.toBase58();
|
||||||
const { tokenRegistry } = useTokenRegistry();
|
const { tokenRegistry } = useTokenRegistry();
|
||||||
|
@ -35,6 +37,14 @@ export function Address({
|
||||||
truncate = true;
|
truncate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let addressLabel = raw
|
||||||
|
? address
|
||||||
|
: displayAddress(address, cluster, tokenRegistry);
|
||||||
|
|
||||||
|
if (truncateChars && addressLabel === address) {
|
||||||
|
addressLabel = addressLabel.slice(0, truncateChars) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<Copyable text={address} replaceText={!alignRight}>
|
<Copyable text={address} replaceText={!alignRight}>
|
||||||
<span className="text-monospace">
|
<span className="text-monospace">
|
||||||
|
@ -43,11 +53,11 @@ export function Address({
|
||||||
className={truncate ? "text-truncate address-truncate" : ""}
|
className={truncate ? "text-truncate address-truncate" : ""}
|
||||||
to={clusterPath(`/address/${address}`)}
|
to={clusterPath(`/address/${address}`)}
|
||||||
>
|
>
|
||||||
{raw ? address : displayAddress(address, cluster, tokenRegistry)}
|
{addressLabel}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className={truncate ? "text-truncate address-truncate" : ""}>
|
<span className={truncate ? "text-truncate address-truncate" : ""}>
|
||||||
{raw ? address : displayAddress(address, cluster, tokenRegistry)}
|
{addressLabel}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ConfirmedSignatureInfo } from "@solana/web3.js";
|
||||||
|
import {
|
||||||
|
getTokenProgramInstructionName,
|
||||||
|
InstructionType,
|
||||||
|
} from "utils/instruction";
|
||||||
|
|
||||||
|
export function InstructionDetails({
|
||||||
|
instructionType,
|
||||||
|
tx,
|
||||||
|
}: {
|
||||||
|
instructionType: InstructionType;
|
||||||
|
tx: ConfirmedSignatureInfo;
|
||||||
|
}) {
|
||||||
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
|
|
||||||
|
let instructionTypes = instructionType.innerInstructions
|
||||||
|
.map((ix) => {
|
||||||
|
if ("parsed" in ix && ix.program === "spl-token") {
|
||||||
|
return getTokenProgramInstructionName(ix, tx);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter((type) => type !== undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p className="tree">
|
||||||
|
{instructionTypes.length > 0 && (
|
||||||
|
<span
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setExpanded(!expanded);
|
||||||
|
}}
|
||||||
|
className={`c-pointer fe mr-2 ${
|
||||||
|
expanded ? "fe-minus-square" : "fe-plus-square"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
)}
|
||||||
|
{instructionType.name}
|
||||||
|
</p>
|
||||||
|
{expanded && (
|
||||||
|
<ul className="tree">
|
||||||
|
{instructionTypes.map((type, index) => {
|
||||||
|
return <li key={index}>{type}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,9 +9,22 @@ type Props = {
|
||||||
alignRight?: boolean;
|
alignRight?: boolean;
|
||||||
link?: boolean;
|
link?: boolean;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
|
truncateChars?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Signature({ signature, alignRight, link, truncate }: Props) {
|
export function Signature({
|
||||||
|
signature,
|
||||||
|
alignRight,
|
||||||
|
link,
|
||||||
|
truncate,
|
||||||
|
truncateChars,
|
||||||
|
}: Props) {
|
||||||
|
let signatureLabel = signature;
|
||||||
|
|
||||||
|
if (truncateChars) {
|
||||||
|
signatureLabel = signature.slice(0, truncateChars) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`d-flex align-items-center ${
|
className={`d-flex align-items-center ${
|
||||||
|
@ -25,10 +38,10 @@ export function Signature({ signature, alignRight, link, truncate }: Props) {
|
||||||
className={truncate ? "text-truncate signature-truncate" : ""}
|
className={truncate ? "text-truncate signature-truncate" : ""}
|
||||||
to={clusterPath(`/tx/${signature}`)}
|
to={clusterPath(`/tx/${signature}`)}
|
||||||
>
|
>
|
||||||
{signature}
|
{signatureLabel}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
signature
|
signatureLabel
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</Copyable>
|
</Copyable>
|
||||||
|
|
|
@ -42,7 +42,8 @@ const InitializeMultisig = type({
|
||||||
m: number(),
|
m: number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const Transfer = type({
|
export type Transfer = Infer<typeof Transfer>;
|
||||||
|
export const Transfer = type({
|
||||||
source: PublicKeyFromString,
|
source: PublicKeyFromString,
|
||||||
destination: PublicKeyFromString,
|
destination: PublicKeyFromString,
|
||||||
amount: union([string(), number()]),
|
amount: union([string(), number()]),
|
||||||
|
@ -126,7 +127,8 @@ const ThawAccount = type({
|
||||||
signers: optional(array(PublicKeyFromString)),
|
signers: optional(array(PublicKeyFromString)),
|
||||||
});
|
});
|
||||||
|
|
||||||
const TransferChecked = type({
|
export type TransferChecked = Infer<typeof TransferChecked>;
|
||||||
|
export const TransferChecked = type({
|
||||||
source: PublicKeyFromString,
|
source: PublicKeyFromString,
|
||||||
mint: PublicKeyFromString,
|
mint: PublicKeyFromString,
|
||||||
destination: PublicKeyFromString,
|
destination: PublicKeyFromString,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { SignatureProps } from "pages/TransactionDetailsPage";
|
||||||
import { useTransactionDetails } from "providers/transactions";
|
import { useTransactionDetails } from "providers/transactions";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
|
|
||||||
type TokenBalanceRow = {
|
export type TokenBalanceRow = {
|
||||||
account: PublicKey;
|
account: PublicKey;
|
||||||
mint: string;
|
mint: string;
|
||||||
balance: TokenAmount;
|
balance: TokenAmount;
|
||||||
|
@ -92,7 +92,7 @@ export function TokenBalancesCard({ signature }: SignatureProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateTokenBalanceRows(
|
export function generateTokenBalanceRows(
|
||||||
preTokenBalances: TokenBalance[],
|
preTokenBalances: TokenBalance[],
|
||||||
postTokenBalances: TokenBalance[],
|
postTokenBalances: TokenBalance[],
|
||||||
accounts: ParsedMessageAccount[]
|
accounts: ParsedMessageAccount[]
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { NavLink, Redirect, useLocation } from "react-router-dom";
|
||||||
import { clusterPath } from "utils/url";
|
import { clusterPath } from "utils/url";
|
||||||
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
|
import { UnknownAccountCard } from "components/account/UnknownAccountCard";
|
||||||
import { OwnedTokensCard } from "components/account/OwnedTokensCard";
|
import { OwnedTokensCard } from "components/account/OwnedTokensCard";
|
||||||
import { TransactionHistoryCard } from "components/account/TransactionHistoryCard";
|
|
||||||
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
|
import { TokenHistoryCard } from "components/account/TokenHistoryCard";
|
||||||
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
|
import { TokenLargestAccountsCard } from "components/account/TokenLargestAccountsCard";
|
||||||
import { VoteAccountSection } from "components/account/VoteAccountSection";
|
import { VoteAccountSection } from "components/account/VoteAccountSection";
|
||||||
|
@ -31,34 +30,58 @@ import { useFlaggedAccounts } from "providers/accounts/flagged-accounts";
|
||||||
import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
|
import { UpgradeableLoaderAccountSection } from "components/account/UpgradeableLoaderAccountSection";
|
||||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||||
import { Identicon } from "components/common/Identicon";
|
import { Identicon } from "components/common/Identicon";
|
||||||
|
import { TransactionHistoryCard } from "components/account/history/TransactionHistoryCard";
|
||||||
|
import { TokenTransfersCard } from "components/account/history/TokenTransfersCard";
|
||||||
|
import { TokenInstructionsCard } from "components/account/history/TokenInstructionsCard";
|
||||||
|
|
||||||
const IDENTICON_WIDTH = 64;
|
const IDENTICON_WIDTH = 64;
|
||||||
const TABS_LOOKUP: { [id: string]: Tab } = {
|
|
||||||
"spl-token:mint": {
|
const TABS_LOOKUP: { [id: string]: Tab[] } = {
|
||||||
|
"spl-token:mint": [
|
||||||
|
{
|
||||||
|
slug: "transfers",
|
||||||
|
title: "Transfers",
|
||||||
|
path: "/transfers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "instructions",
|
||||||
|
title: "Instructions",
|
||||||
|
path: "/instructions",
|
||||||
|
},
|
||||||
|
{
|
||||||
slug: "largest",
|
slug: "largest",
|
||||||
title: "Distribution",
|
title: "Distribution",
|
||||||
path: "/largest",
|
path: "/largest",
|
||||||
},
|
},
|
||||||
vote: {
|
],
|
||||||
|
vote: [
|
||||||
|
{
|
||||||
slug: "vote-history",
|
slug: "vote-history",
|
||||||
title: "Vote History",
|
title: "Vote History",
|
||||||
path: "/vote-history",
|
path: "/vote-history",
|
||||||
},
|
},
|
||||||
"sysvar:recentBlockhashes": {
|
],
|
||||||
|
"sysvar:recentBlockhashes": [
|
||||||
|
{
|
||||||
slug: "blockhashes",
|
slug: "blockhashes",
|
||||||
title: "Blockhashes",
|
title: "Blockhashes",
|
||||||
path: "/blockhashes",
|
path: "/blockhashes",
|
||||||
},
|
},
|
||||||
"sysvar:slotHashes": {
|
],
|
||||||
|
"sysvar:slotHashes": [
|
||||||
|
{
|
||||||
slug: "slot-hashes",
|
slug: "slot-hashes",
|
||||||
title: "Slot Hashes",
|
title: "Slot Hashes",
|
||||||
path: "/slot-hashes",
|
path: "/slot-hashes",
|
||||||
},
|
},
|
||||||
"sysvar:stakeHistory": {
|
],
|
||||||
|
"sysvar:stakeHistory": [
|
||||||
|
{
|
||||||
slug: "stake-history",
|
slug: "stake-history",
|
||||||
title: "Stake History",
|
title: "Stake History",
|
||||||
path: "/stake-history",
|
path: "/stake-history",
|
||||||
},
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOKEN_TABS_HIDDEN = [
|
const TOKEN_TABS_HIDDEN = [
|
||||||
|
@ -248,14 +271,16 @@ type Tab = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MoreTabs =
|
export type MoreTabs =
|
||||||
| "history"
|
| "history"
|
||||||
| "tokens"
|
| "tokens"
|
||||||
| "largest"
|
| "largest"
|
||||||
| "vote-history"
|
| "vote-history"
|
||||||
| "slot-hashes"
|
| "slot-hashes"
|
||||||
| "stake-history"
|
| "stake-history"
|
||||||
| "blockhashes";
|
| "blockhashes"
|
||||||
|
| "transfers"
|
||||||
|
| "instructions";
|
||||||
|
|
||||||
function MoreSection({
|
function MoreSection({
|
||||||
account,
|
account,
|
||||||
|
@ -297,6 +322,8 @@ function MoreSection({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
{tab === "history" && <TransactionHistoryCard pubkey={pubkey} />}
|
||||||
|
{tab === "transfers" && <TokenTransfersCard pubkey={pubkey} />}
|
||||||
|
{tab === "instructions" && <TokenInstructionsCard pubkey={pubkey} />}
|
||||||
{tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />}
|
{tab === "largest" && <TokenLargestAccountsCard pubkey={pubkey} />}
|
||||||
{tab === "vote-history" && data?.program === "vote" && (
|
{tab === "vote-history" && data?.program === "vote" && (
|
||||||
<VotesCard voteAccount={data.parsed} />
|
<VotesCard voteAccount={data.parsed} />
|
||||||
|
@ -335,11 +362,11 @@ function getTabs(data?: ProgramData): Tab[] {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && data.program in TABS_LOOKUP) {
|
if (data && data.program in TABS_LOOKUP) {
|
||||||
tabs.push(TABS_LOOKUP[data.program]);
|
tabs.push(...TABS_LOOKUP[data.program]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data && programTypeKey in TABS_LOOKUP) {
|
if (data && programTypeKey in TABS_LOOKUP) {
|
||||||
tabs.push(TABS_LOOKUP[programTypeKey]);
|
tabs.push(...TABS_LOOKUP[programTypeKey]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -4,19 +4,26 @@ import {
|
||||||
ConfirmedSignatureInfo,
|
ConfirmedSignatureInfo,
|
||||||
TransactionSignature,
|
TransactionSignature,
|
||||||
Connection,
|
Connection,
|
||||||
|
ParsedConfirmedTransaction,
|
||||||
} from "@solana/web3.js";
|
} from "@solana/web3.js";
|
||||||
import { useCluster, Cluster } from "../cluster";
|
import { useCluster, Cluster } from "../cluster";
|
||||||
import * as Cache from "providers/cache";
|
import * as Cache from "providers/cache";
|
||||||
import { ActionType, FetchStatus } from "providers/cache";
|
import { ActionType, FetchStatus } from "providers/cache";
|
||||||
import { reportError } from "utils/sentry";
|
import { reportError } from "utils/sentry";
|
||||||
|
|
||||||
|
const MAX_TRANSACTION_BATCH_SIZE = 10;
|
||||||
|
|
||||||
|
type TransactionMap = Map<string, ParsedConfirmedTransaction>;
|
||||||
|
|
||||||
type AccountHistory = {
|
type AccountHistory = {
|
||||||
fetched: ConfirmedSignatureInfo[];
|
fetched: ConfirmedSignatureInfo[];
|
||||||
|
transactionMap?: TransactionMap;
|
||||||
foundOldest: boolean;
|
foundOldest: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HistoryUpdate = {
|
type HistoryUpdate = {
|
||||||
history?: AccountHistory;
|
history?: AccountHistory;
|
||||||
|
transactionMap?: TransactionMap;
|
||||||
before?: TransactionSignature;
|
before?: TransactionSignature;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,12 +59,19 @@ function reconcile(
|
||||||
update: HistoryUpdate | undefined
|
update: HistoryUpdate | undefined
|
||||||
) {
|
) {
|
||||||
if (update?.history === undefined) return history;
|
if (update?.history === undefined) return history;
|
||||||
|
|
||||||
|
let transactionMap = history?.transactionMap || new Map();
|
||||||
|
if (update.transactionMap) {
|
||||||
|
transactionMap = new Map([...transactionMap, ...update.transactionMap]);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fetched: combineFetched(
|
fetched: combineFetched(
|
||||||
update.history.fetched,
|
update.history.fetched,
|
||||||
history?.fetched,
|
history?.fetched,
|
||||||
update?.before
|
update?.before
|
||||||
),
|
),
|
||||||
|
transactionMap,
|
||||||
foundOldest: update?.history?.foundOldest || history?.foundOldest || false,
|
foundOldest: update?.history?.foundOldest || history?.foundOldest || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -83,12 +97,42 @@ export function HistoryProvider({ children }: HistoryProviderProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchParsedTransactions(
|
||||||
|
url: string,
|
||||||
|
transactionSignatures: string[]
|
||||||
|
) {
|
||||||
|
const transactionMap = new Map();
|
||||||
|
const connection = new Connection(url);
|
||||||
|
|
||||||
|
while (transactionSignatures.length > 0) {
|
||||||
|
const signatures = transactionSignatures.splice(
|
||||||
|
0,
|
||||||
|
MAX_TRANSACTION_BATCH_SIZE
|
||||||
|
);
|
||||||
|
const fetched = await connection.getParsedConfirmedTransactions(signatures);
|
||||||
|
fetched.forEach(
|
||||||
|
(parsed: ParsedConfirmedTransaction | null, index: number) => {
|
||||||
|
if (parsed !== null) {
|
||||||
|
transactionMap.set(signatures[index], parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactionMap;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchAccountHistory(
|
async function fetchAccountHistory(
|
||||||
dispatch: Dispatch,
|
dispatch: Dispatch,
|
||||||
pubkey: PublicKey,
|
pubkey: PublicKey,
|
||||||
cluster: Cluster,
|
cluster: Cluster,
|
||||||
url: string,
|
url: string,
|
||||||
options: { before?: TransactionSignature; limit: number }
|
options: {
|
||||||
|
before?: TransactionSignature;
|
||||||
|
limit: number;
|
||||||
|
additionalSignatures?: string[];
|
||||||
|
},
|
||||||
|
fetchTransactions?: boolean
|
||||||
) {
|
) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
|
@ -116,6 +160,22 @@ async function fetchAccountHistory(
|
||||||
}
|
}
|
||||||
status = FetchStatus.FetchFailed;
|
status = FetchStatus.FetchFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let transactionMap;
|
||||||
|
if (fetchTransactions && history?.fetched) {
|
||||||
|
try {
|
||||||
|
const signatures = history.fetched
|
||||||
|
.map((signature) => signature.signature)
|
||||||
|
.concat(options.additionalSignatures || []);
|
||||||
|
transactionMap = await fetchParsedTransactions(url, signatures);
|
||||||
|
} catch (error) {
|
||||||
|
if (cluster !== Cluster.Custom) {
|
||||||
|
reportError(error, { url });
|
||||||
|
}
|
||||||
|
status = FetchStatus.FetchFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ActionType.Update,
|
type: ActionType.Update,
|
||||||
url,
|
url,
|
||||||
|
@ -123,6 +183,7 @@ async function fetchAccountHistory(
|
||||||
status,
|
status,
|
||||||
data: {
|
data: {
|
||||||
history,
|
history,
|
||||||
|
transactionMap,
|
||||||
before: options?.before,
|
before: options?.before,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -152,6 +213,18 @@ export function useAccountHistory(
|
||||||
return context.entries[address];
|
return context.entries[address];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUnfetchedSignatures(before: Cache.CacheEntry<AccountHistory>) {
|
||||||
|
if (!before.data?.transactionMap) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingMap = before.data.transactionMap;
|
||||||
|
const allSignatures = before.data.fetched.map(
|
||||||
|
(signatureInfo) => signatureInfo.signature
|
||||||
|
);
|
||||||
|
return allSignatures.filter((signature) => !existingMap.has(signature));
|
||||||
|
}
|
||||||
|
|
||||||
export function useFetchAccountHistory() {
|
export function useFetchAccountHistory() {
|
||||||
const { cluster, url } = useCluster();
|
const { cluster, url } = useCluster();
|
||||||
const state = React.useContext(StateContext);
|
const state = React.useContext(StateContext);
|
||||||
|
@ -163,18 +236,39 @@ export function useFetchAccountHistory() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return React.useCallback(
|
return React.useCallback(
|
||||||
(pubkey: PublicKey, refresh?: boolean) => {
|
(pubkey: PublicKey, fetchTransactions?: boolean, refresh?: boolean) => {
|
||||||
const before = state.entries[pubkey.toBase58()];
|
const before = state.entries[pubkey.toBase58()];
|
||||||
if (!refresh && before?.data?.fetched && before.data.fetched.length > 0) {
|
if (!refresh && before?.data?.fetched && before.data.fetched.length > 0) {
|
||||||
if (before.data.foundOldest) return;
|
if (before.data.foundOldest) return;
|
||||||
|
|
||||||
|
let additionalSignatures: string[] = [];
|
||||||
|
if (fetchTransactions) {
|
||||||
|
additionalSignatures = getUnfetchedSignatures(before);
|
||||||
|
}
|
||||||
|
|
||||||
const oldest =
|
const oldest =
|
||||||
before.data.fetched[before.data.fetched.length - 1].signature;
|
before.data.fetched[before.data.fetched.length - 1].signature;
|
||||||
fetchAccountHistory(dispatch, pubkey, cluster, url, {
|
fetchAccountHistory(
|
||||||
|
dispatch,
|
||||||
|
pubkey,
|
||||||
|
cluster,
|
||||||
|
url,
|
||||||
|
{
|
||||||
before: oldest,
|
before: oldest,
|
||||||
limit: 25,
|
limit: 25,
|
||||||
});
|
additionalSignatures,
|
||||||
|
},
|
||||||
|
fetchTransactions
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
fetchAccountHistory(dispatch, pubkey, cluster, url, { limit: 25 });
|
fetchAccountHistory(
|
||||||
|
dispatch,
|
||||||
|
pubkey,
|
||||||
|
cluster,
|
||||||
|
url,
|
||||||
|
{ limit: 25 },
|
||||||
|
fetchTransactions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[state, dispatch, cluster, url]
|
[state, dispatch, cluster, url]
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
// Use this to write your custom SCSS
|
// Use this to write your custom SCSS
|
||||||
//
|
//
|
||||||
|
|
||||||
code, pre {
|
code,
|
||||||
|
pre {
|
||||||
padding: 0.33rem;
|
padding: 0.33rem;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
background-color: $gray-200;
|
background-color: $gray-200;
|
||||||
|
@ -21,7 +22,8 @@ ul.log-messages {
|
||||||
max-height: 20rem;
|
max-height: 20rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||||
|
"Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.popover-container {
|
.popover-container {
|
||||||
|
@ -214,7 +216,8 @@ h4.slot-pill {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.address-truncate, .signature-truncate {
|
.address-truncate,
|
||||||
|
.signature-truncate {
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -239,7 +242,7 @@ p.tree span.c-pointer {
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.tree ul {
|
ul.tree ul {
|
||||||
margin-left: 1.0em;
|
margin-left: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.tree li {
|
ul.tree li {
|
||||||
|
@ -288,8 +291,8 @@ div.inner-cards {
|
||||||
#chartjs-tooltip {
|
#chartjs-tooltip {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
-webkit-transition: all .1s ease;
|
-webkit-transition: all 0.1s ease;
|
||||||
transition: all .1s ease;
|
transition: all 0.1s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
-webkit-transform: translate(-50%, -105%);
|
-webkit-transform: translate(-50%, -105%);
|
||||||
transform: translate(-50%, -105%);
|
transform: translate(-50%, -105%);
|
||||||
|
@ -327,7 +330,7 @@ div.inner-cards {
|
||||||
border-left: 10px solid transparent;
|
border-left: 10px solid transparent;
|
||||||
-webkit-transform: translate(-50%, 0);
|
-webkit-transform: translate(-50%, 0);
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
content:'';
|
content: "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,7 +344,8 @@ div.inner-cards {
|
||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre.data-wrap, pre.json-wrap {
|
pre.data-wrap,
|
||||||
|
pre.json-wrap {
|
||||||
max-width: 23rem;
|
max-width: 23rem;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
white-space: -moz-pre-wrap;
|
white-space: -moz-pre-wrap;
|
||||||
|
@ -356,7 +360,7 @@ pre.json-wrap {
|
||||||
|
|
||||||
.staking-card {
|
.staking-card {
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: .75rem;
|
margin-bottom: 0.75rem;
|
||||||
small {
|
small {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -366,12 +370,12 @@ pre.json-wrap {
|
||||||
}
|
}
|
||||||
em {
|
em {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
color: $primary
|
color: $primary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.updated-time {
|
p.updated-time {
|
||||||
font-size: .66rem;
|
font-size: 0.66rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ export function displayTimestamp(
|
||||||
const expireDate = new Date(unixTimestamp);
|
const expireDate = new Date(unixTimestamp);
|
||||||
const dateString = new Intl.DateTimeFormat("en-US", {
|
const dateString = new Intl.DateTimeFormat("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "long",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
}).format(expireDate);
|
}).format(expireDate);
|
||||||
const timeString = new Intl.DateTimeFormat("en-US", {
|
const timeString = new Intl.DateTimeFormat("en-US", {
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
import { create } from "superstruct";
|
||||||
|
import {
|
||||||
|
IX_TITLES,
|
||||||
|
TokenInstructionType,
|
||||||
|
} from "components/instruction/token/types";
|
||||||
|
import { ParsedInfo } from "validators";
|
||||||
|
import { reportError } from "utils/sentry";
|
||||||
|
import {
|
||||||
|
ConfirmedSignatureInfo,
|
||||||
|
ParsedConfirmedTransaction,
|
||||||
|
ParsedInstruction,
|
||||||
|
PartiallyDecodedInstruction,
|
||||||
|
} from "@solana/web3.js";
|
||||||
|
import { intoTransactionInstruction } from "utils/tx";
|
||||||
|
import {
|
||||||
|
isTokenSwapInstruction,
|
||||||
|
parseTokenSwapInstructionTitle,
|
||||||
|
} from "components/instruction/token-swap/types";
|
||||||
|
import {
|
||||||
|
isTokenLendingInstruction,
|
||||||
|
parseTokenLendingInstructionTitle,
|
||||||
|
} from "components/instruction/token-lending/types";
|
||||||
|
import {
|
||||||
|
isSerumInstruction,
|
||||||
|
parseSerumInstructionTitle,
|
||||||
|
} from "components/instruction/serum/types";
|
||||||
|
import { TOKEN_PROGRAM_ID } from "providers/accounts/tokens";
|
||||||
|
|
||||||
|
export type InstructionType = {
|
||||||
|
name: string;
|
||||||
|
innerInstructions: (ParsedInstruction | PartiallyDecodedInstruction)[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface InstructionItem {
|
||||||
|
instruction: ParsedInstruction | PartiallyDecodedInstruction;
|
||||||
|
inner: (ParsedInstruction | PartiallyDecodedInstruction)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InstructionContainer {
|
||||||
|
readonly instructions: InstructionItem[];
|
||||||
|
|
||||||
|
static create(parsedTransaction: ParsedConfirmedTransaction) {
|
||||||
|
return new InstructionContainer(parsedTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(parsedTransaction: ParsedConfirmedTransaction) {
|
||||||
|
this.instructions = parsedTransaction.transaction.message.instructions.map(
|
||||||
|
(instruction) => {
|
||||||
|
if ("parsed" in instruction) {
|
||||||
|
instruction.parsed = create(instruction.parsed, ParsedInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
instruction,
|
||||||
|
inner: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (parsedTransaction.meta?.innerInstructions) {
|
||||||
|
for (let inner of parsedTransaction.meta.innerInstructions) {
|
||||||
|
this.instructions[inner.index].inner.push(...inner.instructions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenProgramInstructionName(
|
||||||
|
ix: ParsedInstruction,
|
||||||
|
signatureInfo: ConfirmedSignatureInfo
|
||||||
|
): string {
|
||||||
|
try {
|
||||||
|
const parsed = create(ix.parsed, ParsedInfo);
|
||||||
|
const { type: rawType } = parsed;
|
||||||
|
const type = create(rawType, TokenInstructionType);
|
||||||
|
return IX_TITLES[type];
|
||||||
|
} catch (err) {
|
||||||
|
reportError(err, { signature: signatureInfo.signature });
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenInstructionName(
|
||||||
|
transaction: ParsedConfirmedTransaction,
|
||||||
|
ix: ParsedInstruction | PartiallyDecodedInstruction,
|
||||||
|
signatureInfo: ConfirmedSignatureInfo
|
||||||
|
) {
|
||||||
|
let name = "Unknown";
|
||||||
|
|
||||||
|
let transactionInstruction;
|
||||||
|
if (transaction?.transaction) {
|
||||||
|
transactionInstruction = intoTransactionInstruction(
|
||||||
|
transaction.transaction,
|
||||||
|
ix
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("parsed" in ix) {
|
||||||
|
if (ix.program === "spl-token") {
|
||||||
|
name = getTokenProgramInstructionName(ix, signatureInfo);
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
transactionInstruction &&
|
||||||
|
isSerumInstruction(transactionInstruction)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
name = parseSerumInstructionTitle(transactionInstruction);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, { signature: signatureInfo.signature });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
transactionInstruction &&
|
||||||
|
isTokenSwapInstruction(transactionInstruction)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
name = parseTokenSwapInstructionTitle(transactionInstruction);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, { signature: signatureInfo.signature });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
transactionInstruction &&
|
||||||
|
isTokenLendingInstruction(transactionInstruction)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
name = parseTokenLendingInstructionTitle(transactionInstruction);
|
||||||
|
} catch (error) {
|
||||||
|
reportError(error, { signature: signatureInfo.signature });
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
ix.accounts.findIndex((account) => account.equals(TOKEN_PROGRAM_ID)) >= 0
|
||||||
|
) {
|
||||||
|
name = "Unknown (Inner)";
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTokenInstructionType(
|
||||||
|
transaction: ParsedConfirmedTransaction,
|
||||||
|
ix: ParsedInstruction | PartiallyDecodedInstruction,
|
||||||
|
signatureInfo: ConfirmedSignatureInfo,
|
||||||
|
index: number
|
||||||
|
): InstructionType | undefined {
|
||||||
|
const innerInstructions: (
|
||||||
|
| ParsedInstruction
|
||||||
|
| PartiallyDecodedInstruction
|
||||||
|
)[] = [];
|
||||||
|
|
||||||
|
if (transaction.meta?.innerInstructions) {
|
||||||
|
transaction.meta.innerInstructions.forEach((ix) => {
|
||||||
|
if (ix.index === index) {
|
||||||
|
ix.instructions.forEach((inner) => {
|
||||||
|
innerInstructions.push(inner);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let name =
|
||||||
|
getTokenInstructionName(transaction, ix, signatureInfo) || "Unknown";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
innerInstructions,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue