Skip to content

Commit

Permalink
Merge pull request #697 from bvotteler/refactor-move-bitcoin-core-cli…
Browse files Browse the repository at this point in the history
…ent-to-tests

Refactor: move BitcoinCoreClient to tests
  • Loading branch information
bvotteler authored Oct 25, 2023
2 parents e3194d2 + d13b832 commit 4e85d3e
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 305 deletions.
11 changes: 4 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/interbtc-api",
"version": "2.5.1",
"version": "2.5.2",
"description": "JavaScript library to interact with interBTC",
"main": "build/src/index.js",
"type": "module",
Expand Down Expand Up @@ -58,12 +58,10 @@
"@interlay/monetary-js": "0.7.3",
"@polkadot/api": "10.9.1",
"big.js": "6.1.1",
"bitcoin-core": "^3.0.0",
"bitcoinjs-lib": "^5.2.0",
"bn.js": "4.12.0",
"cross-fetch": "^4.0.0",
"isomorphic-fetch": "^3.0.0",
"regtest-client": "^0.2.0"
"yargs": "^17.5.1"
},
"devDependencies": {
"@polkadot/typegen": "10.9.1",
Expand All @@ -74,22 +72,21 @@
"@types/yargs": "^17.0.10",
"@typescript-eslint/eslint-plugin": "^5.59.7",
"@typescript-eslint/parser": "^5.59.7",
"bitcoin-core": "^3.0.0",
"cli-table3": "0.6.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-unused-imports": "^3.0.0",
"husky": "^8.0.3",
"jest": "^29.6.2",
"npm-run-all": "^4.1.5",
"nyc": "^15.1.0",
"prettier": "^3.0.1",
"shelljs": "0.8.5",
"ts-jest": "^29.1.1",
"ts-node": "10.9.1",
"typedoc": "^0.25.0",
"typedoc-plugin-markdown": "^3.16.0",
"typescript": "5.2.2",
"yargs": "^17.5.1"
"typescript": "5.2.2"
},
"resolutions": {
"bn.js": "4.12.0"
Expand Down
2 changes: 1 addition & 1 deletion src/parachain/redeem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {

import { NO_LIQUIDATION_VAULT_FOUND_REJECTION, VaultsAPI } from "./vaults";
import {
allocateAmountsToVaults,
decodeBtcAddress,
decodeFixedPointType,
getTxProof,
Expand All @@ -23,7 +24,6 @@ import {
newMonetaryAmount,
newVaultCurrencyPair,
} from "../utils";
import { allocateAmountsToVaults } from "../utils/issueRedeem";
import { ElectrsAPI } from "../external";
import { TransactionAPI } from "./transaction";
import { OracleAPI } from "./oracle";
Expand Down
45 changes: 2 additions & 43 deletions src/utils/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
import { Block as BitcoinBlock, payments, Network, TxInput, TxOutput } from "bitcoinjs-lib";
import { BufferReader } from "bitcoinjs-lib/src/bufferutils";

import { H160 } from "@polkadot/types/interfaces";
import { bufferToHexString } from "../../src/utils";
import { BitcoinAddress } from "@polkadot/types/lookup";
import { TypeRegistry } from "@polkadot/types";

import { BTCRelayAPI } from "../parachain";
import {
sleep,
addHexPrefix,
reverseEndiannessHex,
SLEEP_TIME_MS,
BitcoinCoreClient,
bufferToHexString,
} from "../utils";
import {
HexString,
MerkleProof,
Expand All @@ -22,7 +11,7 @@ import {
TransactionInputSource,
TransactionLocktime,
TransactionOutput,
} from "../types";
} from "../../src/types";

import { Transaction as BitcoinTransaction } from "bitcoinjs-lib";
import { ElectrsAPI } from "../external";
Expand Down Expand Up @@ -79,16 +68,6 @@ function decode<P extends Payable, O>(p: P, f: (payment: P, options?: O) => P):
}
}

export function btcAddressFromParams(
registry: TypeRegistry,
params: { p2pkh: H160 | string } | { p2sh: H160 | string } | { p2wpkhv0: H160 | string }
): BitcoinAddress {
registry.register;
return registry.createType<BitcoinAddress>("BitcoinAddress", {
...params,
});
}

export function decodeBtcAddress(
address: string,
network: Network
Expand Down Expand Up @@ -205,26 +184,6 @@ export async function getTxProof(
};
}

export async function waitForBlockRelaying(
btcRelayAPI: BTCRelayAPI,
blockHash: string,
sleepMs = SLEEP_TIME_MS
): Promise<void> {
while (!(await btcRelayAPI.isBlockInRelay(blockHash))) {
console.log(`Blockhash ${blockHash} not yet relayed...`);
await sleep(sleepMs);
}
}

export async function waitForBlockFinalization(
bitcoinCoreClient: BitcoinCoreClient,
btcRelayAPI: BTCRelayAPI
): Promise<void> {
const bestBlockHash = addHexPrefix(reverseEndiannessHex(await bitcoinCoreClient.getBestBlockHash()));
// wait for block to be relayed
await waitForBlockRelaying(btcRelayAPI, bestBlockHash);
}

export class BitcoinMerkleProof {
blockHeader: BitcoinBlock;
transactionsCount: number;
Expand Down
1 change: 0 additions & 1 deletion src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ export * from "./bitcoin";
export * from "./currency";
export * from "./encoding";
export * from "./constants";
export * from "./bitcoin-core-client";
export * from "./issueRedeem";
export * from "./storage";
export * from "./loans";
Expand Down
241 changes: 3 additions & 238 deletions src/utils/issueRedeem.ts
Original file line number Diff line number Diff line change
@@ -1,74 +1,8 @@
import { Hash, EventRecord } from "@polkadot/types/interfaces";
import { ApiTypes, AugmentedEvent } from "@polkadot/api/types";
import type { AnyTuple } from "@polkadot/types/types";
import { ApiPromise } from "@polkadot/api";
import { KeyringPair } from "@polkadot/keyring/types";
import { Bitcoin, BitcoinAmount, InterBtcAmount, MonetaryAmount } from "@interlay/monetary-js";
import { MonetaryAmount } from "@interlay/monetary-js";
import { InterbtcPrimitivesVaultId } from "@polkadot/types/lookup";
import { ISubmittableResult } from "@polkadot/types/types";

import { newAccountId } from "../utils";
import { BitcoinCoreClient } from "./bitcoin-core-client";
import { stripHexPrefix } from "../utils/encoding";
import { Issue, IssueStatus, Redeem, RedeemStatus, WrappedCurrency } from "../types";
import { waitForBlockFinalization } from "./bitcoin";
import { atomicToBaseAmount, currencyIdToMonetaryCurrency, newMonetaryAmount } from "./currency";
import { InterBtcApi } from "../interbtc-api";
import { sleep, SLEEP_TIME_MS } from "../utils";

export interface IssueResult {
request: Issue;
initialWrappedTokenBalance: MonetaryAmount<WrappedCurrency>;
finalWrappedTokenBalance: MonetaryAmount<WrappedCurrency>;
}

export enum ExecuteRedeem {
False,
Manually,
Auto,
}

/**
* @param events The EventRecord array returned after sending a transaction
* @param methodToCheck The name of the event method whose existence to check
* @returns The id associated with the transaction. If the EventRecord array does not
* contain required events, the function throws an error.
*/
export function getRequestIdsFromEvents(
events: EventRecord[],
eventToFind: AugmentedEvent<ApiTypes, AnyTuple>,
api: ApiPromise
): Hash[] {
const ids = new Array<Hash>();
for (const { event } of events) {
if (eventToFind.is(event)) {
// the redeem id has type H256 and is the first item of the event data array
const id = api.createType("Hash", event.data[0]);
ids.push(id);
}
}

if (ids.length > 0) return ids;
throw new Error("Transaction failed");
}

export const getIssueRequestsFromExtrinsicResult = async (
interBtcApi: InterBtcApi,
result: ISubmittableResult
): Promise<Array<Issue>> => {
const ids = getRequestIdsFromEvents(result.events, interBtcApi.api.events.issue.RequestIssue, interBtcApi.api);
const issueRequests = await interBtcApi.issue.getRequestsByIds(ids);
return issueRequests;
};

export const getRedeemRequestsFromExtrinsicResult = async (
interBtcApi: InterBtcApi,
result: ISubmittableResult
): Promise<Array<Redeem>> => {
const ids = getRequestIdsFromEvents(result.events, interBtcApi.api.events.redeem.RequestRedeem, interBtcApi.api);
const redeemRequests = await interBtcApi.redeem.getRequestsByIds(ids);
return redeemRequests;
};
import { WrappedCurrency } from "../types";
import { newMonetaryAmount } from "./currency";

/**
* Given a list of vaults with availabilities (e.g. collateral for issue, tokens
Expand Down Expand Up @@ -117,172 +51,3 @@ export function allocateAmountsToVaults(
}
return allocations;
}

export async function issueSingle(
interBtcApi: InterBtcApi,
bitcoinCoreClient: BitcoinCoreClient,
issuingAccount: KeyringPair,
amount: MonetaryAmount<WrappedCurrency>,
vaultId?: InterbtcPrimitivesVaultId,
autoExecute = true,
triggerRefund = false,
atomic = true
): Promise<IssueResult> {
const prevAccount = interBtcApi.account;
interBtcApi.setAccount(issuingAccount);
try {
const requesterAccountId = newAccountId(interBtcApi.api, issuingAccount.address);
const initialWrappedTokenBalance = (await interBtcApi.tokens.balance(amount.currency, requesterAccountId)).free;
const blocksToMine = 3;

const collateralCurrency = vaultId
? await currencyIdToMonetaryCurrency(interBtcApi.api, vaultId.currencies.collateral)
: undefined;
const { extrinsic, event } = await interBtcApi.issue.request(
amount,
vaultId?.accountId,
collateralCurrency,
atomic
);

const result = await interBtcApi.transaction.sendLogged(extrinsic, event);
const issueRequests = await getIssueRequestsFromExtrinsicResult(interBtcApi, result);
if (issueRequests.length !== 1) {
throw new Error("More than one issue request created");
}
const issueRequest = issueRequests[0];

let amountAsBtc = issueRequest.wrappedAmount.add(issueRequest.bridgeFee);
if (triggerRefund) {
// Send 1 more Btc than needed
amountAsBtc = amountAsBtc.add(new BitcoinAmount(1));
} else if (autoExecute === false) {
// Send 1 less Satoshi than requested
// to trigger the user failsafe and disable auto-execution.
const oneSatoshiInBtc = atomicToBaseAmount(1, Bitcoin);
const oneSatoshi = new BitcoinAmount(oneSatoshiInBtc);
amountAsBtc = amountAsBtc.sub(oneSatoshi);
}

// send btc tx
const vaultBtcAddress = issueRequest.vaultWrappedAddress;
if (vaultBtcAddress === undefined) {
throw new Error("Undefined vault address returned from RequestIssue");
}

const txData = await bitcoinCoreClient.sendBtcTxAndMine(vaultBtcAddress, amountAsBtc, blocksToMine);

if (autoExecute === false) {
console.log("Manually executing, waiting for relay to catchup");
await waitForBlockFinalization(bitcoinCoreClient, interBtcApi.btcRelay);
console.log("Block successfully relayed");
await interBtcApi.electrsAPI.waitForTxInclusion(txData.txid, SLEEP_TIME_MS * 10, SLEEP_TIME_MS);
console.log("Transaction included in electrs");
// execute issue, assuming the selected vault has the `--no-issue-execution` flag enabled
const { extrinsic: executeExtrinsic, event: executeEvent } = await interBtcApi.issue.execute(
issueRequest.id,
txData.txid
);
await interBtcApi.transaction.sendLogged(executeExtrinsic, executeEvent);
} else {
console.log("Auto-executing, waiting for vault to submit proof");
// wait for vault to execute issue
while ((await interBtcApi.issue.getRequestById(issueRequest.id)).status !== IssueStatus.Completed) {
await sleep(SLEEP_TIME_MS);
}
}

const finalWrappedTokenBalance = (await interBtcApi.tokens.balance(amount.currency, requesterAccountId)).free;
return {
request: issueRequest,
initialWrappedTokenBalance,
finalWrappedTokenBalance,
};
} catch (e) {
// IssueCompleted errors occur when multiple vaults attempt to execute the same request
return Promise.reject(new Error(`Issuing failed: ${e}`));
} finally {
if (prevAccount) {
interBtcApi.setAccount(prevAccount);
}
}
}

export async function redeem(
interBtcApi: InterBtcApi,
bitcoinCoreClient: BitcoinCoreClient,
redeemingAccount: KeyringPair,
amount: MonetaryAmount<WrappedCurrency>,
vaultId?: InterbtcPrimitivesVaultId,
autoExecute = ExecuteRedeem.Auto,
atomic = true,
timeout = 5 * 60 * 1000
): Promise<Redeem> {
const prevAccount = interBtcApi.account;
interBtcApi.setAccount(redeemingAccount);
const btcAddress = "bcrt1qujs29q4gkyn2uj6y570xl460p4y43ruayxu8ry";
const { extrinsic, event } = await interBtcApi.redeem.request(amount, btcAddress, vaultId, atomic);
const result = await interBtcApi.transaction.sendLogged(extrinsic, event);
const [redeemRequest] = await getRedeemRequestsFromExtrinsicResult(interBtcApi, result);

switch (autoExecute) {
case ExecuteRedeem.Manually: {
const opreturnData = stripHexPrefix(redeemRequest.id.toString());
const btcTxId = await interBtcApi.electrsAPI.waitForOpreturn(opreturnData, timeout, 5000).catch((_) => {
throw new Error("Redeem request was not executed, timeout expired");
});
// Even if the tx was found, the block needs to be relayed to the parachain before `execute` can be called.
await waitForBlockFinalization(bitcoinCoreClient, interBtcApi.btcRelay);

// manually execute issue
await interBtcApi.redeem.execute(redeemRequest.id.toString(), btcTxId);
break;
}
case ExecuteRedeem.Auto: {
// wait for vault to execute issue
while ((await interBtcApi.redeem.getRequestById(redeemRequest.id)).status !== RedeemStatus.Completed) {
await sleep(SLEEP_TIME_MS);
}
break;
}
}
if (prevAccount) {
interBtcApi.setAccount(prevAccount);
}
return redeemRequest;
}

export async function issueAndRedeem(
InterBtcApi: InterBtcApi,
bitcoinCoreClient: BitcoinCoreClient,
account: KeyringPair,
vaultId?: InterbtcPrimitivesVaultId,
issueAmount: MonetaryAmount<WrappedCurrency> = new InterBtcAmount(0.1),
redeemAmount: MonetaryAmount<WrappedCurrency> = new InterBtcAmount(0.009),
autoExecuteIssue = true,
autoExecuteRedeem = ExecuteRedeem.Auto,
triggerRefund = false,
atomic = true
): Promise<[Issue, Redeem]> {
const issueResult = await issueSingle(
InterBtcApi,
bitcoinCoreClient,
account,
issueAmount,
vaultId,
autoExecuteIssue,
triggerRefund,
atomic
);

const redeemRequest = await redeem(
InterBtcApi,
bitcoinCoreClient,
account,
redeemAmount,
issueResult.request.vaultId,
autoExecuteRedeem,
atomic
);
return [issueResult.request, redeemRequest];
}
Loading

0 comments on commit 4e85d3e

Please sign in to comment.