UNPKG

@moonbeam-network/xcm-sdk

Version:

The Moonbeam XCM SDK enables developers to easily deposit and withdraw assets to Moonbeam/Moonriver from the relay chain and other parachains in the Polkadot/Kusama ecosystem

1,002 lines (990 loc) 27.9 kB
// src/getTransferData/getDestinationData.ts import { Parachain as Parachain2 } from "@moonbeam-network/xcm-types"; import { getSovereignAccountAddresses } from "@moonbeam-network/xcm-utils"; // src/getTransferData/getTransferData.utils.ts import { ContractConfig, EvmQueryConfig, SubstrateCallConfig, SubstrateQueryConfig } from "@moonbeam-network/xcm-builder"; import { AssetAmount as AssetAmount2, EvmChain, EvmParachain, Parachain } from "@moonbeam-network/xcm-types"; import { convertDecimals, toBigInt } from "@moonbeam-network/xcm-utils"; import Big from "big.js"; // src/services/evm/EvmService.ts import { createPublicClient, http } from "viem"; var EvmService = class _EvmService { chain; client; static create(chain) { return new _EvmService(chain); } constructor(chain) { this.chain = chain; this.client = createPublicClient({ chain: chain.getViemChain(), transport: http() }); } async query(query) { return this.client[query.func](...query.args); } async read(config) { return this.client.readContract({ abi: config.abi, address: config.address, args: config.args, functionName: config.func }); } async getFee(address, contract, stateOverride) { const gas = await this.client.estimateContractGas({ abi: contract.abi, account: address, address: contract.address, args: contract.args, functionName: contract.func, value: contract.value, stateOverride }); const gasPrice = await this.client.getGasPrice(); return gas * gasPrice; } async getBalance(address, contract) { const balance = await this.client.readContract({ abi: contract.abi, address: contract.address, args: [address], functionName: "balanceOf" }); if (typeof balance !== "bigint") { throw new Error( `Could not get balance on ${this.chain.name} for ${address} from contract ${contract.address}` ); } return balance; } async transfer(signer, contract) { const { request } = await this.client.simulateContract({ abi: contract.abi, account: signer.account, address: contract.address, args: contract.args, functionName: contract.func, value: contract.value }); const hash = await signer.writeContract(request); return hash; } }; // src/services/polkadot/PolkadotService.ts import "@polkadot/api-augment"; import { AssetAmount } from "@moonbeam-network/xcm-types"; import { getPolkadotApi } from "@moonbeam-network/xcm-utils"; var PolkadotService = class _PolkadotService { api; chain; constructor(api, chain) { this.api = api; this.chain = chain; } static async create(chain) { return new _PolkadotService(await getPolkadotApi(chain.ws), chain); } static async createMulti(chains) { return Promise.all( chains.map((chain) => _PolkadotService.create(chain)) ); } get decimals() { return this.api.registry.chainDecimals.at(0) || 12; } get existentialDeposit() { const existentialDeposit = this.api.consts.balances?.existentialDeposit; const eqExistentialDeposit = this.api.consts.eqBalances?.existentialDeposit; const amount = existentialDeposit?.toBigInt() || eqExistentialDeposit?.toBigInt() || 0n; return AssetAmount.fromChainAsset(this.chain.nativeAsset, { amount }); } async query(config) { const queryFn = this.api[config.queryType][config.module][config.func]; const response = await queryFn(...config.args); return config.transform(response); } getExtrinsic(config) { const fn = this.api.tx[config.module][config.func]; const args = config.getArgs(fn); return fn(...args); } getExtrinsicCallHash(config) { return this.getExtrinsic(config).method.toHex(); } async getPaymentInfo(account, config) { const extrinsic = this.getExtrinsic(config); return extrinsic.paymentInfo(account, { nonce: -1 }); } async getFee(account, config) { const info = await this.getPaymentInfo(account, config); return info.partialFee.toBigInt(); } async transfer(account, config, signer, statusCallback) { const extrinsic = this.getExtrinsic(config); const isSigner = this.#isSigner(signer); const signOptions = { nonce: -1, signer: isSigner ? signer : void 0, withSignedTransaction: true }; const hash = await new Promise((resolve, reject) => { extrinsic.signAndSend(isSigner ? account : signer, signOptions, (result) => { if (result.isError || result.dispatchError) { reject( new Error( result.dispatchError?.toString() || "Transaction failed" ) ); } if (result.txHash) { resolve(result.txHash.toString()); } statusCallback?.(result); }).catch(reject); }); return hash; } #isSigner(signer) { return "signPayload" in signer; } }; // src/getTransferData/getTransferData.utils.ts async function getBalance({ address, asset, builder, chain }) { const config = builder.build({ address, asset }); const amount = AssetAmount2.fromChainAsset(asset, { amount: 0n }); if (SubstrateQueryConfig.is(config) && EvmParachain.isAnyParachain(chain)) { const polkadot = await PolkadotService.create(chain); const balance = await polkadot.query(config); const converted = chain.usesChainDecimals ? convertDecimals(balance, polkadot.decimals, asset.decimals) : balance; return amount.copyWith({ amount: converted }); } if (EvmChain.is(chain) || EvmParachain.is(chain)) { const evm = EvmService.create(chain); if (ContractConfig.is(config)) { const balance = await evm.getBalance(address, config); return amount.copyWith({ amount: balance }); } if (EvmQueryConfig.is(config)) { const balance = await evm.query(config); return amount.copyWith({ amount: balance }); } } throw new Error( `Can't get balance for ${address} on chain ${chain.name} and asset ${asset.symbol}` ); } async function getAssetMin({ asset, builder, chain }) { const zero = AssetAmount2.fromChainAsset(chain.getChainAsset(asset), { amount: 0n }); if (builder && EvmParachain.isAnyParachain(chain)) { const polkadot = await PolkadotService.create(chain); const min = await polkadot.query( builder.build({ asset: zero.getMinAssetId(), address: zero.address }) ); return zero.copyWith({ amount: min }); } if (zero.min) { return zero.copyWith({ amount: zero.min }); } return zero; } function getMin({ balance, existentialDeposit, fee, min }) { const result = Big(0).plus(balance.isSame(fee) ? fee.toBig() : Big(0)).plus( existentialDeposit && balance.isSame(existentialDeposit) && balance.toBig().lt(existentialDeposit.toBig()) ? existentialDeposit.toBig() : Big(0) ).plus(balance.toBig().lt(min.toBig()) ? min.toBig() : Big(0)); return balance.copyWith({ amount: BigInt(result.toFixed()) }); } function getMax({ balance, existentialDeposit, fee, min }) { const result = balance.toBig().minus(min.toBig()).minus( existentialDeposit && balance.isSame(existentialDeposit) ? existentialDeposit.toBig() : Big(0) ).minus(balance.isSame(fee) ? fee.toBig() : Big(0)); return balance.copyWith({ amount: result.lt(0) ? 0n : BigInt(result.toFixed()) }); } async function getDestinationFee({ address, asset, destination, fee, feeAsset, source }) { const zero = AssetAmount2.fromChainAsset(destination.getChainAsset(feeAsset), { amount: 0n }); if (typeof fee === "number") { return zero.copyWith({ amount: fee }); } if (EvmParachain.isAnyParachain(destination)) { const polkadot = await PolkadotService.create(destination); const cfg = fee.build({ address, api: polkadot.api, asset: destination.getChainAsset(asset), destination, feeAsset: destination.getChainAsset(feeAsset), source }); if (SubstrateCallConfig.is(cfg)) { return zero.copyWith({ amount: await cfg.call() }); } } return zero; } function convertToChainDecimals({ asset, target }) { return AssetAmount2.fromChainAsset(target, { amount: asset.convertDecimals(target.decimals).amount }); } async function getExistentialDeposit(chain) { if (EvmParachain.isAnyParachain(chain)) { const polkadot = await PolkadotService.create(chain); return polkadot.existentialDeposit; } return void 0; } async function getDestinationFeeBalance({ balance, feeBalance, route, sourceAddress }) { if (route.destination.fee.asset.isEqual(balance)) { return balance; } if (route.destination.fee.asset.isEqual(feeBalance)) { return feeBalance; } if (!route.source.destinationFee?.balance) { throw new Error( "BalanceBuilder must be defined for source.destinationFee.balance for AssetRoute" ); } return getBalance({ address: sourceAddress, asset: route.getDestinationFeeAssetOnSource(), builder: route.source.destinationFee?.balance, chain: route.source.chain }); } async function getExtrinsicFee({ address, balance, chain, extrinsic, feeBalance, feeConfig }) { try { const polkadot = await PolkadotService.create(chain); const fee = await polkadot.getFee(address, extrinsic); const extra = feeConfig?.extra ? toBigInt(feeConfig.extra, feeBalance.decimals) : 0n; const totalFee = fee + extra; const converted = chain.usesChainDecimals ? convertDecimals(totalFee, polkadot.decimals, feeBalance.decimals) : totalFee; return feeBalance.copyWith({ amount: converted }); } catch (error) { if (balance.amount) { throw error; } return feeBalance.copyWith({ amount: 0n }); } } async function getContractFee({ address, balance, chain, contract, destinationFee, feeBalance, feeConfig }) { try { if (balance.amount === 0n) { return feeBalance.copyWith({ amount: 0n }); } const evm = EvmService.create(chain); const fee = await evm.getFee(address, contract); const extra = feeConfig?.extra ? toBigInt(feeConfig.extra, feeBalance.decimals) : 0n; return feeBalance.copyWith({ amount: fee + extra }); } catch (error) { throw new Error( `Can't get a fee, make sure you have ${destinationFee.toDecimal()} ${destinationFee.getSymbol()} in your source balance, needed for destination fees`, { cause: error } ); } } function validateSovereignAccountBalances({ amount, sourceData, destinationData }) { if (!Parachain.is(destinationData.chain) || !destinationData.chain.checkSovereignAccountBalances || !destinationData.sovereignAccountBalances) { return; } const { feeAssetBalance, transferAssetBalance } = destinationData.sovereignAccountBalances; if (amount > transferAssetBalance) { throw new Error( `${sourceData.chain.name} Sovereign account in ${destinationData.chain.name} does not have enough balance for this transaction` ); } if (feeAssetBalance && sourceData.destinationFee.amount > feeAssetBalance) { throw new Error( `${sourceData.chain.name} Sovereign account in ${destinationData.chain.name} does not have enough balance to pay for fees for this transaction` ); } } // src/getTransferData/getDestinationData.ts async function getDestinationData({ route, destinationAddress }) { const destination = route.destination.chain; const asset = destination.getChainAsset(route.destination.asset); const balance = await getBalance({ address: destinationAddress, asset, builder: route.destination.balance, chain: destination }); const min = await getAssetMin({ asset, builder: route.destination.min, chain: destination }); const fee = await getDestinationFee({ address: destinationAddress, feeAsset: route.destination.fee.asset, destination, fee: route.destination.fee.amount, source: route.source.chain, asset: route.source.asset }); const existentialDeposit = await getExistentialDeposit(destination); return { chain: destination, balance, existentialDeposit, fee, min, sovereignAccountBalances: await getSovereignAccountBalances({ source: route.source, destination: route.destination }) }; } async function getSovereignAccountBalances({ destination, source }) { if (!Parachain2.is(source.chain) || !Parachain2.is(destination.chain) || !destination.chain.checkSovereignAccountBalances) { return void 0; } const sovereignAccountAddresses = getSovereignAccountAddresses( source.chain.parachainId ); const destinationFeeAssetBalance = destination.fee.balance; const sovereignAccountAddress = destination.chain.isRelay ? sovereignAccountAddresses.relay : sovereignAccountAddresses.generic; const sovereignAccountBalance = await getBalance({ address: sovereignAccountAddress, asset: destination.chain.getChainAsset(destination.asset), builder: destination.balance, chain: destination.chain }); const sovereignAccountFeeAssetBalance = destinationFeeAssetBalance ? await getBalance({ address: sovereignAccountAddress, asset: destination.chain.getChainAsset(destination.fee.asset), builder: destinationFeeAssetBalance, chain: destination.chain }) : void 0; return { feeAssetBalance: sovereignAccountFeeAssetBalance?.amount, transferAssetBalance: sovereignAccountBalance.amount }; } // src/getTransferData/getSourceData.ts async function getSourceData({ route, destinationAddress, destinationFee, sourceAddress }) { const source = route.source.chain; const destination = route.destination.chain; const [sourcePolkadot, destinationPolkadot] = await PolkadotService.createMulti([source, destination]); const asset = source.getChainAsset(route.source.asset); const feeAsset = route.source.fee ? source.getChainAsset(route.source.fee.asset) : asset; const balance = await getBalance({ address: sourceAddress, asset, builder: route.source.balance, chain: source }); const feeBalance = await getBalance({ address: sourceAddress, asset: feeAsset, builder: route.source.fee.balance, chain: source }); const destinationFeeBalance = await getDestinationFeeBalance({ balance, feeBalance, route, sourceAddress }); const existentialDeposit = await getExistentialDeposit(source); const min = await getAssetMin({ asset, builder: route.source.min, chain: source }); const extrinsic = route.extrinsic?.build({ asset: balance, destination: route.destination.chain, destinationAddress, destinationApi: destinationPolkadot.api, fee: destinationFee, source, sourceAddress, sourceApi: sourcePolkadot.api }); const contract = route.contract?.build({ asset: balance, destination: route.destination.chain, destinationAddress, destinationApi: destinationPolkadot.api, fee: destinationFee, source, sourceAddress, sourceApi: sourcePolkadot.api }); const fee = await getFee({ balance, chain: source, contract, destinationFee, extrinsic, feeBalance, feeConfig: route.source.fee, sourceAddress }); const max = getMax({ balance, existentialDeposit, fee, min }); return { balance, chain: source, destinationFee, destinationFeeBalance, existentialDeposit, fee, feeBalance, max, min }; } async function getFee({ balance, feeBalance, chain, contract, destinationFee, extrinsic, feeConfig, sourceAddress }) { if (!contract && !extrinsic) { throw new Error("Either contract or extrinsic must be provided"); } if (contract) { return getContractFee({ address: sourceAddress, balance, chain, contract, destinationFee, feeBalance, feeConfig }); } return getExtrinsicFee({ address: sourceAddress, balance, chain, extrinsic, feeBalance, feeConfig }); } async function getAssetsBalances({ address, chain, routes }) { const uniqueRoutes = routes.reduce((acc, route) => { if (!acc.some((a) => a.source.asset.isEqual(route.source.asset))) { return [route, ...acc]; } return acc; }, []); const balances = await Promise.all( uniqueRoutes.map( async (route) => getBalance({ address, asset: chain.getChainAsset(route.source.asset), builder: route.source.balance, chain }) ) ); return balances; } // src/sdk.ts import { ConfigService, xcmRoutesMap } from "@moonbeam-network/xcm-config"; import { EvmParachain as EvmParachain2 } from "@moonbeam-network/xcm-types"; // src/getTransferData/getTransferData.ts import { AssetAmount as AssetAmount3 } from "@moonbeam-network/xcm-types"; import { toBigInt as toBigInt2 } from "@moonbeam-network/xcm-utils"; import Big2 from "big.js"; // src/utils/monitoring.ts import { getPolkadotApi as getPolkadotApi2 } from "@moonbeam-network/xcm-utils"; var ENABLE_LOGGING = process.env.ENABLE_LOGGING === "true"; function log(label, message) { if (ENABLE_LOGGING) { console.log(`[${label}]`, message ?? ""); } } async function listenToDestinationEvents({ route, monitoringConfig, messageId, onDestinationFinalized, onDestinationError }) { if (!route?.destination?.chain || !("ws" in route.destination.chain)) { log("No destination WS endpoint available"); return; } try { const api = await getPolkadotApi2(route.destination.chain.ws); log("Subscribing to destination events..."); const unsubscribe = await api.query.system.events((events) => { log("Destination events", events.toHuman()); const destinationResult = monitoringConfig.checkDestination( events, messageId ); if (destinationResult.matched) { log("Destination event matched:", destinationResult.event?.toHuman()); unsubscribe(); if (destinationResult.success) { onDestinationFinalized?.(); } else { const error = new Error( `Message processing failed on destination chain: ${route.destination.chain.name}` ); console.error( "Destination message processing failed:", destinationResult.event?.toHuman() ); onDestinationError?.(error); } } }); } catch (error) { console.error("Error listening to destination events:", error); onDestinationError?.(error); } } function processSourceEvents({ events, sourceAddress, route, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError, unsubscribe }) { const monitoringConfig = route.monitoring; if (!monitoringConfig) { log("No monitoring config found"); unsubscribe?.(); return; } const extrinsicFailedEvent = events.find((event) => { return event.event.section === "system" && event.event.method === "ExtrinsicFailed"; }); if (extrinsicFailedEvent) { onSourceError?.(new Error("Extrinsic failed")); unsubscribe?.(); return; } try { const sourceResult = monitoringConfig.checkSource(events, sourceAddress); if (sourceResult.matched) { log("Source event matched:", sourceResult.event?.toHuman()); onSourceFinalized?.(); log("Message ID:", sourceResult.messageId); if (unsubscribe) { log("Unsubscribing from source events..."); unsubscribe(); } listenToDestinationEvents({ route, monitoringConfig, messageId: sourceResult.messageId, onDestinationFinalized, onDestinationError }); return; } } catch (error) { console.error("Error in MonitoringBuilder config:", error); return; } } function createMonitoringCallback({ sourceAddress, route, statusCallback, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }) { return (status) => { statusCallback?.(status); processSourceEvents({ events: status.events, sourceAddress, route, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }); }; } async function listenToSourceEvents({ route, sourceAddress, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }) { if (!route?.source?.chain || !("ws" in route.source.chain)) { log("No source WS endpoint available"); return; } try { const api = await getPolkadotApi2(route.source.chain.ws); log("Subscribing to source events..."); const unsubscribe = await api.query.system.events((events) => { log("Source events:", events.toHuman()); processSourceEvents({ events, sourceAddress, route, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError, unsubscribe }); }); } catch (error) { console.error("Error listening to source events:", error); } } // src/getTransferData/getTransferData.ts async function getTransferData({ route, sourceAddress, destinationAddress }) { const destinationData = await getDestinationData({ route, destinationAddress }); const destinationFee = convertToChainDecimals({ asset: destinationData.fee, target: route.getDestinationFeeAssetOnSource() }); const sourceData = await getSourceData({ route, destinationAddress, destinationFee, sourceAddress }); return { destination: destinationData, getEstimate(amount) { const bigAmount = Big2( toBigInt2(amount, sourceData.balance.decimals).toString() ); const result = bigAmount.minus( sourceData.balance.isSame(destinationFee) ? destinationFee.toBig() : Big2(0) ); return sourceData.balance.copyWith({ amount: result.lt(0) ? 0n : BigInt(result.toFixed()) }); }, max: sourceData.max, min: getMin(destinationData), source: sourceData, async transfer({ amount, signers: { evmSigner, polkadotSigner }, statusCallback, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }) { const source = route.source.chain; const destination = route.destination.chain; const bigintAmount = toBigInt2(amount, sourceData.balance.decimals); validateSovereignAccountBalances({ amount: bigintAmount, destinationData, sourceData }); const asset = AssetAmount3.fromChainAsset( route.source.chain.getChainAsset(route.source.asset), { amount: bigintAmount } ); const [sourcePolkadot, destinationPolkadot] = await PolkadotService.createMulti([source, destination]); const contract = route.contract?.build({ asset, destination, destinationAddress, destinationApi: destinationPolkadot.api, fee: destinationFee, source, sourceAddress, sourceApi: sourcePolkadot.api }); const extrinsic = route.extrinsic?.build({ asset, destination, destinationAddress, destinationApi: destinationPolkadot.api, fee: destinationFee, source, sourceAddress, sourceApi: sourcePolkadot.api }); const shouldListenToEvents = !!onSourceFinalized || !!onSourceError || !!onDestinationFinalized || !!onDestinationError; if (contract) { if (!evmSigner) { throw new Error("EVM Signer must be provided"); } const evm = EvmService.create(source); const hash = await evm.transfer(evmSigner, contract); if (shouldListenToEvents) { listenToSourceEvents({ route, sourceAddress, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }); } return hash; } if (extrinsic) { if (!polkadotSigner) { throw new Error("Polkadot signer must be provided"); } const monitoringCallback = shouldListenToEvents ? createMonitoringCallback({ sourceAddress, route, statusCallback, onSourceFinalized, onSourceError, onDestinationFinalized, onDestinationError }) : void 0; return sourcePolkadot.transfer( sourceAddress, extrinsic, polkadotSigner, monitoringCallback || statusCallback ); } throw new Error("Either contract or extrinsic must be provided"); } }; } // src/sdk.ts var DEFAULT_SERVICE = new ConfigService({ routes: xcmRoutesMap }); function Sdk({ configService, ecosystem } = {}) { const service = configService ?? DEFAULT_SERVICE; const assets = service.getEcosystemAssets(ecosystem); return { assets, setAsset(asset) { const sources = service.getSourceChains({ asset, ecosystem }); return { sources, setSource(source) { const destinations = service.getDestinationChains({ asset, source }); return { destinations, setDestination(destination) { const route = service.getAssetRoute({ asset, source, destination }); return { setAddresses({ sourceAddress, destinationAddress }) { const sourceChain = service.getChain(source); if (!EvmParachain2.isAnyParachain(sourceChain)) { throw new Error( "Source chain should be a Parachain or EvmParachain" ); } if (!EvmParachain2.isAnyParachain(route.destination.chain)) { throw new Error( "Destination chain should be a Parachain or EvmParachain" ); } return getTransferData({ route, sourceAddress, destinationAddress }); } }; } }; } }; } }; } async function getParachainBalances(chain, address, service = DEFAULT_SERVICE) { const routes = service.getChainRoutes(chain).getRoutes(); const balances = await getAssetsBalances({ chain, routes, address }); return balances; } // src/utils/evm.ts import { encodeAbiParameters, keccak256, maxUint256, numberToHex, parseAbiParameters } from "viem"; function getAllowanceSlot(owner, spender, allowanceSlot) { const mappingSlot = allowanceSlot; const outer = keccak256( encodeAbiParameters(parseAbiParameters("address, uint256"), [ owner, BigInt(mappingSlot) ]) ); return keccak256( encodeAbiParameters(parseAbiParameters("address, bytes32"), [ spender, outer ]) ); } var MAX_ALLOWANCE_HEX = numberToHex(maxUint256); export { EvmService, MAX_ALLOWANCE_HEX, PolkadotService, Sdk, convertToChainDecimals, createMonitoringCallback, getAllowanceSlot, getAssetMin, getAssetsBalances, getBalance, getContractFee, getDestinationData, getDestinationFee, getDestinationFeeBalance, getExistentialDeposit, getExtrinsicFee, getFee, getMax, getMin, getParachainBalances, getSourceData, listenToDestinationEvents, listenToSourceEvents, processSourceEvents, validateSovereignAccountBalances }; //# sourceMappingURL=index.mjs.map