@debridge-finance/solana-contracts-client
Version:
[Docs](docs/README.md)
1,026 lines (1,025 loc) • 153 kB
JavaScript
import { __awaiter } from "tslib";
import { Buffer } from "buffer";
import { BN, Program, setProvider, AnchorProvider } from "@coral-xyz/anchor";
import { PublicKey, Secp256k1Program, Transaction, TransactionInstruction, SYSVAR_RENT_PUBKEY, VersionedTransaction, MessageV0, SystemProgram, ComputeBudgetProgram, } from "@solana/web3.js";
import logger from "loglevel";
import * as prefix from "loglevel-plugin-prefix";
import memoize from "micro-memoize";
import { f64, struct, nu64, u8 } from "@solana/buffer-layout";
import * as wasm from "@debridge-finance/debridge-external-call";
import { SolanaParser } from "@debridge-finance/solana-transaction-parser";
import { DeBridgeResolver, crypto, helpers, findAssociatedTokenAddress as findATA, constants, TOKEN_PROGRAM_ID, WRAPPED_SOL_MINT, getTokenMetadataAddress, interfaces as solanaInterfaces, TOKEN_METADATA_PROGRAM_ID, spl, txs, } from "@debridge-finance/solana-utils";
import { SEND_HASHED_DATA, EXT_CALL_STORAGE_OFFSET, LOG_EVENT } from "./constants";
import { packSignatures } from "./generateSignatures";
import { BRIDGED_EVENT, TRANSFERRED_EVENT } from "./constants";
import { AlreadyClaimed, AssetFeeNotSupported, AssociatedWalletNotInitialized, BridgeFeeMalformed, BridgeFeeNotInitialized, BridgeMalformed, BridgeNotExists, BridgePaused, BridgeStateMalformed, BridgeStateNotExists, ChainSupportInfoMalformed, ChainSupportInfoNotInitialized, ChainSupportInfoNotSupported, ConfirmationStorageMalformed, DiscountInfoMalformed, DiscountNotActive, ExternalCallMetaNotExists, NotEnoughTokens, StructDecodeError, SubmissionInfoNotExists, } from "./errors";
import * as instructions from "./instructions";
import { DEFAULT_CONFIG } from "./config";
import { BridgeState, isSupportedChainInfoType, isMintBridge, isSendBridge, isActiveDiscount, isExtCallMetaExecuted, FixedFeeType, WalletCheckEnum, isExtCallMetaAccumulation, isExtCallMetaExecution, isExtCallMetaFailed, } from "./interfaces";
import { IDL as programIdl } from "./idl/debridge_program_v31";
import { IDL as settingsIdl } from "./idl/debridge_settings_program_v31";
import { checkFlag, isAccountEmpty } from "./utils";
import * as Submission from "./submission";
import { buildDebridgeDecoder } from "./decoder";
logger.setLevel(logger.levels.INFO);
prefix.reg(logger);
prefix.apply(logger);
const isBuffer = solanaInterfaces.isBuffer;
const CALLDATA_CHUNK_SIZE = 800;
export function getRemainingAccountsAndBumps(data, offset, count, submission, submissionAuth, submissionWallet) {
const context = wasm.get_external_call_account_meta(data, offset, data.length, count, submission.toBase58(), submissionAuth.toBase58(), submissionWallet.toBase58());
const subsitutionBumps = context.reversed_subsitution_bumps();
const remainingAccounts = context.remaning_accounts().map((item, index) => {
const pk = new PublicKey(item.pubkey);
return {
isSigner: item.is_signer,
isWritable: item.is_writable,
pubkey: pk,
};
});
context.free();
return [remainingAccounts, subsitutionBumps];
}
export function extCallDataToInstructions(data, offset = 0) {
// if (offset !== 0) {
// offset -= 8;
// }
const iter = wasm.get_external_call_instructions(data, offset, data.length);
let item;
const ixs = [];
do {
item = iter.next();
if (!item)
continue;
const start = item.position_start;
const end = item.position_end;
// item is beeing freed after the next call, hence we can't get any properties of item later
const ix = item.instruction();
const jsObjCopy = {
expenses: ix.expenses,
reward: ix.reward,
instruction: ix.instruction,
start: Number(start),
end: Number(end),
};
ix.free();
if (item)
ixs.push(jsObjCopy);
} while (item != undefined);
iter.free();
return ixs;
}
function newTxWithOptionalPriorityFee(priorityFee) {
const tx = new Transaction();
if (priorityFee) {
tx.add(ComputeBudgetProgram.setComputeUnitLimit({ units: priorityFee.limit }));
if (priorityFee.microLamports)
tx.add(ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFee.microLamports }));
}
return tx;
}
function customIsPubkey(obj) {
const casted = obj;
// WARN!: Don't use Object.prototype.hasOwnPropery here because obj can be built dynamically
const containsMethods = "toBase58" in casted && "equals" in casted;
return containsMethods || obj.constructor.name == "PublicKey";
}
function findAssociatedTokenAddress(wallet, tokenMint, associatedTokenProgramId) {
return findATA(wallet, tokenMint, TOKEN_PROGRAM_ID, associatedTokenProgramId);
}
export function initWasm(input) {
return __awaiter(this, void 0, void 0, function* () {
return isBuffer(input) ? wasm.initSync(input) : wasm.default(input);
});
}
export class DeBridgeSolanaClient {
constructor(connection, chainId, wallet, params) {
this.debug = false;
if (!chainId) {
throw new Error(`chainId is required`);
}
this.chainId = chainId;
this._wallet = undefined;
if (wallet) {
if ("payer" in wallet) {
this._wallet = new helpers.Wallet(wallet.payer);
}
else {
this._wallet = wallet;
}
}
this._provider = new AnchorProvider(connection, wallet || {}, {});
setProvider(this._provider);
const programId = (params === null || params === void 0 ? void 0 : params.programId) || DEFAULT_CONFIG.DEBRIDGE_PROGRAM_ID;
programIdl.address = programId.toString();
const settingsProgramId = (params === null || params === void 0 ? void 0 : params.settingsProgramId) || DEFAULT_CONFIG.SETTINGS_PROGRAM_ID;
settingsIdl.address = settingsProgramId.toString();
this.associatedTokenProgramId = new PublicKey((params === null || params === void 0 ? void 0 : params.associatedTokenProgramId) || DEFAULT_CONFIG.ASSOCIATED_TOKEN_PROGRAM_ID);
if (params === null || params === void 0 ? void 0 : params.debug) {
logger.setLevel(logger.levels.DEBUG);
this.debug = true;
}
this.priorityFeeConfig = params === null || params === void 0 ? void 0 : params.priorityFeeConfig;
this._settingsProgram = new Program(settingsIdl, this._provider);
this._program = new Program(programIdl, this._provider);
this.accountsResolver = DeBridgeResolver(this.program.programId, this.settingsProgram.programId).methods;
[this.statePublicKey] = this.accountsResolver.getStateAddress();
this.getStateSafe = memoize(this.getStateSafe.bind(this), { maxSize: 1, isPromise: true });
this.getBridgeFeeSafe = memoize(this.getBridgeFeeSafe.bind(this), {
transformKey: ((args) => [args[0].toBase58()]),
maxSize: 5,
isPromise: true,
});
//this.getBridgeInfoSafe = memoize<DeBridgeSolanaClient["getBridgeInfoSafe"]>(this.getBridgeInfoSafe.bind(this), {
// transformKey: ((args: [PublicKey]) => [args[0].toBase58()]) as MicroMemoize.KeyTransformer,
// maxSize: 5,
//});
this.getChainSupportInfoSafe = memoize(this.getChainSupportInfoSafe.bind(this), {
transformKey: ((args) => [args[0].toBase58(), args[1] ? 1 : 0]),
isPromise: true,
});
this.getDiscountInfoSafe = memoize(this.getDiscountInfoSafe.bind(this), {
maxSize: 1,
isPromise: true,
});
this.getBridgeFee = memoize(this.getBridgeFee.bind(this), {
transformKey: ((args) => [args[0].toBase58(), args[1].toString()]),
maxSize: 10,
isPromise: true,
});
this.getRent = memoize(this.getRent.bind(this), { isPromise: true });
this.decoder = buildDebridgeDecoder(this.program, this.settingsProgram);
}
/**
* Async constructor for this class
*/
init() {
return __awaiter(this, void 0, void 0, function* () {
try {
const state = yield this.getStateSafe();
this.feeBeneficiarAccount = state.feeBeneficiary;
return yield Promise.resolve();
}
catch (error) {
return Promise.reject(error);
}
});
}
getSubmissionState(submissionId, calldata, subscriptionCommitment = "confirmed") {
return __awaiter(this, void 0, void 0, function* () {
submissionId = isBuffer(submissionId) ? submissionId : helpers.hexToBuffer(submissionId);
let externalCallStorage = undefined;
let externalCallMeta = undefined;
if (calldata !== undefined) {
try {
const existingSubmission = yield this.getSubmissionInfoSafe(this.accountsResolver.getSubmissionAddress(submissionId)[0]);
if (!existingSubmission.claimer.equals(calldata.executor)) {
logger.warn(`[getSubmissionState] Submission already is claimed by ${existingSubmission.claimer.toBase58()}, using it's calldata storage`);
externalCallStorage = this.accountsResolver.getExternalCallStorageAddress(submissionId, existingSubmission.claimer, calldata.sourceChain)[0];
}
else {
externalCallStorage = this.accountsResolver.getExternalCallStorageAddress(submissionId, calldata.executor, calldata.sourceChain)[0];
}
}
catch (e) {
externalCallStorage = this.accountsResolver.getExternalCallStorageAddress(submissionId, calldata.executor, calldata.sourceChain)[0];
}
finally {
// account exists
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
externalCallMeta = this.accountsResolver.getExternalCallMetaAddress(externalCallStorage)[0];
}
}
const state = new Submission.SubmissionState(this.decoder, this._connection, submissionId, {
confirmationStorage: this.accountsResolver.getConfirmationsStorageAddress(submissionId)[0],
submission: this.accountsResolver.getSubmissionAddress(submissionId)[0],
externalCall: externalCallMeta && externalCallStorage
? {
externalCallStorage,
externalCallMeta,
}
: null,
}, logger.debug.bind(logger), undefined, subscriptionCommitment);
return state.getInitialState();
});
}
/**
* Checks if instance is initialized
* @returns true if this.init() was called before and client is ready for work
*/
isInitialized() {
return (this === null || this === void 0 ? void 0 : this.feeBeneficiarAccount) !== undefined;
}
get _connection() {
return this._provider.connection;
}
updateConnection(newConnection) {
this._provider = new AnchorProvider(newConnection, this._provider.wallet, this._provider.opts);
// TODO: investigate if we can update provider for anchor.program without initializing new object
this._settingsProgram = new Program(settingsIdl, this._provider);
this._program = new Program(programIdl, this._provider);
}
updateWallet(newWallet) {
this._provider = new AnchorProvider(this._provider.connection, newWallet, this.provider.opts);
this._settingsProgram = new Program(settingsIdl, this._provider);
this._program = new Program(programIdl, this._provider);
this._wallet = newWallet;
}
updateWalletAndConnection(newConnection, newWallet) {
this._provider = new AnchorProvider(newConnection, newWallet, this.provider.opts);
this._settingsProgram = new Program(settingsIdl, this._provider);
this._program = new Program(programIdl, this._provider);
this._wallet = newWallet;
}
getAccountInfo(account, commitment) {
return __awaiter(this, void 0, void 0, function* () {
const info = yield this._connection.getAccountInfo(new PublicKey(account), commitment || this._connection.commitment);
if (info && info.lamports !== 0) {
return info;
}
else {
return null;
}
});
}
checkIfAccountExists(account) {
return __awaiter(this, void 0, void 0, function* () {
const info = yield this.getAccountInfo(account);
return info !== null;
});
}
/**
* Get number of confirmations in provided storage
* @param confirmationsStorage address of confirmation storage
* @returns number of confirmations in storage
*/
getConfirmationsCount(confirmationsStorage) {
return __awaiter(this, void 0, void 0, function* () {
confirmationsStorage = new PublicKey(confirmationsStorage);
const stateStorage = yield this.getStateSafe();
const requiredConfirmations = stateStorage.confirmationGuard.minConfirmations;
let haveConfirmations = 0;
try {
const confirmationsStorageData = yield this.getConfirmationStorageSafe(confirmationsStorage);
haveConfirmations = confirmationsStorageData.oracles.length;
return { haveConfirmations, requiredConfirmations };
}
catch (error) {
throw new Error("Failed to get confirmations");
}
});
}
/**
* Waits until confirmation storage is filed
* @param confStorage address of confirmations storage
* @param retries number of retries before code will fail
* @returns true when confirmation storage is filled properly
*/
waitForConfirmations(confStorage, retries = 10, timeout = 2500) {
return __awaiter(this, void 0, void 0, function* () {
confStorage = new PublicKey(confStorage);
for (let i = 0; i < retries; i++) {
try {
yield helpers.sleep(timeout);
const { haveConfirmations, requiredConfirmations } = yield this.getConfirmationsCount(confStorage);
if (this.debug)
logger.debug(`got confirmations: ${haveConfirmations}, required: ${requiredConfirmations}`);
if (haveConfirmations >= requiredConfirmations) {
return true;
}
}
catch (e) {
logger.info("Failed to get confirmation storage");
}
}
return false;
});
}
/**
* Checks if confirmations storage contains enough signatures
* @param storage confirmations storage account
* @returns true if stored confirmations count is enough for claim
*/
isEnoughConfirmationsStored(storage) {
return __awaiter(this, void 0, void 0, function* () {
storage = new PublicKey(storage);
try {
const confirmations = yield this.getConfirmationsCount(storage);
return confirmations.haveConfirmations >= confirmations.requiredConfirmations;
}
catch (_a) {
return false;
}
});
}
buildInitNonceMasterTranscation(payer) {
return __awaiter(this, void 0, void 0, function* () {
payer = new PublicKey(payer);
return {
instructions: [
yield instructions
.initNonceMasterInstruction(this._program, { nonceStorage: this.accountsResolver.getNonceAddress()[0], payer })
.instruction(),
],
payer,
};
});
}
/**
* Checks if bridge initialized
* @param debridgeId hex-encoded string
* @returns true if bridge initialized
*/
isMintBridgeInitialized(debridgeId) {
return __awaiter(this, void 0, void 0, function* () {
const [tokenMintAccount] = this.accountsResolver.getTokenMintAddress(helpers.hexToBuffer(debridgeId));
const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMintAccount);
try {
yield this.getBridgeInfoSafe(bridgeAccount);
return true;
}
catch (error) {
return false;
}
});
}
/**
* Checks if bridge fee info initialized
* @param debridgeId
* @param chainIdFrom
* @returns true if bridge fee info initialized
*/
isBridgeFeeInfoInitialized(debridgeId, chainIdFrom) {
return __awaiter(this, void 0, void 0, function* () {
const chainIdBuffer = crypto.normalizeChainId(chainIdFrom);
const [tokenMintAccount] = this.accountsResolver.getTokenMintAddress(helpers.hexToBuffer(debridgeId));
const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMintAccount);
const [bridgeFeeAccount] = this.accountsResolver.getBridgeFeeAddress(bridgeAccount, chainIdBuffer);
try {
yield this.getBridgeFeeSafe(bridgeFeeAccount);
return true;
}
catch (error) {
return false;
}
});
}
/**
* Extracts deBridge events from the transaction logs
* @param txHash trnasaction hash
* @returns deBridge and deBridgeSettings events
*/
getEventsFromTransaction(txHash) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const txData = yield this._connection.getTransaction(txHash, { maxSupportedTransactionVersion: 0 });
if (!txData)
throw new Error("Failed to get tx");
const eventMarker = "Program data: ";
const events = [];
for (const log of ((_a = txData.meta) === null || _a === void 0 ? void 0 : _a.logMessages) || []) {
if (!log.startsWith(eventMarker))
continue;
const slicedLog = log.slice(eventMarker.length);
let decoded = this.program.coder.events.decode(slicedLog);
if (!decoded)
decoded = this.settingsProgram.coder.events.decode(slicedLog);
if (decoded)
events.push(decoded);
}
return events;
});
}
/**
* Invokes the given callback every time the Transferred event is emitted.
*
* @param handler The function to invoke whenever the Transferred event is emitted from
* program logs
* @returns subscription id
*/
onTransferred(handler) {
return this.program.addEventListener(TRANSFERRED_EVENT, handler);
}
/**
* Invokes the given callback every time the Bridged event is emitted.
*
* @param handler The function to invoke whenever the Bridged event is emitted from
* program logs
* @returns subscription id
*/
onBridged(handler) {
return this.program.addEventListener(BRIDGED_EVENT, handler);
}
/**
* Removes subscription on transferred or bridged events
*
* @param subscriptionId id of subscription returned from {@link onBridged} or {@link onTransferred}
*/
removeOnEvent(subscriptionId) {
return this.program.removeEventListener(subscriptionId);
}
/**
* Returns balance of the bridge associated with the mint or throws error if no such bridge were found
* @param tokenMint address of mint for some token
* @returns balance of the bridge or throws error if no bridge were found
*/
getBridgeBalance(tokenMint) {
return __awaiter(this, void 0, void 0, function* () {
tokenMint = new PublicKey(tokenMint);
const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint);
const bridgeData = yield this.getBridgeInfoSafe(bridgeAccount);
return bridgeData.info.balance;
});
}
/**
* Gets fix fee and transfer fee from chain support info (if exists, else global values)
* @param chainSupportInfo
* @returns fixed fee, transfer fee bps
*/
getFeesOrGlobal(chainSupportInfo) {
return __awaiter(this, void 0, void 0, function* () {
if (customIsPubkey(chainSupportInfo))
chainSupportInfo = yield this.getChainSupportInfoSafe(chainSupportInfo, false);
let supportedChainInfo;
const state = yield this.getStateSafe();
if (isSupportedChainInfoType(chainSupportInfo.data)) {
supportedChainInfo = chainSupportInfo.data;
}
else {
throw new Error(`Chain not supported!`);
}
return {
fixedFee: supportedChainInfo.supported.fixedFee || state.globalFixedFee,
transferFeeBps: supportedChainInfo.supported.transferFeeBps || state.globalTransferFeeBps,
};
});
}
static calculateFeeInternal(amount, useAssetFee, isSol, fees, bridgeFee, activeDiscount) {
let fixedFee = fees.fixedFee;
if (useAssetFee) {
if (!bridgeFee)
throw new Error("useAssetFee is true, but bridgeFee object is null");
fixedFee = (bridgeFee === null || bridgeFee === void 0 ? void 0 : bridgeFee.assetChainFee) || fixedFee;
}
const fixFeeVariant = useAssetFee ? FixedFeeType.ASSET : FixedFeeType.NATIVE;
// fixed fee
if (activeDiscount) {
const discount = fixedFee.muln(activeDiscount.active.fixBps).divn(constants.BPS_DENOMINATOR);
fixedFee = fixedFee.sub(discount);
}
if (useAssetFee) {
amount = amount.sub(fixedFee);
}
// transfer fee
let calculatedTransferFee = amount.mul(fees.transferFeeBps).divn(constants.BPS_DENOMINATOR);
if (activeDiscount) {
const discount = calculatedTransferFee.muln(activeDiscount.active.transferBps).divn(constants.BPS_DENOMINATOR);
calculatedTransferFee = calculatedTransferFee.sub(discount);
}
const discountResult = activeDiscount ? Object.assign({}, activeDiscount.active) : { fixBps: 0, transferBps: 0 };
return {
fixed: { amount: fixedFee, type: fixFeeVariant },
transfer: calculatedTransferFee,
discount: discountResult,
finalAmount: amount.sub(calculatedTransferFee),
};
}
getBridgeFee(tokenMint, chainIdBuffer) {
return __awaiter(this, void 0, void 0, function* () {
const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint);
const [bridgeFeeAccount] = this.accountsResolver.getBridgeFeeAddress(bridgeAccount, chainIdBuffer);
const bridgeFee = yield this.getBridgeFeeSafe(bridgeFeeAccount);
return [bridgeFeeAccount, bridgeFee];
});
}
/**
* Gets submission status from the chain
* @param submissionId
* @returns submission status with context
*/
getSubmissionStatus(submissionId) {
return __awaiter(this, void 0, void 0, function* () {
submissionId = isBuffer(submissionId) ? submissionId : helpers.hexToBuffer(submissionId);
let submission;
const [submissionAccount] = this.accountsResolver.getSubmissionAddress(submissionId);
try {
submission = yield this.getSubmissionInfoSafe(submissionAccount);
}
catch (_a) {
logger.info(`Failed to get submission account: ${submissionAccount.toBase58()}`);
return {
type: "notClaimed",
data: null,
};
}
const [extCallStorageAccount] = this.accountsResolver.getExternalCallStorageAddress(submissionId, submission.claimer, Buffer.from(submission.sourceChainId));
const [extCallMeta] = this.accountsResolver.getExternalCallMetaAddress(extCallStorageAccount);
let meta;
try {
meta = yield this.getExternalCallMetaSafe(extCallMeta);
// logger.warn(meta);
}
catch (_b) {
// No meta - ext call was executed or fallbacked
const parser = new SolanaParser([{ idl: programIdl, programId: this._program.programId }]);
const transactions = yield this._connection.getSignaturesForAddress(extCallMeta, { limit: 100 });
for (const txHash of transactions) {
const parsed = yield parser.parseTransactionByHash(this._connection, txHash.signature, true);
if (!parsed)
throw new Error(`Failed to get tx: ${txHash.signature}`);
// start from latest instruction
for (const parsedIx of parsed.reverse()) {
if (!parsedIx.programId.equals(this._program.programId))
continue;
if (parsedIx.name === "executeExternalCall")
return { type: "executed", data: null };
else if (parsedIx.name === "makeFallbackForExternalCall")
return { type: "fallback", data: null };
}
yield helpers.sleep(1000);
}
return null;
}
if (isExtCallMetaAccumulation(meta.data)) {
return {
type: "accumulation",
data: meta.data,
};
}
else if (isExtCallMetaExecuted(meta.data)) {
return {
type: "executed",
data: meta.data,
};
}
else if (isExtCallMetaFailed(meta.data)) {
return {
type: "fallback",
data: meta.data,
};
}
else {
return {
type: "executing",
data: meta.data,
};
}
});
}
/*
Calculate decimals for Solana wrapped token based on common decimals value for EVM networks.
This is necessary for compatibility with the general protocol. In Solana the amount of tokens
in wallet is stored in the u64 type, and in EVM networks in u256 type.
By agreement, if the token decimal from the EVM network is less than 8, then it remains unchanged in Solana.
If it is more than 8, then the decimals of the wrapped token created in Solana are equal 8.
*/
getWrapperDecimals(decimals) {
return Math.min(decimals, 8);
}
/**
* Calculate fee of claim execution in solana
* @param senderAddressLength length of sender in bytes
* @param bridgeId id of bridge used to bridge assets
* @param solPrice price of 1 SOL in usd
* @param tokenPrice price of token to send into solana in usd
* @param tokenDecimals decimals of token to send into solana
* @param walletExists is receiver wallet exists
* @param executionFeeMultiplier multiplier of profitability for claimers
* @returns execution fee
*/
calculateExecutionFee(senderAddressLength, bridgeId, solPrice, tokenPrice, tokenDecimals, walletExists, executionFeeMultiplier, isRequiredTempRentCost, extCall) {
return __awaiter(this, void 0, void 0, function* () {
const [rent, bridge, guard] = yield Promise.all([
this.getRent(),
this.getBridgeByDeBridgeId(bridgeId),
(yield this.getStateSafe()).confirmationGuard,
]);
const signaturePrice = BigInt(5000);
const costInput = new wasm.CostCalculationInput(senderAddressLength, isRequiredTempRentCost, guard.excessConfirmations, BigInt(rent.lamportsPerByteYear), rent.exemptionThreshold, signaturePrice, BigInt(0), solPrice, tokenPrice, tokenDecimals, bridge !== null, walletExists, false, executionFeeMultiplier - 1, extCall ? BigInt(extCall.length) : undefined);
const weiPrice = BigInt(yield costInput.calculate_recomended_claim_execution_fee());
costInput.free();
const rewards = [];
if (extCall) {
const solanaDimensionCostInput = new wasm.CostCalculationInput(senderAddressLength, isRequiredTempRentCost, guard.excessConfirmations, BigInt(rent.lamportsPerByteYear), rent.exemptionThreshold, signaturePrice, BigInt(0), solPrice, tokenPrice, this.getWrapperDecimals(tokenDecimals), bridge !== null, walletExists, false, executionFeeMultiplier - 1, extCall ? BigInt(extCall.length) : undefined);
const externalCallWithRewards = solanaDimensionCostInput.calculate_recomended_reward_for_external_call(extCall);
const ixs = extCallDataToInstructions(externalCallWithRewards);
for (const ix of ixs) {
rewards.push(ix.reward);
}
extCall = externalCallWithRewards;
solanaDimensionCostInput.free();
}
const denormalize = (amount, decimals) => BigInt(new BN(amount.toString()).mul(new BN(10).pow(new BN(decimals - 8))).toString());
return {
claimCost: weiPrice,
rewards,
executionCost: rewards.map((reward) => denormalize(reward, tokenDecimals)).reduce((acc, val) => acc + val, BigInt(0)),
externalCallWithRewards: extCall,
total: weiPrice + rewards.reduce((acc, val) => acc + val, BigInt(0)),
};
});
}
/**
* Calculates fee for specified transfer params
* @param tokenMint mint of token to transfer
* @param chainId id of the destination chain we want to send tokens
* @param discountAccount account for which we'll try to find discount
* @param useAssetFee asset or native execution fee
* @param amount amount of transferrable assets
* @returns information about fee
*/
calculateFee(tokenMint, chainId, discountAccount, useAssetFee, amount) {
return __awaiter(this, void 0, void 0, function* () {
tokenMint = new PublicKey(tokenMint);
const chainIdBuffer = crypto.normalizeChainId(chainId);
const [chainSupportInfoAccount] = this.accountsResolver.getChainSupportInfoAddress(chainIdBuffer);
const fees = yield this.getFeesOrGlobal(chainSupportInfoAccount);
let activeDiscount = undefined;
if (discountAccount) {
const [discountInfoAccount] = this.accountsResolver.getDiscountInfoAddress(new PublicKey(discountAccount));
const discountInfo = yield this.getDiscountInfoSafe(discountInfoAccount, true);
if (discountInfo) {
activeDiscount = discountInfo;
}
}
let brdigeFeeOrNull = undefined;
// fixed fee
if (useAssetFee) {
const [bridgeFeeAccount, bridgeFee] = yield this.getBridgeFee(tokenMint, chainIdBuffer);
if (!bridgeFee.assetChainFee) {
throw new AssetFeeNotSupported(bridgeFeeAccount);
}
brdigeFeeOrNull = bridgeFee;
}
return DeBridgeSolanaClient.calculateFeeInternal(new BN(amount), useAssetFee, tokenMint == WRAPPED_SOL_MINT, fees, brdigeFeeOrNull, activeDiscount);
});
}
/**
* Returns bridge info from solana by debridgeId
* @param deBridgeId bridge id in deBridge network, {@link hashDebridgeId}
* @returns null if bridge not exists or bridge token address and bridge info from solana blockchain
*/
getBridgeByDeBridgeId(deBridgeId) {
return __awaiter(this, void 0, void 0, function* () {
deBridgeId = isBuffer(deBridgeId) ? deBridgeId : helpers.hexToBuffer(deBridgeId);
const [mapAccount] = this.accountsResolver.getBridgeMapAddress(deBridgeId);
const accountData = yield this.getAccountInfo(mapAccount);
if (!accountData)
return null;
let tokenMint;
if (TOKEN_PROGRAM_ID.equals(accountData.owner)) {
tokenMint = mapAccount;
}
else {
const decoded = this.settingsProgram.coder.accounts.decode("sendBridgeMap", accountData.data);
tokenMint = decoded.tokenMint;
}
const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint);
const bridge = yield this.getBridgeInfoSafe(bridgeAccount);
return { bridge, tokenMint };
});
}
getRent() {
return __awaiter(this, void 0, void 0, function* () {
const rentData = yield this.getAccountInfo(SYSVAR_RENT_PUBKEY);
if (!rentData)
throw new Error("unexpected! Failed to get RENT_SYSVAR data");
const rentStruct = struct([nu64("lamportsPerByteYear"), f64("exemptionThreshold"), u8("burnPercent")]);
return rentStruct.decode(rentData.data);
});
}
/**
* Returns parsed state of smart-contract from blockchain
* @returns parsed bridge state
*/
getStateSafe() {
return __awaiter(this, void 0, void 0, function* () {
const stateData = yield this.getAccountInfo(this.statePublicKey);
if (!stateData) {
throw new BridgeStateNotExists(this.statePublicKey);
}
let parsedState;
try {
parsedState = this.settingsProgram.coder.accounts.decode("state", stateData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new BridgeStateMalformed(this.statePublicKey);
throw error;
}
return parsedState;
});
}
/**
* Returns parsed confirmation storage, may be used to get current confirmations count
* @param confirmationStorage account of ConfirmationStorage for specific deployInfo/submission
* @returns parsed confirmation storage
*/
getConfirmationStorageSafe(confirmationStorage) {
return __awaiter(this, void 0, void 0, function* () {
confirmationStorage = new PublicKey(confirmationStorage);
if (!confirmationStorage)
throw new Error("confirmationStorageAccount not set");
const confirmationStorageData = yield this.getAccountInfo(confirmationStorage);
if (!confirmationStorageData)
throw new Error("No data in confirmation storage account " + confirmationStorage.toString());
let parsedConfirmationStorage;
try {
parsedConfirmationStorage = this.settingsProgram.coder.accounts.decode("confirmationStorage", confirmationStorageData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new ConfirmationStorageMalformed(confirmationStorage);
throw error;
}
return parsedConfirmationStorage;
});
}
/**
* Returns parsed chainSupportInfo from blockchain or raises error
* @param chainSupportInfo account of chainSupportInfo
* @param checkIfSupported check if chainSupportInfo is supported
* @returns parsed chainSupportInfo
*/
getChainSupportInfoSafe(chainSupportInfo, checkIfSupported = true) {
return __awaiter(this, void 0, void 0, function* () {
chainSupportInfo = new PublicKey(chainSupportInfo);
const chainSupportInfoData = yield this.getAccountInfo(chainSupportInfo);
if (!chainSupportInfoData) {
throw new ChainSupportInfoNotInitialized(chainSupportInfo);
}
let parsedChainSupportInfo;
try {
parsedChainSupportInfo = this.settingsProgram.coder.accounts.decode("chainSupportInfo", chainSupportInfoData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new ChainSupportInfoMalformed(chainSupportInfo);
throw error;
}
if (checkIfSupported && !isSupportedChainInfoType(parsedChainSupportInfo.data)) {
throw new ChainSupportInfoNotSupported(chainSupportInfo);
}
return parsedChainSupportInfo;
});
}
static getBridgeInfoFromBridgeType(bridge) {
if (bridge === null) {
throw new Error("Empty parsed bridge data");
}
// TODO
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (isMintBridge(bridge.data)) {
return bridge.data.mint.info;
// TODO
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
}
else if (isSendBridge(bridge.data)) {
return bridge.data.send.info;
}
else {
throw new Error("Bridge not supported!");
}
}
/**
* Returns parsed bridgeInfo from blockchain or raises error
* @param bridge account of bridgeInfo
* @returns parsed bridgeInfo
*/
getBridgeInfoSafe(bridge) {
return __awaiter(this, void 0, void 0, function* () {
bridge = new PublicKey(bridge);
const bridgeData = yield this.getAccountInfo(bridge);
if (!bridgeData) {
throw new BridgeNotExists(bridge);
}
let parsedBridge;
try {
parsedBridge = this.decoder.decodeBridge(bridgeData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new BridgeMalformed(bridge);
throw error;
}
if (parsedBridge === null)
throw new BridgeMalformed(bridge);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return Object.assign(Object.assign({}, parsedBridge), { info: DeBridgeSolanaClient.getBridgeInfoFromBridgeType(parsedBridge) });
});
}
/**
* Returns parsed bridgeFee from blockchain or raises error
* @param bridgeFee account of bridgeFee
* @returns parsed bridgeFee
*/
getBridgeFeeSafe(bridgeFee) {
return __awaiter(this, void 0, void 0, function* () {
bridgeFee = new PublicKey(bridgeFee);
const bridgeFeeData = yield this.getAccountInfo(bridgeFee);
if (!bridgeFeeData) {
throw new BridgeFeeNotInitialized(bridgeFee);
}
let parsedBridgeFee;
try {
parsedBridgeFee = this.settingsProgram.coder.accounts.decode("bridgeFeeInfo", bridgeFeeData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new BridgeFeeMalformed(bridgeFee);
throw error;
}
return parsedBridgeFee;
});
}
/**
* Returns parsed activeDiscountInfo from blockchain or raises error
* @param discount account of discount info
* @param returnNullIfNoDiscount dont raise error if discount not found/malformed/not active, just return null
* @returns parsed activeDiscountInfo
*/
getDiscountInfoSafe(discount, returnNullIfNoDiscount = true) {
return __awaiter(this, void 0, void 0, function* () {
discount = new PublicKey(discount);
const discountData = yield this.getAccountInfo(discount);
if (!discountData) {
return undefined;
}
let parsedDiscount;
try {
parsedDiscount = this.settingsProgram.coder.accounts.decode("discountInfo", discountData.data);
}
catch (error) {
if (error instanceof StructDecodeError)
throw new DiscountInfoMalformed(discount);
throw error;
}
if (!isActiveDiscount(parsedDiscount.data)) {
if (returnNullIfNoDiscount) {
return undefined;
}
else {
throw new DiscountNotActive(discount);
}
}
return parsedDiscount.data;
});
}
/**
* Fetches submission info from blockchain
* @param submission account of submissioon
* @returns parsed submission info
*/
getSubmissionInfoSafe(submission, commitment) {
return __awaiter(this, void 0, void 0, function* () {
submission = new PublicKey(submission);
commitment = commitment || this._connection.commitment;
const fetched = yield this.program.account.submissionAccount.fetchNullable(submission, commitment);
if (fetched) {
return fetched;
}
else {
throw new SubmissionInfoNotExists(submission);
}
});
}
/**
* Fetches extCallMeta from blockchain
* @param externalCallMeta account of ext call meta
* @returns parsed external call metadata
*/
getExternalCallMetaSafe(externalCallMeta, commitment) {
return __awaiter(this, void 0, void 0, function* () {
externalCallMeta = new PublicKey(externalCallMeta);
commitment = commitment || this._connection.commitment;
const fetched = yield this.program.account.externalCallMeta.fetchNullable(externalCallMeta, commitment);
if (fetched) {
return fetched;
}
else {
throw new ExternalCallMetaNotExists(externalCallMeta);
}
});
}
/**
* Builds fallback transaction
* @param submissionId
* @param executor who pays for the actions
* @returns list of transactions with fallback ix and close unused wallets ixs
*/
buildFallbackTransactions(submissionId, executor) {
return __awaiter(this, void 0, void 0, function* () {
executor = new PublicKey(executor);
const result = [{ instructions: [], payer: executor }];
const submissionIdBuffer = isBuffer(submissionId) ? submissionId : helpers.hexToBuffer(submissionId);
const [submission] = this.accountsResolver.getSubmissionAddress(submissionIdBuffer);
const [submissionAuth, submissionAuthBump] = this.accountsResolver.getSubmissionAuthAddress(submission);
const submissionInfo = yield this.getSubmissionInfoSafe(submission);
const [submissionWallet] = findAssociatedTokenAddress(submissionAuth, submissionInfo.tokenMint, this.associatedTokenProgramId);
const [externalCallStorage] = this.accountsResolver.getExternalCallStorageAddress(submissionIdBuffer, submissionInfo.claimer, Buffer.from(submissionInfo.sourceChainId));
const [externalCallMeta] = this.accountsResolver.getExternalCallMetaAddress(externalCallStorage);
const [bridge] = this.accountsResolver.getBridgeAddress(submissionInfo.tokenMint);
const [fallbackWallet] = findAssociatedTokenAddress(submissionInfo.fallbackAddress, submissionInfo.tokenMint, this.associatedTokenProgramId);
if ((yield this.getAccountInfo(fallbackWallet)) === null)
result[0].instructions.push(spl.createAssociatedWalletInstruction(submissionInfo.tokenMint, fallbackWallet, submissionInfo.fallbackAddress, executor, false));
// make fallback
const fallbackBuilder = instructions.buildMakeFallbackForExternalCallInstruction(this.program, {
submission,
tokenMint: submissionInfo.tokenMint,
originalClaimer: submissionInfo.claimer,
state: this.statePublicKey,
fallbackAddress: submissionInfo.fallbackAddress,
bridge,
externalCallStorage,
externalCallMeta,
submissionAuth,
submissionWallet,
fallbackAddressWallet: fallbackWallet,
executor,
rewardBeneficiaryWallet: findAssociatedTokenAddress(executor, submissionInfo.tokenMint, this.associatedTokenProgramId)[0],
}, { submissionId: submissionIdBuffer, submissionAuthBump });
result[0].instructions.push(yield fallbackBuilder.instruction());
// find submissionAuth ATAs
const walletsToClose = yield spl.getAllTokenAccountsWithBalances(this._connection, submissionAuth);
const closePairs = yield Promise.all(walletsToClose
.filter((wallet) => !wallet.address.equals(submissionWallet))
.map((wallet) => __awaiter(this, void 0, void 0, function* () {
const ixs = [];
const [fallbackForWallet] = findAssociatedTokenAddress(submissionInfo.fallbackAddress, wallet.mint, this.associatedTokenProgramId);
if (!(yield this.checkIfAccountExists(fallbackForWallet)))
ixs.push(spl.createAssociatedWalletInstruction(wallet.mint, fallbackForWallet, submissionInfo.fallbackAddress, executor, false));
const closeSubAuthWalletBuilder = instructions.buildCloseSubmissionAuthWalletInstruction(this.program, {
externalCallMeta,
externalCallStorage,
originalClaimer: submissionInfo.claimer,
tokenMint: wallet.mint,
submission,
submissionAuth,
submissionAuthLostWallet: wallet.address,
fallbackAddressWallet: fallbackForWallet,
}, { submissionId: submissionIdBuffer, submissionAuthBump });
ixs.push(yield closeSubAuthWalletBuilder.instruction());
return ixs;
}), this));
// pack [init wallet]+close ix as tight as possible
for (const closePair of closePairs) {
const clonedTx = new Transaction().add(...result[result.length - 1].instructions);
clonedTx.add(...closePair);
const txSize = txs.getTransactionSize(clonedTx);
if (txSize != null && txSize <= 1230) {
result[result.length - 1].instructions.push(...closePair);
}
else {
result.push({ instructions: closePair, payer: executor });
}
}
return result;
});
}
/**
* Builds transaction for mint bridge initialization
* @param payer who pays for initialization
* @param chainId native chain id
* @param tokenAddress hex-encoded native token address
* @param tokenName name of the token
* @param tokenSymbol token symbol
* @param decimals otken decimals
* @returns built transaction
*/
buildInitializeMintBridgeTransaction(payer, chainId, tokenAddress, tokenName, tokenSymbol, decimals) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.isInitialized())
yield this.init();
payer = new PublicKey(payer);
const chainIdBuffer = crypto.normalizeChainId(chainId);
const [chainSupportInfoAccount] = this.accountsResolver.getChainSupportInfoAddress(chainIdBuffer);
const chainSupportInfo = yield this.getChainSupportInfoSafe(chainSupportInfoAccount);
if (!isSupportedChainInfoType(chainSupportInfo.data)) {
throw new Error("chain supoprt info not supported!");
}
const nativeTokenAddress = helpers.hexToBuffer(tokenAddress);
if (nativeTokenAddress.length != chainSupportInfo.data.supported.chainAddressLen) {
throw new Error(`Bad token address len, expected: ${chainSupportInfo.data.supported.chainAddressLen}, got: ${tokenAddress.length}`);
}
const debridgeId = crypto.hashDebridgeId(chainIdBuffer, "0x" + nativeTokenAddress.toString("hex"));
const message = crypto.hashDeployInfo({ decimals, tokenName, tokenSymbol, debridgeId });
const [confirmationStorage] = this.accountsResolver.getConfirmationsStorageAddress(helpers.hexToBuffer(message));
let confirmationStorageCreator = payer;
try {
const storage = yield this.getConfirmationStorageSafe(confirmationStorage);
confirmationStorageCreator = storage.creator;
}
catch (_a) {
logger.debug("No confirmation storage exists -> confirmationStorageCreator is payer");
}
const [tokenMint] = this.accountsResolver.getTokenMintAddress(helpers.hexToBuffer(debridgeId));
const [bridgeData] = this.accountsResolver.getBridgeAddress(tokenMint);
const [mintAuthority] = this.accountsResolver.getMintAuthorityAddress(bridgeData);
const initMintBuilder = instructions.initializeMintBridgeInstruction(this._settingsProgram, {
tokenMint,
bridgeData,
mintAuthority,
confi