UNPKG

solana-framework

Version:

solana-framework is solana uni-tools for typescript

499 lines 21.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.confirmSignature = exports.getTokenAccountsByOwner = exports.getProgramAccounts = exports.getParsedBlockTransactions = exports.getParsedTransactionsFromSlot = exports.getTokenInfos = exports.getTokenAccountInfos = exports.parseTransfers = exports.parseTransfersBySignatures = exports.mustGetMultipleParsedAccounts = exports.createTokenTransferInstructions = exports.isExistAccount = exports.getPriorityFee = exports.sendTransaction = exports.sendInstructionsV0 = exports.sendInstructions = exports.getRecentBlockhash = exports.getSignatures = exports.mustGetParsedTransactions = exports.getSimulationComputeUnits = exports.CONFIG = void 0; const spl_token_1 = require("@solana/spl-token"); const web3_js_1 = require("@solana/web3.js"); const sleep_1 = require("./sleep"); exports.CONFIG = { RETRY_SEC: 0.5, QUICK_RETRY_SEC: 0.1, // getRecentBlockhash / Blockhash not found }; // Was getSimulationUnits // https://github.com/solana-developers/helpers/blob/7bfb9f6f77c04877764f373116ccdc14bf214b71/src/index.ts#L330 async function getSimulationComputeUnits(connection, instructions, payer, lookupTables = []) { const testInstructions = [ // Set an arbitrarily high number in simulation // so we can be sure the transaction will succeed // and get the real compute units used web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: 1400000 }), ...instructions, ]; const testTransaction = new web3_js_1.VersionedTransaction(new web3_js_1.TransactionMessage({ instructions: testInstructions, payerKey: payer, // RecentBlockhash can by any public key during simulation // since 'replaceRecentBlockhash' is set to 'true' below recentBlockhash: web3_js_1.PublicKey.default.toString(), }).compileToV0Message(lookupTables)); const rpcResponse = await connection.simulateTransaction(testTransaction, { replaceRecentBlockhash: true, sigVerify: false, }); getErrorFromRPCResponse(rpcResponse); return rpcResponse.value.unitsConsumed || null; } exports.getSimulationComputeUnits = getSimulationComputeUnits; const getErrorFromRPCResponse = (rpcResponse) => { // Note: `confirmTransaction` does not throw an error if the confirmation does not succeed, // but rather a `TransactionError` object. so we handle that here // See https://solana-labs.github.io/solana-web3.js/classes/Connection.html#confirmTransaction.confirmTransaction-1 const error = rpcResponse.value.err; if (error) { // Can be a string or an object (literally just {}, no further typing is provided by the library) // https://github.com/solana-labs/solana-web3.js/blob/4436ba5189548fc3444a9f6efb51098272926945/packages/library-legacy/src/connection.ts#L2930 // TODO: if still occurs in web3.js 2 (unlikely), fix it. if (typeof error === "object") { const errorKeys = Object.keys(error); if (errorKeys.length === 1) { if (errorKeys[0] !== "InstructionError") { throw new Error(`Unknown RPC error: ${error}`); } // @ts-ignore due to missing typing information mentioned above. const instructionError = error["InstructionError"]; // An instruction error is a custom program error and looks like: // [ // 1, // { // "Custom": 1 // } // ] // See also https://solana.stackexchange.com/a/931/294 throw new Error(`Error in transaction: instruction index ${instructionError[0]}, custom program error ${instructionError[1]["Custom"]}`); } } throw Error(error.toString()); } }; async function mustGetParsedTransactions(connection, signatures) { while (true) { try { const txs = await connection.getParsedTransactions(signatures, { commitment: "confirmed", maxSupportedTransactionVersion: 0, }); if (!txs.some((tx) => !tx)) { return txs; } } catch (e) { console.log(`[WARN] mustGetParsedTransactions: ${e}`); } await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } exports.mustGetParsedTransactions = mustGetParsedTransactions; // 1- The `start` is not include // 2- The returned array is from end to start, like [end, ..., start) or [] async function getSignatures(connection, target, start, includeErrTx) { const limit = 1000; async function _getSignatures(end) { let signatures; while (true) { try { signatures = await connection.getSignaturesForAddress(target, { limit: limit, until: start, // stop position (not include) before: end, // start position (not include) }, "confirmed"); if (signatures.length === 0) return []; break; } catch (e) { console.log(`[WARN] getSignatures: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } if (signatures.length === limit) { return [ ...signatures, ...await _getSignatures(signatures.at(-1).signature), ]; } return signatures; } const signatures = await _getSignatures(); return includeErrTx ? signatures : signatures.filter((s) => !s.err); } exports.getSignatures = getSignatures; async function getRecentBlockhash(connection) { while (true) { try { const blockhash = await connection.getLatestBlockhash(); return blockhash.blockhash; } catch (e) { console.log(`[WARN] getRecentBlockhash: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.QUICK_RETRY_SEC); } } } exports.getRecentBlockhash = getRecentBlockhash; async function sendInstructions(connection, ixs, signers, maxExecErrorRetry = 1, commitment, skipPreflight = true) { const recentSignedTransaction = async () => { const transaction = new web3_js_1.Transaction(); transaction.add(...ixs); transaction.feePayer = signers[0].publicKey; transaction.recentBlockhash = await getRecentBlockhash(connection); transaction.sign(...signers); return transaction; }; return sendTransaction(connection, recentSignedTransaction, maxExecErrorRetry, commitment, skipPreflight); } exports.sendInstructions = sendInstructions; async function sendInstructionsV0(connection, ixs, addressLookupTableAccounts, signers, maxExecErrorRetry = 1, commitment, skipPreflight = true) { const recentSignedTransaction = async () => { const messageV0 = new web3_js_1.TransactionMessage({ payerKey: signers[0].publicKey, recentBlockhash: await getRecentBlockhash(connection), instructions: ixs, }).compileToV0Message(addressLookupTableAccounts); const transaction = new web3_js_1.VersionedTransaction(messageV0); transaction.sign(signers); return transaction; }; return sendTransaction(connection, recentSignedTransaction, maxExecErrorRetry, commitment, skipPreflight); } exports.sendInstructionsV0 = sendInstructionsV0; async function sendTransaction(connection, recentSignedTransaction, maxExecErrorRetry = 1, commitment, skipPreflight = true) { for (let i = 0; i < maxExecErrorRetry;) { try { const transaction = await recentSignedTransaction(); const signature = await connection.sendRawTransaction(transaction.serialize(), { skipPreflight: skipPreflight }); console.log(`TX = ${signature}`); if (commitment) { await connection.confirmTransaction({ signature }, commitment); } return signature; } catch (e) { const msg = `${e}`; if (msg.indexOf("Blockhash not found") !== -1) { console.log(`[WARN] sendTransactionInstructions: ${msg}`); await (0, sleep_1.sleep)(exports.CONFIG.QUICK_RETRY_SEC); } else if (msg.indexOf("Retrying") !== -1) { console.log(`[WARN] sendTransactionInstructions: ${msg}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } else if (msg.indexOf("Unable to perform request")) { console.log(`[WARN] sendTransactionInstructions: ${msg}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } else { // UNEXPECTED ERROR!!! console.log(`[ERROR] sendTransactionInstructions: ${msg}`); if (i++ < maxExecErrorRetry) { await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } } } exports.sendTransaction = sendTransaction; async function getPriorityFee(connection) { while (true) { try { let prioritizationFees = await connection.getRecentPrioritizationFees(); let length = prioritizationFees.length; let prioritizationZeros = 0, prioritizationTotal = 0; for (let i = 0; i < length; i++) { if (prioritizationFees[i].prioritizationFee === 0) { prioritizationZeros++; } else { prioritizationTotal += prioritizationFees[i].prioritizationFee; } } if (prioritizationZeros >= length / 3) { return 0; } return Math.ceil(prioritizationTotal / (length - prioritizationZeros) * 1.05); } catch (e) { console.log(`[WARNING] ${getPriorityFee.name}: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } exports.getPriorityFee = getPriorityFee; async function isExistAccount(connection, address, commitmentOrConfig) { while (true) { try { const info = await connection.getAccountInfo(address, commitmentOrConfig); return !!info?.lamports; } catch (e) { console.log(`[WARNING] ${isExistAccount.name}: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } exports.isExistAccount = isExistAccount; async function createTokenTransferInstructions(connection, mint, // token from, to, amount, isDelegated = false, // Is `to` delegated by `from`? (default: false). if true, `to` is payer and receiver tokenProgram = spl_token_1.TOKEN_PROGRAM_ID, AssociatedTokenProgram = spl_token_1.ASSOCIATED_TOKEN_PROGRAM_ID) { const associatedFrom = await (0, spl_token_1.getAssociatedTokenAddress)(mint, from, true, tokenProgram, AssociatedTokenProgram); const associatedTo = await (0, spl_token_1.getAssociatedTokenAddress)(mint, to, true, tokenProgram, AssociatedTokenProgram); const transfer = (0, spl_token_1.createTransferInstruction)(associatedFrom, associatedTo, isDelegated ? to : from, // if delegated, use `to` amount, undefined, tokenProgram); const isExist = await isExistAccount(connection, associatedTo); if (isExist) { return [transfer]; } const create = (0, spl_token_1.createAssociatedTokenAccountInstruction)(isDelegated ? to : from, // `to` is payer associatedTo, to, mint, tokenProgram, AssociatedTokenProgram); return [create, transfer]; } exports.createTokenTransferInstructions = createTokenTransferInstructions; // only the accounts are all existing! async function mustGetMultipleParsedAccounts(connection, publicKeys) { while (true) { try { const accounts = await connection.getMultipleParsedAccounts(publicKeys, { commitment: "confirmed" }); if (accounts.value.some((a) => !a)) { continue; } return accounts; } catch (e) { console.log(mustGetMultipleParsedAccounts.name, e); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } exports.mustGetMultipleParsedAccounts = mustGetMultipleParsedAccounts; async function parseTransfersBySignatures(connection, signatures, options = {}) { const transactions = await mustGetParsedTransactions(connection, signatures.map((s) => (typeof s === "string" ? s : s.signature))); return parseTransfers(connection, transactions, options); } exports.parseTransfersBySignatures = parseTransfersBySignatures; async function parseTransfers(connection, transactions, options = {}) { const list = []; for (let i = 0; i < transactions.length; i++) { const signature = transactions[i].transaction.signatures[0]; const transaction = transactions[i]; const instructions = transaction?.transaction?.message?.instructions; if (instructions) { const proms = []; for (let j = 0; j < instructions.length; j++) { const instruction = instructions[j]; switch (instruction.program) { case "spl-token": { if (instruction.parsed?.type === "transfer" || instruction.parsed?.type === "transferChecked") { proms.push(parseTokenTransferDataIfValid(connection, instruction, signature, j, transaction, options.filterTokenTransfer)); } break; } case "system": { const data = parseTransferDataIfValid(instruction, signature, j, transaction, options.filterTransfer); if (data) list.push(data); break; } } } const txs = (await Promise.all(proms)).filter((d) => d); list.push(...txs); } } return list; } exports.parseTransfers = parseTransfers; async function parseTokenTransferDataIfValid(connection, instruction, signature, index, tx, filterTokenTransfer = () => true) { const { info } = instruction.parsed; const amount = info.amount ?? info?.tokenAmount?.amount ?? "0"; if (Number(amount) > 0 && filterTokenTransfer(info)) { const publicKeys = [ new web3_js_1.PublicKey(info.source), new web3_js_1.PublicKey(info.destination), ]; const accountsContext = await mustGetMultipleParsedAccounts(connection, publicKeys); const accounts = accountsContext.value; const { info: acc0 } = accounts[0].data.parsed; const { info: acc1 } = accounts[1].data.parsed; const data = { chain: "solana", token: acc0.mint, from_wallet: acc0.owner, to_wallet: acc1.owner, hash: signature, index: index, amount: amount, extra: { program: instruction.program, program_id: instruction.programId.toBase58(), authority: info.authority, associated_from: info.source, associated_to: info.destination, type: instruction.parsed.type, }, error: !!tx.meta?.err, fee: tx.meta?.fee ? String(tx.meta.fee) : "-1", version: tx.version?.toString() ?? "", processed_at: tx.blockTime, }; return data; } } function parseTransferDataIfValid(instruction, signature, index, tx, filterTransfer = () => true) { const { info } = instruction.parsed; if (info.lamports > 0 && filterTransfer(info)) { const data = { chain: "solana", token: "", from_wallet: info.source, to_wallet: info.destination, hash: signature, index: index, amount: String(info.lamports), extra: { program: instruction.program, program_id: instruction.programId.toBase58(), authority: (info?.authority ?? ""), type: instruction.parsed.type, }, error: !!tx.meta?.err, fee: tx.meta?.fee ? String(tx.meta.fee) : "-1", version: tx.version?.toString() ?? "", processed_at: tx.blockTime, }; return data; } } async function getTokenAccountInfos(connection, accounts) { while (true) { try { const infos = await connection.getMultipleAccountsInfo(accounts); return infos.map((info, i) => { if (info) { return (0, spl_token_1.unpackAccount)(accounts[i], info); } return undefined; }); } catch (e) { console.log(`[WARNING] ${getTokenAccountInfos.name}: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } exports.getTokenAccountInfos = getTokenAccountInfos; async function getTokenInfos(connection, tokens) { try { const infos = await connection.getMultipleAccountsInfo(tokens); return infos.map((info, i) => { if (info) { try { return (0, spl_token_1.unpackMint)(tokens[i], info); } catch (e) { return undefined; } } return undefined; }); } catch (e) { return undefined; } } exports.getTokenInfos = getTokenInfos; async function getParsedTransactionsFromSlot(connection, fromSlot, isSync) { let toSlot; while (true) { try { toSlot = await connection.getSlot('confirmed'); if (fromSlot > toSlot) { console.log(`[WARN] getParsedTransactionsFromSlot: fromSlot(${fromSlot}) > toSlot(${toSlot}), wait for next slot(${exports.CONFIG.RETRY_SEC * 10}s)...`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC * 10); continue; } break; } catch (e) { await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } const transactions = []; if (isSync) { for (let i = fromSlot; i <= toSlot; i++) { transactions.push(...(await getParsedBlockTransactions(connection, i))); } } else { const proms = []; for (let i = fromSlot; i <= toSlot; i++) { proms.push(getParsedBlockTransactions(connection, i)); } (await Promise.all(proms)).map((txs) => transactions.push(...txs)); } return { transactions, nextSlot: toSlot + 1 }; } exports.getParsedTransactionsFromSlot = getParsedTransactionsFromSlot; async function getParsedBlockTransactions(connection, blocknumber) { while (true) { try { const block = await connection.getParsedBlock(blocknumber, { commitment: 'confirmed', maxSupportedTransactionVersion: 0 }); if (block.transactions) { block.transactions.forEach((tx) => tx.blockTime = block.blockTime); return block.transactions; } return []; } catch (e) { console.log(e); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } } exports.getParsedBlockTransactions = getParsedBlockTransactions; async function getProgramAccounts(connection, wallet) { const response = await connection.getProgramAccounts(spl_token_1.TOKEN_PROGRAM_ID, { filters: [ { dataSize: 165 }, { memcmp: { offset: 32, bytes: wallet } } ] }); return response.map(v => (0, spl_token_1.unpackAccount)(v.pubkey, v.account)); } exports.getProgramAccounts = getProgramAccounts; async function getTokenAccountsByOwner(connection, wallet) { const response = await connection.getTokenAccountsByOwner(new web3_js_1.PublicKey(wallet), { programId: spl_token_1.TOKEN_PROGRAM_ID }); return response.value.map(v => (0, spl_token_1.unpackAccount)(v.pubkey, v.account)); } exports.getTokenAccountsByOwner = getTokenAccountsByOwner; async function confirmSignature(connection, signature, preWaitSeconds = 0.5, maxTry = 3, everyWaitSeconds = 2.8) { await (0, sleep_1.sleep)(preWaitSeconds); for (let i = 1; i <= maxTry; i++) { try { const response = await connection.getSignatureStatus(signature, { searchTransactionHistory: true }); if (response && response.value) { if (response.value.err) { return { hash: signature, error: response.value.err }; } const { confirmationStatus } = response.value; if (confirmationStatus === "processed") { i--; } else if (confirmationStatus === "confirmed" || confirmationStatus === "finalized") { return { hash: signature, error: undefined, raw: response.value }; } } if (i < maxTry) { await (0, sleep_1.sleep)(everyWaitSeconds); } } catch (e) { console.log(`[ERROR] Connection::confirmSignature: ${e}`); await (0, sleep_1.sleep)(exports.CONFIG.RETRY_SEC); } } return { hash: signature, error: "Network error! Transaction not detected!" }; } exports.confirmSignature = confirmSignature; //# sourceMappingURL=Connection.js.map