From ba153a1375a39404e4cbd819686c95f1a659008d Mon Sep 17 00:00:00 2001 From: Brendon Votteler Date: Mon, 9 Oct 2023 13:36:33 +0200 Subject: [PATCH 1/3] refactor: move bitcoin-core-client utility methods to tests folder; exclude test files from build output --- src/parachain/redeem.ts | 2 +- src/utils/bitcoin.ts | 45 +--- src/utils/index.ts | 1 - src/utils/issueRedeem.ts | 241 +----------------- .../external/staging/electrs.test.ts | 2 +- .../staging/sequential/issue.partial.ts | 6 +- .../staging/sequential/nomination.partial.ts | 5 +- .../staging/sequential/redeem.partial.ts | 6 +- .../staging/sequential/replace.partial.ts | 4 +- .../staging/setup/initialize.test.ts | 2 +- {src => test}/utils/bitcoin-core-client.ts | 2 +- test/utils/bitcoin-utils.ts | 28 ++ test/utils/helpers.ts | 2 +- test/utils/issue-redeem.ts | 240 +++++++++++++++++ tsconfig.json | 2 +- 15 files changed, 290 insertions(+), 298 deletions(-) rename {src => test}/utils/bitcoin-core-client.ts (98%) create mode 100644 test/utils/bitcoin-utils.ts create mode 100644 test/utils/issue-redeem.ts diff --git a/src/parachain/redeem.ts b/src/parachain/redeem.ts index 4e0137973..18a093803 100644 --- a/src/parachain/redeem.ts +++ b/src/parachain/redeem.ts @@ -14,6 +14,7 @@ import { import { NO_LIQUIDATION_VAULT_FOUND_REJECTION, VaultsAPI } from "./vaults"; import { + allocateAmountsToVaults, decodeBtcAddress, decodeFixedPointType, getTxProof, @@ -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"; diff --git a/src/utils/bitcoin.ts b/src/utils/bitcoin.ts index a7fe250a5..f115e5184 100644 --- a/src/utils/bitcoin.ts +++ b/src/utils/bitcoin.ts @@ -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, @@ -22,7 +11,7 @@ import { TransactionInputSource, TransactionLocktime, TransactionOutput, -} from "../types"; +} from "../../src/types"; import { Transaction as BitcoinTransaction } from "bitcoinjs-lib"; import { ElectrsAPI } from "../external"; @@ -79,16 +68,6 @@ function decode

(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", { - ...params, - }); -} - export function decodeBtcAddress( address: string, network: Network @@ -205,26 +184,6 @@ export async function getTxProof( }; } -export async function waitForBlockRelaying( - btcRelayAPI: BTCRelayAPI, - blockHash: string, - sleepMs = SLEEP_TIME_MS -): Promise { - while (!(await btcRelayAPI.isBlockInRelay(blockHash))) { - console.log(`Blockhash ${blockHash} not yet relayed...`); - await sleep(sleepMs); - } -} - -export async function waitForBlockFinalization( - bitcoinCoreClient: BitcoinCoreClient, - btcRelayAPI: BTCRelayAPI -): Promise { - const bestBlockHash = addHexPrefix(reverseEndiannessHex(await bitcoinCoreClient.getBestBlockHash())); - // wait for block to be relayed - await waitForBlockRelaying(btcRelayAPI, bestBlockHash); -} - export class BitcoinMerkleProof { blockHeader: BitcoinBlock; transactionsCount: number; diff --git a/src/utils/index.ts b/src/utils/index.ts index 4ed23658b..35eb80620 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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"; diff --git a/src/utils/issueRedeem.ts b/src/utils/issueRedeem.ts index cede739db..f478290fa 100644 --- a/src/utils/issueRedeem.ts +++ b/src/utils/issueRedeem.ts @@ -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; - finalWrappedTokenBalance: MonetaryAmount; -} - -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, - api: ApiPromise -): Hash[] { - const ids = new Array(); - 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> => { - 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> => { - 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 @@ -117,172 +51,3 @@ export function allocateAmountsToVaults( } return allocations; } - -export async function issueSingle( - interBtcApi: InterBtcApi, - bitcoinCoreClient: BitcoinCoreClient, - issuingAccount: KeyringPair, - amount: MonetaryAmount, - vaultId?: InterbtcPrimitivesVaultId, - autoExecute = true, - triggerRefund = false, - atomic = true -): Promise { - 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, - vaultId?: InterbtcPrimitivesVaultId, - autoExecute = ExecuteRedeem.Auto, - atomic = true, - timeout = 5 * 60 * 1000 -): Promise { - 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 = new InterBtcAmount(0.1), - redeemAmount: MonetaryAmount = 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]; -} diff --git a/test/integration/external/staging/electrs.test.ts b/test/integration/external/staging/electrs.test.ts index a8da99ec4..0a42ced22 100644 --- a/test/integration/external/staging/electrs.test.ts +++ b/test/integration/external/staging/electrs.test.ts @@ -12,7 +12,7 @@ import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, } from "../../../config"; -import { BitcoinCoreClient } from "../../../../src/utils/bitcoin-core-client"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; import { BitcoinAmount } from "@interlay/monetary-js"; import { makeRandomBitcoinAddress, runWhileMiningBTCBlocks, waitSuccess } from "../../../utils/helpers"; diff --git a/test/integration/parachain/staging/sequential/issue.partial.ts b/test/integration/parachain/staging/sequential/issue.partial.ts index 73af8bfff..b3e871ee4 100644 --- a/test/integration/parachain/staging/sequential/issue.partial.ts +++ b/test/integration/parachain/staging/sequential/issue.partial.ts @@ -5,7 +5,6 @@ import { CollateralCurrencyExt, currencyIdToMonetaryCurrency, DefaultInterBtcApi, - getIssueRequestsFromExtrinsicResult, InterBtcApi, InterbtcPrimitivesVaultId, IssueStatus, @@ -26,8 +25,9 @@ import { PARACHAIN_ENDPOINT, ESPLORA_BASE_PATH, } from "../../../../config"; -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; -import { issueSingle } from "../../../../../src/utils/issueRedeem"; +import { getIssueRequestsFromExtrinsicResult } from "../../../../utils/issue-redeem"; +import { BitcoinCoreClient } from "../../../../utils/bitcoin-core-client"; +import { issueSingle } from "../../../../utils/issue-redeem"; import { newVaultId, WrappedCurrency } from "../../../../../src"; import { getCorrespondingCollateralCurrenciesForTests, diff --git a/test/integration/parachain/staging/sequential/nomination.partial.ts b/test/integration/parachain/staging/sequential/nomination.partial.ts index ebf8a0fa6..cb67a30e6 100644 --- a/test/integration/parachain/staging/sequential/nomination.partial.ts +++ b/test/integration/parachain/staging/sequential/nomination.partial.ts @@ -4,7 +4,6 @@ import BN from "bn.js"; import { DefaultInterBtcApi, InterBtcApi, InterbtcPrimitivesVaultId } from "../../../../../src/index"; import { - BitcoinCoreClient, CollateralCurrencyExt, currencyIdToMonetaryCurrency, encodeUnsignedFixedPoint, @@ -12,7 +11,9 @@ import { newVaultId, WrappedCurrency, } from "../../../../../src"; -import { setRawStorage, issueSingle, newMonetaryAmount } from "../../../../../src/utils"; +import { setRawStorage, newMonetaryAmount } from "../../../../../src/utils"; +import { issueSingle } from "../../../../utils/issue-redeem"; +import { BitcoinCoreClient } from "../../../../utils/bitcoin-core-client"; import { createSubstrateAPI } from "../../../../../src/factory"; import { SUDO_URI, diff --git a/test/integration/parachain/staging/sequential/redeem.partial.ts b/test/integration/parachain/staging/sequential/redeem.partial.ts index 668dadd06..e95badc37 100644 --- a/test/integration/parachain/staging/sequential/redeem.partial.ts +++ b/test/integration/parachain/staging/sequential/redeem.partial.ts @@ -21,10 +21,10 @@ import { VAULT_2_URI, ESPLORA_BASE_PATH, } from "../../../../config"; -import { issueAndRedeem, newMonetaryAmount } from "../../../../../src/utils"; -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; +import { newMonetaryAmount } from "../../../../../src/utils"; +import { BitcoinCoreClient } from "../../../../utils/bitcoin-core-client"; import { newVaultId, WrappedCurrency } from "../../../../../src"; -import { ExecuteRedeem } from "../../../../../src/utils/issueRedeem"; +import { issueAndRedeem, ExecuteRedeem } from "../../../../utils/issue-redeem"; import { getAUSDForeignAsset, getCorrespondingCollateralCurrenciesForTests } from "../../../../utils/helpers"; export type RequestResult = { hash: Hash; vault: VaultRegistryVault }; diff --git a/test/integration/parachain/staging/sequential/replace.partial.ts b/test/integration/parachain/staging/sequential/replace.partial.ts index f86bfaad7..2d7ab0f3a 100644 --- a/test/integration/parachain/staging/sequential/replace.partial.ts +++ b/test/integration/parachain/staging/sequential/replace.partial.ts @@ -15,8 +15,8 @@ import { BlockHash } from "@polkadot/types/interfaces"; import { FrameSystemEventRecord } from "@polkadot/types/lookup"; import { WrappedCurrency, currencyIdToMonetaryCurrency, newAccountId, newVaultId } from "../../../../../src"; import { createSubstrateAPI } from "../../../../../src/factory"; -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; -import { issueSingle } from "../../../../../src/utils/issueRedeem"; +import { BitcoinCoreClient } from "../../../../utils/bitcoin-core-client"; +import { issueSingle } from "../../../../utils/issue-redeem"; import { BITCOIN_CORE_HOST, BITCOIN_CORE_NETWORK, diff --git a/test/integration/parachain/staging/setup/initialize.test.ts b/test/integration/parachain/staging/setup/initialize.test.ts index 5c4c65d7e..36e1274a5 100644 --- a/test/integration/parachain/staging/setup/initialize.test.ts +++ b/test/integration/parachain/staging/setup/initialize.test.ts @@ -5,7 +5,6 @@ import { Bitcoin, ExchangeRate, Kintsugi, Kusama, MonetaryAmount, Polkadot } fro import Big from "big.js"; import { - BitcoinCoreClient, createSubstrateAPI, VaultsAPI, newAccountId, @@ -51,6 +50,7 @@ import { } from "../../../../utils/helpers"; import { DefaultAssetRegistryAPI } from "../../../../../src/parachain/asset-registry"; import { BN } from "bn.js"; +import { BitcoinCoreClient } from "../../../../utils/bitcoin-core-client"; function getSetBalanceExtrinsic( api: ApiPromise, diff --git a/src/utils/bitcoin-core-client.ts b/test/utils/bitcoin-core-client.ts similarity index 98% rename from src/utils/bitcoin-core-client.ts rename to test/utils/bitcoin-core-client.ts index c999458d9..ca446475e 100644 --- a/src/utils/bitcoin-core-client.ts +++ b/test/utils/bitcoin-core-client.ts @@ -2,7 +2,7 @@ import { MonetaryAmount } from "@interlay/monetary-js"; import Big from "big.js"; -import { WrappedCurrency } from "../types"; +import { WrappedCurrency } from "../../src/types"; // eslint-disable-next-line const Client = require("bitcoin-core"); diff --git a/test/utils/bitcoin-utils.ts b/test/utils/bitcoin-utils.ts new file mode 100644 index 000000000..5e97c8e8e --- /dev/null +++ b/test/utils/bitcoin-utils.ts @@ -0,0 +1,28 @@ +import { BTCRelayAPI } from "../../src/parachain"; +import { + sleep, + addHexPrefix, + reverseEndiannessHex, + SLEEP_TIME_MS, +} from "../../src/utils"; +import { BitcoinCoreClient } from "./bitcoin-core-client"; + +export async function waitForBlockRelaying( + btcRelayAPI: BTCRelayAPI, + blockHash: string, + sleepMs = SLEEP_TIME_MS +): Promise { + while (!(await btcRelayAPI.isBlockInRelay(blockHash))) { + console.log(`Blockhash ${blockHash} not yet relayed...`); + await sleep(sleepMs); + } +} + +export async function waitForBlockFinalization( + bitcoinCoreClient: BitcoinCoreClient, + btcRelayAPI: BTCRelayAPI +): Promise { + const bestBlockHash = addHexPrefix(reverseEndiannessHex(await bitcoinCoreClient.getBestBlockHash())); + // wait for block to be relayed + await waitForBlockRelaying(btcRelayAPI, bestBlockHash); +} diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 9cc7f4fee..800b7e8c1 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -5,7 +5,6 @@ import { mnemonicGenerate } from "@polkadot/util-crypto"; import Big from "big.js"; import * as bitcoinjs from "bitcoinjs-lib"; import { - BitcoinCoreClient, InterBtcApi, VaultStatusExt, CurrencyExt, @@ -17,6 +16,7 @@ import { ExtrinsicData, newExtrinsicStatus, } from "../../src"; +import { BitcoinCoreClient } from "./bitcoin-core-client"; import { setStorageAtKey, setRawStorage, diff --git a/test/utils/issue-redeem.ts b/test/utils/issue-redeem.ts new file mode 100644 index 000000000..acee630a7 --- /dev/null +++ b/test/utils/issue-redeem.ts @@ -0,0 +1,240 @@ +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 { InterbtcPrimitivesVaultId } from "@polkadot/types/lookup"; +import { ISubmittableResult } from "@polkadot/types/types"; + +import { newAccountId } from "../../src/utils"; +import { BitcoinCoreClient } from "./bitcoin-core-client"; +import { stripHexPrefix } from "../../src/utils/encoding"; +import { Issue, IssueStatus, Redeem, RedeemStatus, WrappedCurrency } from "../../src/types"; +import { waitForBlockFinalization } from "./bitcoin-utils"; +import { atomicToBaseAmount, currencyIdToMonetaryCurrency } from "../../src/utils/currency"; +import { InterBtcApi } from "../../src/interbtc-api"; +import { sleep, SLEEP_TIME_MS } from "../../src/utils"; + +export interface IssueResult { + request: Issue; + initialWrappedTokenBalance: MonetaryAmount; + finalWrappedTokenBalance: MonetaryAmount; +} + +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, + api: ApiPromise +): Hash[] { + const ids = new Array(); + 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> => { + 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> => { + const ids = getRequestIdsFromEvents(result.events, interBtcApi.api.events.redeem.RequestRedeem, interBtcApi.api); + const redeemRequests = await interBtcApi.redeem.getRequestsByIds(ids); + return redeemRequests; +}; + +export async function issueSingle( + interBtcApi: InterBtcApi, + bitcoinCoreClient: BitcoinCoreClient, + issuingAccount: KeyringPair, + amount: MonetaryAmount, + vaultId?: InterbtcPrimitivesVaultId, + autoExecute = true, + triggerRefund = false, + atomic = true +): Promise { + 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, + vaultId?: InterbtcPrimitivesVaultId, + autoExecute = ExecuteRedeem.Auto, + atomic = true, + timeout = 5 * 60 * 1000 +): Promise { + 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 = new InterBtcAmount(0.1), + redeemAmount: MonetaryAmount = 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]; +} diff --git a/tsconfig.json b/tsconfig.json index 301907bd6..32e359b17 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,6 +25,6 @@ "@interlay/interbtc-api/*": ["src/*"] } }, - "include": ["src/**/*", "test/**/*.ts", "scripts/**/*.ts"], + "include": ["src/**/*", "scripts/**/*.ts"], "exclude": ["./node_modules/*"] } From 640c2e1d2cf5d0d4a6ed1591bfe14f08b192dbc8 Mon Sep 17 00:00:00 2001 From: Brendon Votteler Date: Mon, 9 Oct 2023 13:37:12 +0200 Subject: [PATCH 2/3] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 931b3f9a4..d0386a34e 100644 --- a/package.json +++ b/package.json @@ -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", From d13b832271f2a92154a468e542cf7f35ba1c2bb2 Mon Sep 17 00:00:00 2001 From: Brendon Votteler Date: Wed, 11 Oct 2023 08:12:40 +0200 Subject: [PATCH 3/3] chore: clean up dependencies --- package.json | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index d0386a34e..04c4d3af2 100644 --- a/package.json +++ b/package.json @@ -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", @@ -74,6 +72,7 @@ "@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", @@ -81,15 +80,13 @@ "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"