@arcana/ca-sdk
Version:
Arcana Network's chain abstraction SDK for unified balance in Web3 apps
275 lines (274 loc) • 9.52 kB
JavaScript
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, };