UNPKG

@debridge-finance/solana-contracts-client

Version:
1,029 lines (1,028 loc) 146 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DeBridgeSolanaClient = exports.initWasm = exports.extCallDataToInstructions = exports.getRemainingAccountsAndBumps = void 0; const tslib_1 = require("tslib"); const buffer_1 = require("buffer"); const anchor_1 = require("@coral-xyz/anchor"); const web3_js_1 = require("@solana/web3.js"); const loglevel_1 = tslib_1.__importDefault(require("loglevel")); const prefix = tslib_1.__importStar(require("loglevel-plugin-prefix")); const micro_memoize_1 = tslib_1.__importDefault(require("micro-memoize")); const buffer_layout_1 = require("@solana/buffer-layout"); const wasm = tslib_1.__importStar(require("@debridge-finance/debridge-external-call")); const solana_transaction_parser_1 = require("@debridge-finance/solana-transaction-parser"); const solana_utils_1 = require("@debridge-finance/solana-utils"); const constants_1 = require("./constants"); const generateSignatures_1 = require("./generateSignatures"); const constants_2 = require("./constants"); const errors_1 = require("./errors"); const instructions = tslib_1.__importStar(require("./instructions")); const config_1 = require("./config"); const interfaces_1 = require("./interfaces"); const debridge_program_v31_1 = require("./idl/debridge_program_v31"); const debridge_settings_program_v31_1 = require("./idl/debridge_settings_program_v31"); const utils_1 = require("./utils"); const Submission = tslib_1.__importStar(require("./submission")); const decoder_1 = require("./decoder"); loglevel_1.default.setLevel(loglevel_1.default.levels.INFO); prefix.reg(loglevel_1.default); prefix.apply(loglevel_1.default); const isBuffer = solana_utils_1.interfaces.isBuffer; const CALLDATA_CHUNK_SIZE = 800; 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 web3_js_1.PublicKey(item.pubkey); return { isSigner: item.is_signer, isWritable: item.is_writable, pubkey: pk, }; }); context.free(); return [remainingAccounts, subsitutionBumps]; } exports.getRemainingAccountsAndBumps = getRemainingAccountsAndBumps; 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; } exports.extCallDataToInstructions = extCallDataToInstructions; function newTxWithOptionalPriorityFee(priorityFee) { const tx = new web3_js_1.Transaction(); if (priorityFee) { tx.add(web3_js_1.ComputeBudgetProgram.setComputeUnitLimit({ units: priorityFee.limit })); if (priorityFee.microLamports) tx.add(web3_js_1.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 (0, solana_utils_1.findAssociatedTokenAddress)(wallet, tokenMint, solana_utils_1.TOKEN_PROGRAM_ID, associatedTokenProgramId); } async function initWasm(input) { return isBuffer(input) ? wasm.initSync(input) : wasm.default(input); } exports.initWasm = initWasm; 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 solana_utils_1.helpers.Wallet(wallet.payer); } else { this._wallet = wallet; } } this._provider = new anchor_1.AnchorProvider(connection, wallet || {}, {}); (0, anchor_1.setProvider)(this._provider); const programId = (params === null || params === void 0 ? void 0 : params.programId) || config_1.DEFAULT_CONFIG.DEBRIDGE_PROGRAM_ID; debridge_program_v31_1.IDL.address = programId.toString(); const settingsProgramId = (params === null || params === void 0 ? void 0 : params.settingsProgramId) || config_1.DEFAULT_CONFIG.SETTINGS_PROGRAM_ID; debridge_settings_program_v31_1.IDL.address = settingsProgramId.toString(); this.associatedTokenProgramId = new web3_js_1.PublicKey((params === null || params === void 0 ? void 0 : params.associatedTokenProgramId) || config_1.DEFAULT_CONFIG.ASSOCIATED_TOKEN_PROGRAM_ID); if (params === null || params === void 0 ? void 0 : params.debug) { loglevel_1.default.setLevel(loglevel_1.default.levels.DEBUG); this.debug = true; } this.priorityFeeConfig = params === null || params === void 0 ? void 0 : params.priorityFeeConfig; this._settingsProgram = new anchor_1.Program(debridge_settings_program_v31_1.IDL, this._provider); this._program = new anchor_1.Program(debridge_program_v31_1.IDL, this._provider); this.accountsResolver = (0, solana_utils_1.DeBridgeResolver)(this.program.programId, this.settingsProgram.programId).methods; [this.statePublicKey] = this.accountsResolver.getStateAddress(); this.getStateSafe = (0, micro_memoize_1.default)(this.getStateSafe.bind(this), { maxSize: 1, isPromise: true }); this.getBridgeFeeSafe = (0, micro_memoize_1.default)(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 = (0, micro_memoize_1.default)(this.getChainSupportInfoSafe.bind(this), { transformKey: ((args) => [args[0].toBase58(), args[1] ? 1 : 0]), isPromise: true, }); this.getDiscountInfoSafe = (0, micro_memoize_1.default)(this.getDiscountInfoSafe.bind(this), { maxSize: 1, isPromise: true, }); this.getBridgeFee = (0, micro_memoize_1.default)(this.getBridgeFee.bind(this), { transformKey: ((args) => [args[0].toBase58(), args[1].toString()]), maxSize: 10, isPromise: true, }); this.getRent = (0, micro_memoize_1.default)(this.getRent.bind(this), { isPromise: true }); this.decoder = (0, decoder_1.buildDebridgeDecoder)(this.program, this.settingsProgram); } /** * Async constructor for this class */ async init() { try { const state = await this.getStateSafe(); this.feeBeneficiarAccount = state.feeBeneficiary; return await Promise.resolve(); } catch (error) { return Promise.reject(error); } } async getSubmissionState(submissionId, calldata, subscriptionCommitment = "confirmed") { submissionId = isBuffer(submissionId) ? submissionId : solana_utils_1.helpers.hexToBuffer(submissionId); let externalCallStorage = undefined; let externalCallMeta = undefined; if (calldata !== undefined) { try { const existingSubmission = await this.getSubmissionInfoSafe(this.accountsResolver.getSubmissionAddress(submissionId)[0]); if (!existingSubmission.claimer.equals(calldata.executor)) { loglevel_1.default.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, }, loglevel_1.default.debug.bind(loglevel_1.default), 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 anchor_1.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 anchor_1.Program(debridge_settings_program_v31_1.IDL, this._provider); this._program = new anchor_1.Program(debridge_program_v31_1.IDL, this._provider); } updateWallet(newWallet) { this._provider = new anchor_1.AnchorProvider(this._provider.connection, newWallet, this.provider.opts); this._settingsProgram = new anchor_1.Program(debridge_settings_program_v31_1.IDL, this._provider); this._program = new anchor_1.Program(debridge_program_v31_1.IDL, this._provider); this._wallet = newWallet; } updateWalletAndConnection(newConnection, newWallet) { this._provider = new anchor_1.AnchorProvider(newConnection, newWallet, this.provider.opts); this._settingsProgram = new anchor_1.Program(debridge_settings_program_v31_1.IDL, this._provider); this._program = new anchor_1.Program(debridge_program_v31_1.IDL, this._provider); this._wallet = newWallet; } async getAccountInfo(account, commitment) { const info = await this._connection.getAccountInfo(new web3_js_1.PublicKey(account), commitment || this._connection.commitment); if (info && info.lamports !== 0) { return info; } else { return null; } } async checkIfAccountExists(account) { const info = await 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 */ async getConfirmationsCount(confirmationsStorage) { confirmationsStorage = new web3_js_1.PublicKey(confirmationsStorage); const stateStorage = await this.getStateSafe(); const requiredConfirmations = stateStorage.confirmationGuard.minConfirmations; let haveConfirmations = 0; try { const confirmationsStorageData = await 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 */ async waitForConfirmations(confStorage, retries = 10, timeout = 2500) { confStorage = new web3_js_1.PublicKey(confStorage); for (let i = 0; i < retries; i++) { try { await solana_utils_1.helpers.sleep(timeout); const { haveConfirmations, requiredConfirmations } = await this.getConfirmationsCount(confStorage); if (this.debug) loglevel_1.default.debug(`got confirmations: ${haveConfirmations}, required: ${requiredConfirmations}`); if (haveConfirmations >= requiredConfirmations) { return true; } } catch (e) { loglevel_1.default.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 */ async isEnoughConfirmationsStored(storage) { storage = new web3_js_1.PublicKey(storage); try { const confirmations = await this.getConfirmationsCount(storage); return confirmations.haveConfirmations >= confirmations.requiredConfirmations; } catch { return false; } } async buildInitNonceMasterTranscation(payer) { payer = new web3_js_1.PublicKey(payer); return { instructions: [ await 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 */ async isMintBridgeInitialized(debridgeId) { const [tokenMintAccount] = this.accountsResolver.getTokenMintAddress(solana_utils_1.helpers.hexToBuffer(debridgeId)); const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMintAccount); try { await 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 */ async isBridgeFeeInfoInitialized(debridgeId, chainIdFrom) { const chainIdBuffer = solana_utils_1.crypto.normalizeChainId(chainIdFrom); const [tokenMintAccount] = this.accountsResolver.getTokenMintAddress(solana_utils_1.helpers.hexToBuffer(debridgeId)); const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMintAccount); const [bridgeFeeAccount] = this.accountsResolver.getBridgeFeeAddress(bridgeAccount, chainIdBuffer); try { await 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 */ async getEventsFromTransaction(txHash) { var _a; const txData = await 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(constants_2.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(constants_2.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 */ async getBridgeBalance(tokenMint) { tokenMint = new web3_js_1.PublicKey(tokenMint); const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint); const bridgeData = await 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 */ async getFeesOrGlobal(chainSupportInfo) { if (customIsPubkey(chainSupportInfo)) chainSupportInfo = await this.getChainSupportInfoSafe(chainSupportInfo, false); let supportedChainInfo; const state = await this.getStateSafe(); if ((0, interfaces_1.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 ? interfaces_1.FixedFeeType.ASSET : interfaces_1.FixedFeeType.NATIVE; // fixed fee if (activeDiscount) { const discount = fixedFee.muln(activeDiscount.active.fixBps).divn(solana_utils_1.constants.BPS_DENOMINATOR); fixedFee = fixedFee.sub(discount); } if (useAssetFee) { amount = amount.sub(fixedFee); } // transfer fee let calculatedTransferFee = amount.mul(fees.transferFeeBps).divn(solana_utils_1.constants.BPS_DENOMINATOR); if (activeDiscount) { const discount = calculatedTransferFee.muln(activeDiscount.active.transferBps).divn(solana_utils_1.constants.BPS_DENOMINATOR); calculatedTransferFee = calculatedTransferFee.sub(discount); } const discountResult = activeDiscount ? { ...activeDiscount.active } : { fixBps: 0, transferBps: 0 }; return { fixed: { amount: fixedFee, type: fixFeeVariant }, transfer: calculatedTransferFee, discount: discountResult, finalAmount: amount.sub(calculatedTransferFee), }; } async getBridgeFee(tokenMint, chainIdBuffer) { const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint); const [bridgeFeeAccount] = this.accountsResolver.getBridgeFeeAddress(bridgeAccount, chainIdBuffer); const bridgeFee = await this.getBridgeFeeSafe(bridgeFeeAccount); return [bridgeFeeAccount, bridgeFee]; } /** * Gets submission status from the chain * @param submissionId * @returns submission status with context */ async getSubmissionStatus(submissionId) { submissionId = isBuffer(submissionId) ? submissionId : solana_utils_1.helpers.hexToBuffer(submissionId); let submission; const [submissionAccount] = this.accountsResolver.getSubmissionAddress(submissionId); try { submission = await this.getSubmissionInfoSafe(submissionAccount); } catch { loglevel_1.default.info(`Failed to get submission account: ${submissionAccount.toBase58()}`); return { type: "notClaimed", data: null, }; } const [extCallStorageAccount] = this.accountsResolver.getExternalCallStorageAddress(submissionId, submission.claimer, buffer_1.Buffer.from(submission.sourceChainId)); const [extCallMeta] = this.accountsResolver.getExternalCallMetaAddress(extCallStorageAccount); let meta; try { meta = await this.getExternalCallMetaSafe(extCallMeta); // logger.warn(meta); } catch { // No meta - ext call was executed or fallbacked const parser = new solana_transaction_parser_1.SolanaParser([{ idl: debridge_program_v31_1.IDL, programId: this._program.programId }]); const transactions = await this._connection.getSignaturesForAddress(extCallMeta, { limit: 100 }); for (const txHash of transactions) { const parsed = await 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 }; } await solana_utils_1.helpers.sleep(1000); } return null; } if ((0, interfaces_1.isExtCallMetaAccumulation)(meta.data)) { return { type: "accumulation", data: meta.data, }; } else if ((0, interfaces_1.isExtCallMetaExecuted)(meta.data)) { return { type: "executed", data: meta.data, }; } else if ((0, interfaces_1.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 */ async calculateExecutionFee(senderAddressLength, bridgeId, solPrice, tokenPrice, tokenDecimals, walletExists, executionFeeMultiplier, isRequiredTempRentCost, extCall) { const [rent, bridge, guard] = await Promise.all([ this.getRent(), this.getBridgeByDeBridgeId(bridgeId), (await 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(await 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 anchor_1.BN(amount.toString()).mul(new anchor_1.BN(10).pow(new anchor_1.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 */ async calculateFee(tokenMint, chainId, discountAccount, useAssetFee, amount) { tokenMint = new web3_js_1.PublicKey(tokenMint); const chainIdBuffer = solana_utils_1.crypto.normalizeChainId(chainId); const [chainSupportInfoAccount] = this.accountsResolver.getChainSupportInfoAddress(chainIdBuffer); const fees = await this.getFeesOrGlobal(chainSupportInfoAccount); let activeDiscount = undefined; if (discountAccount) { const [discountInfoAccount] = this.accountsResolver.getDiscountInfoAddress(new web3_js_1.PublicKey(discountAccount)); const discountInfo = await this.getDiscountInfoSafe(discountInfoAccount, true); if (discountInfo) { activeDiscount = discountInfo; } } let brdigeFeeOrNull = undefined; // fixed fee if (useAssetFee) { const [bridgeFeeAccount, bridgeFee] = await this.getBridgeFee(tokenMint, chainIdBuffer); if (!bridgeFee.assetChainFee) { throw new errors_1.AssetFeeNotSupported(bridgeFeeAccount); } brdigeFeeOrNull = bridgeFee; } return DeBridgeSolanaClient.calculateFeeInternal(new anchor_1.BN(amount), useAssetFee, tokenMint == solana_utils_1.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 */ async getBridgeByDeBridgeId(deBridgeId) { deBridgeId = isBuffer(deBridgeId) ? deBridgeId : solana_utils_1.helpers.hexToBuffer(deBridgeId); const [mapAccount] = this.accountsResolver.getBridgeMapAddress(deBridgeId); const accountData = await this.getAccountInfo(mapAccount); if (!accountData) return null; let tokenMint; if (solana_utils_1.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 = await this.getBridgeInfoSafe(bridgeAccount); return { bridge, tokenMint }; } async getRent() { const rentData = await this.getAccountInfo(web3_js_1.SYSVAR_RENT_PUBKEY); if (!rentData) throw new Error("unexpected! Failed to get RENT_SYSVAR data"); const rentStruct = (0, buffer_layout_1.struct)([(0, buffer_layout_1.nu64)("lamportsPerByteYear"), (0, buffer_layout_1.f64)("exemptionThreshold"), (0, buffer_layout_1.u8)("burnPercent")]); return rentStruct.decode(rentData.data); } /** * Returns parsed state of smart-contract from blockchain * @returns parsed bridge state */ async getStateSafe() { const stateData = await this.getAccountInfo(this.statePublicKey); if (!stateData) { throw new errors_1.BridgeStateNotExists(this.statePublicKey); } let parsedState; try { parsedState = this.settingsProgram.coder.accounts.decode("state", stateData.data); } catch (error) { if (error instanceof errors_1.StructDecodeError) throw new errors_1.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 */ async getConfirmationStorageSafe(confirmationStorage) { confirmationStorage = new web3_js_1.PublicKey(confirmationStorage); if (!confirmationStorage) throw new Error("confirmationStorageAccount not set"); const confirmationStorageData = await 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 errors_1.StructDecodeError) throw new errors_1.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 */ async getChainSupportInfoSafe(chainSupportInfo, checkIfSupported = true) { chainSupportInfo = new web3_js_1.PublicKey(chainSupportInfo); const chainSupportInfoData = await this.getAccountInfo(chainSupportInfo); if (!chainSupportInfoData) { throw new errors_1.ChainSupportInfoNotInitialized(chainSupportInfo); } let parsedChainSupportInfo; try { parsedChainSupportInfo = this.settingsProgram.coder.accounts.decode("chainSupportInfo", chainSupportInfoData.data); } catch (error) { if (error instanceof errors_1.StructDecodeError) throw new errors_1.ChainSupportInfoMalformed(chainSupportInfo); throw error; } if (checkIfSupported && !(0, interfaces_1.isSupportedChainInfoType)(parsedChainSupportInfo.data)) { throw new errors_1.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 ((0, interfaces_1.isMintBridge)(bridge.data)) { return bridge.data.mint.info; // TODO // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } else if ((0, interfaces_1.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 */ async getBridgeInfoSafe(bridge) { bridge = new web3_js_1.PublicKey(bridge); const bridgeData = await this.getAccountInfo(bridge); if (!bridgeData) { throw new errors_1.BridgeNotExists(bridge); } let parsedBridge; try { parsedBridge = this.decoder.decodeBridge(bridgeData.data); } catch (error) { if (error instanceof errors_1.StructDecodeError) throw new errors_1.BridgeMalformed(bridge); throw error; } if (parsedBridge === null) throw new errors_1.BridgeMalformed(bridge); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return { ...parsedBridge, info: DeBridgeSolanaClient.getBridgeInfoFromBridgeType(parsedBridge) }; } /** * Returns parsed bridgeFee from blockchain or raises error * @param bridgeFee account of bridgeFee * @returns parsed bridgeFee */ async getBridgeFeeSafe(bridgeFee) { bridgeFee = new web3_js_1.PublicKey(bridgeFee); const bridgeFeeData = await this.getAccountInfo(bridgeFee); if (!bridgeFeeData) { throw new errors_1.BridgeFeeNotInitialized(bridgeFee); } let parsedBridgeFee; try { parsedBridgeFee = this.settingsProgram.coder.accounts.decode("bridgeFeeInfo", bridgeFeeData.data); } catch (error) { if (error instanceof errors_1.StructDecodeError) throw new errors_1.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 */ async getDiscountInfoSafe(discount, returnNullIfNoDiscount = true) { discount = new web3_js_1.PublicKey(discount); const discountData = await this.getAccountInfo(discount); if (!discountData) { return undefined; } let parsedDiscount; try { parsedDiscount = this.settingsProgram.coder.accounts.decode("discountInfo", discountData.data); } catch (error) { if (error instanceof errors_1.StructDecodeError) throw new errors_1.DiscountInfoMalformed(discount); throw error; } if (!(0, interfaces_1.isActiveDiscount)(parsedDiscount.data)) { if (returnNullIfNoDiscount) { return undefined; } else { throw new errors_1.DiscountNotActive(discount); } } return parsedDiscount.data; } /** * Fetches submission info from blockchain * @param submission account of submissioon * @returns parsed submission info */ async getSubmissionInfoSafe(submission, commitment) { submission = new web3_js_1.PublicKey(submission); commitment = commitment || this._connection.commitment; const fetched = await this.program.account.submissionAccount.fetchNullable(submission, commitment); if (fetched) { return fetched; } else { throw new errors_1.SubmissionInfoNotExists(submission); } } /** * Fetches extCallMeta from blockchain * @param externalCallMeta account of ext call meta * @returns parsed external call metadata */ async getExternalCallMetaSafe(externalCallMeta, commitment) { externalCallMeta = new web3_js_1.PublicKey(externalCallMeta); commitment = commitment || this._connection.commitment; const fetched = await this.program.account.externalCallMeta.fetchNullable(externalCallMeta, commitment); if (fetched) { return fetched; } else { throw new errors_1.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 */ async buildFallbackTransactions(submissionId, executor) { executor = new web3_js_1.PublicKey(executor); const result = [{ instructions: [], payer: executor }]; const submissionIdBuffer = isBuffer(submissionId) ? submissionId : solana_utils_1.helpers.hexToBuffer(submissionId); const [submission] = this.accountsResolver.getSubmissionAddress(submissionIdBuffer); const [submissionAuth, submissionAuthBump] = this.accountsResolver.getSubmissionAuthAddress(submission); const submissionInfo = await this.getSubmissionInfoSafe(submission); const [submissionWallet] = findAssociatedTokenAddress(submissionAuth, submissionInfo.tokenMint, this.associatedTokenProgramId); const [externalCallStorage] = this.accountsResolver.getExternalCallStorageAddress(submissionIdBuffer, submissionInfo.claimer, buffer_1.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 ((await this.getAccountInfo(fallbackWallet)) === null) result[0].instructions.push(solana_utils_1.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(await fallbackBuilder.instruction()); // find submissionAuth ATAs const walletsToClose = await solana_utils_1.spl.getAllTokenAccountsWithBalances(this._connection, submissionAuth); const closePairs = await Promise.all(walletsToClose .filter((wallet) => !wallet.address.equals(submissionWallet)) .map(async (wallet) => { const ixs = []; const [fallbackForWallet] = findAssociatedTokenAddress(submissionInfo.fallbackAddress, wallet.mint, this.associatedTokenProgramId); if (!(await this.checkIfAccountExists(fallbackForWallet))) ixs.push(solana_utils_1.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(await closeSubAuthWalletBuilder.instruction()); return ixs; }, this)); // pack [init wallet]+close ix as tight as possible for (const closePair of closePairs) { const clonedTx = new web3_js_1.Transaction().add(...result[result.length - 1].instructions); clonedTx.add(...closePair); const txSize = solana_utils_1.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 */ async buildInitializeMintBridgeTransaction(payer, chainId, tokenAddress, tokenName, tokenSymbol, decimals) { if (!this.isInitialized()) await this.init(); payer = new web3_js_1.PublicKey(payer); const chainIdBuffer = solana_utils_1.crypto.normalizeChainId(chainId); const [chainSupportInfoAccount] = this.accountsResolver.getChainSupportInfoAddress(chainIdBuffer); const chainSupportInfo = await this.getChainSupportInfoSafe(chainSupportInfoAccount); if (!(0, interfaces_1.isSupportedChainInfoType)(chainSupportInfo.data)) { throw new Error("chain supoprt info not supported!"); } const nativeTokenAddress = solana_utils_1.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 = solana_utils_1.crypto.hashDebridgeId(chainIdBuffer, "0x" + nativeTokenAddress.toString("hex")); const message = solana_utils_1.crypto.hashDeployInfo({ decimals, tokenName, tokenSymbol, debridgeId }); const [confirmationStorage] = this.accountsResolver.getConfirmationsStorageAddress(solana_utils_1.helpers.hexToBuffer(message)); let confirmationStorageCreator = payer; try { const storage = await this.getConfirmationStorageSafe(confirmationStorage); confirmationStorageCreator = storage.creator; } catch { loglevel_1.default.debug("No confirmation storage exists -> confirmationStorageCreator is payer"); } const [tokenMint] = this.accountsResolver.getTokenMintAddress(solana_utils_1.helpers.hexToBuffer(debridgeId)); const [bridgeData] = this.accountsResolver.getBridgeAddress(tokenMint); const [mintAuthority] = this.accountsResolver.getMintAuthorityAddress(bridgeData); const initMintBuilder = instructions.initializeMintBridgeInstruction(this._settingsProgram, { tokenMint, bridgeData, mintAuthority, confirmationStorage, confirmationStorageCreator, payer: payer, state: this.statePublicKey, feeBeneficiary: this.feeBeneficiarAccount, tokenMetadata: (0, solana_utils_1.getTokenMetadataAddress)(tokenMint, solana_utils_1.TOKEN_METADATA_PROGRAM_ID)[0], tokenMetadataMaster: this.accountsResolver.getTokenMetadataMasterAddress()[0], }, { nativeTokenAddress, tokenName, tokenSymbol, decimals, chainId: chainIdBuffer, }); const result = { instructions: [await initMintBuilder.instruction()], payer }; return { debridgeId, transaction: result }; } /** * Builds transaction for staking wallet creation (if not exists) and send bridge initializaion * @param tokenMint token mint account to init bridge * @param payer who pays for transaction * @returns transaction for [staking wallet creation] and send bridge initialization */ async buildInitializeSendBridgeTransaction(tokenMint, payer) { const result = []; tokenMint = new web3_js_1.PublicKey(tokenMint); payer = new web3_js_1.PublicKey(payer); const [bridgeAccount] = this.accountsResolver.getBridgeAddress(tokenMint); const [mintAuthority] = this.accountsResolver.getMintAuthorityAddress(bridgeAccount); const [stakingWallet] = findAssociatedTokenAddress(mintAuthority, tokenMint, this.associatedTokenProgramId); const bridgeId = solana_utils_1.helpers.hexToBuffer(solana_utils_1.crypto.hashDebridgeId(this.chainId, tokenMint.toBuffer())); const [bridgeMap] = this.accountsResolver.getBridgeMapAddress(bridgeId); if (!(await this.checkIfAccountExists(stakingWallet))) { result.push(solana_utils_1.spl.createAssociatedWalletInstruction(tokenMint, stakingWallet, mintAuthority, payer, false)); } const initSendBuilder = instructions.initializeSendBridgeInstruction(this._settingsProgram, { payer, state: this.statePublicKey, tokenMint, bridgeData: bridgeAccount, mintAuthority, stakingWallet, bridgeIdMap: bridgeMap, tokenMetadata: (0, solana_utils_1.getTokenMetadataAddress)(tokenMint, solana_utils_1.TOKEN_METADATA_PROGRAM_ID)[0], }); result.push(await initSendBuilder.instruction()); return { instructions: result, payer }; } /** * Builds transaction which updates bridge fee info * @param chainId * @param tokenMint * @param chainFee new fee value * @param payer * @returns built transaction */ async buildUpdateFeeBridgeInfoTransaction(chainId, tokenMint, chainFee, paye