@atomiqlabs/chain-evm
Version:
EVM specific base implementation
780 lines (677 loc) • 38.3 kB
text/typescript
import {
BigIntBufferUtils,
BitcoinRpc,
BtcTx,
RelaySynchronizer,
SpvVaultContract,
SpvVaultTokenData,
SpvWithdrawalState,
SpvWithdrawalStateType,
SpvWithdrawalTransactionData,
TransactionConfirmationOptions
} from "@atomiqlabs/base";
import {Buffer} from "buffer";
import {EVMTx} from "../chain/modules/EVMTransactions";
import {EVMContractBase} from "../contract/EVMContractBase";
import {EVMSigner} from "../wallet/EVMSigner";
import {SpvVaultContractAbi} from "./SpvVaultContractAbi";
import {SpvVaultManager, SpvVaultParametersStructOutput} from "./SpvVaultContractTypechain";
import {EVMBtcRelay} from "../btcrelay/EVMBtcRelay";
import {getLogger} from "../../utils/Utils";
import {EVMChainInterface} from "../chain/EVMChainInterface";
import {AbiCoder, getAddress, hexlify, keccak256, TransactionRequest, ZeroAddress, ZeroHash} from "ethers";
import {EVMAddresses} from "../chain/modules/EVMAddresses";
import {EVMSpvVaultData, getVaultParamsCommitment, getVaultUtxoFromState} from "./EVMSpvVaultData";
import {EVMSpvWithdrawalData} from "./EVMSpvWithdrawalData";
import {EVMFees} from "../chain/modules/EVMFees";
import {EVMBtcStoredHeader} from "../btcrelay/headers/EVMBtcStoredHeader";
import {TypedEventLog} from "../typechain/common";
import {PromiseLruCache} from "promise-cache-ts";
function decodeUtxo(utxo: string): {txHash: string, vout: bigint} {
const [txId, vout] = utxo.split(":");
return {
txHash: "0x"+Buffer.from(txId, "hex").reverse().toString("hex"),
vout: BigInt(vout)
}
}
export function packOwnerAndVaultId(owner: string, vaultId: bigint): string {
if(owner.length!==42) throw new Error("Invalid owner address");
return owner.toLowerCase() + BigIntBufferUtils.toBuffer(vaultId, "be", 12).toString("hex");
}
export function unpackOwnerAndVaultId(data: string): [string, bigint] {
return [getAddress(data.substring(0, 42)), BigInt("0x"+data.substring(42, 66))];
}
export class EVMSpvVaultContract<ChainId extends string>
extends EVMContractBase<SpvVaultManager>
implements SpvVaultContract<
EVMTx,
EVMSigner,
ChainId,
EVMSpvVaultData,
EVMSpvWithdrawalData
>
{
public static readonly GasCosts = {
DEPOSIT_BASE: 15_000 + 21_000,
DEPOSIT_ERC20: 40_000,
OPEN: 80_000 + 21_000,
CLAIM_BASE: 85_000 + 21_000,
CLAIM_NATIVE_TRANSFER: 85_000,
CLAIM_ERC20_TRANSFER: 40_000,
CLAIM_EXECUTION_SCHEDULE: 30_000,
FRONT_BASE: 75_000 + 21_000,
FRONT_NATIVE_TRANSFER: 85_000,
FRONT_ERC20_TRANSFER: 40_000,
FRONT_EXECUTION_SCHEDULE: 30_000
};
readonly chainId: ChainId;
readonly btcRelay: EVMBtcRelay<any>;
readonly bitcoinRpc: BitcoinRpc<any>;
readonly claimTimeout: number = 180;
readonly logger = getLogger("EVMSpvVaultContract: ");
constructor(
chainInterface: EVMChainInterface<ChainId>,
btcRelay: EVMBtcRelay<any>,
bitcoinRpc: BitcoinRpc<any>,
contractAddress: string,
contractDeploymentHeight?: number
) {
super(chainInterface, contractAddress, SpvVaultContractAbi, contractDeploymentHeight);
this.btcRelay = btcRelay;
this.bitcoinRpc = bitcoinRpc;
}
//Transactions
protected async Open(signer: string, vault: EVMSpvVaultData, feeRate: string): Promise<TransactionRequest> {
const {txHash, vout} = decodeUtxo(vault.getUtxo());
const tokens = vault.getTokenData();
if(tokens.length!==2) throw new Error("Must specify exactly 2 tokens for vault!");
const tx = await this.contract.open.populateTransaction(vault.vaultId, vault.getVaultParamsStruct(), txHash, vout);
tx.from = signer;
EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.OPEN, feeRate);
return tx;
}
protected async Deposit(signer: string, vault: EVMSpvVaultData, rawAmounts: bigint[], feeRate: string): Promise<TransactionRequest> {
let totalGas = EVMSpvVaultContract.GasCosts.DEPOSIT_BASE;
let value = 0n;
if(vault.token0.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) {
value += rawAmounts[0] * vault.token0.multiplier;
} else {
if(rawAmounts[0] > 0n) totalGas += EVMSpvVaultContract.GasCosts.DEPOSIT_ERC20;
}
if(vault.token1.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase()) {
value += (rawAmounts[1] ?? 0n) * vault.token1.multiplier;
} else {
if(rawAmounts[1]!=null && rawAmounts[1] > 0n && vault.token0.token.toLowerCase()!==vault.token1.token.toLowerCase())
totalGas += EVMSpvVaultContract.GasCosts.DEPOSIT_ERC20;
}
const tx = await this.contract.deposit.populateTransaction(
vault.owner, vault.vaultId, vault.getVaultParamsStruct(),
rawAmounts[0], rawAmounts[1] ?? 0n, { value }
);
tx.from = signer;
EVMFees.applyFeeRate(tx, totalGas, feeRate);
return tx;
}
protected async Front(
signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData, withdrawalSequence: number, feeRate: string
): Promise<TransactionRequest> {
let value = 0n;
const frontingAmount = data.getFrontingAmount();
if(vault.token0.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase())
value += frontingAmount[0] * vault.token0.multiplier;
if(vault.token1.token.toLowerCase()===this.Chain.getNativeCurrencyAddress().toLowerCase())
value += (frontingAmount[1] ?? 0n) * vault.token1.multiplier;
const tx = await this.contract.front.populateTransaction(
vault.owner, vault.vaultId, vault.getVaultParamsStruct(),
withdrawalSequence, data.getTxHash(), data.serializeToStruct(),
{ value }
);
tx.from = signer;
EVMFees.applyFeeRate(tx, this.getFrontGas(signer, vault, data), feeRate);
return tx;
}
protected async Claim(
signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData,
blockheader: EVMBtcStoredHeader, merkle: Buffer[], position: number, feeRate: string
): Promise<TransactionRequest> {
const tx = await this.contract.claim.populateTransaction(
vault.owner, vault.vaultId, vault.getVaultParamsStruct(), "0x"+data.btcTx.hex,
blockheader.serializeToStruct(), merkle, position
)
tx.from = signer;
EVMFees.applyFeeRate(tx, this.getClaimGas(signer, vault, data), feeRate);
return tx;
}
async checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void> {
const result = await this.contract.parseBitcoinTx(Buffer.from(tx.btcTx.hex, "hex"));
if(result==null) throw new Error("Failed to parse transaction!");
}
createVaultData(
owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[]
): Promise<EVMSpvVaultData> {
if(tokenData.length!==2) throw new Error("Must specify 2 tokens in tokenData!");
const vaultParams = {
btcRelayContract: this.btcRelay.contractAddress,
token0: tokenData[0].token,
token1: tokenData[1].token,
token0Multiplier: tokenData[0].multiplier,
token1Multiplier: tokenData[1].multiplier,
confirmations: BigInt(confirmations)
};
const spvVaultParametersCommitment = keccak256(AbiCoder.defaultAbiCoder().encode(
["address", "address", "address", "uint192", "uint192", "uint256"],
[vaultParams.btcRelayContract, vaultParams.token0, vaultParams.token1, vaultParams.token0Multiplier, vaultParams.token1Multiplier, vaultParams.confirmations]
));
return Promise.resolve(new EVMSpvVaultData(owner, vaultId, {
spvVaultParametersCommitment,
utxoTxHash: ZeroHash,
utxoVout: 0n,
openBlockheight: 0n,
withdrawCount: 0n,
depositCount: 0n,
token0Amount: 0n,
token1Amount: 0n
}, vaultParams, utxo));
}
//Getters
async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null> {
const frontingAddress = await this.contract.getFronterById(owner, vaultId, "0x"+withdrawal.getFrontingId());
if(frontingAddress===ZeroAddress) return null;
return frontingAddress;
}
async getFronterAddresses(withdrawals: {owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData}[]): Promise<{[btcTxId: string]: string | null}> {
const result: {
[btcTxId: string]: string | null
} = {};
let promises: Promise<void>[] = [];
//TODO: We can upgrade this to use multicall
for(let {owner, vaultId, withdrawal} of withdrawals) {
promises.push(this.getFronterAddress(owner, vaultId, withdrawal).then(val => {
result[withdrawal.getTxId()] = val;
}));
if(promises.length>=this.Chain.config.maxParallelCalls) {
await Promise.all(promises);
promises = [];
}
}
await Promise.all(promises);
return result;
}
private vaultParamsCache: PromiseLruCache<string, SpvVaultParametersStructOutput> = new PromiseLruCache<string, SpvVaultParametersStructOutput>(5000);
async getVaultData(owner: string, vaultId: bigint): Promise<EVMSpvVaultData> {
const vaultState = await this.contract.getVault(owner, vaultId);
const vaultParams = await this.vaultParamsCache.getOrComputeAsync(vaultState.spvVaultParametersCommitment, async () => {
const blockheight = Number(vaultState.openBlockheight);
const events = await this.Events.getContractBlockEvents(
["Opened"],
[
"0x"+owner.substring(2).padStart(64, "0"),
hexlify(BigIntBufferUtils.toBuffer(vaultId, "be", 32))
],
blockheight
);
const foundEvent = events.find(
event => getVaultParamsCommitment(event.args.params)===vaultState.spvVaultParametersCommitment
);
if(foundEvent==null) throw new Error("Valid open event not found!");
return foundEvent.args.params;
});
if(vaultParams.btcRelayContract.toLowerCase()!==this.btcRelay.contractAddress.toLowerCase()) return null;
return new EVMSpvVaultData(owner, vaultId, vaultState, vaultParams);
}
async getMultipleVaultData(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: EVMSpvVaultData}}> {
const result: {[owner: string]: {[vaultId: string]: EVMSpvVaultData}} = {};
let promises: Promise<void>[] = [];
//TODO: We can upgrade this to use multicall
for(let {owner, vaultId} of vaults) {
promises.push(this.getVaultData(owner, vaultId).then(val => {
result[owner] ??= {};
result[owner][vaultId.toString(10)] = val;
}));
if(promises.length>=this.Chain.config.maxParallelCalls) {
await Promise.all(promises);
promises = [];
}
}
await Promise.all(promises);
return result;
}
async getVaultLatestUtxo(owner: string, vaultId: bigint): Promise<string | null> {
const vaultState = await this.contract.getVault(owner, vaultId);
const utxo = getVaultUtxoFromState(vaultState);
if(utxo==="0000000000000000000000000000000000000000000000000000000000000000:0") return null;
return utxo;
}
async getVaultLatestUtxos(vaults: {owner: string, vaultId: bigint}[]): Promise<{[owner: string]: {[vaultId: string]: string | null}}> {
const result: {[owner: string]: {[vaultId: string]: string | null}} = {};
let promises: Promise<void>[] = [];
//TODO: We can upgrade this to use multicall
for(let {owner, vaultId} of vaults) {
promises.push(this.getVaultLatestUtxo(owner, vaultId).then(val => {
result[owner] ??= {};
result[owner][vaultId.toString(10)] = val;
}));
if(promises.length>=this.Chain.config.maxParallelCalls) {
await Promise.all(promises);
promises = [];
}
}
await Promise.all(promises);
return result;
}
async getAllVaults(owner?: string): Promise<EVMSpvVaultData[]> {
const openedVaults = new Map<string, SpvVaultParametersStructOutput>();
await this.Events.findInContractEventsForward(
["Opened", "Closed"],
owner==null ? null : [
"0x"+owner.substring(2).padStart(64, "0")
],
(event) => {
const vaultIdentifier = event.args.owner+":"+event.args.vaultId.toString(10);
if(event.eventName==="Opened") {
const _event = event as TypedEventLog<SpvVaultManager["filters"]["Opened"]>;
openedVaults.set(vaultIdentifier, _event.args.params);
} else {
openedVaults.delete(vaultIdentifier);
}
return null;
}
);
const vaults: EVMSpvVaultData[] = [];
for(let [identifier, vaultParams] of openedVaults.entries()) {
const [owner, vaultIdStr] = identifier.split(":");
const vaultState = await this.contract.getVault(owner, BigInt(vaultIdStr));
if(vaultState.spvVaultParametersCommitment === getVaultParamsCommitment(vaultParams)) {
vaults.push(new EVMSpvVaultData(owner, BigInt(vaultIdStr), vaultState, vaultParams))
}
}
return vaults;
}
private parseWithdrawalEvent(event: TypedEventLog<SpvVaultManager["filters"][keyof SpvVaultManager["filters"]]>): SpvWithdrawalState | null {
switch(event.eventName) {
case "Fronted":
const frontedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Fronted"]>;
const [ownerFront, vaultIdFront] = unpackOwnerAndVaultId(frontedEvent.args.ownerAndVaultId);
return {
type: SpvWithdrawalStateType.FRONTED,
txId: event.transactionHash,
owner: ownerFront,
vaultId: vaultIdFront,
recipient: frontedEvent.args.recipient,
fronter: frontedEvent.args.caller
};
case "Claimed":
const claimedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Claimed"]>;
const [ownerClaim, vaultIdClaim] = unpackOwnerAndVaultId(claimedEvent.args.ownerAndVaultId);
return {
type: SpvWithdrawalStateType.CLAIMED,
txId: event.transactionHash,
owner: ownerClaim,
vaultId: vaultIdClaim,
recipient: claimedEvent.args.recipient,
claimer: claimedEvent.args.caller,
fronter: claimedEvent.args.frontingAddress
};
case "Closed":
const closedEvent = event as TypedEventLog<SpvVaultManager["filters"]["Closed"]>;
return {
type: SpvWithdrawalStateType.CLOSED,
txId: event.transactionHash,
owner: closedEvent.args.owner,
vaultId: closedEvent.args.vaultId,
error: closedEvent.args.error
};
default:
return null;
}
}
async getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState> {
const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
const keys = [null, null, hexlify(txHash)];
let result: SpvWithdrawalState;
if(scStartHeight==null) {
result = await this.Events.findInContractEvents(
events, keys,
async (event) => {
const result = this.parseWithdrawalEvent(event);
if(result!=null) return result;
}
);
} else {
result = await this.Events.findInContractEventsForward(
events, keys,
async (event) => {
const result = this.parseWithdrawalEvent(event);
if(result==null) return;
if(result.type===SpvWithdrawalStateType.FRONTED) {
//Check if still fronted
const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx);
//Not fronted now, there should be a claim/close event after the front event, continue
if(fronterAddress==null) return;
}
return result;
},
scStartHeight
);
}
result ??= {
type: SpvWithdrawalStateType.NOT_FOUND
};
return result;
}
async getWithdrawalStates(withdrawalTxs: {withdrawal: EVMSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> {
const result: {[btcTxId: string]: SpvWithdrawalState} = {};
const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxLogTopics) {
const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxLogTopics);
const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId() as string, val.withdrawal]));
let scStartHeight = null;
for(let val of checkWithdrawalTxs) {
if(val.scStartBlockheight==null) {
scStartHeight = null;
break;
}
scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight);
}
const keys = [null, null, checkWithdrawalTxs.map(withdrawal => hexlify(Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))];
if(scStartHeight==null) {
await this.Events.findInContractEvents(
events, keys,
async (event) => {
const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
if(!checkWithdrawalTxsMap.has(btcTxId)) {
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
return null;
}
const eventResult = this.parseWithdrawalEvent(event);
if(eventResult==null) return null;
checkWithdrawalTxsMap.delete(btcTxId);
result[btcTxId] = eventResult;
if(checkWithdrawalTxsMap.size===0) return true; //All processed
}
);
} else {
await this.Events.findInContractEventsForward(
events, keys,
async (event) => {
const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId);
if(withdrawalTx==null) {
this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
return;
}
const eventResult = this.parseWithdrawalEvent(event);
if(eventResult==null) return;
if(eventResult.type===SpvWithdrawalStateType.FRONTED) {
//Check if still fronted
const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx);
//Not fronted now, so there should be a claim/close event after the front event, continue
if(fronterAddress==null) return;
//Fronted still, so this should be the latest current state
}
checkWithdrawalTxsMap.delete(btcTxId);
result[btcTxId] = eventResult;
if(checkWithdrawalTxsMap.size===0) return true; //All processed
},
scStartHeight
);
}
}
for(let val of withdrawalTxs) {
result[val.withdrawal.getTxId()] ??= {
type: SpvWithdrawalStateType.NOT_FOUND
};
}
return result;
}
getWithdrawalData(btcTx: BtcTx): Promise<EVMSpvWithdrawalData> {
return Promise.resolve(new EVMSpvWithdrawalData(btcTx));
}
//OP_RETURN data encoding/decoding
fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash: string } {
return EVMSpvVaultContract.fromOpReturnData(data);
}
static fromOpReturnData(data: Buffer): { recipient: string; rawAmounts: bigint[]; executionHash: string } {
let rawAmount0: bigint = 0n;
let rawAmount1: bigint = 0n;
let executionHash: string = null;
if(data.length===28) {
rawAmount0 = data.readBigInt64BE(20).valueOf();
} else if(data.length===36) {
rawAmount0 = data.readBigInt64BE(20).valueOf();
rawAmount1 = data.readBigInt64BE(28).valueOf();
} else if(data.length===60) {
rawAmount0 = data.readBigInt64BE(20).valueOf();
executionHash = data.slice(28, 60).toString("hex");
} else if(data.length===68) {
rawAmount0 = data.readBigInt64BE(20).valueOf();
rawAmount1 = data.readBigInt64BE(28).valueOf();
executionHash = data.slice(36, 68).toString("hex");
} else {
throw new Error("Invalid OP_RETURN data length!");
}
const recipient = "0x"+data.slice(0, 20).toString("hex");
if(!EVMAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified");
return {executionHash, rawAmounts: [rawAmount0, rawAmount1], recipient: getAddress(recipient)};
}
toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer {
return EVMSpvVaultContract.toOpReturnData(recipient, rawAmounts, executionHash);
}
static toOpReturnData(recipient: string, rawAmounts: bigint[], executionHash?: string): Buffer {
if(!EVMAddresses.isValidAddress(recipient)) throw new Error("Invalid recipient specified");
if(rawAmounts.length < 1) throw new Error("At least 1 amount needs to be specified");
if(rawAmounts.length > 2) throw new Error("At most 2 amounts need to be specified");
rawAmounts.forEach(val => {
if(val < 0n) throw new Error("Negative raw amount specified");
if(val >= 2n**64n) throw new Error("Raw amount overflow");
});
if(executionHash!=null) {
if(Buffer.from(executionHash, "hex").length !== 32)
throw new Error("Invalid execution hash");
}
const recipientBuffer = Buffer.from(recipient.substring(2).padStart(40, "0"), "hex");
const amount0Buffer = BigIntBufferUtils.toBuffer(rawAmounts[0], "be", 8);
const amount1Buffer = rawAmounts[1]==null || rawAmounts[1]===0n ? Buffer.alloc(0) : BigIntBufferUtils.toBuffer(rawAmounts[1], "be", 8);
const executionHashBuffer = executionHash==null ? Buffer.alloc(0) : Buffer.from(executionHash, "hex");
return Buffer.concat([
recipientBuffer,
amount0Buffer,
amount1Buffer,
executionHashBuffer
]);
}
//Actions
async claim(signer: EVMSigner, vault: EVMSpvVaultData, txs: {tx: EVMSpvWithdrawalData, storedHeader?: EVMBtcStoredHeader}[], synchronizer?: RelaySynchronizer<any, any, any>, initAta?: boolean, txOptions?: TransactionConfirmationOptions): Promise<string> {
const result = await this.txsClaim(signer.getAddress(), vault, txs, synchronizer, initAta, txOptions?.feeRate);
const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
return signature;
}
async deposit(signer: EVMSigner, vault: EVMSpvVaultData, rawAmounts: bigint[], txOptions?: TransactionConfirmationOptions): Promise<string> {
const result = await this.txsDeposit(signer.getAddress(), vault, rawAmounts, txOptions?.feeRate);
const txHashes = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
return txHashes[txHashes.length - 1];
}
async frontLiquidity(signer: EVMSigner, vault: EVMSpvVaultData, realWithdrawalTx: EVMSpvWithdrawalData, withdrawSequence: number, txOptions?: TransactionConfirmationOptions): Promise<string> {
const result = await this.txsFrontLiquidity(signer.getAddress(), vault, realWithdrawalTx, withdrawSequence, txOptions?.feeRate);
const txHashes = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
return txHashes[txHashes.length - 1];
}
async open(signer: EVMSigner, vault: EVMSpvVaultData, txOptions?: TransactionConfirmationOptions): Promise<string> {
const result = await this.txsOpen(signer.getAddress(), vault, txOptions?.feeRate);
const [signature] = await this.Chain.sendAndConfirm(signer, result, txOptions?.waitForConfirmation, txOptions?.abortSignal);
return signature;
}
//Transactions
async txsClaim(
signer: string, vault: EVMSpvVaultData, txs: {
tx: EVMSpvWithdrawalData,
storedHeader?: EVMBtcStoredHeader
}[], synchronizer?: RelaySynchronizer<any, any, any>,
initAta?: boolean, feeRate?: string
): Promise<EVMTx[]> {
if(!vault.isOpened()) throw new Error("Cannot claim from a closed vault!");
feeRate ??= await this.Chain.Fees.getFeeRate();
const txsWithMerkleProofs: {
tx: EVMSpvWithdrawalData,
reversedTxId: Buffer,
pos: number,
blockheight: number,
merkle: Buffer[],
storedHeader?: EVMBtcStoredHeader
}[] = [];
for(let tx of txs) {
const merkleProof = await this.bitcoinRpc.getMerkleProof(tx.tx.btcTx.txid, tx.tx.btcTx.blockhash);
this.logger.debug("txsClaim(): merkle proof computed: ", merkleProof);
txsWithMerkleProofs.push({
...merkleProof,
...tx
});
}
const evmTxs: EVMTx[] = [];
const storedHeaders: {[blockhash: string]: EVMBtcStoredHeader} = await EVMBtcRelay.getCommitedHeadersAndSynchronize(
signer, this.btcRelay, txsWithMerkleProofs.filter(tx => tx.storedHeader==null).map(tx => {
return {
blockhash: tx.tx.btcTx.blockhash,
blockheight: tx.blockheight,
requiredConfirmations: vault.getConfirmations()
}
}), evmTxs, synchronizer, feeRate
);
if(storedHeaders==null) throw new Error("Cannot fetch committed header!");
for(let tx of txsWithMerkleProofs) {
evmTxs.push(await this.Claim(signer, vault, tx.tx, tx.storedHeader ?? storedHeaders[tx.tx.btcTx.blockhash], tx.merkle, tx.pos, feeRate));
}
this.logger.debug("txsClaim(): "+evmTxs.length+" claim TXs created claiming "+txs.length+" txs, owner: "+vault.getOwner()+
" vaultId: "+vault.getVaultId().toString(10));
return evmTxs;
}
async txsDeposit(signer: string, vault: EVMSpvVaultData, rawAmounts: bigint[], feeRate?: string): Promise<EVMTx[]> {
if(!vault.isOpened()) throw new Error("Cannot deposit to a closed vault!");
feeRate ??= await this.Chain.Fees.getFeeRate();
const txs: EVMTx[] = [];
let realAmount0: bigint = 0n;
let realAmount1: bigint = 0n;
//Approve first
const requiredApprovals: {[address: string]: bigint} = {};
if(rawAmounts[0]!=null && rawAmounts[0]!==0n) {
if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
realAmount0 = rawAmounts[0] * vault.token0.multiplier;
requiredApprovals[vault.token0.token.toLowerCase()] = realAmount0;
}
}
if(rawAmounts[1]!=null && rawAmounts[1]!==0n) {
if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
realAmount1 = rawAmounts[1] * vault.token1.multiplier;
requiredApprovals[vault.token1.token.toLowerCase()] ??= 0n;
requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1;
}
}
const requiredApprovalTxns = await Promise.all(
Object.keys(requiredApprovals).map(token => this.Chain.Tokens.checkAndGetApproveTx(signer, token, requiredApprovals[token], this.contractAddress, feeRate))
);
requiredApprovalTxns.forEach(tx => tx!=null && txs.push(tx));
txs.push(await this.Deposit(signer, vault, rawAmounts, feeRate));
this.logger.debug("txsDeposit(): deposit TX created,"+
" token0: "+vault.token0.token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+
" token1: "+vault.token1.token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10));
return txs;
}
async txsFrontLiquidity(signer: string, vault: EVMSpvVaultData, realWithdrawalTx: EVMSpvWithdrawalData, withdrawSequence: number, feeRate?: string): Promise<EVMTx[]> {
if(!vault.isOpened()) throw new Error("Cannot front on a closed vault!");
feeRate ??= await this.Chain.Fees.getFeeRate();
const txs: EVMTx[] = [];
let realAmount0 = 0n;
let realAmount1 = 0n;
//Approve first
const rawAmounts = realWithdrawalTx.getFrontingAmount();
//Approve first
const requiredApprovals: {[address: string]: bigint} = {};
if(rawAmounts[0]!=null && rawAmounts[0]!==0n) {
if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
realAmount0 = rawAmounts[0] * vault.token0.multiplier;
requiredApprovals[vault.token0.token.toLowerCase()] = realAmount0;
}
}
if(rawAmounts[1]!=null && rawAmounts[1]!==0n) {
if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
realAmount1 = rawAmounts[1] * vault.token1.multiplier;
requiredApprovals[vault.token1.token.toLowerCase()] ??= 0n;
requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1;
}
}
const requiredApprovalTxns = await Promise.all(
Object.keys(requiredApprovals).map(token => this.Chain.Tokens.checkAndGetApproveTx(signer, token, requiredApprovals[token], this.contractAddress, feeRate))
);
requiredApprovalTxns.forEach(tx => tx!=null && txs.push(tx));
txs.push(await this.Front(signer, vault, realWithdrawalTx, withdrawSequence, feeRate));
this.logger.debug("txsFrontLiquidity(): front TX created,"+
" token0: "+vault.token0.token+" rawAmount0: "+rawAmounts[0].toString(10)+" amount0: "+realAmount0.toString(10)+
" token1: "+vault.token1.token+" rawAmount1: "+(rawAmounts[1] ?? 0n).toString(10)+" amount1: "+realAmount1.toString(10));
return txs;
}
async txsOpen(signer: string, vault: EVMSpvVaultData, feeRate?: string): Promise<EVMTx[]> {
if(vault.isOpened()) throw new Error("Cannot open an already opened vault!");
feeRate ??= await this.Chain.Fees.getFeeRate();
const tx = await this.Open(signer, vault, feeRate);
this.logger.debug("txsOpen(): open TX created, owner: "+vault.getOwner()+
" vaultId: "+vault.getVaultId().toString(10));
return [tx];
}
getClaimGas(signer: string, vault: EVMSpvVaultData, data?: EVMSpvWithdrawalData): number {
let totalGas = EVMSpvVaultContract.GasCosts.CLAIM_BASE;
if (data==null || (data.rawAmounts[0] != null && data.rawAmounts[0] > 0n)) {
const transferFee = vault==null || vault.token0.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ?
EVMSpvVaultContract.GasCosts.CLAIM_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.CLAIM_ERC20_TRANSFER;
totalGas += transferFee;
if (data==null || data.frontingFeeRate > 0n) totalGas += transferFee; //Also needs to pay out to fronter
if (data==null || (data.callerFeeRate > 0n && !data.isRecipient(signer))) totalGas += transferFee; //Also needs to pay out to caller
}
if (data==null || (data.rawAmounts[1] != null && data.rawAmounts[1] > 0n)) {
const transferFee = vault==null || vault.token1.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ?
EVMSpvVaultContract.GasCosts.CLAIM_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.CLAIM_ERC20_TRANSFER;
totalGas += transferFee;
if (data==null || data.frontingFeeRate > 0n) totalGas += transferFee; //Also needs to pay out to fronter
if (data==null || (data.callerFeeRate > 0n && !data.isRecipient(signer))) totalGas += transferFee; //Also needs to pay out to caller
}
if (data==null || (data.executionHash != null && data.executionHash !== ZeroHash)) totalGas += EVMSpvVaultContract.GasCosts.CLAIM_EXECUTION_SCHEDULE;
return totalGas;
}
getFrontGas(signer: string, vault: EVMSpvVaultData, data?: EVMSpvWithdrawalData): number {
let totalGas = EVMSpvVaultContract.GasCosts.FRONT_BASE;
if (data==null || (data.rawAmounts[0] != null && data.rawAmounts[0] > 0n)) {
totalGas += vault==null || vault.token0.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ?
EVMSpvVaultContract.GasCosts.FRONT_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.FRONT_ERC20_TRANSFER;
}
if (data==null || (data.rawAmounts[1] != null && data.rawAmounts[1] > 0n)) {
totalGas += vault==null || vault.token1.token.toLowerCase() === this.Chain.getNativeCurrencyAddress() ?
EVMSpvVaultContract.GasCosts.FRONT_NATIVE_TRANSFER : EVMSpvVaultContract.GasCosts.FRONT_ERC20_TRANSFER;
}
if (data==null || (data.executionHash != null && data.executionHash !== ZeroHash)) totalGas += EVMSpvVaultContract.GasCosts.FRONT_EXECUTION_SCHEDULE;
return totalGas;
}
async getClaimFee(signer: string, vault: EVMSpvVaultData, withdrawalData: EVMSpvWithdrawalData, feeRate?: string): Promise<bigint> {
feeRate ??= await this.Chain.Fees.getFeeRate();
return EVMFees.getGasFee(this.getClaimGas(signer, vault, withdrawalData), feeRate);
}
async getFrontFee(signer: string, vault?: EVMSpvVaultData, withdrawalData?: EVMSpvWithdrawalData, feeRate?: string): Promise<bigint> {
vault ??= EVMSpvVaultData.randomVault();
feeRate ??= await this.Chain.Fees.getFeeRate();
let totalFee = EVMFees.getGasFee(this.getFrontGas(signer, vault, withdrawalData), feeRate);
if(withdrawalData==null || (withdrawalData.rawAmounts[0]!=null && withdrawalData.rawAmounts[0]>0n)) {
if(vault.token0.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
totalFee += await this.Chain.Tokens.getApproveFee(feeRate);
}
}
if(withdrawalData==null || (withdrawalData.rawAmounts[1]!=null && withdrawalData.rawAmounts[1]>0n)) {
if(vault.token1.token.toLowerCase()!==this.Chain.getNativeCurrencyAddress().toLowerCase()) {
if(vault.token1.token.toLowerCase()!==vault.token0.token.toLowerCase() || withdrawalData==null || withdrawalData.rawAmounts[0]==null || withdrawalData.rawAmounts[0]===0n) {
totalFee += await this.Chain.Tokens.getApproveFee(feeRate);
}
}
}
return totalFee;
}
}