UNPKG

@debridge-finance/solana-utils

Version:

Common utils package to power communication with Solana contracts at deBridge

491 lines 20 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccountType = void 0; exports.getTokenInfo = getTokenInfo; exports.createAssociatedWalletInstruction = createAssociatedWalletInstruction; exports.parseSplAccount = parseSplAccount; exports.isWalletCorrectATA = isWalletCorrectATA; exports.getAccountWithType = getAccountWithType; exports.getNativeWalletBalance = getNativeWalletBalance; exports.getAssocSPLWalletBalance = getAssocSPLWalletBalance; exports.getSPLWalletBalance = getSPLWalletBalance; exports.getAllTokenAccountsWithBalances = getAllTokenAccountsWithBalances; exports.getTokenAccountsWithBalance = getTokenAccountsWithBalance; exports.buildReplenishWsolBalanceTransaction = buildReplenishWsolBalanceTransaction; exports.updateWsolBalance = updateWsolBalance; exports.checkIfAssociatedWalletExists = checkIfAssociatedWalletExists; exports.getTransactionAccountsForSimulationDiff = getTransactionAccountsForSimulationDiff; exports.parseMultipleAccounts = parseMultipleAccounts; exports.getSplDiff = getSplDiff; exports.getTransactionDiff = getTransactionDiff; const tslib_1 = require("tslib"); const web3_js_1 = require("@solana/web3.js"); const spl_token_1 = require("@solana/spl-token"); const spl_token_metadata_1 = require("@solana/spl-token-metadata"); const bl = tslib_1.__importStar(require("@solana/buffer-layout")); const blu = tslib_1.__importStar(require("@solana/buffer-layout-utils")); const bn_js_1 = tslib_1.__importDefault(require("bn.js")); const buffer_1 = require("buffer"); const accounts_1 = require("./accounts"); const helpers_1 = require("./helpers"); var AccountState; (function (AccountState) { AccountState[AccountState["Uninitialized"] = 0] = "Uninitialized"; AccountState[AccountState["Initialized"] = 1] = "Initialized"; AccountState[AccountState["Frozen"] = 2] = "Frozen"; })(AccountState || (AccountState = {})); const AccountLayout = bl.struct([ blu.publicKey("mint"), blu.publicKey("owner"), blu.u64("amount"), bl.u32("delegateOption"), blu.publicKey("delegate"), bl.u8("state"), bl.u32("isNativeOption"), blu.u64("isNative"), blu.u64("delegatedAmount"), bl.u32("closeAuthorityOption"), blu.publicKey("closeAuthority"), ]); var AccountType; (function (AccountType) { AccountType[AccountType["System"] = 0] = "System"; AccountType[AccountType["Token"] = 1] = "Token"; AccountType[AccountType["CorrectATA"] = 2] = "CorrectATA"; AccountType[AccountType["Unknown"] = 3] = "Unknown"; AccountType[AccountType["NotExists"] = 4] = "NotExists"; })(AccountType || (exports.AccountType = AccountType = {})); /** * Get name, symbol and decimals for provided token * @param connection solana web3 connection * @param tokenAddress address to get metaplex info for * @returns */ async function getTokenInfo(connection, tokenAddress) { const tokenMetaProgramId = new web3_js_1.PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); const findMetaplexMetadataPda = (tokenMint) => web3_js_1.PublicKey.findProgramAddressSync([ buffer_1.Buffer.from("metadata", "utf-8"), tokenMetaProgramId.toBuffer(), tokenMint.toBuffer(), ], tokenMetaProgramId)[0]; const decodeMetaplexMetadataPartial = (data) => { const rawName = data.subarray(65 + 4, 65 + 36); // omit first 4 bytes, size is already hardcoded const rawSymbol = data.subarray(101 + 4, 115); // omit first 4 bytes, size is already hardcoded const rawUri = data.subarray(115 + 4, 204); // omit first 4 bytes, size is already hardcoded return { uri: rawUri.toString("utf-8").replace(/\u0000/g, ""), symbol: rawSymbol.toString("utf-8").replace(/\u0000/g, ""), name: rawName.toString("utf-8").replace(/\u0000/g, ""), }; }; const pda = findMetaplexMetadataPda(tokenAddress); const [tokenAccount, pdaAccount] = await connection.getMultipleAccountsInfo([ tokenAddress, pda, ]); if (tokenAccount === null) { return null; } const mint2022 = (0, spl_token_1.unpackMint)(tokenAddress, tokenAccount, tokenAccount.owner); const extTypes = (0, spl_token_1.getExtensionTypes)(mint2022.tlvData ?? buffer_1.Buffer.alloc(0)); const tokenMetaBuf = extTypes.includes(spl_token_1.ExtensionType.TokenMetadata) ? (0, spl_token_1.getExtensionData)(spl_token_1.ExtensionType.TokenMetadata, mint2022.tlvData) : null; if (tokenMetaBuf) { const meta = (0, spl_token_metadata_1.unpack)(tokenMetaBuf); return { address: tokenAddress, decimals: mint2022.decimals, name: meta.name, symbol: meta.symbol, json: meta.uri, }; } if (extTypes.includes(spl_token_1.ExtensionType.MetadataPointer)) { const mpState = (0, spl_token_1.getMetadataPointerState)(mint2022); if (mpState?.metadataAddress && mpState.metadataAddress.equals(tokenAddress)) { const inlineMetaBuf = (0, spl_token_1.getExtensionData)(spl_token_1.ExtensionType.TokenMetadata, mint2022.tlvData); if (inlineMetaBuf) { const meta = (0, spl_token_metadata_1.unpack)(inlineMetaBuf); return { address: tokenAddress, decimals: mint2022.decimals, name: meta.name, symbol: meta.symbol, json: meta.uri, }; } } } if (pdaAccount === null || pdaAccount?.lamports === 0) { return null; } // parse token account // mint_authority - coption(pubkey) = 4 or 36 // supply - u64 = 8 // decimals - u8 let decimalsOffset = 12; if (tokenAccount.data[0] === 1) decimalsOffset += 32; const decimals = tokenAccount.data[decimalsOffset]; const { name, symbol, uri } = decodeMetaplexMetadataPartial(pdaAccount.data); return { address: tokenAddress, decimals, name, symbol, json: uri, }; } /** * Builds instruction for creation of associated wallet for specified token * @param tokenMint mint account of SPL-token * @param associatedAccount associated account address * @param owner owner of the associated account * @param payer who pays for account creation */ function createAssociatedWalletInstruction(tokenMint, associatedAccount, owner, payer, associatedTokenProramId) { tokenMint = new web3_js_1.PublicKey(tokenMint); payer = new web3_js_1.PublicKey(payer); owner = new web3_js_1.PublicKey(owner); return new web3_js_1.TransactionInstruction({ programId: associatedTokenProramId || accounts_1.ASSOCIATED_TOKEN_PROGRAM_ID, data: buffer_1.Buffer.from([1]), keys: [ { pubkey: payer, isSigner: true, isWritable: true, }, { pubkey: associatedAccount, isSigner: false, isWritable: true, }, { pubkey: owner, isSigner: false, isWritable: false, }, { pubkey: tokenMint, isSigner: false, isWritable: false, }, { pubkey: web3_js_1.SystemProgram.programId, isSigner: false, isWritable: false, }, { pubkey: accounts_1.TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], }); } function parseSplAccount(dataOrInfo) { if (dataOrInfo === null) { return null; } const data = "executable" in dataOrInfo ? dataOrInfo.data : dataOrInfo; const truncated = data.subarray(0, 165); // only use static part // if (data.length === 82) return null; // token mint try { return AccountLayout.decode(data); } catch (e) { console.error(e); } return null; } function isWalletCorrectATA(walletAddress, data) { const decoded = parseSplAccount(data); if (!decoded) return false; const [realAta] = (0, accounts_1.findAssociatedTokenAddress)(decoded.owner, decoded.mint); return realAta.equals(new web3_js_1.PublicKey(walletAddress)); } /** * Requests account from blockchain and returns result with account type * @param account address of account * @returns account info and account type */ async function getAccountWithType(connection, account) { account = new web3_js_1.PublicKey(account); const accountData = await (0, helpers_1.getAccountInfo)(connection, account); if (!accountData) return [null, AccountType.NotExists]; if (accountData.owner.equals(accounts_1.TOKEN_PROGRAM_ID)) { return isWalletCorrectATA(account, accountData.data) ? [accountData, AccountType.CorrectATA] : [accountData, AccountType.Token]; } if (accountData.owner.equals(web3_js_1.SystemProgram.programId)) return [accountData, AccountType.System]; return [accountData, AccountType.Unknown]; } /** * Returns balance of specified wallet in native tokens * @param wallet wallet to inspect */ async function getNativeWalletBalance(connection, wallet) { wallet = new web3_js_1.PublicKey(wallet); const result = await connection.getBalance(wallet); return new bn_js_1.default(result.toString()); } /** * Returns balance of associated wallet in SPL-tokens * @param originalWallet native tokens wallet address * @param tokenMint mint of SPL-Token */ async function getAssocSPLWalletBalance(connection, originalWallet, tokenMint) { originalWallet = new web3_js_1.PublicKey(originalWallet); tokenMint = new web3_js_1.PublicKey(tokenMint); const [wallet] = (0, accounts_1.findAssociatedTokenAddress)(originalWallet, tokenMint); const result = await connection.getTokenAccountBalance(wallet, "finalized"); return new bn_js_1.default(result.value.amount); } /** * Returns balance of specified wallet in SPL-tokens * @param wallet spl-tokens wallet address */ async function getSPLWalletBalance(connection, wallet) { wallet = new web3_js_1.PublicKey(wallet); const result = await connection.getTokenAccountBalance(wallet, "finalized"); return new bn_js_1.default(result.value.amount); } /** * Gets list of user's SPL-token accounts * @param owner owner of the SPL wallets * @returns list of accounts with amount on them */ async function getAllTokenAccountsWithBalances(connection, owner) { const walletsData = await connection.getParsedTokenAccountsByOwner(new web3_js_1.PublicKey(owner), { programId: accounts_1.TOKEN_PROGRAM_ID }); const wallets = walletsData.value; return wallets.map((wallet) => { const tokenInfo = wallet.account.data.parsed; return { address: wallet.pubkey, mint: new web3_js_1.PublicKey(tokenInfo.info.mint), amount: new bn_js_1.default(tokenInfo.info.tokenAmount.amount), decimals: tokenInfo.info.tokenAmount.decimals, isInitialized: true, isNative: tokenInfo.info.isNative, owner: new web3_js_1.PublicKey(tokenInfo.info.owner), }; }); } /** * Gets list of user's tokenMint token accounts * @param tokenMint * @param owner owner of the wallets * @returns list of accounts with amount on them */ async function getTokenAccountsWithBalance(connection, tokenMint, owner) { const walletsData = await connection.getParsedTokenAccountsByOwner(new web3_js_1.PublicKey(owner), { mint: new web3_js_1.PublicKey(tokenMint) }); const wallets = walletsData.value; return wallets.map((wallet) => { const tokenInfo = wallet.account.data.parsed; return { address: wallet.pubkey, mint: new web3_js_1.PublicKey(tokenInfo.info.mint), amount: new bn_js_1.default(tokenInfo.info.tokenAmount.amount), decimals: tokenInfo.info.tokenAmount.decimals, isInitialized: true, isNative: tokenInfo.info.isNative, owner: new web3_js_1.PublicKey(tokenInfo.info.owner), }; }); } /** * Builds transaction to transfer&wrap native sol from src wallet to dst wallet * @param amount number of lamports to transfer and wrap * @param transferFrom source native account * @param transferTo destination wallet * @returns transaction to transfer&wrap sol */ function buildReplenishWsolBalanceTransaction(amount, transferFrom, transferTo) { transferFrom = new web3_js_1.PublicKey(transferFrom); transferTo = new web3_js_1.PublicKey(transferTo); const tx = new web3_js_1.Transaction(); tx.add(web3_js_1.SystemProgram.transfer({ fromPubkey: transferFrom, toPubkey: transferTo, lamports: new bn_js_1.default(amount).toNumber(), })); tx.add( // sync native new web3_js_1.TransactionInstruction({ keys: [{ pubkey: transferTo, isSigner: false, isWritable: true }], programId: accounts_1.TOKEN_PROGRAM_ID, data: buffer_1.Buffer.from([17]), })); tx.feePayer = transferFrom; return tx; } /** * Builds transaction that wraps specified amount of lamports into spl, if account is missing will create it * @param amount lamports to wrap, ATA will contain exact amount of lamports * @param owner owner of created wallet */ async function updateWsolBalance(connection, amount, owner) { owner = new web3_js_1.PublicKey(owner); const tx = new web3_js_1.Transaction(); const [wSolATA] = (0, accounts_1.findAssociatedTokenAddress)(owner, accounts_1.WRAPPED_SOL_MINT); const existingATA = await (0, helpers_1.getAccountInfo)(connection, wSolATA); let amountToWrap = new bn_js_1.default(amount); if (existingATA === null) { tx.add(createAssociatedWalletInstruction(accounts_1.WRAPPED_SOL_MINT, wSolATA, owner, owner)); } else { const parsed = parseSplAccount(existingATA.data); if (!parsed) throw new Error("WSol account exists, but failed to decode its content"); const parsedAmount = new bn_js_1.default(parsed.amount.toString()); if (amountToWrap.lte(parsedAmount)) return null; // don't need to wrap amountToWrap = amountToWrap.sub(parsedAmount); } const replenishTx = buildReplenishWsolBalanceTransaction(amountToWrap, owner, wSolATA); tx.add(replenishTx); return tx; } /** * Checks existance of associated wallet for specified token * @param mintAccount mint account of SPL-token * @param originalWallet account of solana wallet * @returns true if account exists */ async function checkIfAssociatedWalletExists(connection, mintAccount, originalWallet) { const [walletAccount] = (0, accounts_1.findAssociatedTokenAddress)(originalWallet, mintAccount); const accountData = await (0, helpers_1.getAccountInfo)(connection, walletAccount); return accountData !== null; } async function getTransactionAccountsForSimulationDiff(connection, tx) { const result = new Set(); if ("version" in tx) { const ALTs = tx.message.addressTableLookups.map((alt) => alt.accountKey); const fetchedALTs = await connection.getMultipleAccountsInfo(ALTs); const parsedALTs = fetchedALTs .map((fetched, i) => fetched ? new web3_js_1.AddressLookupTableAccount({ state: web3_js_1.AddressLookupTableAccount.deserialize(fetched.data), key: ALTs[i], }) : null) .filter((x) => x != null); const accGetter = tx.message.getAccountKeys({ addressLookupTableAccounts: parsedALTs, }); const staticWritableOrSignerAccounts = []; for (let idx = 0; idx < accGetter.staticAccountKeys.length; idx += 1) { if (tx.message.isAccountSigner(idx) || tx.message.isAccountWritable(idx)) staticWritableOrSignerAccounts.push(accGetter.staticAccountKeys[idx]); } for (const acc of [ ...staticWritableOrSignerAccounts, ...(accGetter.accountKeysFromLookups?.writable ?? []), ]) { result.add(acc.toBase58()); } } else { const compiled = tx.compileMessage(); for (let idx = 0; idx < compiled.accountKeys.length; idx += 1) { if (compiled.isAccountSigner(idx) || compiled.isAccountWritable(idx)) result.add(compiled.staticAccountKeys[idx].toBase58()); } } return Array.from(result); } function parseMultipleAccounts(accounts) { return accounts.map((account) => { let parsedSplAccount; if (account) { const owner = new web3_js_1.PublicKey(account.owner); if (owner.equals(accounts_1.TOKEN_PROGRAM_ID) || owner.equals(accounts_1.TOKEN_2022_PROGRAM_ID)) { if (account.data instanceof buffer_1.Buffer) { parsedSplAccount = parseSplAccount(account.data); } else if (account.data instanceof Array) { parsedSplAccount = parseSplAccount(buffer_1.Buffer.from(account.data[0], account.data[1])); } } } const parsedAccount = { balance: account?.lamports ?? 0, }; if (parsedSplAccount) { parsedAccount.spl = { mint: parsedSplAccount.mint, owner: parsedSplAccount.owner, balance: parsedSplAccount.amount ?? BigInt(0), }; } return parsedAccount; }); } function getSplDiff(pre, post) { const preBalance = pre?.balance ?? BigInt(0); const postBalance = post?.balance ?? BigInt(0); return { mint: pre?.mint ?? post?.mint, owner: pre?.owner ?? post?.owner, preBalance: pre?.balance ?? 0, postBalance: post?.balance ?? 0, diff: postBalance - preBalance, }; } async function getTransactionDiff(connection, tx, txAccounts) { const accountsStrings = txAccounts ? txAccounts.map((acc) => acc.toBase58()) : await getTransactionAccountsForSimulationDiff(connection, tx); const accountsPubKeys = txAccounts ?? accountsStrings.map((key) => new web3_js_1.PublicKey(key)); let [accountsInfo, simulatedTx] = await Promise.all([ connection.getMultipleAccountsInfo(accountsPubKeys), connection.simulateTransaction(tx, { replaceRecentBlockhash: true, accounts: { encoding: "base64", addresses: accountsStrings, }, }), ]); if (simulatedTx.value.err !== null) throw new Error(JSON.stringify({ err: simulatedTx.value.err, logs: simulatedTx.value.logs, })); const simulatesInfo = simulatedTx.value.accounts; const preBalances = parseMultipleAccounts(accountsInfo); const postBalances = parseMultipleAccounts(simulatesInfo); const balancesDiff = accountsPubKeys.reduce((map, pubKey, i) => { if (accountsInfo[i]?.executable) { return map; } const preBalance = preBalances[i]; const postBalance = postBalances[i]; const diff = { pubKey: pubKey, preBalance: preBalance.balance, postBalance: postBalance.balance, diff: postBalance.balance - preBalance.balance, }; if (preBalance.spl || postBalance.spl) { const splDiff = getSplDiff(preBalance.spl, postBalance.spl); if (splDiff.diff) { diff.spl = splDiff; } } if (diff.diff || diff.spl) { map.set(pubKey.toString(), diff); } return map; }, new Map()); return balancesDiff; } //# sourceMappingURL=spl.js.map