UNPKG

@arcana/ca-sdk

Version:

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

275 lines (274 loc) 9.52 kB
import { ChaindataMap, OmniversalChainID, PermitVariant, signPermitForAddressAndValue, Universe, } from "@arcana/ca-common"; import { CHAIN_IDS } from "fuels"; import { connect } from "it-ws"; import { createPublicClient, decodeFunctionData, getContract, hexToBytes, http, pad, parseSignature, SwitchChainError, } from "viem"; import ERC20ABI from "../abi/erc20"; import gasOracleABI from "../abi/gasOracle"; import { FillEvent } from "../abi/vault"; import { ErrorLiquidityTimeout } from "../errors"; import { getLogger } from "../logger"; import { checkIntentFilled, vscCreateSponsoredApprovals } from "./api.utils"; import { convertTo32Bytes, minutesToMs } from "./common.utils"; const logger = getLogger(); export const isEVMTx = (tx) => { logger.debug("isEVMTx", tx); if (typeof tx !== "object") { return false; } if (!tx) { return false; } if (!("to" in tx)) { return false; } if (!("data" in tx || "value" in tx)) { return false; } return true; }; export const getAllowance = (chain, address, tokenContract, chainList) => { const publicClient = createPublicClient({ transport: http(chain.rpcUrls.default.http[0]), }); return publicClient.readContract({ abi: ERC20ABI, address: tokenContract, args: [address, chainList.getVaultContractAddress(chain.id)], functionName: "allowance", }); }; export const getERC20Contract = (tokenContract, client) => { const contract = getContract({ abi: ERC20ABI, address: tokenContract, client: { public: client, wallet: client }, }); return contract; }; export const getAllowances = async (input, address, chainList) => { const values = {}; const promises = []; for (const i of input) { if (i.chainID === CHAIN_IDS.fuel.mainnet) { promises.push(Promise.resolve(0n)); } else { const chain = chainList.getChainByID(i.chainID); if (!chain) { throw new Error("chain not found"); } promises.push(getAllowance(chain, address, i.tokenContract, chainList)); } } const result = await Promise.all(promises); for (const i in result) { values[input[i].chainID] = result[i]; } return values; }; export const waitForIntentFulfilment = async (publicClient, vaultContractAddr, requestHash, ac) => { return new Promise((resolve) => { const unwatch = publicClient.watchContractEvent({ abi: [FillEvent], address: vaultContractAddr, args: { requestHash }, eventName: "Fill", onLogs: (logs) => { logger.debug("waitForIntentFulfilment", { logs }); ac.abort(); return resolve("ok"); }, poll: false, }); ac.signal.addEventListener("abort", () => { unwatch(); return resolve("ok from outside"); }, { once: true }); }); }; export const requestTimeout = (timeout, ac) => { return new Promise((_, reject) => { const t = window.setTimeout(() => { ac.abort(); return reject(ErrorLiquidityTimeout); }, minutesToMs(timeout)); ac.signal.addEventListener("abort", () => { window.clearTimeout(t); }, { once: true }); }); }; export const getTokenTxFunction = (data) => { try { const { args, functionName } = decodeFunctionData({ abi: ERC20ABI, data, }); return { args, functionName }; } catch (e) { logger.debug("getTokenTxFunction", e); return { args: [], functionName: "unknown" }; } }; export const setAllowances = async (tokenContractAddresses, client, networkConfig, chainList, chain, amount) => { const vaultAddr = chainList.getVaultContractAddress(chain.id); const p = []; const address = (await client.getAddresses())[0]; const chainId = new OmniversalChainID(Universe.ETHEREUM, chain.id); const chainDatum = ChaindataMap.get(chainId); if (!chainDatum) { throw new Error("Chain data not found"); } const account = { address, type: "json-rpc", }; const sponsoredApprovalParams = { address: hexToBytes(pad(address, { dir: "left", size: 32, })), chain_id: chainDatum.ChainID32, operations: [], universe: chainDatum.Universe, }; for (const addr of tokenContractAddresses) { const currency = chainDatum.CurrencyMap.get(convertTo32Bytes(addr)); if (!currency) { throw new Error("Currency not found"); } if (currency.permitVariant === PermitVariant.Unsupported) { const contract = getERC20Contract(addr, client); const hash = await contract.write.approve([vaultAddr, amount], { account: address, chain, }); p.push((async function () { const publicClient = createPublicClient({ transport: http(chain.rpcUrls.default.http[0]), }); const result = await publicClient.waitForTransactionReceipt({ confirmations: 2, hash, }); if (result.status === "reverted") { throw new Error("setAllowance failed with tx revert"); } })()); } else { const signed = parseSignature(await signPermitForAddressAndValue(currency, client, account, vaultAddr, amount)); sponsoredApprovalParams.operations.push({ sig_r: hexToBytes(signed.r), sig_s: hexToBytes(signed.s), sig_v: signed.yParity < 27 ? signed.yParity + 27 : signed.yParity, token_address: currency.tokenAddress, value: convertTo32Bytes(amount), variant: currency.permitVariant === PermitVariant.PolygonEMT ? 2 : 1, }); } } if (p.length) { await Promise.all(p); } if (sponsoredApprovalParams.operations.length) { await vscCreateSponsoredApprovals(networkConfig.VSC_DOMAIN, [ sponsoredApprovalParams, ]); } return; }; const DEFAULT_GAS_ORACLE_ADDRESS = "0x420000000000000000000000000000000000000F"; const L1_GAS_ORACLES = { 10: DEFAULT_GAS_ORACLE_ADDRESS, 11155420: DEFAULT_GAS_ORACLE_ADDRESS, 534352: "0x5300000000000000000000000000000000000002", 8453: DEFAULT_GAS_ORACLE_ADDRESS, 84532: DEFAULT_GAS_ORACLE_ADDRESS, }; const chainsWithGasOracles = Object.keys(L1_GAS_ORACLES).map(Number); export const getL1Fee = async (chain, input = "0x") => { let fee = 0n; if (chainsWithGasOracles.includes(chain.id)) { fee = await fetchL1Fee(chain, input); } return fee; }; const fetchL1Fee = async (chain, input) => { const rpcURL = chain.rpcUrls.default.http[0]; const contract = getContract({ abi: gasOracleABI, address: L1_GAS_ORACLES[chain.id], client: { public: createPublicClient({ transport: http(rpcURL), }), }, }); return await contract.read.getL1Fee([input]); }; const decoder = new TextDecoder("utf-8"); const cosmosFillCheck = (intentID, grpcURL, cosmosURL, ac) => { return Promise.any([ waitForCosmosFillEvent(intentID, cosmosURL, ac), checkIntentFilled(intentID, grpcURL), ]); }; const waitForCosmosFillEvent = async (intentID, cosmosURL, ac) => { const u = new URL("/websocket", cosmosURL); u.protocol = "wss"; u.port = "26650"; const connection = connect(u.toString()); await connection.connected(); ac.signal.addEventListener("abort", () => { connection.close(); return Promise.resolve("ok from outside"); }, { once: true }); const EVENT = "xarchain.chainabstraction.RFFFulfilledEvent.id"; try { connection.socket.send(JSON.stringify({ id: "0", jsonrpc: "2.0", method: "subscribe", params: { query: `${EVENT}='"${intentID}"'`, }, })); for await (const resp of connection.source) { const decodedResponse = JSON.parse(decoder.decode(resp)); if (decodedResponse.result.events && EVENT in decodedResponse.result.events && decodedResponse.result.events[EVENT].includes(`"${intentID}"`)) { ac.abort(); return "ok"; } } throw new Error("waitForCosmosFillEvent: out of loop but no events"); } finally { connection.close(); } }; const waitForTxReceipt = async (hash, publicClient) => { const r = await publicClient.waitForTransactionReceipt({ confirmations: 2, hash, }); if (r.status === "reverted") { throw new Error(`Transaction reverted: ${hash}`); } }; const switchChain = async (client, chain) => { try { await client.switchChain({ id: chain.id }); } catch (e) { if (e instanceof SwitchChainError && e.code === SwitchChainError.code) { return await client.addChain({ chain, }); } throw e; } }; export { cosmosFillCheck, switchChain, waitForCosmosFillEvent, waitForTxReceipt, };