@arcana/ca-sdk
Version:
Arcana Network's chain abstraction SDK for unified balance in Web3 apps
166 lines (165 loc) • 6.72 kB
JavaScript
import { Universe } from "@arcana/ca-common";
import Decimal from "decimal.js";
import { createPublicClient, decodeFunctionData, serializeTransaction, webSocket, } from "viem";
import { ERC20TransferABI } from "../../abi/erc20";
import { KAIA_CHAIN_ID, SOPHON_CHAIN_ID } from "../../chains";
import { AaveTokenContracts, TOKEN_MINTER_CONTRACTS, TOP_OWNER, } from "../../constants";
import { getLogger } from "../../logger";
import ERC20TransferBase from "../../requestHandlers/common/erc20.base";
import { simulateTransaction } from "../../simulate";
import { divDecimals, evmWaitForFill, getL1Fee } from "../../utils";
const logger = getLogger();
class ERC20Transfer extends ERC20TransferBase {
constructor(input) {
super(input);
this.input = input;
this.destinationUniverse = Universe.ETHEREUM;
this.publicClient = createPublicClient({
transport: webSocket(this.input.chain.rpcUrls.default.webSocket[0]),
});
}
async simulateTx() {
const { data, to } = this.input.evm.tx;
const from = this.input.evm.address;
const token = this.chainList.getTokenByAddress(this.input.chain.id, to);
const nativeToken = this.chainList.getNativeToken(this.input.chain.id);
if (!token) {
return null;
}
const { args } = decodeFunctionData({
abi: [ERC20TransferABI],
data: data ?? "0x00",
});
if (this.input.chain.id === KAIA_CHAIN_ID) {
this.simulateTxRes = {
amount: divDecimals(args[1], token.decimals),
gas: 0n,
gasFee: new Decimal("0.002"),
token,
};
return this.simulateTxRes;
}
if (this.simulateTxRes) {
let gasFee = 0n;
if (!this.input.options.bridge) {
const [{ gasPrice, maxFeePerGas }, l1Fee] = await Promise.all([
this.publicClient.estimateFeesPerGas(),
getL1Fee(this.input.chain, serializeTransaction({
chainId: this.input.chain.id,
data: data ?? "0x00",
to: to,
type: "eip1559",
})),
]);
const gasUnitPrice = maxFeePerGas ?? gasPrice ?? 0n;
if (gasUnitPrice === 0n) {
throw new Error("could not get maxFeePerGas or gasPrice from RPC");
}
gasFee = this.simulateTxRes.gas * gasUnitPrice + l1Fee;
}
return {
...this.simulateTxRes,
gasFee: divDecimals(gasFee, nativeToken.decimals),
};
}
const amountToAdd = new Decimal(args[1].toString())
.toHexadecimal()
.split("0x")[1]
.padStart(40, "0");
let txsToSimulate = [];
if (AaveTokenContracts[this.input.chain.id]?.[token.symbol]) {
txsToSimulate.push({
from: AaveTokenContracts[this.input.chain.id][token.symbol],
input: `0xa9059cbb000000000000000000000000${from
.replace("0x", "")
.toLowerCase()}000000000000000000000000${amountToAdd}`,
to: token.contractAddress,
});
}
else if (TOKEN_MINTER_CONTRACTS[this.input.chain.id]?.[token.symbol]) {
txsToSimulate.push({
from: TOKEN_MINTER_CONTRACTS[this.input.chain.id]?.[token.symbol],
input: `0x40c10f19000000000000000000000000${from
.replace("0x", "")
.toLowerCase()}000000000000000000000000000000000000000000000000000000003b9aca00`,
to: token.contractAddress,
});
}
txsToSimulate.push({
from,
input: data,
to,
});
if (TOP_OWNER[this.input.chain.id]?.[token.symbol]) {
const ownerAddress = TOP_OWNER[this.input.chain.id][token.symbol];
txsToSimulate = [
{
from: ownerAddress,
input: data,
to,
},
];
}
const [simulation, feeData, l1Fee] = await Promise.all([
simulateTransaction(this.input.chain.id, txsToSimulate, this.input.options.networkConfig.SIMULATION_URL),
this.publicClient.estimateFeesPerGas(),
getL1Fee(this.input.chain, serializeTransaction({
chainId: this.input.chain.id,
data: data ?? "0x00",
to: to,
type: "eip1559",
})),
]);
logger.debug("simulateTx", { feeData });
const gasUnitPrice = feeData.maxFeePerGas ?? feeData.gasPrice ?? 0n;
if (gasUnitPrice === 0n) {
throw new Error("could not get maxFeePerGas or gasPrice from RPC");
}
let gasFee = (this.input.chain.id === SOPHON_CHAIN_ID
? BigInt(simulation.data.gas)
: BigInt(simulation.data.gas_used)) *
gasUnitPrice +
l1Fee;
logger.debug("erc20:simulateTx", {
args,
feeData,
l1Fee,
maxFeePerGas: gasUnitPrice,
simulation,
totalGas: gasFee,
totalGasInDecimal: divDecimals(gasFee, nativeToken.decimals).toFixed(),
});
if (this.input.options.bridge) {
gasFee = 0n;
}
const amount = simulation.data.amount === ""
? args[1].toString()
: simulation.data.amount;
this.simulateTxRes = {
amount: divDecimals(amount, token.decimals),
gas: this.input.chain.id === SOPHON_CHAIN_ID
? BigInt(simulation.data.gas)
: BigInt(simulation.data.gas_used),
gasFee: divDecimals(gasFee, nativeToken.decimals),
token,
};
return this.simulateTxRes;
}
async waitForFill(requestHash, intentID, waitForDoubleCheckTx) {
logger.debug("waitForFill", {
intentID,
requestHash,
waitForDoubleCheckTx,
});
try {
await Promise.all([
waitForDoubleCheckTx(),
evmWaitForFill(this.input.chainList.getVaultContractAddress(this.input.chain.id), this.publicClient, requestHash, intentID, this.input.options.networkConfig.GRPC_URL, this.input.options.networkConfig.COSMOS_URL),
]);
}
finally {
(await this.publicClient.transport.getRpcClient()).close();
}
}
}
export default ERC20Transfer;