UNPKG

@arcana/ca-sdk

Version:

Arcana Network's chain abstraction SDK for unified balance in Web3 apps

378 lines (377 loc) 17.7 kB
import { ArcanaVault, EVMVaultABI, MsgCreateRequestForFunds, OmniversalRFF, Universe, } from "@arcana/ca-common"; import Decimal from "decimal.js"; import { Account, BN, CHAIN_IDS, hexlify } from "fuels"; import Long from "long"; import { createPublicClient, http, toBytes, toHex } from "viem"; import { INTENT_EXPIRY, isNativeAddress } from "../../constants"; import { getLogger } from "../../logger"; import { createSteps, INTENT_DEPOSIT_REQ, INTENT_DEPOSITS_CONFIRMED, INTENT_FULFILLED, INTENT_HASH_SIGNED, INTENT_SUBMITTED, } from "../../steps"; import { convertTo32Bytes, convertTo32BytesHex, cosmosCreateRFF, createDepositDoubleCheckTx, createRequestEVMSignature, createRequestFuelSignature, getExplorerURL, getSourcesAndDestinationsForRFF, mulDecimals, removeIntentHashFromStore, storeIntentHashToStore, switchChain, vscCreateRFF, vscPublishRFF, waitForTxReceipt, } from "../../utils"; const logger = getLogger(); class BaseRequest { constructor(input) { this.input = input; this.markStepDone = (step, data) => { const s = this.steps.find((s) => s.typeID === step.typeID); if (s) { this.input.options.emit("step_complete", { ...s, ...(data ? { data } : {}), }); } }; this.chainList = this.input.chainList; } async processIntent(intent) { logger.debug("intent", { intent }); const { id, requestHash, waitForDoubleCheckTx } = await this.processRFF(intent); storeIntentHashToStore(this.input.evm.address, id.toNumber()); await this.waitForFill(requestHash, id, waitForDoubleCheckTx); removeIntentHashFromStore(this.input.evm.address, id); this.markStepDone(INTENT_FULFILLED); } async processRFF(intent) { const { destinations, sourceCounts, sources, universes } = getSourcesAndDestinationsForRFF(intent, this.input.chainList, this.destinationUniverse); const parties = []; for (const universe of universes) { if (universe === Universe.ETHEREUM) { parties.push({ address: convertTo32BytesHex(this.input.evm.address), universe: universe, }); } if (universe === Universe.FUEL) { parties.push({ address: convertTo32BytesHex(this.input.fuel.address), universe, }); } } logger.debug("processRFF:1", { destinations, parties, sourceCounts, sources, universes, }); const omniversalRff = new OmniversalRFF({ destinationChainID: convertTo32Bytes(intent.destination.chainID), destinations: destinations.map((dest) => ({ tokenAddress: toBytes(dest.tokenAddress), value: toBytes(dest.value), })), destinationUniverse: intent.destination.universe, expiry: Long.fromString((BigInt(Date.now() + INTENT_EXPIRY) / 1000n).toString()), nonce: window.crypto.getRandomValues(new Uint8Array(32)), // @ts-ignore signatureData: parties.map((p) => ({ address: toBytes(p.address), universe: p.universe, })), // @ts-ignore sources: sources.map((source) => ({ chainID: convertTo32Bytes(source.chainID), tokenAddress: convertTo32Bytes(source.tokenAddress), universe: source.universe, value: toBytes(source.value), })), }); const signatureData = []; for (const universe of universes) { if (universe === Universe.ETHEREUM) { const { requestHash, signature } = await createRequestEVMSignature(omniversalRff.asEVMRFF(), this.input.evm.address, this.input.evm.client); signatureData.push({ address: convertTo32Bytes(this.input.evm.address), requestHash, signature, universe: Universe.ETHEREUM, }); } if (universe === Universe.FUEL) { if (!this.input.fuel?.address || !this.input.fuel?.provider || !this.input.fuel?.connector) { logger.error("universe has fuel but not expected input", { fuelInput: this.input.fuel, }); throw new Error("universe has fuel but not expected input"); } const { requestHash, signature } = await createRequestFuelSignature(this.input.chainList.getVaultContractAddress(CHAIN_IDS.fuel.mainnet), this.input.fuel.provider, this.input.fuel.connector, omniversalRff.asFuelRFF()); signatureData.push({ address: toBytes(this.input.fuel.address), requestHash, signature, universe: Universe.FUEL, }); } } logger.debug("processRFF:2", { omniversalRff, signatureData }); this.markStepDone(INTENT_HASH_SIGNED); const cosmosWalletAddress = (await this.input.cosmosWallet.getAccounts())[0] .address; const msgBasicCosmos = MsgCreateRequestForFunds.create({ destinationChainID: omniversalRff.protobufRFF.destinationChainID, destinations: omniversalRff.protobufRFF.destinations, destinationUniverse: omniversalRff.protobufRFF.destinationUniverse, expiry: omniversalRff.protobufRFF.expiry, nonce: omniversalRff.protobufRFF.nonce, signatureData: signatureData.map((s) => ({ address: s.address, signature: s.signature, universe: s.universe, })), sources: omniversalRff.protobufRFF.sources, user: cosmosWalletAddress, }); logger.debug("processRFF:3", { msgBasicCosmos }); const intentID = await cosmosCreateRFF({ address: cosmosWalletAddress, cosmosURL: this.input.options.networkConfig.COSMOS_URL, msg: msgBasicCosmos, wallet: this.input.cosmosWallet, }); this.markStepDone(INTENT_SUBMITTED, { explorerURL: getExplorerURL(this.input.options.networkConfig.EXPLORER_URL, intentID), intentID: intentID.toNumber(), }); if (!this.isNative) { await vscCreateRFF(this.input.options.networkConfig.VSC_DOMAIN, intentID, this.markStepDone, sourceCounts[Universe.ETHEREUM]); } const evmDeposits = []; const fuelDeposits = []; const evmSignatureData = signatureData.find((d) => d.universe === Universe.ETHEREUM); if (!evmSignatureData && universes.has(Universe.ETHEREUM)) { throw new Error("ethereum in universe list but no signature data present"); } const fuelSignatureData = signatureData.find((d) => d.universe === Universe.FUEL); if (!fuelSignatureData && universes.has(Universe.FUEL)) { throw new Error("fuel in universe list but no signature data present"); } const doubleCheckTxs = []; for (const [i, s] of sources.entries()) { const chain = this.input.chainList.getChainByID(Number(s.chainID)); if (!chain) { throw new Error("chain not found"); } if (s.universe === Universe.FUEL) { if (!this.input.fuel) { throw new Error("fuel is involved but no associated data"); } const account = new Account(this.input.fuel.address, this.input.fuel.provider, this.input.fuel.connector); const vault = new ArcanaVault(this.chainList.getVaultContractAddress(CHAIN_IDS.fuel.mainnet), account); const tx = await vault.functions .deposit(omniversalRff.asFuelRFF(), hexlify(fuelSignatureData.signature), i) .callParams({ forward: { amount: new BN(s.value.toString()), assetId: s.tokenAddress, }, }) .call(); this.markStepDone(INTENT_DEPOSIT_REQ(i + 1)); fuelDeposits.push((async function () { const txResult = await tx.waitForResult(); logger.debug("PostIntentSubmission: Fuel deposit result", { txResult, }); if (txResult.transactionResult.isStatusFailure) { throw new Error("fuel deposit failed"); } })()); } else if (s.universe === Universe.ETHEREUM && isNativeAddress(s.universe, s.tokenAddress)) { const chain = this.input.chainList.getChainByID(Number(s.chainID)); if (!chain) { throw new Error("chain not found"); } await switchChain(this.input.evm.client, chain); const publicClient = createPublicClient({ transport: http(chain.rpcUrls.default.http[0]), }); const { request } = await publicClient.simulateContract({ abi: EVMVaultABI, account: this.input.evm.address, address: this.input.chainList.getVaultContractAddress(chain.id), args: [ omniversalRff.asEVMRFF(), toHex(evmSignatureData.signature), BigInt(i), ], chain: chain, functionName: "deposit", value: s.value, }); const hash = await this.input.evm.client.writeContract(request); this.markStepDone(INTENT_DEPOSIT_REQ(i + 1)); evmDeposits.push(waitForTxReceipt(hash, publicClient)); } doubleCheckTxs.push(createDepositDoubleCheckTx(convertTo32Bytes(chain.id), { address: cosmosWalletAddress, wallet: this.input.cosmosWallet, }, intentID, this.input.options.networkConfig)); } if (evmDeposits.length || fuelDeposits.length) { await Promise.all([Promise.all(evmDeposits), Promise.all(fuelDeposits)]); this.markStepDone(INTENT_DEPOSITS_CONFIRMED); } logger.debug("PostIntentSubmission: Intent ID", { id: intentID.toNumber(), }); if (this.isNative) { await vscPublishRFF(this.input.options.networkConfig.VSC_DOMAIN, intentID); } const destinationSigData = signatureData.find((s) => s.universe === intent.destination.universe); if (!destinationSigData) { throw new Error("requestHash not found for destination"); } return { id: intentID, requestHash: destinationSigData.requestHash, waitForDoubleCheckTx: waitForDoubleCheckTx(doubleCheckTxs), }; } createExpectedSteps(intent, unallowedSources) { this.steps = createSteps(intent, this.isNative ? "native" : "erc20", this.chainList, unallowedSources); this.input.options.emit("expected_steps", this.steps); logger.debug("ExpectedSteps", this.steps); } createIntent(input) { const { amount, assets, feeStore, gas, gasInToken, token } = input; const intent = { destination: { amount: new Decimal("0"), chainID: this.input.chain.id, decimals: token.decimals, gas: 0n, tokenContract: token.contractAddress, universe: this.destinationUniverse, }, fees: { caGas: "0", collection: "0", fulfilment: "0", gasSupplied: input.gasInToken.toFixed(), protocol: "0", solver: "0", }, isAvailableBalanceInsufficient: false, sources: [], }; const asset = assets.find(token.symbol); if (!asset) { throw new Error(`Asset ${token.symbol} not found in UserAssets`); } const destinationBalance = asset.getBalanceOnChain(this.input.chain.id, token.contractAddress); let borrow = new Decimal(0); if (this.input.options.bridge) { borrow = amount; } else { if (amount.greaterThan(destinationBalance)) { borrow = amount.minus(destinationBalance); } if (destinationBalance !== "0") { intent.sources.push({ amount: amount.greaterThan(destinationBalance) ? new Decimal(destinationBalance) : amount, chainID: this.input.chain.id, tokenContract: token.contractAddress, universe: this.destinationUniverse, }); } } const protocolFee = feeStore.calculateProtocolFee(borrow); intent.fees.protocol = protocolFee.toFixed(); let borrowWithFee = borrow.add(gasInToken).add(protocolFee); logger.debug("createIntent:0", { borrow: borrow.toFixed(), borrowWithFee: borrowWithFee.toFixed(), destinationBalance, gasInToken: gasInToken.toFixed(), protocolFee: protocolFee.toFixed(), }); const fulfilmentFee = feeStore.calculateFulfilmentFee({ decimals: token.decimals, destinationChainID: this.input.chain.id, destinationTokenAddress: token.contractAddress, }); logger.debug("createIntent:1", { fulfilmentFee }); intent.fees.fulfilment = fulfilmentFee.toFixed(); borrowWithFee = borrowWithFee.add(fulfilmentFee); let accountedAmount = new Decimal(0); for (const assetC of asset.iterate(feeStore)) { if (accountedAmount.greaterThanOrEqualTo(borrowWithFee)) { break; } if (assetC.chainID === this.input.chain.id) { continue; } if (assetC.chainID === CHAIN_IDS.fuel.mainnet) { const fuelChain = this.chainList.getChainByID(CHAIN_IDS.fuel.mainnet); const baseAssetBalanceOnFuel = assets.getNativeBalance(fuelChain); if (new Decimal(baseAssetBalanceOnFuel).lessThan("0.000_003")) { logger.debug("fuel base asset balance is lesser than min expected deposit fee, so skip", { current: baseAssetBalanceOnFuel, minimum: "0.000_003", }); continue; } } if (!this.isNative) { const collectionFee = feeStore.calculateCollectionFee({ decimals: assetC.decimals, sourceChainID: assetC.chainID, sourceTokenAddress: assetC.tokenContract, }); intent.fees.collection = collectionFee .add(intent.fees.collection) .toFixed(); borrowWithFee = borrowWithFee.add(collectionFee); logger.debug("createIntent:2", { collectionFee }); } const unaccountedAmount = borrowWithFee.minus(accountedAmount); let borrowFromThisChain = new Decimal(assetC.balance).lessThanOrEqualTo(unaccountedAmount) ? new Decimal(assetC.balance) : unaccountedAmount; const solverFee = feeStore.calculateSolverFee({ borrowAmount: borrowFromThisChain, decimals: assetC.decimals, destinationChainID: this.input.chain.id, destinationTokenAddress: token.contractAddress, sourceChainID: assetC.chainID, sourceTokenAddress: assetC.tokenContract, }); intent.fees.solver = solverFee.add(intent.fees.solver).toFixed(); logger.debug("createIntent:3", { solverFee }); borrowWithFee = borrowWithFee.add(solverFee); const unaccountedBalance = borrowWithFee.minus(accountedAmount); borrowFromThisChain = new Decimal(assetC.balance).lessThanOrEqualTo(unaccountedBalance) ? new Decimal(assetC.balance) : unaccountedBalance; intent.sources.push({ amount: borrowFromThisChain, chainID: assetC.chainID, tokenContract: assetC.tokenContract, universe: assetC.universe, }); accountedAmount = accountedAmount.add(borrowFromThisChain); } intent.destination.amount = borrow; if (accountedAmount < borrowWithFee) { intent.isAvailableBalanceInsufficient = true; } if (!gas.equals(0)) { intent.destination.gas = mulDecimals(gas, this.input.chain.nativeCurrency.decimals); } logger.debug("createIntent:4", { intent }); return intent; } } const waitForDoubleCheckTx = (input) => { return async () => { await Promise.allSettled(input.map((i) => i())); }; }; export default BaseRequest;