solana-framework
Version:
solana-framework is solana uni-tools for typescript
499 lines • 21.8 kB
JavaScript
;
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