UNPKG

@arcana/ca-sdk

Version:

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

412 lines (411 loc) 14.7 kB
import { ChaindataMap, OmniversalChainID, PermitCreationError, PermitVariant, Universe, } from "@arcana/ca-common"; import { ERC20ABI as ERC20ABIC } from "@arcana/ca-common"; import { CHAIN_IDS } from "fuels"; import { bytesToHex, createPublicClient, decodeFunctionData, encodeFunctionData, fallback, getContract, hexToBigInt, hexToBytes, http, maxUint256, pad, parseSignature, SwitchChainError, } from "viem"; import ERC20ABI from "../abi/erc20"; import gasOracleABI from "../abi/gasOracle"; import { FillEvent } from "../abi/vault"; import { ZERO_ADDRESS } from "../constants"; import { ErrorLiquidityTimeout } from "../errors"; import { getLogger } from "../logger"; import { vscCreateSponsoredApprovals } from "./api.utils"; import { convertTo32Bytes, equalFold, minutesToMs } from "./common.utils"; const logger = getLogger(); 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; }; const getAllowance = (chain, address, tokenContract, chainList) => { logger.debug("getAllowance", { tokenContract, ZERO_ADDRESS, }); if (equalFold(ZERO_ADDRESS, tokenContract)) { return Promise.resolve(maxUint256); } const publicClient = createPublicClientWithFallback(chain); return publicClient.readContract({ abi: ERC20ABI, address: tokenContract, args: [address, chainList.getVaultContractAddress(chain.id)], functionName: "allowance", }); }; 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; }; 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 }); }); }; 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 }); }); }; const getTokenTxFunction = (data) => { try { const { args, functionName } = decodeFunctionData({ abi: ERC20ABI, data, }); return { args, functionName }; } catch (e) { logger.debug("getTokenTxFunction", e); return { args: [], functionName: "unknown" }; } }; 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 publicClient = createPublicClientWithFallback(chain); 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 hash = await client.writeContract({ abi: ERC20ABI, account: address, address: addr, args: [vaultAddr, amount], chain, functionName: "approve", }); p.push((async function () { 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, publicClient, 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); const getL1Fee = async (chain, input = "0x") => { let fee = 0n; if (chainsWithGasOracles.includes(chain.id)) { fee = await fetchL1Fee(chain, input); } return fee; }; const fetchL1Fee = (chain, input) => { const pc = createPublicClientWithFallback(chain); return pc.readContract({ abi: gasOracleABI, address: L1_GAS_ORACLES[chain.id], args: [input], functionName: "getL1Fee", }); }; 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) { await client.addChain({ chain, }); await client.switchChain({ id: chain.id }); return; } throw e; } }; const EIP712Domain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, ]; const PolygonDomain = [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "verifyingContract", type: "address" }, { name: "salt", type: "bytes32" }, ]; async function signPermitForAddressAndValue(cur, client, publicClient, account, spender, value, deadline) { const contract = getContract({ abi: ERC20ABIC, address: bytesToHex(cur.tokenAddress.subarray(12)), client: { public: publicClient }, }); const walletAddress = account.address; deadline = deadline ?? 2n ** 256n - 1n; // eslint-disable-next-line @typescript-eslint/no-explicit-any const requestsToBeMade = [ (() => { // Hack for sophon ETH return contract.read.name().catch(() => { return ""; }); })(), client.request({ method: "eth_chainId" }, { dedupe: true }), ]; switch (cur.permitVariant) { case PermitVariant.Unsupported: default: { throw new PermitCreationError("Permits are unsupported on this currency"); } case PermitVariant.DAI: case PermitVariant.EIP2612Canonical: case PermitVariant.Polygon2612: { requestsToBeMade[2] = contract.read.nonces([walletAddress]); break; } case PermitVariant.PolygonEMT: { requestsToBeMade[2] = contract.read.getNonce([walletAddress]); } } const [name, chainID, nonce] = await Promise.all(requestsToBeMade); switch (cur.permitVariant) { case PermitVariant.DAI: { return client.signTypedData({ account, domain: { chainId: hexToBigInt(chainID), name, verifyingContract: contract.address, version: cur.permitContractVersion.toString(10), }, message: { allowed: true, expiry: deadline, holder: walletAddress, nonce, spender: spender, }, primaryType: "Permit", types: { EIP712Domain, Permit: [ { name: "holder", type: "address" }, { name: "spender", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "allowed", type: "bool" }, ], }, }); } case PermitVariant.EIP2612Canonical: { return client.signTypedData({ account, domain: { chainId: hexToBigInt(chainID), name, verifyingContract: contract.address, version: cur.permitContractVersion.toString(10), }, message: { deadline, nonce, owner: walletAddress, spender, value, }, primaryType: "Permit", types: { EIP712Domain, Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }, }); } case PermitVariant.Polygon2612: { return client.signTypedData({ account, domain: { name, salt: pad(chainID, { dir: "left", size: 32, }), verifyingContract: contract.address, version: cur.permitContractVersion.toString(10), }, message: { allowed: true, expiry: deadline, holder: walletAddress, nonce, spender: spender, }, primaryType: "Permit", types: { EIP712Domain: PolygonDomain, Permit: [ { name: "holder", type: "address" }, { name: "spender", type: "address" }, { name: "nonce", type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "allowed", type: "bool" }, ], }, }); } case PermitVariant.PolygonEMT: { const funcSig = encodeFunctionData({ abi: ERC20ABI, args: [spender, value], functionName: "approve", }); return client.signTypedData({ account, domain: { name, salt: pad(chainID, { dir: "left", size: 32, }), verifyingContract: contract.address, version: cur.permitContractVersion.toString(10), }, message: { from: walletAddress, functionSignature: funcSig, nonce, }, primaryType: "MetaTransaction", types: { EIP712Domain: PolygonDomain, MetaTransaction: [ { name: "nonce", type: "uint256" }, { name: "from", type: "address" }, { name: "functionSignature", type: "bytes" }, ], }, }); } } } const createPublicClientWithFallback = (chain) => { if (chain.rpcUrls.default.http.length === 1) { return createPublicClient({ transport: http(chain.rpcUrls.default.http[0]), }); } return createPublicClient({ transport: fallback(chain.rpcUrls.default.http.map((s) => http(s))), }); }; export { createPublicClientWithFallback, getAllowance, getAllowances, getL1Fee, getTokenTxFunction, isEVMTx, requestTimeout, setAllowances, signPermitForAddressAndValue, switchChain, waitForIntentFulfilment, waitForTxReceipt, };