@debridge-finance/solana-utils
Version:
Common utils package to power communication with Solana contracts at deBridge
460 lines • 19.1 kB
JavaScript
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