@debridge-finance/solana-utils
Version:
Common utils package to power communication with Solana contracts at deBridge
454 lines • 18.3 kB
JavaScript
"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 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 ||
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;
// 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)) {
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