This README provides a detailed explanation of how to fetch, parse, and display activities related to NFTs and SPL tokens on the Solana blockchain along with submitted Backend transaction submitted to blockchain. It includes example code snippets and references to additional resources for further reading.
Use Solana's RPC method getsignaturesforaddress
to get a list of confirmed transaction signatures.
- Import the necessary classes from the Solana Web3.js library.
- Establish a connection to the Solana mainnet using the class
Connection
new Connection().
- Use the getSignaturesForAddress method to fetch the transaction signatures for the specified wallet address.
- Map the fetched signatures to a format suitable for further processing.
import { Connection, PublicKey } from "@solana/web3.js";
async function fetchActivities(walletAddress: string) {
const connection = new Connection('https://api.mainnet-beta.solana.com');
const signatures = await connection.getSignaturesForAddress(
new PublicKey(walletAddress)
);
return signatures.map(signature => {
// Additional parsing logic here
return signature;
});
}
const walletAddress = "Your_Solana_Wallet_Address";
fetchActivities(walletAddress).then(activityLog => console.log(activityLog));
To convert raw signatures into a user-readable format, we need to parse each transaction and extract relevant details.
- For each fetched signature, retrieve the corresponding transaction using the getParsedTransaction method.
- Extract relevant details such as fee, balances, account keys, and instructions from each transaction.
- Return the parsed transactions in a structured format.
import { Connection, PublicKey, ParsedTransactionWithMeta } from "@solana/web3.js";
async function fetchAndParseActivities(walletAddress: string) {
const connection = new Connection('https://api.mainnet-beta.solana.com');
const signatures = await connection.getSignaturesForAddress(
new PublicKey(walletAddress)
);
const activities = await Promise.all(signatures.map(async (signature) => {
const transaction = await connection.getParsedTransaction(signature.signature);
return parseTransaction(transaction);
}));
return activities;
}
function parseTransaction(transaction: ParsedTransactionWithMeta) {
const { meta, transaction: { message } } = transaction;
const { postBalances, preBalances } = meta;
const { accountKeys, instructions } = message;
return {
signature: transaction.transaction.signatures[0],
fee: meta.fee,
postBalances,
preBalances,
accountKeys: accountKeys.map(key => key.toBase58()),
instructions: instructions.map(instruction => ({
programId: instruction.programId.toBase58(),
data: instruction.data,
keys: instruction.keys.map(key => key.pubkey.toBase58())
}))
};
}
const walletAddress = "Your_Solana_Wallet_Address";
fetchAndParseActivities(walletAddress).then(activityLog => console.log(activityLog));
To display the activities, we need to map the parsed transactions to an activity log format that users can easily understand.
- Fetch the transaction signatures and corresponding parsed transactions for the specified wallet address.
- Format the fetched transactions into a user-readable activity log.
- Log the formatted activities to the console or display them in the UI.
1. The function formatTransactionToActivity
takes an object as a parameter. This object contains the following properties:
transactions
: An array of parsed transactions.signaturesInfo
: An array of confirmed signature information.chainId
: The ID of the blockchain network.blockExplorerUrl
: The URL of the block explorer.selectedAddress
: The wallet address for which activities are being fetched.
- Initialize an array
finalTxs
to store the formatted activities. Loop through each signature information and corresponding transaction.
- For each transaction, create a base activity object with common properties such as slot, status, updatedAt, signature, and blockExplorerUrl.
Identify the relevant instructions within the transaction. This involves checking for specific instruction types such as create, transfer, and burn.
Parse the relevant instructions to extract details such as source, destination, amount, and type of transaction. Update the activity object with these details.
import { Connection, PublicKey, ParsedTransactionWithMeta, ConfirmedSignatureInfo } from "@solana/web3.js";
async function displayActivities(walletAddress: string) {
const transactions = fetchAndParseActivities(walletAddress)
const formattedActivities = formatTransactionToActivity({
transactions,
signaturesInfo: signatures,
chainId: 'mainnet-beta',
blockExplorerUrl: 'https://explorer.solana.com',
selectedAddress: walletAddress,
});
console.log(formattedActivities);
}
const formatTransactionToActivity = (params: {
transactions: ParsedTransactionWithMeta[];
signaturesInfo: ConfirmedSignatureInfo[];
chainId: string;
blockExplorerUrl: string;
selectedAddress: string;
}) => {
const { transactions, signaturesInfo, chainId, blockExplorerUrl, selectedAddress } = params;
const finalTxs = signaturesInfo.map((info, index) => {
const tx = transactions[index];
const finalObject: SolanaTransactionActivity = {
slot: info.slot.toString(),
status: tx?.meta?.err ? TransactionStatus.failed : TransactionStatus.confirmed,
updatedAt: info.blockTime * 1000,
signature: info.signature,
txReceipt: info.signature,
blockExplorerUrl: `${blockExplorerUrl}/tx/${info.signature}?cluster=${chainId}`,
chainId,
network: chainId,
rawDate: new Date(info.blockTime * 1000).toISOString(),
action: 'unknown',
type: "unknown",
decimal: 9,
};
if (!tx?.meta) return finalObject;
let interestedTransactionInstructionIdx = -1;
const instructionLength = tx.transaction.message.instructions.length;
if (instructionLength > 1 && instructionLength <= 3) {
const createInstructionIdx = tx.transaction.message.instructions.findIndex((inst) => {
if (inst.programId.equals(ASSOCIATED_TOKEN_PROGRAM_ID)) {
return (inst as unknown as ParsedInstruction).parsed?.type === "create";
}
return false;
});
if (createInstructionIdx >= 0) {
const transferIdx = tx.transaction.message.instructions.findIndex((inst) => {
return ["transfer", "transferChecked"].includes((inst as unknown as ParsedInstruction).parsed?.type);
});
interestedTransactionInstructionIdx = transferIdx;
} else {
const burnIdx = tx.transaction.message.instructions.findIndex((inst) => {
return ["burn", "burnChecked"].includes((inst as unknown as ParsedInstruction).parsed?.type);
});
interestedTransactionInstructionIdx = burnIdx;
}
}
const interestedTransactionType = ["transfer", "transferChecked", "burn", "burnChecked"];
if (tx.transaction.message.instructions.length === 1 || interestedTransactionInstructionIdx >= 0) {
if (tx.transaction.message.instructions.length === 1) interestedTransactionInstructionIdx = 0;
const inst: ParsedInstruction = tx.transaction.message.instructions[interestedTransactionInstructionIdx] as unknown as ParsedInstruction;
if (inst.parsed && interestedTransactionType.includes(inst.parsed.type)) {
if (inst.program === "spl-token") {
const source = inst.parsed.info.authority;
if (tx.meta.postTokenBalances?.length <= 1) {
finalObject.from = source;
finalObject.to = source;
} else {
finalObject.from = source;
finalObject.to = tx.meta.postTokenBalances[0].owner === source ? tx.meta.postTokenBalances[1].owner : tx.meta.postTokenBalances[0].owner;
}
const { mint } = ["burn", "burnChecked"].includes(inst.parsed.type) ? inst.parsed.info : tx.meta.postTokenBalances[0];
const amount = ["burnChecked", "transferChecked"].includes(inst.parsed.type)
? inst.parsed.info.tokenAmount.amount
: inst.parsed.info.amount;
const decimals = ["burnChecked", "transferChecked"].includes(inst.parsed.type)
? inst.parsed.info.tokenAmount.decimals
: inst.parsed.info.decimals;
finalObject.cryptoAmount = amount;
finalObject.cryptoCurrency = "-";
finalObject.fee = tx.meta.fee;
finalObject.type = inst.parsed.type;
finalObject.send = finalObject.from === selectedAddress;
finalObject.action = finalObject.send ? 'send' : 'receive';
finalObject.decimal = decimals;
finalObject.totalAmountString = (amount / Math.pow(10, decimals)).toString();
finalObject.logoURI = "";
finalObject.mintAddress = mint;
} else if (inst.program === "system") {
finalObject.from = inst.parsed.info.source;
finalObject.to = inst.parsed.info.destination;
finalObject.cryptoAmount = inst.parsed.info.lamports;
finalObject.cryptoCurrency = "SOL";
finalObject.fee = tx.meta.fee;
finalObject.type = inst.parsed.type;
finalObject.send = inst.parsed.info.source === selectedAddress;
finalObject.action = finalObject.send ? 'send' : 'receive';
finalObject.decimal = 9;
finalObject.totalAmountString = (inst.parsed.info.lamports / Math.pow(10, 9)).toString();
}
}
}
return finalObject;
});
return finalTxs;
};
const walletAddress = "Your_Solana_Wallet_Address";
displayActivities(walletAddress);
This method fetches incoming backend transactions and stores them in the state.
- Use the
getBackendTransactions
method to fetch transactions from the backend.
- Format the fetched transactions using the
formatBackendTxToActivity
helper function.
- Update the state with the formatted transactions.
async fetchIncomingBackendTransaction(address: string) {
try {
const data = await this.getWalletOrders<FetchedTransaction>(address);
if (data.length > 0) {
const fmtData = data.map((item) => formatBackendTxToActivity(item, address));
this.updateState({ incomingBackendTransactions: fmtData }, address);
} else {
this.updateState({ incomingBackendTransactions: [] }, address);
}
} catch (error) {
log.error("unable to fetch wallet orders", error);
}
}
This method updates the display activities with the latest data from the blockchain and merges them with the transactions stored in the backend.
- Fetch the latest transaction signatures from the blockchain.
- Filter out transactions that are already confirmed.
- Fetch and parse the transactions corresponding to the filtered signatures.
- Format the parsed transactions into a user-readable activity log.
- Merge the formatted transactions with the transactions stored in the backend.
- Update the state with the merged transactions.
async updateDisplayActivities(newActivities?: { [keyof: string]: SolanaTransactionActivity }): Promise<SolanaTransactionActivity[]> {
const address = this.state.selectedAddress;
const { chainId, blockExplorerUrl } = this.getProviderConfig();
const connection = this.getConnection();
// Get latest signature from blockchain for main Account
const signatureInfo = await connection.getSignaturesForAddress(new PublicKey(address), { limit: this.config.TX_LIMIT || 40 });
// Filter out local's signature that is confirmed
const displayActivities: { [keyof: string]: SolanaTransactionActivity } = newActivities || this.getAddressState(address)?.displayActivities || {};
const filteredSignaturesInfo = signatureInfo.filter((info) => {
const activity = displayActivities[info.signature];
if (activity) {
return activity.status !== TransactionStatus.confirmed;
}
return true;
});
// get parsed confirmed transactions and format it to local activity display
let incomingBlockchainTransactions: ParsedTransactionWithMeta[] = [];
if (filteredSignaturesInfo.length > 0) {
incomingBlockchainTransactions = await connection.parseTransaction(
filteredSignaturesInfo.map((s) => s.signature),
{ maxSupportedTransactionVersion: 0 }
);
}
const incomingBlockchainActivities = formatTransactionToActivity({
transactions: incomingBlockchainTransactions,
signaturesInfo: filteredSignaturesInfo,
chainId,
blockExplorerUrl,
selectedAddress: this.state.selectedAddress,
});
// patch backend and merge new activities with local's
incomingBlockchainActivities.forEach((item) => {
const activity = displayActivities[item.signature];
// new incoming transaction from blockchain
if (!activity) {
displayActivities[item.signature] = item;
} else if (item.status !== activity.status) {
activity.status = item.status;
if (activity.id) {
this.patchPastTx({ id: activity.id.toString(), status: activity.status, updated_at: new Date().toISOString() }, address);
this.updateIncomingTransaction(activity.status, activity.id);
}
}
});
this.updateState({ displayActivities }, address);
return Object.values(displayActivities);
}