@atomiqlabs/chain-evm
Version:
EVM specific base implementation
411 lines (410 loc) • 21.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.EVMSpvVaultContract = exports.unpackOwnerAndVaultId = exports.packOwnerAndVaultId = void 0;
const base_1 = require("@atomiqlabs/base");
const buffer_1 = require("buffer");
const EVMContractBase_1 = require("../contract/EVMContractBase");
const SpvVaultContractAbi_1 = require("./SpvVaultContractAbi");
const EVMBtcRelay_1 = require("../btcrelay/EVMBtcRelay");
const Utils_1 = require("../../utils/Utils");
const ethers_1 = require("ethers");
const EVMAddresses_1 = require("../chain/modules/EVMAddresses");
const EVMSpvVaultData_1 = require("./EVMSpvVaultData");
const EVMSpvWithdrawalData_1 = require("./EVMSpvWithdrawalData");
const EVMFees_1 = require("../chain/modules/EVMFees");
function decodeUtxo(utxo) {
const [txId, vout] = utxo.split(":");
return {
txHash: "0x" + buffer_1.Buffer.from(txId, "hex").reverse().toString("hex"),
vout: BigInt(vout)
};
}
function packOwnerAndVaultId(owner, vaultId) {
if (owner.length !== 42)
throw new Error("Invalid owner address");
return owner.toLowerCase() + base_1.BigIntBufferUtils.toBuffer(vaultId, "be", 12).toString("hex");
}
exports.packOwnerAndVaultId = packOwnerAndVaultId;
function unpackOwnerAndVaultId(data) {
return [(0, ethers_1.getAddress)(data.substring(0, 42)), BigInt("0x" + data.substring(42, 66))];
}
exports.unpackOwnerAndVaultId = unpackOwnerAndVaultId;
class EVMSpvVaultContract extends EVMContractBase_1.EVMContractBase {
constructor(chainInterface, btcRelay, bitcoinRpc, contractAddress, contractDeploymentHeight) {
super(chainInterface, contractAddress, SpvVaultContractAbi_1.SpvVaultContractAbi, contractDeploymentHeight);
this.claimTimeout = 180;
this.logger = (0, Utils_1.getLogger)("EVMSpvVaultContract: ");
this.btcRelay = btcRelay;
this.bitcoinRpc = bitcoinRpc;
}
//Transactions
async Open(signer, vault, feeRate) {
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_1.EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.OPEN, feeRate);
return tx;
}
async Deposit(signer, vault, rawAmounts, feeRate) {
let value = 0n;
if (vault.token0.token.toLowerCase() === this.Chain.getNativeCurrencyAddress().toLowerCase())
value += rawAmounts[0] * vault.token0.multiplier;
if (vault.token1.token.toLowerCase() === this.Chain.getNativeCurrencyAddress().toLowerCase())
value += (rawAmounts[1] ?? 0n) * vault.token1.multiplier;
const tx = await this.contract.deposit.populateTransaction(vault.owner, vault.vaultId, vault.getVaultParamsStruct(), rawAmounts[0], rawAmounts[1] ?? 0n, { value });
tx.from = signer;
EVMFees_1.EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.DEPOSIT, feeRate);
return tx;
}
async Front(signer, vault, data, withdrawalSequence, feeRate) {
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_1.EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.FRONT, feeRate);
return tx;
}
async Claim(signer, vault, data, blockheader, merkle, position, feeRate) {
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_1.EVMFees.applyFeeRate(tx, EVMSpvVaultContract.GasCosts.CLAIM, feeRate);
return tx;
}
async checkWithdrawalTx(tx) {
const result = await this.contract.parseBitcoinTx(buffer_1.Buffer.from(tx.btcTx.hex, "hex"));
if (result == null)
throw new Error("Failed to parse transaction!");
}
createVaultData(owner, vaultId, utxo, confirmations, tokenData) {
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 = (0, ethers_1.keccak256)(ethers_1.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_1.EVMSpvVaultData(owner, vaultId, {
spvVaultParametersCommitment,
utxoTxHash: ethers_1.ZeroHash,
utxoVout: 0n,
openBlockheight: 0n,
withdrawCount: 0n,
depositCount: 0n,
token0Amount: 0n,
token1Amount: 0n
}, vaultParams, utxo));
}
//Getters
async getFronterAddress(owner, vaultId, withdrawal) {
const frontingAddress = await this.contract.getFronterById(owner, vaultId, "0x" + withdrawal.getFrontingId());
if (frontingAddress === ethers_1.ZeroAddress)
return null;
return frontingAddress;
}
async getVaultData(owner, vaultId) {
const vaultState = await this.contract.getVault(owner, vaultId);
const blockheight = Number(vaultState.openBlockheight);
const events = await this.Events.getContractBlockEvents(["Opened"], [
"0x" + owner.substring(2).padStart(64, "0"),
(0, ethers_1.hexlify)(base_1.BigIntBufferUtils.toBuffer(vaultId, "be", 32))
], blockheight);
const foundEvent = events.find(event => (0, EVMSpvVaultData_1.getVaultParamsCommitment)(event.args.params) === vaultState.spvVaultParametersCommitment);
if (foundEvent == null)
throw new Error("Valid open event not found!");
const vaultParams = foundEvent.args.params;
if (vaultParams.btcRelayContract.toLowerCase() !== this.btcRelay.contractAddress.toLowerCase())
return null;
return new EVMSpvVaultData_1.EVMSpvVaultData(owner, vaultId, vaultState, vaultParams);
}
async getAllVaults(owner) {
const openedVaults = new Map();
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;
openedVaults.set(vaultIdentifier, _event.args.params);
}
else {
openedVaults.delete(vaultIdentifier);
}
return null;
});
const vaults = [];
for (let [identifier, vaultParams] of openedVaults.entries()) {
const [owner, vaultIdStr] = identifier.split(":");
const vaultState = await this.contract.getVault(owner, BigInt(vaultIdStr));
if (vaultState.spvVaultParametersCommitment === (0, EVMSpvVaultData_1.getVaultParamsCommitment)(vaultParams)) {
vaults.push(new EVMSpvVaultData_1.EVMSpvVaultData(owner, BigInt(vaultIdStr), vaultState, vaultParams));
}
}
return vaults;
}
async getWithdrawalState(btcTxId) {
const txHash = buffer_1.Buffer.from(btcTxId, "hex").reverse();
let result = await this.Events.findInContractEvents(["Fronted", "Claimed", "Closed"], [
null,
null,
(0, ethers_1.hexlify)(txHash)
], async (event) => {
switch (event.eventName) {
case "Fronted":
const frontedEvent = event;
const [ownerFront, vaultIdFront] = unpackOwnerAndVaultId(frontedEvent.args.ownerAndVaultId);
return {
type: base_1.SpvWithdrawalStateType.FRONTED,
txId: event.transactionHash,
owner: ownerFront,
vaultId: vaultIdFront,
recipient: frontedEvent.args.recipient,
fronter: frontedEvent.args.caller
};
case "Claimed":
const claimedEvent = event;
const [ownerClaim, vaultIdClaim] = unpackOwnerAndVaultId(claimedEvent.args.ownerAndVaultId);
return {
type: base_1.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;
return {
type: base_1.SpvWithdrawalStateType.CLOSED,
txId: event.transactionHash,
owner: closedEvent.args.owner,
vaultId: closedEvent.args.vaultId,
error: closedEvent.args.error
};
}
});
result ?? (result = {
type: base_1.SpvWithdrawalStateType.NOT_FOUND
});
return result;
}
getWithdrawalData(btcTx) {
return Promise.resolve(new EVMSpvWithdrawalData_1.EVMSpvWithdrawalData(btcTx));
}
//OP_RETURN data encoding/decoding
fromOpReturnData(data) {
return EVMSpvVaultContract.fromOpReturnData(data);
}
static fromOpReturnData(data) {
let rawAmount0 = 0n;
let rawAmount1 = 0n;
let executionHash = 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_1.EVMAddresses.isValidAddress(recipient))
throw new Error("Invalid recipient specified");
return { executionHash, rawAmounts: [rawAmount0, rawAmount1], recipient: (0, ethers_1.getAddress)(recipient) };
}
toOpReturnData(recipient, rawAmounts, executionHash) {
return EVMSpvVaultContract.toOpReturnData(recipient, rawAmounts, executionHash);
}
static toOpReturnData(recipient, rawAmounts, executionHash) {
if (!EVMAddresses_1.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_1.Buffer.from(executionHash, "hex").length !== 32)
throw new Error("Invalid execution hash");
}
const recipientBuffer = buffer_1.Buffer.from(recipient.substring(2).padStart(40, "0"), "hex");
const amount0Buffer = base_1.BigIntBufferUtils.toBuffer(rawAmounts[0], "be", 8);
const amount1Buffer = rawAmounts[1] == null || rawAmounts[1] === 0n ? buffer_1.Buffer.alloc(0) : base_1.BigIntBufferUtils.toBuffer(rawAmounts[1], "be", 8);
const executionHashBuffer = executionHash == null ? buffer_1.Buffer.alloc(0) : buffer_1.Buffer.from(executionHash, "hex");
return buffer_1.Buffer.concat([
recipientBuffer,
amount0Buffer,
amount1Buffer,
executionHashBuffer
]);
}
//Actions
async claim(signer, vault, txs, synchronizer, initAta, txOptions) {
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, vault, rawAmounts, txOptions) {
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, vault, realWithdrawalTx, withdrawSequence, txOptions) {
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, vault, txOptions) {
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, vault, txs, synchronizer, initAta, feeRate) {
if (!vault.isOpened())
throw new Error("Cannot claim from a closed vault!");
feeRate ?? (feeRate = await this.Chain.Fees.getFeeRate());
const txsWithMerkleProofs = [];
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 = [];
const storedHeaders = await EVMBtcRelay_1.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, vault, rawAmounts, feeRate) {
var _a;
if (!vault.isOpened())
throw new Error("Cannot deposit to a closed vault!");
feeRate ?? (feeRate = await this.Chain.Fees.getFeeRate());
const txs = [];
let realAmount0 = 0n;
let realAmount1 = 0n;
//Approve first
const requiredApprovals = {};
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[_a = vault.token1.token.toLowerCase()] ?? (requiredApprovals[_a] = 0n);
requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1;
}
}
for (let tokenAddress in requiredApprovals) {
txs.push(await this.Chain.Tokens.Approve(signer, tokenAddress, requiredApprovals[tokenAddress], this.contractAddress, feeRate));
}
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, vault, realWithdrawalTx, withdrawSequence, feeRate) {
var _a;
if (!vault.isOpened())
throw new Error("Cannot front on a closed vault!");
feeRate ?? (feeRate = await this.Chain.Fees.getFeeRate());
const txs = [];
let realAmount0 = 0n;
let realAmount1 = 0n;
//Approve first
const rawAmounts = realWithdrawalTx.getFrontingAmount();
//Approve first
const requiredApprovals = {};
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[_a = vault.token1.token.toLowerCase()] ?? (requiredApprovals[_a] = 0n);
requiredApprovals[vault.token1.token.toLowerCase()] += realAmount1;
}
}
for (let tokenAddress in requiredApprovals) {
txs.push(await this.Chain.Tokens.Approve(signer, tokenAddress, requiredApprovals[tokenAddress], this.contractAddress, feeRate));
}
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, vault, feeRate) {
if (vault.isOpened())
throw new Error("Cannot open an already opened vault!");
feeRate ?? (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];
}
async getClaimFee(signer, withdrawalData, feeRate) {
feeRate ?? (feeRate = await this.Chain.Fees.getFeeRate());
return EVMFees_1.EVMFees.getGasFee(EVMSpvVaultContract.GasCosts.CLAIM, feeRate);
}
async getFrontFee(signer, withdrawalData, feeRate) {
feeRate ?? (feeRate = await this.Chain.Fees.getFeeRate());
return EVMFees_1.EVMFees.getGasFee(EVMSpvVaultContract.GasCosts.FRONT, feeRate);
}
}
exports.EVMSpvVaultContract = EVMSpvVaultContract;
EVMSpvVaultContract.GasCosts = {
DEPOSIT: 150000,
OPEN: 100000,
FRONT: 250000,
CLAIM: 250000
};