UNPKG

@debridge-finance/solana-utils

Version:

Common utils package to power communication with Solana contracts at deBridge

460 lines 19.1 kB
import { __awaiter } from "tslib"; import { AddressLookupTableAccount, PublicKey, SystemProgram, Transaction, TransactionInstruction, } from "@solana/web3.js"; import * as bl from "@solana/buffer-layout"; import * as blu from "@solana/buffer-layout-utils"; import BN from "bn.js"; import { Buffer } from "buffer"; import { ASSOCIATED_TOKEN_PROGRAM_ID, findAssociatedTokenAddress, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT, } from "./accounts"; import { getAccountInfo } from "./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"), ]); export 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 || (AccountType = {})); /** * Get name, symbol and decimals for provided token * @param connection solana web3 connection * @param tokenAddress address to get metaplex info for * @returns */ export function getTokenInfo(connection, tokenAddress) { return __awaiter(this, void 0, void 0, function* () { const tokenMetaProgramId = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); const findMetaplexMetadataPda = (tokenMint) => PublicKey.findProgramAddressSync([ 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] = yield connection.getMultipleAccountsInfo([ tokenAddress, pda, ]); if (tokenAccount === null || pdaAccount === null || (pdaAccount === null || pdaAccount === void 0 ? void 0 : 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 */ export function createAssociatedWalletInstruction(tokenMint, associatedAccount, owner, payer, associatedTokenProramId) { tokenMint = new PublicKey(tokenMint); payer = new PublicKey(payer); owner = new PublicKey(owner); return new TransactionInstruction({ programId: associatedTokenProramId || ASSOCIATED_TOKEN_PROGRAM_ID, data: 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: SystemProgram.programId, isSigner: false, isWritable: false, }, { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false, }, ], }); } export function parseSplAccount(dataOrInfo) { if (dataOrInfo === null) { return null; } const data = "executable" in dataOrInfo ? dataOrInfo.data : dataOrInfo; // if (data.length === 82) return null; // token mint try { return AccountLayout.decode(data); } catch (e) { console.error(e); } return null; } export function isWalletCorrectATA(walletAddress, data) { const decoded = parseSplAccount(data); if (!decoded) return false; const [realAta] = findAssociatedTokenAddress(decoded.owner, decoded.mint); return realAta.equals(new PublicKey(walletAddress)); } /** * Requests account from blockchain and returns result with account type * @param account address of account * @returns account info and account type */ export function getAccountWithType(connection, account) { return __awaiter(this, void 0, void 0, function* () { account = new PublicKey(account); const accountData = yield getAccountInfo(connection, account); if (!accountData) return [null, AccountType.NotExists]; if (accountData.owner.equals(TOKEN_PROGRAM_ID)) { return isWalletCorrectATA(account, accountData.data) ? [accountData, AccountType.CorrectATA] : [accountData, AccountType.Token]; } if (accountData.owner.equals(SystemProgram.programId)) return [accountData, AccountType.System]; return [accountData, AccountType.Unknown]; }); } /** * Returns balance of specified wallet in native tokens * @param wallet wallet to inspect */ export function getNativeWalletBalance(connection, wallet) { return __awaiter(this, void 0, void 0, function* () { wallet = new PublicKey(wallet); const result = yield connection.getBalance(wallet); return new BN(result.toString()); }); } /** * Returns balance of associated wallet in SPL-tokens * @param originalWallet native tokens wallet address * @param tokenMint mint of SPL-Token */ export function getAssocSPLWalletBalance(connection, originalWallet, tokenMint) { return __awaiter(this, void 0, void 0, function* () { originalWallet = new PublicKey(originalWallet); tokenMint = new PublicKey(tokenMint); const [wallet] = findAssociatedTokenAddress(originalWallet, tokenMint); const result = yield connection.getTokenAccountBalance(wallet, "finalized"); return new BN(result.value.amount); }); } /** * Returns balance of specified wallet in SPL-tokens * @param wallet spl-tokens wallet address */ export function getSPLWalletBalance(connection, wallet) { return __awaiter(this, void 0, void 0, function* () { wallet = new PublicKey(wallet); const result = yield connection.getTokenAccountBalance(wallet, "finalized"); return new BN(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 */ export function getAllTokenAccountsWithBalances(connection, owner) { return __awaiter(this, void 0, void 0, function* () { const walletsData = yield connection.getParsedTokenAccountsByOwner(new PublicKey(owner), { programId: TOKEN_PROGRAM_ID }); const wallets = walletsData.value; return wallets.map((wallet) => { const tokenInfo = wallet.account.data.parsed; return { address: wallet.pubkey, mint: new PublicKey(tokenInfo.info.mint), amount: new BN(tokenInfo.info.tokenAmount.amount), decimals: tokenInfo.info.tokenAmount.decimals, isInitialized: true, isNative: tokenInfo.info.isNative, owner: new 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 */ export function getTokenAccountsWithBalance(connection, tokenMint, owner) { return __awaiter(this, void 0, void 0, function* () { const walletsData = yield connection.getParsedTokenAccountsByOwner(new PublicKey(owner), { mint: new PublicKey(tokenMint) }); const wallets = walletsData.value; return wallets.map((wallet) => { const tokenInfo = wallet.account.data.parsed; return { address: wallet.pubkey, mint: new PublicKey(tokenInfo.info.mint), amount: new BN(tokenInfo.info.tokenAmount.amount), decimals: tokenInfo.info.tokenAmount.decimals, isInitialized: true, isNative: tokenInfo.info.isNative, owner: new 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 */ export function buildReplenishWsolBalanceTransaction(amount, transferFrom, transferTo) { transferFrom = new PublicKey(transferFrom); transferTo = new PublicKey(transferTo); const tx = new Transaction(); tx.add(SystemProgram.transfer({ fromPubkey: transferFrom, toPubkey: transferTo, lamports: new BN(amount).toNumber(), })); tx.add( // sync native new TransactionInstruction({ keys: [{ pubkey: transferTo, isSigner: false, isWritable: true }], programId: TOKEN_PROGRAM_ID, data: 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 */ export function updateWsolBalance(connection, amount, owner) { return __awaiter(this, void 0, void 0, function* () { owner = new PublicKey(owner); const tx = new Transaction(); const [wSolATA] = findAssociatedTokenAddress(owner, WRAPPED_SOL_MINT); const existingATA = yield getAccountInfo(connection, wSolATA); let amountToWrap = new BN(amount); if (existingATA === null) { tx.add(createAssociatedWalletInstruction(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(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 */ export function checkIfAssociatedWalletExists(connection, mintAccount, originalWallet) { return __awaiter(this, void 0, void 0, function* () { const [walletAccount] = findAssociatedTokenAddress(originalWallet, mintAccount); const accountData = yield getAccountInfo(connection, walletAccount); return accountData !== null; }); } export function getTransactionAccountsForSimulationDiff(connection, tx) { return __awaiter(this, void 0, void 0, function* () { var _a, _b; const result = new Set(); if ("version" in tx) { const ALTs = tx.message.addressTableLookups.map((alt) => alt.accountKey); const fetchedALTs = yield connection.getMultipleAccountsInfo(ALTs); const parsedALTs = fetchedALTs .map((fetched, i) => fetched ? new AddressLookupTableAccount({ state: 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, ...((_b = (_a = accGetter.accountKeysFromLookups) === null || _a === void 0 ? void 0 : _a.writable) !== null && _b !== void 0 ? _b : []), ]) { 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); }); } export function parseMultipleAccounts(accounts) { return accounts.map((account) => { var _a, _b; let parsedSplAccount; if (account) { const owner = new PublicKey(account.owner); if (owner.equals(TOKEN_PROGRAM_ID)) { if (account.data instanceof Buffer) { parsedSplAccount = parseSplAccount(account.data); } else if (account.data instanceof Array) { parsedSplAccount = parseSplAccount(Buffer.from(account.data[0], account.data[1])); } } } const parsedAccount = { balance: (_a = account === null || account === void 0 ? void 0 : account.lamports) !== null && _a !== void 0 ? _a : 0, }; if (parsedSplAccount) { parsedAccount.spl = { mint: parsedSplAccount.mint, owner: parsedSplAccount.owner, balance: (_b = parsedSplAccount.amount) !== null && _b !== void 0 ? _b : BigInt(0), }; } return parsedAccount; }); } export function getSplDiff(pre, post) { var _a, _b, _c, _d, _e, _f; const preBalance = (_a = pre === null || pre === void 0 ? void 0 : pre.balance) !== null && _a !== void 0 ? _a : BigInt(0); const postBalance = (_b = post === null || post === void 0 ? void 0 : post.balance) !== null && _b !== void 0 ? _b : BigInt(0); return { mint: (_c = pre === null || pre === void 0 ? void 0 : pre.mint) !== null && _c !== void 0 ? _c : post === null || post === void 0 ? void 0 : post.mint, owner: (_d = pre === null || pre === void 0 ? void 0 : pre.owner) !== null && _d !== void 0 ? _d : post === null || post === void 0 ? void 0 : post.owner, preBalance: (_e = pre === null || pre === void 0 ? void 0 : pre.balance) !== null && _e !== void 0 ? _e : 0, postBalance: (_f = post === null || post === void 0 ? void 0 : post.balance) !== null && _f !== void 0 ? _f : 0, diff: postBalance - preBalance, }; } export function getTransactionDiff(connection, tx, txAccounts) { return __awaiter(this, void 0, void 0, function* () { const accountsStrings = txAccounts ? txAccounts.map((acc) => acc.toBase58()) : yield getTransactionAccountsForSimulationDiff(connection, tx); const accountsPubKeys = txAccounts !== null && txAccounts !== void 0 ? txAccounts : accountsStrings.map((key) => new PublicKey(key)); let [accountsInfo, simulatedTx] = yield 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) => { var _a; if ((_a = accountsInfo[i]) === null || _a === void 0 ? void 0 : _a.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