Skip to content

Commit

Permalink
Merge pull request #279 from interlay/dan/use-test-utils
Browse files Browse the repository at this point in the history
Make test utilities reusable
  • Loading branch information
gregdhill authored Apr 19, 2021
2 parents c475c34 + f8b57f3 commit 534f3b7
Show file tree
Hide file tree
Showing 38 changed files with 649 additions and 486 deletions.
23 changes: 0 additions & 23 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,26 +147,3 @@ services:
vault --keyring=bob --auto-register-with-collateral 1000000000000 --polka-btc-url 'ws://polkabtc:9944'
environment:
<<: *client-env
testdata_gen:
image: "registry.gitlab.com/interlay/polkabtc-clients/testdata-gen:0.6.1"
command:
- /bin/sh
- -c
- |
echo "Sleeping..."
sleep 15
testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=alice set-issue-period --period=50
testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=alice set-redeem-period --period=50
# the exchange rate value is 3855.23187
testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=bob set-exchange-rate --exchange-rate=385523187
# wait for the vault to register
sleep 45
ALICE_ADDRESS=bcrt1qefxeckts7tkgz7uach9dnwer4qz5nyehl4sjcc
testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=alice request-issue --issue-amount=10000 --vault=dave --griefing-collateral 1000000000000
sleep 30
REDEEM_ID=$$(testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=alice request-redeem --redeem-amount=5000 --btc-address=$${ALICE_ADDRESS} --vault=dave)
testdata-gen --polka-btc-url 'ws://polkabtc:9944' --keyring=alice execute-redeem --redeem-id=$${REDEEM_ID}
environment:
<<: *client-env
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/polkabtc",
"version": "0.13.0",
"version": "0.14.0",
"description": "JavaScript library to interact with PolkaBTC",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand Down Expand Up @@ -41,14 +41,15 @@
"@interlay/esplora-btc-api": "0.4.0",
"@interlay/polkabtc-types": "0.6.2",
"@types/big.js": "^4.0.5",
"big.js": "^6.0.1",
"big.js": "6.0.3",
"bitcoinjs-lib": "^5.2.0",
"cross-fetch": "^3.0.6",
"regtest-client": "^0.2.0",
"sinon": "^9.0.3",
"ts-mock-imports": "^1.3.0",
"@polkadot/api": "3.11.1",
"@polkadot/typegen": "3.11.1"
"@polkadot/typegen": "3.11.1",
"bitcoin-core": "^3.0.0"
},
"peerDependencies": {
"@polkadot/api": "3.11.1"
Expand All @@ -61,7 +62,6 @@
"@types/sinon": "^9.0.5",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"bitcoin-core": "^3.0.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^7.8.0",
Expand Down
71 changes: 52 additions & 19 deletions src/external/btc-core.ts → src/external/electrs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import {
} from "@interlay/esplora-btc-api";
import { AxiosResponse } from "axios";
import * as bitcoinjs from "bitcoinjs-lib";
import { btcToSat } from "../utils/currency";
import { TypeRegistry } from "@polkadot/types";
import { Bytes } from "@polkadot/types";
import Big from "big.js";

const mainnetApiBasePath = "https://blockstream.info/api";
const testnetApiBasePath = "https://electr-testnet.do.polkabtc.io";
import { MAINNET_ESPLORA_BASE_PATH, REGTEST_ESPLORA_BASE_PATH, TESTNET_ESPLORA_BASE_PATH } from "../utils/constants";
import { btcToSat } from "../utils/currency";

export type TxStatus = {
confirmed: boolean;
Expand Down Expand Up @@ -48,7 +48,7 @@ export type TxInput = {
* Bitcoin Core API
* @category Bitcoin Core
*/
export interface BTCCoreAPI {
export interface ElectrsAPI {
/**
* @returns The block hash of the latest Bitcoin block
*/
Expand Down Expand Up @@ -86,12 +86,12 @@ export interface BTCCoreAPI {
*
* @param opReturn Data string used for matching the OP_CODE of Bitcoin transactions
* @param recipientAddress Match the receiving address of a transaction that contains said op_return
* @param amountAsBTC Match the amount (in BTC) of a transaction that contains said op_return and recipientAddress.
* @param amount Match the amount (in BTC) of a transaction that contains said op_return and recipientAddress.
* This parameter is only considered if `recipientAddress` is defined.
*
* @returns A Bitcoin transaction ID
*/
getTxIdByOpReturn(opReturn: string, recipientAddress?: string, amountAsBTC?: string): Promise<string>;
getTxIdByOpReturn(opReturn: string, recipientAddress?: string, amount?: Big): Promise<string>;
/**
* Fetch the last bitcoin transaction ID based on the recipient address and amount.
* Throw an error if no such transaction is found.
Expand All @@ -104,7 +104,7 @@ export interface BTCCoreAPI {
*
* @returns A Bitcoin transaction ID
*/
getTxIdByRecipientAddress(recipientAddress: string, amountAsBTC?: string): Promise<string>;
getTxIdByRecipientAddress(recipientAddress: string, amountAsBTC?: Big): Promise<string>;
/**
* Fetch the Bitcoin transaction that matches the given TxId
*
Expand Down Expand Up @@ -139,9 +139,23 @@ export interface BTCCoreAPI {
* @returns A tuple of Bytes object, representing [merkleProof, rawTx]
*/
getParsedExecutionParameters(txid: string): Promise<[Bytes, Bytes]>;
/**
* Return a promise that either resolves to the first txid with the given opreturn `data`,
* or rejects if the `timeout` has elapsed.
*
* @remarks
* Every 5 seconds, performs the lookup using an external service, Esplora
*
* @param data The opReturn of the bitcoin transaction
* @param timeoutMs The duration until the Promise times out (in milliseconds)
* @param retryIntervalMs The time to wait (in milliseconds) between retries
*
* @returns The Bitcoin txid
*/
waitForOpreturn(data: string, timeoutMs: number, retryIntervalMs: number): Promise<string>;
}

export class DefaultBTCCoreAPI implements BTCCoreAPI {
export class DefaultElectrsAPI implements ElectrsAPI {
private blockApi: BlockApi;
private txApi: TxApi;
private scripthashApi: ScripthashApi;
Expand All @@ -151,10 +165,13 @@ export class DefaultBTCCoreAPI implements BTCCoreAPI {
let basePath = "";
switch (network) {
case "mainnet":
basePath = mainnetApiBasePath;
basePath = MAINNET_ESPLORA_BASE_PATH;
break;
case "testnet":
basePath = testnetApiBasePath;
basePath = TESTNET_ESPLORA_BASE_PATH;
break;
case "regtest":
basePath = REGTEST_ESPLORA_BASE_PATH;
break;
default:
basePath = network;
Expand Down Expand Up @@ -199,11 +216,11 @@ export class DefaultBTCCoreAPI implements BTCCoreAPI {
return amount;
}

async getTxIdByRecipientAddress(recipientAddress: string, amountAsBTC?: string): Promise<string> {
async getTxIdByRecipientAddress(recipientAddress: string, amount?: Big): Promise<string> {
try {
const utxos = await this.getData(this.addressApi.getAddressUtxo(recipientAddress));
for (const utxo of utxos.reverse()) {
if (this.utxoHasAmount(utxo, amountAsBTC)) {
if (this.utxoHasAtLeastAmount(utxo, amount)) {
return utxo.txid;
}
}
Expand All @@ -220,17 +237,17 @@ export class DefaultBTCCoreAPI implements BTCCoreAPI {
* @param amountAsBTC (Optional) Amount the recipient must receive
* @returns Boolean value
*/
private utxoHasAmount(utxo: UTXO | VOut, amountAsBTC?: string): boolean {
if (amountAsBTC) {
const expectedBtcAsSatoshi = Number(btcToSat(amountAsBTC));
private utxoHasAtLeastAmount(utxo: UTXO | VOut, amount?: Big): boolean {
if (amount) {
const expectedBtcAsSatoshi = Number(btcToSat(amount.toString()));
if (utxo.value === undefined || expectedBtcAsSatoshi > utxo.value) {
return false;
}
}
return true;
}

async getTxIdByOpReturn(opReturn: string, recipientAddress?: string, amountAsBTC?: string): Promise<string> {
async getTxIdByOpReturn(opReturn: string, recipientAddress?: string, amount?: Big): Promise<string> {
const data = Buffer.from(opReturn, "hex");
if (data.length !== 32) {
return Promise.reject("Requires a 32 byte hash as OP_RETURN");
Expand All @@ -250,14 +267,30 @@ export class DefaultBTCCoreAPI implements BTCCoreAPI {
continue;
}
for (const vout of tx.vout) {
if (this.txOutputHasRecipientAndAmount(vout, recipientAddress, amountAsBTC)) {
if (this.txOutputHasRecipientAndAmount(vout, recipientAddress, amount)) {
return tx.txid;
}
}
}
return Promise.reject("No transaction id found");
}

async waitForOpreturn(data: string, timeoutMs: number, retryIntervalMs: number): Promise<string> {
return new Promise<string>((resolve, reject) => {
this.getTxIdByOpReturn(data)
.then(resolve)
.catch((_error) => {
setTimeout(() => {
console.log("Did not find opreturn, retrying...");
if(timeoutMs < retryIntervalMs) {
reject("Timeout elapsed");
}
this.waitForOpreturn(data, timeoutMs - retryIntervalMs, retryIntervalMs).then(resolve);
}, retryIntervalMs);
});
});
}

/**
* Check if a given UTXO sends at least `amountAsBTC` to a certain `recipientAddress`
*
Expand All @@ -267,12 +300,12 @@ export class DefaultBTCCoreAPI implements BTCCoreAPI {
* `recipientAddress` is defined too
* @returns Boolean value
*/
private txOutputHasRecipientAndAmount(vout: VOut, recipientAddress?: string, amountAsBTC?: string): boolean {
private txOutputHasRecipientAndAmount(vout: VOut, recipientAddress?: string, amount?: Big): boolean {
if (recipientAddress) {
if (recipientAddress !== vout.scriptpubkey_address) {
return false;
}
return this.utxoHasAmount(vout, amountAsBTC);
return this.utxoHasAtLeastAmount(vout, amount);
}
return true;
}
Expand Down
2 changes: 1 addition & 1 deletion src/external/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { BTCCoreAPI } from "./btc-core";
export { ElectrsAPI } from "./electrs";
6 changes: 3 additions & 3 deletions src/parachain/btc-relay.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiPromise } from "@polkadot/api";
import { BTCCoreAPI } from "../external/btc-core";
import { ElectrsAPI } from "../external/electrs";
import { u32 } from "@polkadot/types/primitive";
import { H256Le } from "../interfaces/default";

Expand Down Expand Up @@ -33,7 +33,7 @@ export interface BTCRelayAPI {
}

export class DefaultBTCRelayAPI implements BTCRelayAPI {
constructor(private api: ApiPromise, private btcCore: BTCCoreAPI) { }
constructor(private api: ApiPromise, private electrsAPI: ElectrsAPI) { }

async getStableBitcoinConfirmations(): Promise<number> {
const head = await this.api.rpc.chain.getFinalizedHead();
Expand All @@ -54,7 +54,7 @@ export class DefaultBTCRelayAPI implements BTCRelayAPI {
txid: string,
confirmations: number = DEFAULT_STABLE_CONFIRMATIONS
): Promise<void> {
const merkleProof = await this.btcCore.getMerkleProof(txid);
const merkleProof = await this.electrsAPI.getMerkleProof(txid);
const confirmationsU32 = this.api.createType("u32", confirmations);
// TODO: change this to RPC call
this.api.tx.btcRelay.verifyTransactionInclusion(txid, merkleProof, confirmationsU32);
Expand Down
5 changes: 4 additions & 1 deletion src/parachain/collateral.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { AccountId } from "@polkadot/types/interfaces/runtime";
import { ApiPromise } from "@polkadot/api";
import { dotToPlanck, planckToDOT, DefaultTransactionAPI, TransactionAPI } from "../utils";
import { AddressOrPair } from "@polkadot/api/types";
import Big from "big.js";

import { dotToPlanck, planckToDOT } from "../utils";
import { DefaultTransactionAPI, TransactionAPI } from "./transaction";

/**
* @category PolkaBTC Bridge
* The type Big represents DOT or PolkaBTC denominations,
Expand Down
1 change: 1 addition & 0 deletions src/parachain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { SystemAPI } from "./system";
export { ConstantsAPI } from "./constants";
export { ReplaceAPI, ReplaceRequestExt } from "./replace";
export { FeeAPI } from "./fee";
export * from "./transaction";
56 changes: 36 additions & 20 deletions src/parachain/issue.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { ApiPromise } from "@polkadot/api";
import { AddressOrPair } from "@polkadot/api/types";
import { Bytes } from "@polkadot/types";
import { AccountId, H256, Hash } from "@polkadot/types/interfaces";
import { EventRecord } from "@polkadot/types/interfaces/system";
import { AccountId, H256, Hash, EventRecord } from "@polkadot/types/interfaces";
import { Network } from "bitcoinjs-lib";
import Big from "big.js";

import { DOT, IssueRequest, PolkaBTC } from "../interfaces/default";
import { DefaultVaultsAPI, VaultsAPI } from "./vaults";
import {
pagedIterator,
decodeFixedPointType,
DefaultTransactionAPI,
roundUpBtcToNearestSatoshi,
encodeParachainRequest,
TransactionAPI,
} from "../utils";
import { BlockNumber } from "@polkadot/types/interfaces/runtime";
import { Network } from "bitcoinjs-lib";
import Big from "big.js";
import { DefaultFeeAPI, FeeAPI } from "./fee";
import { BTCCoreAPI } from "../external";
import { ElectrsAPI } from "../external";
import { DefaultTransactionAPI, TransactionAPI } from "./transaction";

export type IssueRequestResult = { id: Hash; issueRequest: IssueRequestExt };

Expand Down Expand Up @@ -62,6 +60,20 @@ export interface IssueAPI extends TransactionAPI {
* @param issueId The ID returned by the issue request transaction
*/
cancel(issueId: H256): Promise<void>;
/**
* @remarks Testnet utility function
* @param blocks The time difference in number of blocks between an issue request is created
* and required completion time by a user. The issue period has an upper limit
* to prevent griefing of vault collateral.
*/
setIssuePeriod(blocks: number): Promise<void>;
/**
*
* @returns The time difference in number of blocks between an issue request is created
* and required completion time by a user. The issue period has an upper limit
* to prevent griefing of vault collateral.
*/
getIssuePeriod(): Promise<number>;
/**
* Set an account to use when sending transactions from this API
* @param account Keyring account
Expand All @@ -87,11 +99,6 @@ export interface IssueAPI extends TransactionAPI {
* @returns An issue request object
*/
getRequestById(issueId: H256): Promise<IssueRequestExt>;
/**
* @returns The time difference in number of blocks between when an issue request is created
* and required completion time by a user.
*/
getIssuePeriod(): Promise<BlockNumber>;
/**
* @returns The fee charged for issuing. For instance, "0.005" stands for 0.5%
*/
Expand All @@ -112,7 +119,7 @@ export class DefaultIssueAPI extends DefaultTransactionAPI implements IssueAPI
private vaultsAPI: VaultsAPI;
private feeAPI: FeeAPI;

constructor(api: ApiPromise, private btcNetwork: Network, private btcCoreAPI: BTCCoreAPI, account?: AddressOrPair) {
constructor(api: ApiPromise, private btcNetwork: Network, private electrsAPI: ElectrsAPI, account?: AddressOrPair) {
super(api, account);
this.vaultsAPI = new DefaultVaultsAPI(api, btcNetwork);
this.feeAPI = new DefaultFeeAPI(api);
Expand Down Expand Up @@ -157,7 +164,7 @@ export class DefaultIssueAPI extends DefaultTransactionAPI implements IssueAPI
"0x" + Buffer.from(btcTxId, "hex").reverse().toString("hex")
);
if (!merkleProof || !rawTx) {
[merkleProof, rawTx] = await this.btcCoreAPI.getParsedExecutionParameters(btcTxId);
[merkleProof, rawTx] = await this.electrsAPI.getParsedExecutionParameters(btcTxId);
}
const executeIssueTx = this.api.tx.issue.executeIssue(parsedRequestId, parsedBtcTxId, merkleProof, rawTx);
await this.sendLogged(executeIssueTx, this.api.events.issue.ExecuteIssue);
Expand All @@ -168,6 +175,20 @@ export class DefaultIssueAPI extends DefaultTransactionAPI implements IssueAPI
await this.sendLogged(cancelIssueTx, this.api.events.issue.CancelIssue);
}

async setIssuePeriod(blocks: number): Promise<void> {
const period = this.api.createType("BlockNumber", blocks);
const tx = this.api.tx.sudo
.sudo(
this.api.tx.issue.setIssuePeriod(period)
);
await this.sendLogged(tx);
}

async getIssuePeriod(): Promise<number> {
const blockNumber = await this.api.query.issue.issuePeriod();
return blockNumber.toNumber();
}

async list(): Promise<IssueRequestExt[]> {
const head = await this.api.rpc.chain.getFinalizedHead();
const issueRequests = await this.api.query.issue.issueRequests.entriesAt(head);
Expand Down Expand Up @@ -209,11 +230,6 @@ export class DefaultIssueAPI extends DefaultTransactionAPI implements IssueAPI
return pagedIterator<IssueRequest>(this.api.query.issue.issueRequests, perPage);
}

async getIssuePeriod(): Promise<BlockNumber> {
const head = await this.api.rpc.chain.getFinalizedHead();
return (await this.api.query.issue.issuePeriod.at(head)) as BlockNumber;
}

async getRequestById(issueId: H256): Promise<IssueRequestExt> {
const head = await this.api.rpc.chain.getFinalizedHead();
return encodeIssueRequest(await this.api.query.issue.issueRequests.at(head, issueId), this.btcNetwork);
Expand Down
Loading

0 comments on commit 534f3b7

Please sign in to comment.