From 6aded4902b3f481d4415b941796a44eb9bb00af2 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Wed, 31 Mar 2021 18:27:47 +0100 Subject: [PATCH 1/9] feat(redeem): Liquidation (burn) redeem --- docker-compose.yml | 17 +++++++- package.json | 2 +- src/parachain/collateral.ts | 14 +++--- src/parachain/redeem.ts | 52 ++++++++++++++++++++++- src/parachain/vaults.ts | 22 +++++++++- src/utils/transaction.ts | 21 +++++++-- test/integration/parachain/issue.test.ts | 2 - test/integration/parachain/redeem.test.ts | 41 +++++++++++++++++- test/mock/parachain/redeem.ts | 9 ++++ test/mock/parachain/vaults.ts | 8 +++- test/utils/bitcoin-core-client.ts | 9 +++- 11 files changed, 176 insertions(+), 21 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 21d5cf430..08edd5f6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -90,11 +90,11 @@ services: - | echo "Sleeping..." sleep 5 - faucet --keyring=ferdie --polka-btc-url 'ws://polkabtc:9944' --user-allowance 1 --vault-allowance 500 --http-addr '[::0]:3035' + faucet --keyring=ferdie --polka-btc-url 'ws://polkabtc:9944' --user-allowance 1 --vault-allowance 500 --http-addr '[::0]:3036' environment: RUST_LOG: info ports: - - "3035:3035" + - "3036:3036" vault_1: image: "registry.gitlab.com/interlay/polkabtc-clients/vault:0.6.1" command: @@ -134,6 +134,19 @@ services: vault --keyring=eve --auto-register-with-collateral 1000000000000 --no-issue-execution --polka-btc-url 'ws://polkabtc:9944' environment: <<: *client-env + vault_to_liquidate: + image: "registry.gitlab.com/interlay/polkabtc-clients/vault:0.6.1" + command: + - /bin/sh + - -c + - | + echo "Sleeping..." + # sleep for 30s to wait for bitcoin to create the Ferdie wallet + # and also to ensure that the issue period and redeem period are set + sleep 30 + vault --keyring=ferdie --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: diff --git a/package.json b/package.json index 5c6cc1aac..210955765 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@interlay/polkabtc", - "version": "0.11.2", + "version": "0.11.3", "description": "JavaScript library to interact with PolkaBTC", "main": "build/index.js", "typings": "build/index.d.ts", diff --git a/src/parachain/collateral.ts b/src/parachain/collateral.ts index 994da24cd..929f2d359 100644 --- a/src/parachain/collateral.ts +++ b/src/parachain/collateral.ts @@ -1,4 +1,4 @@ -import { AccountId, Balance } from "@polkadot/types/interfaces/runtime"; +import { AccountId, Balance as BN } from "@polkadot/types/interfaces/runtime"; import { ApiPromise } from "@polkadot/api"; import { ACCOUNT_NOT_SET_ERROR_MESSAGE, Transaction } from "../utils"; import { AddressOrPair } from "@polkadot/api/submittable/types"; @@ -15,17 +15,17 @@ export interface CollateralAPI { /** * @returns Total locked DOT collateral */ - totalLockedDOT(): Promise; + totalLockedDOT(): Promise; /** * @param id The ID of an account * @returns The reserved DOT balance of the given account */ - balanceLockedDOT(id: AccountId): Promise; + balanceLockedDOT(id: AccountId): Promise; /** * @param id The ID of an account * @returns The free DOT balance of the given account */ - balanceDOT(id: AccountId): Promise; + balanceDOT(id: AccountId): Promise; /** * Send a transaction that transfers DOT from the caller's address to another address * @param address The recipient of the DOT transfer @@ -41,18 +41,18 @@ export class DefaultCollateralAPI implements CollateralAPI { this.transaction = new Transaction(api); } - async totalLockedDOT(): Promise { + async totalLockedDOT(): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); return this.api.query.collateral.totalCollateral.at(head); } - async balanceLockedDOT(id: AccountId): Promise { + async balanceLockedDOT(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); return account.reserved; } - async balanceDOT(id: AccountId): Promise { + async balanceDOT(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); return account.free; diff --git a/src/parachain/redeem.ts b/src/parachain/redeem.ts index 4874ca76f..984130a04 100644 --- a/src/parachain/redeem.ts +++ b/src/parachain/redeem.ts @@ -11,7 +11,10 @@ import { decodeFixedPointType, Transaction, encodeParachainRequest, - ACCOUNT_NOT_SET_ERROR_MESSAGE + ACCOUNT_NOT_SET_ERROR_MESSAGE, + btcToSat, + satToBTC, + planckToDOT } from "../utils"; import { BlockNumber } from "@polkadot/types/interfaces/runtime"; import { stripHexPrefix } from "../utils"; @@ -19,6 +22,8 @@ import { Network } from "bitcoinjs-lib"; import Big from "big.js"; import { ApiTypes, AugmentedEvent } from "@polkadot/api/types"; import type { AnyTuple } from "@polkadot/types/types"; +import { CollateralAPI } from "."; +import { DefaultCollateralAPI } from "./collateral"; export type RequestResult = { id: Hash; redeemRequest: RedeemRequestExt }; @@ -118,16 +123,33 @@ export interface RedeemAPI { * and required completion time by a user. */ getRedeemPeriod(): Promise; + /** + * Send a transaction to set the DOT/BTC exchange rate + * @param amount The amount of PolkaBTC to burn, denominated as PolkaBTC + */ + burn(amount: Big): Promise; + /** + * @returns The maximum amount of tokens that can be burned through a liquidation redeem + */ + getMaxBurnableTokens(): Promise; + /** + * @returns The exchange rate (collateral currency to wrapped token currency) + * used when burning tokens. This exchange rate is at least as favourable to the + * burn requester as the regular exchange rate between the two assets. + */ + getBurnExchangeRate(): Promise; } export class DefaultRedeemAPI { private vaultsAPI: VaultsAPI; + private collateralAPI: CollateralAPI; requestHash: Hash = this.api.createType("Hash"); events: EventRecord[] = []; transaction: Transaction; constructor(private api: ApiPromise, private btcNetwork: Network, private account?: AddressOrPair) { - this.vaultsAPI = new DefaultVaultsAPI(api, btcNetwork); + this.vaultsAPI = new DefaultVaultsAPI(api, btcNetwork, account); + this.collateralAPI = new DefaultCollateralAPI(api, account); this.transaction = new Transaction(api); } @@ -187,6 +209,32 @@ export class DefaultRedeemAPI { await this.transaction.sendLogged(cancelRedeemTx, this.account, this.api.events.redeem.CancelRedeem); } + async burn(amount: Big): Promise { + if (!this.account) { + return Promise.reject(ACCOUNT_NOT_SET_ERROR_MESSAGE); + } + const amountSat = this.api.createType("Balance", btcToSat(amount.toString())); + const cancelRedeemTx = this.api.tx.redeem.liquidationRedeem(amountSat); + await this.transaction.sendLogged(cancelRedeemTx, this.account, this.api.events.redeem.LiquidationRedeem); + } + + async getMaxBurnableTokens(): Promise { + const liquidationVault = await this.vaultsAPI.getLiquidationVault(); + return new Big(satToBTC(liquidationVault.issued_tokens.toString())); + } + + async getBurnExchangeRate(): Promise { + const liquidationVault = await this.vaultsAPI.getLiquidationVault(); + const wrappedSatoshi = liquidationVault.issued_tokens.add(liquidationVault.to_be_issued_tokens); + if(wrappedSatoshi.isZero()) { + return Promise.reject("There are no burnable tokens. The burn exchange rate is undefined"); + } + const wrappedBtc = new Big(satToBTC(wrappedSatoshi.toString())); + const collateralPlanck = await this.collateralAPI.balanceLockedDOT(liquidationVault.id); + const collateralDot = new Big(planckToDOT(collateralPlanck.toString())); + return collateralDot.div(wrappedBtc); + } + async list(): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const redeemRequests = await this.api.query.redeem.redeemRequests.entriesAt(head); diff --git a/src/parachain/vaults.ts b/src/parachain/vaults.ts index b14fa9385..ac5c7a2ba 100644 --- a/src/parachain/vaults.ts +++ b/src/parachain/vaults.ts @@ -1,4 +1,4 @@ -import { PolkaBTC, Vault, IssueRequest, RedeemRequest, ReplaceRequest, DOT, Wallet } from "../interfaces/default"; +import { PolkaBTC, Vault, IssueRequest, RedeemRequest, ReplaceRequest, DOT, Wallet, SystemVault } from "../interfaces/default"; import { ApiPromise } from "@polkadot/api"; import { AccountId, H256, Balance } from "@polkadot/types/interfaces"; import { @@ -253,6 +253,14 @@ export interface VaultsAPI { * @param amountAsPlanck Value to increase stake by */ lockAdditionalCollateral(amountAsPlanck: DOT): Promise; + /** + * @returns The account id of the liquidation vault + */ + getLiquidationVaultId(): Promise; + /** + * @returns A vault object representing the liquidation vault + */ + getLiquidationVault(): Promise; } export class DefaultVaultsAPI { @@ -362,6 +370,18 @@ export class DefaultVaultsAPI { return encodeVault(vault, this.btcNetwork); } + async getLiquidationVaultId(): Promise { + const head = await this.api.rpc.chain.getFinalizedHead(); + const liquidationVaultId = await this.api.query.vaultRegistry.liquidationVaultAccountId.at(head); + return liquidationVaultId.toString(); + } + + async getLiquidationVault(): Promise { + const head = await this.api.rpc.chain.getFinalizedHead(); + const liquidationVault = await this.api.query.vaultRegistry.liquidationVault.at(head); + return liquidationVault; + } + private isNoTokensIssuedError(e: Error): boolean { return e.message.includes("NoTokensIssued"); } diff --git a/src/utils/transaction.ts b/src/utils/transaction.ts index bee05f97c..8dd9aa3fe 100644 --- a/src/utils/transaction.ts +++ b/src/utils/transaction.ts @@ -59,7 +59,7 @@ export class Transaction implements TransactionAPI { unsubscribe(result); this.printEvents(result.events); - if (successEventType && !this.isSuccessful(result.events, successEventType)) { + if (successEventType && !this.doesArrayContainEvent(result.events, successEventType)) { Promise.reject("Transaction failed"); } return result; @@ -69,7 +69,7 @@ export class Transaction implements TransactionAPI { let foundErrorEvent = false; let errorMessage = ""; events - .flatMap(({ event }) => event.data) + .map(({ event }) => event.data) .forEach((eventData) => { if (this.isDispatchError(eventData)) { try { @@ -95,12 +95,27 @@ export class Transaction implements TransactionAPI { throw new Error(errorMessage); } } + + async waitForEvent(event: AugmentedEvent): Promise { + // Use this function with a timeout. + // Unless the awaited event occurs, this Promise will never resolve. + + await new Promise((resolve, _reject) => { + this.api.query.system.events((eventsVec) => { + const events = eventsVec.toArray(); + if(this.doesArrayContainEvent(events, event)) { + resolve(); + } + }); + }); + return true; + } isDispatchError(eventData: unknown): eventData is DispatchError { return (eventData as DispatchError).isModule !== undefined; } - isSuccessful( + doesArrayContainEvent( events: EventRecord[], eventType: AugmentedEvent ): boolean { diff --git a/test/integration/parachain/issue.test.ts b/test/integration/parachain/issue.test.ts index eddde9517..29c79b890 100644 --- a/test/integration/parachain/issue.test.ts +++ b/test/integration/parachain/issue.test.ts @@ -17,8 +17,6 @@ import { Buffer } from "buffer"; import sinon from "sinon"; import { DefaultCollateralAPI } from "../../../src/parachain/collateral"; import Big from "big.js"; -import { Transaction } from "../../../src/utils"; -import { ISubmittableResult } from "@polkadot/types/types"; function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/test/integration/parachain/redeem.test.ts b/test/integration/parachain/redeem.test.ts index 16df48b69..923927705 100644 --- a/test/integration/parachain/redeem.test.ts +++ b/test/integration/parachain/redeem.test.ts @@ -7,11 +7,14 @@ import { Vault } from "../../../src/interfaces/default"; import { assert } from "../../chai"; import { defaultParachainEndpoint } from "../../config"; import { DefaultIssueAPI } from "../../../src/parachain/issue"; -import { btcToSat, stripHexPrefix, satToBTC } from "../../../src/utils"; +import { btcToSat, stripHexPrefix, satToBTC, Transaction } from "../../../src/utils"; import * as bitcoin from "bitcoinjs-lib"; import { DefaultTreasuryAPI } from "../../../src/parachain/treasury"; import { BitcoinCoreClient } from "../../utils/bitcoin-core-client"; import BN from "bn.js"; +import Big from "big.js"; +import { issue } from "./issue.test"; +import { DefaultBTCCoreAPI } from "../../../src/external/btc-core"; export type RequestResult = { hash: Hash; vault: Vault }; @@ -19,17 +22,23 @@ describe("redeem", () => { let redeemAPI: DefaultRedeemAPI; let issueAPI: DefaultIssueAPI; let treasuryAPI: DefaultTreasuryAPI; + let btcCoreAPI: DefaultBTCCoreAPI; + let transaction: Transaction; let api: ApiPromise; let keyring: Keyring; // alice is the root account let alice: KeyringPair; let charlie: KeyringPair; + let ferdie: KeyringPair; const randomDecodedAccountId = "0xD5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5"; before(async () => { api = await createPolkadotAPI(defaultParachainEndpoint); keyring = new Keyring({ type: "sr25519" }); alice = keyring.addFromUri("//Alice"); + ferdie = keyring.addFromUri("//Ferdie"); + transaction = new Transaction(api); + btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); }); beforeEach(() => { @@ -153,4 +162,34 @@ describe("redeem", () => { assert.equal(premiumRedeemFee, "0.05"); }); }); + + describe("liquidation redeem", () => { + it("should liquidate a vault that committed theft", async () => { + const vaultToLiquidate = "Ferdie"; + const aliceBitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); + await issue(api, btcCoreAPI, aliceBitcoinCoreClient, keyring, "0.0001", "Alice", vaultToLiquidate, true, false); + + const vaultBitcoinCoreClient = new BitcoinCoreClient( + "regtest", + "0.0.0.0", + "rpcuser", + "rpcpassword", + "18443", + vaultToLiquidate + ); + + // Steal some bitcoin (spend from the vault's account) + const foreignBitcoinAddress = "bcrt1qefxeckts7tkgz7uach9dnwer4qz5nyehl4sjcc"; + const amount = new Big("0.00001"); + await vaultBitcoinCoreClient.sendToAddress(foreignBitcoinAddress, amount); + await vaultBitcoinCoreClient.mineBlocks(3); + await transaction.waitForEvent(api.events.stakedRelayers.VaultTheft); + + // Burn PolkaBTC for a premium, to restore peg + redeemAPI.setAccount(ferdie); + await redeemAPI.burn(amount); + + // it takes about 15 mins for the theft to be reported + }).timeout(15 * 60000); + }); }); diff --git a/test/mock/parachain/redeem.ts b/test/mock/parachain/redeem.ts index 0ffc6cb46..7cb868928 100644 --- a/test/mock/parachain/redeem.ts +++ b/test/mock/parachain/redeem.ts @@ -8,6 +8,15 @@ import Big from "big.js"; import { RedeemAPI, RedeemRequestExt, RequestResult } from "../../../src/parachain/redeem"; export class MockRedeemAPI implements RedeemAPI { + burn(amount: Big): Promise { + throw new Error("Method not implemented."); + } + getMaxBurnableTokens(): Promise { + throw new Error("Method not implemented."); + } + getBurnExchangeRate(): Promise { + throw new Error("Method not implemented."); + } execute(_redeemId: H256, _txId: H256Le, _merkleProof: Bytes, _rawTx: Bytes): Promise { throw new Error("Method not implemented."); } diff --git a/test/mock/parachain/vaults.ts b/test/mock/parachain/vaults.ts index f6d86fe12..371c3d071 100644 --- a/test/mock/parachain/vaults.ts +++ b/test/mock/parachain/vaults.ts @@ -1,4 +1,4 @@ -import { PolkaBTC, Vault, DOT } from "../../../src/interfaces/default"; +import { PolkaBTC, Vault, DOT, SystemVault } from "../../../src/interfaces/default"; import { AccountId, H256 } from "@polkadot/types/interfaces"; import { GenericAccountId } from "@polkadot/types/generic"; import { TypeRegistry } from "@polkadot/types"; @@ -13,6 +13,12 @@ import Big from "big.js"; import { AddressOrPair } from "@polkadot/api/types"; export class MockVaultsAPI implements VaultsAPI { + getLiquidationVaultId(): Promise { + throw new Error("Method not implemented."); + } + getLiquidationVault(): Promise { + throw new Error("Method not implemented."); + } setAccount(_account: AddressOrPair): void { return; } diff --git a/test/utils/bitcoin-core-client.ts b/test/utils/bitcoin-core-client.ts index 2559884d3..f4df99765 100644 --- a/test/utils/bitcoin-core-client.ts +++ b/test/utils/bitcoin-core-client.ts @@ -1,4 +1,7 @@ // disabling linting as `bitcoin-core` has no types, causing the import to fail + +import Big from "big.js"; + // eslint-disable-next-line const Client = require("bitcoin-core"); @@ -87,9 +90,13 @@ export class BitcoinCoreClient { await delay(n * relayPeriodWithBuffer); } - async getBalance() { + async getBalance(): Promise { return await this.client.command("getbalance"); } + + async sendToAddress(address: string, amount: Big): Promise { + return await this.client.command("sendtoaddress", address, amount.toString()); + } } function delay(ms: number) { From cc88294770288de5c6c15bc61d3f27be45ad4490 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Thu, 1 Apr 2021 11:13:09 +0100 Subject: [PATCH 2/9] fix(redeem): Review comments, failing tests --- docker-compose.yml | 2 +- package.json | 2 +- src/parachain/collateral.ts | 28 +++++++++++------------ src/parachain/redeem.ts | 11 ++++----- src/parachain/staked-relayer.ts | 4 ++-- src/parachain/vaults.ts | 4 ++-- test/config.ts | 2 +- test/integration/parachain/issue.test.ts | 4 ++-- test/integration/parachain/redeem.test.ts | 2 +- test/integration/parachain/vaults.test.ts | 6 +++-- test/mock/parachain/collateral.ts | 8 +++---- 11 files changed, 37 insertions(+), 36 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 08edd5f6a..bf8fb1ec1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -144,7 +144,7 @@ services: # sleep for 30s to wait for bitcoin to create the Ferdie wallet # and also to ensure that the issue period and redeem period are set sleep 30 - vault --keyring=ferdie --auto-register-with-collateral 1000000000000 --polka-btc-url 'ws://polkabtc:9944' + vault --keyring=bob --auto-register-with-collateral 1000000000000 --polka-btc-url 'ws://polkabtc:9944' environment: <<: *client-env testdata_gen: diff --git a/package.json b/package.json index 210955765..7ba625632 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@interlay/polkabtc", - "version": "0.11.3", + "version": "0.12.0", "description": "JavaScript library to interact with PolkaBTC", "main": "build/index.js", "typings": "build/index.d.ts", diff --git a/src/parachain/collateral.ts b/src/parachain/collateral.ts index 929f2d359..b0b9dcc1e 100644 --- a/src/parachain/collateral.ts +++ b/src/parachain/collateral.ts @@ -13,25 +13,25 @@ export interface CollateralAPI { */ setAccount(account: AddressOrPair): void; /** - * @returns Total locked DOT collateral + * @returns Total locked collateral */ - totalLockedDOT(): Promise; + totalLocked(): Promise; /** * @param id The ID of an account - * @returns The reserved DOT balance of the given account + * @returns The reserved balance of the given account */ - balanceLockedDOT(id: AccountId): Promise; + balanceLocked(id: AccountId): Promise; /** * @param id The ID of an account - * @returns The free DOT balance of the given account + * @returns The free balance of the given account */ - balanceDOT(id: AccountId): Promise; + balance(id: AccountId): Promise; /** - * Send a transaction that transfers DOT from the caller's address to another address - * @param address The recipient of the DOT transfer - * @param amount The DOT balance to transfer + * Send a transaction that transfers from the caller's address to another address + * @param address The recipient of the transfer + * @param amount The balance to transfer */ - transferDOT(address: string, amount: string | number): Promise; + transfer(address: string, amount: string | number): Promise; } export class DefaultCollateralAPI implements CollateralAPI { @@ -41,24 +41,24 @@ export class DefaultCollateralAPI implements CollateralAPI { this.transaction = new Transaction(api); } - async totalLockedDOT(): Promise { + async totalLocked(): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); return this.api.query.collateral.totalCollateral.at(head); } - async balanceLockedDOT(id: AccountId): Promise { + async balanceLocked(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); return account.reserved; } - async balanceDOT(id: AccountId): Promise { + async balance(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); return account.free; } - async transferDOT(address: string, amount: string | number): Promise { + async transfer(address: string, amount: string | number): Promise { if (!this.account) { return Promise.reject(ACCOUNT_NOT_SET_ERROR_MESSAGE); } diff --git a/src/parachain/redeem.ts b/src/parachain/redeem.ts index 984130a04..31aeb0d2c 100644 --- a/src/parachain/redeem.ts +++ b/src/parachain/redeem.ts @@ -124,7 +124,7 @@ export interface RedeemAPI { */ getRedeemPeriod(): Promise; /** - * Send a transaction to set the DOT/BTC exchange rate + * Burn wrapped tokens for a premium * @param amount The amount of PolkaBTC to burn, denominated as PolkaBTC */ burn(amount: Big): Promise; @@ -134,8 +134,7 @@ export interface RedeemAPI { getMaxBurnableTokens(): Promise; /** * @returns The exchange rate (collateral currency to wrapped token currency) - * used when burning tokens. This exchange rate is at least as favourable to the - * burn requester as the regular exchange rate between the two assets. + * used when burning tokens */ getBurnExchangeRate(): Promise; } @@ -214,8 +213,8 @@ export class DefaultRedeemAPI { return Promise.reject(ACCOUNT_NOT_SET_ERROR_MESSAGE); } const amountSat = this.api.createType("Balance", btcToSat(amount.toString())); - const cancelRedeemTx = this.api.tx.redeem.liquidationRedeem(amountSat); - await this.transaction.sendLogged(cancelRedeemTx, this.account, this.api.events.redeem.LiquidationRedeem); + const burnRedeemTx = this.api.tx.redeem.liquidationRedeem(amountSat); + await this.transaction.sendLogged(burnRedeemTx, this.account, this.api.events.redeem.LiquidationRedeem); } async getMaxBurnableTokens(): Promise { @@ -230,7 +229,7 @@ export class DefaultRedeemAPI { return Promise.reject("There are no burnable tokens. The burn exchange rate is undefined"); } const wrappedBtc = new Big(satToBTC(wrappedSatoshi.toString())); - const collateralPlanck = await this.collateralAPI.balanceLockedDOT(liquidationVault.id); + const collateralPlanck = await this.collateralAPI.balanceLocked(liquidationVault.id); const collateralDot = new Big(planckToDOT(collateralPlanck.toString())); return collateralDot.div(wrappedBtc); } diff --git a/src/parachain/staked-relayer.ts b/src/parachain/staked-relayer.ts index a013bc30c..619a86a0a 100644 --- a/src/parachain/staked-relayer.ts +++ b/src/parachain/staked-relayer.ts @@ -306,7 +306,7 @@ export class DefaultStakedRelayerAPI implements StakedRelayerAPI { const vaults = await this.vaultsAPI.list(); const collateralizationRates = await Promise.all( - vaults.map>(async (vault) => [ + vaults.filter(vault => vault.status.isActive).map>(async (vault) => [ vault.id, await this.vaultsAPI.getVaultCollateralization(vault.id), ]) @@ -387,7 +387,7 @@ export class DefaultStakedRelayerAPI implements StakedRelayerAPI { await this.getFeesPolkaBTC(stakedRelayerId), await this.getFeesDOT(stakedRelayerId), await this.oracleAPI.getExchangeRate(), - await (await this.collateralAPI.balanceLockedDOT(stakedRelayerId)).toString(), + await (await this.collateralAPI.balanceLocked(stakedRelayerId)).toString(), ]); return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT, dotToBtcRate); } diff --git a/src/parachain/vaults.ts b/src/parachain/vaults.ts index ac5c7a2ba..681930829 100644 --- a/src/parachain/vaults.ts +++ b/src/parachain/vaults.ts @@ -464,7 +464,7 @@ export class DefaultVaultsAPI { } async getPolkaBTCCapacity(): Promise { - const totalLockedDotAsPlanck = await this.collateralAPI.totalLockedDOT(); + const totalLockedDotAsPlanck = await this.collateralAPI.totalLocked(); const totalLockedDot = new Big(planckToDOT(totalLockedDotAsPlanck.toString())); const oracle = new DefaultOracleAPI(this.api); const exchangeRate = await oracle.getExchangeRate(); @@ -565,7 +565,7 @@ export class DefaultVaultsAPI { await this.getFeesPolkaBTC(vaultId), await this.getFeesDOT(vaultId), await this.oracleAPI.getExchangeRate(), - await (await this.collateralAPI.balanceLockedDOT(vaultId)).toString(), + await (await this.collateralAPI.balanceLocked(vaultId)).toString(), ]); return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT, dotToBtcRate); } diff --git a/test/config.ts b/test/config.ts index 70779ac3d..d263a09c3 100644 --- a/test/config.ts +++ b/test/config.ts @@ -1,2 +1,2 @@ export const defaultParachainEndpoint = "ws://0.0.0.0:9944"; -export const defaultFaucetEndpoint = "http://0.0.0.0:3035"; +export const defaultFaucetEndpoint = "http://0.0.0.0:3036"; diff --git a/test/integration/parachain/issue.test.ts b/test/integration/parachain/issue.test.ts index 29c79b890..6ab67154d 100644 --- a/test/integration/parachain/issue.test.ts +++ b/test/integration/parachain/issue.test.ts @@ -321,7 +321,7 @@ export async function issue( const requester = keyring.addFromUri("//" + requesterName); issueAPI.setAccount(requester); const requesterAccountId = api.createType("AccountId", requester.address); - const initialBalanceDOT = await collateralAPI.balanceDOT(requesterAccountId); + const initialBalanceDOT = await collateralAPI.balance(requesterAccountId); const initialBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); const blocksToMine = 3; keyring = new Keyring({ type: "sr25519" }); @@ -380,7 +380,7 @@ export async function issue( // check issuing worked const finalBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); - const finalBalanceDOT = await collateralAPI.balanceDOT(requesterAccountId); + const finalBalanceDOT = await collateralAPI.balance(requesterAccountId); return { request: requestResult, diff --git a/test/integration/parachain/redeem.test.ts b/test/integration/parachain/redeem.test.ts index 923927705..1a026cdf9 100644 --- a/test/integration/parachain/redeem.test.ts +++ b/test/integration/parachain/redeem.test.ts @@ -165,7 +165,7 @@ describe("redeem", () => { describe("liquidation redeem", () => { it("should liquidate a vault that committed theft", async () => { - const vaultToLiquidate = "Ferdie"; + const vaultToLiquidate = "Bob"; const aliceBitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); await issue(api, btcCoreAPI, aliceBitcoinCoreClient, keyring, "0.0001", "Alice", vaultToLiquidate, true, false); diff --git a/test/integration/parachain/vaults.test.ts b/test/integration/parachain/vaults.test.ts index 07cc3016f..53e9efd5e 100644 --- a/test/integration/parachain/vaults.test.ts +++ b/test/integration/parachain/vaults.test.ts @@ -47,7 +47,8 @@ describe("vaultsAPI", () => { assert.isTrue( randomVault.toHuman() === dave.address || randomVault.toHuman() === charlie.address || - randomVault.toHuman() === eve.address + randomVault.toHuman() === eve.address || + randomVault.toHuman() === bob.address ); }); @@ -62,7 +63,8 @@ describe("vaultsAPI", () => { assert.isTrue( randomVault.toHuman() === dave.address || randomVault.toHuman() === charlie.address || - randomVault.toHuman() === eve.address + randomVault.toHuman() === eve.address || + randomVault.toHuman() === bob.address ); }); diff --git a/test/mock/parachain/collateral.ts b/test/mock/parachain/collateral.ts index 6f2c729f9..82302ba94 100644 --- a/test/mock/parachain/collateral.ts +++ b/test/mock/parachain/collateral.ts @@ -5,22 +5,22 @@ import { TypeRegistry } from "@polkadot/types"; import { AddressOrPair } from "@polkadot/api/submittable/types"; export class MockCollateralAPI implements CollateralAPI { - async totalLockedDOT(): Promise { + async totalLocked(): Promise { const registry = new TypeRegistry(); return new u128(registry, 128); } - async balanceLockedDOT(_id: AccountId): Promise { + async balanceLocked(_id: AccountId): Promise { const registry = new TypeRegistry(); return new u128(registry, 64); } - async balanceDOT(_id: AccountId): Promise { + async balance(_id: AccountId): Promise { const registry = new TypeRegistry(); return new u128(registry, 32); } - async transferDOT(_address: string, _amount: string | number): Promise { + async transfer(_address: string, _amount: string | number): Promise { return; } From 868e7badc0499060754dd4f35ca4b76a6252a1d4 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 13:06:33 +0100 Subject: [PATCH 3/9] feat: Separate integration tests into staging and release --- .../workflows/{test.yml => test-release.yml} | 7 +- .github/workflows/test-staging.yml | 20 + package.json | 6 +- .../clients/{ => staging}/faucet.test.ts | 8 +- .../external/{ => staging}/btc-core.test.ts | 8 +- test/integration/parachain/issue.test.ts | 392 ------------------ .../parachain/release/issue.test.ts | 56 +++ .../parachain/release/redeem.test.ts | 71 ++++ .../parachain/{ => staging}/btc-relay.test.ts | 10 +- .../parachain/{ => staging}/constants.test.ts | 6 +- .../{ => parachain/staging}/factory.test.ts | 4 +- .../parachain/staging/issue.test.ts | 211 ++++++++++ .../parachain/{ => staging}/oracle.test.ts | 8 +- .../parachain/{ => staging}/redeem.test.ts | 56 +-- .../parachain/{ => staging}/refund.test.ts | 18 +- .../{ => staging}/staked-relayer.test.ts | 10 +- .../parachain/{ => staging}/vaults.test.ts | 16 +- test/utils/issue.ts | 110 +++++ 18 files changed, 529 insertions(+), 488 deletions(-) rename .github/workflows/{test.yml => test-release.yml} (83%) create mode 100644 .github/workflows/test-staging.yml rename test/integration/clients/{ => staging}/faucet.test.ts (90%) rename test/integration/external/{ => staging}/btc-core.test.ts (96%) delete mode 100644 test/integration/parachain/issue.test.ts create mode 100644 test/integration/parachain/release/issue.test.ts create mode 100644 test/integration/parachain/release/redeem.test.ts rename test/integration/parachain/{ => staging}/btc-relay.test.ts (75%) rename test/integration/parachain/{ => staging}/constants.test.ts (93%) rename test/integration/{ => parachain/staging}/factory.test.ts (69%) create mode 100644 test/integration/parachain/staging/issue.test.ts rename test/integration/parachain/{ => staging}/oracle.test.ts (90%) rename test/integration/parachain/{ => staging}/redeem.test.ts (72%) rename test/integration/parachain/{ => staging}/refund.test.ts (80%) rename test/integration/parachain/{ => staging}/staked-relayer.test.ts (95%) rename test/integration/parachain/{ => staging}/vaults.test.ts (92%) create mode 100644 test/utils/issue.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test-release.yml similarity index 83% rename from .github/workflows/test.yml rename to .github/workflows/test-release.yml index 204f49f7a..6b3d24767 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test-release.yml @@ -1,6 +1,9 @@ name: test -on: push +on: + push: + branches: + - master jobs: build: @@ -14,4 +17,4 @@ jobs: - name: Run and set up the parachain, oracle, staked relayer and vault run: docker-compose up -d - run: yarn install - - run: yarn ci:test + - run: yarn ci:test:release diff --git a/.github/workflows/test-staging.yml b/.github/workflows/test-staging.yml new file mode 100644 index 000000000..bc9097d83 --- /dev/null +++ b/.github/workflows/test-staging.yml @@ -0,0 +1,20 @@ +name: test + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: setup node + uses: actions/setup-node@v1 + with: + node-version: "14.x" + - name: Run and set up the parachain, oracle, staked relayer and vault + run: docker-compose up -d + - run: yarn install + - run: yarn ci:test:staging diff --git a/package.json b/package.json index 7ba625632..b4ee11bab 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "fix": "run-s fix:*", "fix:prettier": "prettier \"src/**/*.ts\" --write", "fix:lint": "eslint --fix . --ext .ts", - "ci:test": "run-s build test:lint test:unit test:integration", + "ci:test:staging": "run-s build test:lint test:unit test:integration:staging", + "ci:test:release": "run-s build test:integration:release", "ci:test-with-coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" yarn ci:test", "docs": "./generate_docs", "generate:defs": "ts-node --skip-project node_modules/.bin/polkadot-types-from-defs --package @interlay/polkabtc/interfaces --input ./src/interfaces", @@ -25,7 +26,8 @@ "test": "run-s build test:*", "test:lint": "eslint src --ext .ts", "test:unit": "mocha test/unit/*.test.ts test/unit/**/*.test.ts", - "test:integration": "mocha test/integration/**/*.test.ts --timeout 10000000", + "test:integration:staging": "mocha test/integration/**/staging/*.test.ts --timeout 10000000", + "test:integration:release": "mocha test/integration/**/release/*.test.ts --timeout 10000000", "watch:build": "tsc -p tsconfig.json -w", "watch:test": "mocha --watch test/**/*.test.ts" }, diff --git a/test/integration/clients/faucet.test.ts b/test/integration/clients/staging/faucet.test.ts similarity index 90% rename from test/integration/clients/faucet.test.ts rename to test/integration/clients/staging/faucet.test.ts index efa30c1bb..6134a4f4e 100644 --- a/test/integration/clients/faucet.test.ts +++ b/test/integration/clients/staging/faucet.test.ts @@ -1,10 +1,10 @@ import { ApiPromise, Keyring } from "@polkadot/api"; -import { FaucetClient } from "../../../src/clients"; -import { createPolkadotAPI } from "../../../src/factory"; -import { defaultParachainEndpoint, defaultFaucetEndpoint } from "../../config"; +import { FaucetClient } from "../../../../src/clients"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint, defaultFaucetEndpoint } from "../../../config"; import { KeyringPair } from "@polkadot/keyring/types"; import { AccountData } from "@polkadot/types/interfaces/balances"; -import { assert } from "../../chai"; +import { assert } from "../../../chai"; import Big from "big.js"; describe("Faucet", function () { diff --git a/test/integration/external/btc-core.test.ts b/test/integration/external/staging/btc-core.test.ts similarity index 96% rename from test/integration/external/btc-core.test.ts rename to test/integration/external/staging/btc-core.test.ts index 2a8245271..40ed4b6da 100644 --- a/test/integration/external/btc-core.test.ts +++ b/test/integration/external/staging/btc-core.test.ts @@ -1,9 +1,9 @@ import { ApiPromise } from "@polkadot/api"; import { assert } from "chai"; -import { BTCCoreAPI, DefaultBTCCoreAPI } from "../../../src/external/btc-core"; -import { createPolkadotAPI } from "../../../src/factory"; -import { defaultParachainEndpoint } from "../../config"; -import { BitcoinCoreClient } from "../../utils/bitcoin-core-client"; +import { BTCCoreAPI, DefaultBTCCoreAPI } from "../../../../src/external/btc-core"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint } from "../../../config"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; describe("BTCCore testnet", function () { this.timeout(10000); // API can be slightly slow diff --git a/test/integration/parachain/issue.test.ts b/test/integration/parachain/issue.test.ts deleted file mode 100644 index 6ab67154d..000000000 --- a/test/integration/parachain/issue.test.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { H256 } from "@polkadot/types/interfaces"; -import { Bytes } from "@polkadot/types/primitive"; -import { DefaultBTCCoreAPI } from "../../../src/external/btc-core"; -import { DefaultIssueAPI, IssueRequestExt, IssueRequestResult } from "../../../src/parachain/issue"; -import { createPolkadotAPI } from "../../../src/factory"; -import { H256Le, PolkaBTC } from "../../../src/interfaces/default"; -import { btcToSat, dotToPlanck, satToBTC } from "../../../src/utils"; -import { assert, expect } from "../../chai"; -import { defaultParachainEndpoint } from "../../config"; -import * as bitcoin from "bitcoinjs-lib"; -import { DefaultTreasuryAPI } from "../../../src/parachain/treasury"; -import { fail } from "assert"; -import { BitcoinCoreClient } from "../../utils/bitcoin-core-client"; -import { Buffer } from "buffer"; -import sinon from "sinon"; -import { DefaultCollateralAPI } from "../../../src/parachain/collateral"; -import Big from "big.js"; - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -describe("issue", () => { - let api: ApiPromise; - let issueAPI: DefaultIssueAPI; - let btcCoreAPI: DefaultBTCCoreAPI; - let bitcoinCoreClient: BitcoinCoreClient; - let keyring: Keyring; - let sandbox: sinon.SinonSandbox; - - // alice is the root account - let alice: KeyringPair; - let bob: KeyringPair; - - before(async function () { - api = await createPolkadotAPI(defaultParachainEndpoint); - keyring = new Keyring({ type: "sr25519" }); - // Alice is also the root account - alice = keyring.addFromUri("//Alice"); - bob = keyring.addFromUri("//Bob"); - - btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); - bitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); - - issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); - sandbox = sinon.createSandbox(); - }); - - after(async () => { - api.disconnect(); - }); - - describe("load requests", () => { - it("should load existing requests", async () => { - keyring = new Keyring({ type: "sr25519" }); - alice = keyring.addFromUri("//Alice"); - issueAPI.setAccount(alice); - - const issueRequests = await issueAPI.list(); - assert.isAtLeast( - issueRequests.length, - 1, - "Error in docker-compose setup. Should have at least 1 issue request" - ); - }); - }); - - describe("request", () => { - it("should fail if no account is set", async () => { - const tmpIssueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); - const amount = api.createType("Balance", 10); - await assert.isRejected(tmpIssueAPI.request(amount)); - }); - - it("should request issue", async () => { - keyring = new Keyring({ type: "sr25519" }); - alice = keyring.addFromUri("//Alice"); - issueAPI.setAccount(alice); - const amount = api.createType("Balance", 100000) as PolkaBTC; - const requestResult = await issueAPI.request(amount); - assert.equal(requestResult.id.length, 32); - - const issueRequest = await issueAPI.getRequestById(requestResult.id); - assert.deepEqual(issueRequest.amount, amount, "Amount different than expected"); - }); - - it("should getGriefingCollateral (rounded)", async () => { - const amountBtc = "0.001"; - const amountAsSatoshiString = btcToSat(amountBtc) as string; - const amountAsSat = api.createType("Balance", amountAsSatoshiString) as PolkaBTC; - const griefingCollateralPlanck = await issueAPI.getGriefingCollateralInPlanck(amountAsSat); - assert.equal(griefingCollateralPlanck.toString(), "1927616"); - }); - }); - - describe("execute", () => { - it("should fail if no account is set", async () => { - const tmpIssueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); - const issueId: H256 = {}; - const txId: H256Le = {}; - const merkleProof: Bytes = {}; - const rawTx: Bytes = {}; - await assert.isRejected(tmpIssueAPI.execute(issueId, txId, merkleProof, rawTx)); - }); - - it("should request and auto-execute issue", async () => { - const amount = "0.001"; - const issueResult = await issue( - api, - btcCoreAPI, - bitcoinCoreClient, - keyring, - amount, - "Alice", - "Charlie", - true, - false - ); - - assert.equal( - satToBTC( - issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() - ), - amount, - "Final balance was not increased by the exact amount specified" - ); - - assert.isTrue( - issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), - "Issue-Redeem were more expensive than 1 DOT" - ); - }).timeout(500000); - - it("should fail to request a value finer than 1 Satoshi", async () => { - const amount = "0.00000121"; - await assert.isRejected( - issue(api, btcCoreAPI, bitcoinCoreClient, keyring, amount, "Alice", "Charlie", true, false) - ); - }).timeout(500000); - - it("should request and auto-execute issue", async () => { - const amount = "0.0000121"; - const issueResult = await issue( - api, - btcCoreAPI, - bitcoinCoreClient, - keyring, - amount, - "Alice", - "Charlie", - true, - false - ); - assert.equal( - satToBTC( - issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() - ), - amount, - "Final balance was not increased by the exact amount specified" - ); - - assert.isTrue( - issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), - "Issue-Redeem were more expensive than 1 DOT" - ); - }).timeout(500000); - - it("should request and manually execute issue", async () => { - const amount = "0.001"; - const issueResult = await issue( - api, - btcCoreAPI, - bitcoinCoreClient, - keyring, - amount, - "Alice", - "Dave", - false, - false - ); - assert.equal( - satToBTC( - issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() - ), - amount, - "Final balance was not increased by the exact amount specified" - ); - - assert.isTrue( - issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), - "Issue-Redeem were more expensive than 1 DOT" - ); - }).timeout(500000); - }); - - describe("cancel", () => { - it("should cancel a request issue", async () => { - keyring = new Keyring({ type: "sr25519" }); - alice = keyring.addFromUri("//Alice"); - - // request issue - issueAPI.setAccount(alice); - const amountAsBtcString = "0.0000121"; - const amountAsSatoshiString = btcToSat(amountAsBtcString); - const amountAsSatoshi = api.createType("Balance", amountAsSatoshiString); - const requestResult = await issueAPI.request(amountAsSatoshi); - - // The cancellation period set by docker-compose is 50 blocks, each being relayed every 6s - await bitcoinCoreClient.mineBlocks(50); - await issueAPI.cancel(requestResult.id); - - const issueRequest = await issueAPI.getRequestById(requestResult.id); - - assert.isTrue(issueRequest.status.isCancelled, "Failed to cancel issue request"); - }); - }).timeout(700000); - - describe("fees", () => { - it("should getFeesToPay", async () => { - const amount = "2"; - const feesToPay = await issueAPI.getFeesToPay(amount); - assert.equal(feesToPay, "0.01"); - }); - - it("should getFeeRate", async () => { - const feePercentage = await issueAPI.getFeeRate(); - assert.equal(feePercentage.toString(), "0.005"); - }); - }); - - describe("check getIssuePeriod method ", () => { - it("should getIssuePeriod", async () => { - try { - issueAPI.setAccount(alice); - const period = await issueAPI.getIssuePeriod(); - expect(period.toString()).equal("50"); - } catch (error) { - console.log(error); - } - }); - }); - - type ExecutionData = { - issueId: H256; - txId: H256Le; - merkleProof: Bytes; - rawTx: Bytes; - }; - - function makeExecutionData(): ExecutionData { - const issueId = api.createType("H256", "0x81dd458cd3bb82cf68b52dce27a5ac1f616b0278b2f36c4c05bfd528c2e1e8e9"); - const txId: H256Le = api.createType( - "H256", - // prettier-ignore - [ - 70, 103, 64, 2, 223, 149, 66, 146, 36, 69, 6, 199, 80, 43, 96, - 106, 174, 205, 77, 120, 69, 217, 253, 57, 140, 255, 196, 198, 144, 61, 18, 248 - ] - ) as H256Le; - const merkleProof: Bytes = api.createType( - "Bytes", - // prettier-ignore - [ - 2, 0, 0, 0, 244, 134, 26, 210, 38, 249, 3, 175, 137, 182, 208, - 251, 9, 59, 211, 77, 200, 110, 67, 171, 216, 90, 255, 111, 72, - 97, 187, 76, 166, 94, 240, 19, 213, 54, 6, 130, 156, 254, 180, - 145, 173, 84, 28, 196, 222, 78, 232, 5, 160, 160, 3, 117, 203, - 70, 200, 166, 141, 180, 204, 179, 134, 5, 232, 26, 243, 99, 179, - 94, 0, 0, 64, 32, 1, 0, 0, 0, 2, 0, 0, 0, 2, 56, 108, 49, 183, 51, - 251, 228, 176, 233, 214, 204, 196, 69, 202, 199, 158, 219, 253, 38, - 85, 184, 163, 65, 215, 198, 31, 60, 181, 117, 8, 151, 212, 70, 103, - 64, 2, 223, 149, 66, 146, 36, 69, 6, 199, 80, 43, 96, 106, 174, 205, - 77, 120, 69, 217, 253, 57, 140, 255, 196, 198, 144, 61, 18, 248, 1, 5 - ] - ); - const rawTx: Bytes = api.createType( - "Bytes", - // prettier-ignore - [ - 2, 0, 0, 0, 1, 22, 59, 241, 41, 121, 78, 23, 16, 159, 81, - 145, 67, 102, 63, 131, 101, 232, 183, 64, 173, 236, 247, - 249, 134, 8, 242, 39, 166, 231, 131, 49, 251, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 2, 160, 134, 1, 0, 0, 0, 0, 0, 25, 118, 169, - 20, 191, 52, 8, 246, 192, 222, 192, 135, 159, 124, 29, 77, - 10, 94, 136, 19, 252, 13, 181, 105, 136, 172, 0, 0, 0, 0, - 0, 0, 0, 0, 34, 106, 32, 129, 221, 69, 140, 211, 187, 130, - 207, 104, 181, 45, 206, 39, 165, 172, 31, 97, 107, 2, 120, - 178, 243, 108, 76, 5, 191, 213, 40, 194, 225, 232, 233, - 0, 0, 0, 0 - ] - ); - return { issueId, txId, merkleProof, rawTx }; - } -}); - -export interface IssueResult { - request: IssueRequestResult; - initialDotBalance: Big; - finalDotBalance: Big; - initialPolkaBtcBalance: Big; - finalPolkaBtcBalance: Big; -} - -export async function issue( - api: ApiPromise, - btcCoreAPI: DefaultBTCCoreAPI, - bitcoinCoreClient: BitcoinCoreClient, - keyring: Keyring, - amount: string, - requesterName: string, - vaultName: string, - autoExecute: boolean, - triggerRefund: boolean -): Promise { - const treasuryAPI = new DefaultTreasuryAPI(api); - const issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); - const collateralAPI = new DefaultCollateralAPI(api); - - const requester = keyring.addFromUri("//" + requesterName); - issueAPI.setAccount(requester); - const requesterAccountId = api.createType("AccountId", requester.address); - const initialBalanceDOT = await collateralAPI.balance(requesterAccountId); - const initialBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); - const blocksToMine = 3; - keyring = new Keyring({ type: "sr25519" }); - const vault = keyring.addFromUri("//" + vaultName); - const vaultAccountId = api.createType("AccountId", vault.address); - - // request issue - let amountAsBtcString = amount; - const amountAsSatoshiString = btcToSat(amountAsBtcString); - if (amountAsSatoshiString === undefined) { - fail(); - } - const amountAsSatoshi = api.createType("Balance", amountAsSatoshiString); - const requestResult = await issueAPI.request(amountAsSatoshi, vaultAccountId); - let issueRequest; - try { - issueRequest = await issueAPI.getRequestById(requestResult.id); - } catch (e) { - // IssueCompleted errors occur when multiple vaults attempt to execute the same request - console.log(e); - } - - amountAsBtcString = satToBTC( - (issueRequest as IssueRequestExt).amount.add((issueRequest as IssueRequestExt).fee).toString() - ); - - if (triggerRefund) { - // Send 1 more Btc than needed - amountAsBtcString = new Big(amountAsBtcString).add(1).toString(); - } - - // send btc tx - const vaultBtcAddress = requestResult.issueRequest.btc_address; - if (vaultBtcAddress === undefined) { - throw new Error("Undefined vault address returned from RequestIssue"); - } - - const txData = await bitcoinCoreClient.sendBtcTxAndMine(vaultBtcAddress, amountAsBtcString, blocksToMine); - - if (autoExecute === false) { - // execute issue, assuming the selected vault has the `--no-issue-execution` flag enabled - const merkleProof = await btcCoreAPI.getMerkleProof(txData.txid); - const parsedIssuedId = api.createType("H256", requestResult.id); - // reverse endianness (expects little-endian) - const parsedTxId = api.createType("H256", "0x" + Buffer.from(txData.txid, "hex").reverse().toString("hex")); - const parsedMerkleProof = api.createType("Bytes", "0x" + merkleProof); - const parsedRawTx = api.createType("Bytes", "0x" + txData.rawTx); - await issueAPI.execute(parsedIssuedId, parsedTxId, parsedMerkleProof, parsedRawTx); - } else { - // wait for vault to execute issue - while (!(await issueAPI.getRequestById(requestResult.id)).status.isCompleted) { - await sleep(1000); - } - } - - // check issuing worked - const finalBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); - - const finalBalanceDOT = await collateralAPI.balance(requesterAccountId); - - return { - request: requestResult, - initialDotBalance: new Big(initialBalanceDOT.toString()), - finalDotBalance: new Big(finalBalanceDOT.toString()), - initialPolkaBtcBalance: new Big(initialBalancePolkaBTC.toString()), - finalPolkaBtcBalance: new Big(finalBalancePolkaBTC.toString()), - }; -} diff --git a/test/integration/parachain/release/issue.test.ts b/test/integration/parachain/release/issue.test.ts new file mode 100644 index 000000000..5aa1c5f8b --- /dev/null +++ b/test/integration/parachain/release/issue.test.ts @@ -0,0 +1,56 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { DefaultIssueAPI } from "../../../../src/parachain/issue"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { btcToSat } from "../../../../src/utils"; +import { assert } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; +import * as bitcoin from "bitcoinjs-lib"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; + +describe("issue", () => { + let api: ApiPromise; + let issueAPI: DefaultIssueAPI; + let bitcoinCoreClient: BitcoinCoreClient; + let keyring: Keyring; + + // alice is the root account + let alice: KeyringPair; + + before(async function () { + api = await createPolkadotAPI(defaultParachainEndpoint); + keyring = new Keyring({ type: "sr25519" }); + // Alice is also the root account + alice = keyring.addFromUri("//Alice"); + + bitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); + + issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); + }); + + after(async () => { + api.disconnect(); + }); + + it("should cancel a request issue", async () => { + keyring = new Keyring({ type: "sr25519" }); + alice = keyring.addFromUri("//Alice"); + + // request issue + issueAPI.setAccount(alice); + const amountAsBtcString = "0.0000121"; + const amountAsSatoshiString = btcToSat(amountAsBtcString); + const amountAsSatoshi = api.createType("Balance", amountAsSatoshiString); + const requestResult = await issueAPI.request(amountAsSatoshi); + + // The cancellation period set by docker-compose is 50 blocks, each being relayed every 6s + await bitcoinCoreClient.mineBlocks(50); + await issueAPI.cancel(requestResult.id); + + const issueRequest = await issueAPI.getRequestById(requestResult.id); + + assert.isTrue(issueRequest.status.isCancelled, "Failed to cancel issue request"); + }).timeout(700000); + +}); + diff --git a/test/integration/parachain/release/redeem.test.ts b/test/integration/parachain/release/redeem.test.ts new file mode 100644 index 000000000..82be38ad7 --- /dev/null +++ b/test/integration/parachain/release/redeem.test.ts @@ -0,0 +1,71 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Hash } from "@polkadot/types/interfaces"; +import { DefaultRedeemAPI } from "../../../../src/parachain/redeem"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { Vault } from "../../../../src/interfaces/default"; +import { defaultParachainEndpoint } from "../../../config"; +import { Transaction } from "../../../../src/utils"; +import * as bitcoin from "bitcoinjs-lib"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; +import Big from "big.js"; +import { DefaultBTCCoreAPI } from "../../../../src/external/btc-core"; +import { issue } from "../../../utils/issue"; + +export type RequestResult = { hash: Hash; vault: Vault }; + +describe("redeem", () => { + let redeemAPI: DefaultRedeemAPI; + let btcCoreAPI: DefaultBTCCoreAPI; + let transaction: Transaction; + let api: ApiPromise; + let keyring: Keyring; + // alice is the root account + let ferdie: KeyringPair; + + before(async () => { + api = await createPolkadotAPI(defaultParachainEndpoint); + keyring = new Keyring({ type: "sr25519" }); + ferdie = keyring.addFromUri("//Ferdie"); + transaction = new Transaction(api); + btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); + }); + + beforeEach(() => { + redeemAPI = new DefaultRedeemAPI(api, bitcoin.networks.regtest); + }); + + after(() => { + return api.disconnect(); + }); + + describe("liquidation redeem", () => { + it("should liquidate a vault that committed theft", async () => { + const vaultToLiquidate = "Bob"; + const aliceBitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); + await issue(api, btcCoreAPI, aliceBitcoinCoreClient, keyring, "0.0001", "Alice", vaultToLiquidate, true, false); + + const vaultBitcoinCoreClient = new BitcoinCoreClient( + "regtest", + "0.0.0.0", + "rpcuser", + "rpcpassword", + "18443", + vaultToLiquidate + ); + + // Steal some bitcoin (spend from the vault's account) + const foreignBitcoinAddress = "bcrt1qefxeckts7tkgz7uach9dnwer4qz5nyehl4sjcc"; + const amount = new Big("0.00001"); + await vaultBitcoinCoreClient.sendToAddress(foreignBitcoinAddress, amount); + await vaultBitcoinCoreClient.mineBlocks(3); + await transaction.waitForEvent(api.events.stakedRelayers.VaultTheft); + + // Burn PolkaBTC for a premium, to restore peg + redeemAPI.setAccount(ferdie); + await redeemAPI.burn(amount); + + // it takes about 15 mins for the theft to be reported + }).timeout(18 * 60000); + }); +}); diff --git a/test/integration/parachain/btc-relay.test.ts b/test/integration/parachain/staging/btc-relay.test.ts similarity index 75% rename from test/integration/parachain/btc-relay.test.ts rename to test/integration/parachain/staging/btc-relay.test.ts index f553d3ef4..d7f8b8a17 100644 --- a/test/integration/parachain/btc-relay.test.ts +++ b/test/integration/parachain/staging/btc-relay.test.ts @@ -1,10 +1,10 @@ import { ApiPromise } from "@polkadot/api"; import { assert } from "chai"; -import { BTCRelayAPI } from "../../../src/parachain"; -import { BTCCoreAPI, DefaultBTCCoreAPI } from "../../../src/external/btc-core"; -import { DefaultBTCRelayAPI } from "../../../src/parachain/btc-relay"; -import { createPolkadotAPI } from "../../../src/factory"; -import { defaultParachainEndpoint } from "../../config"; +import { BTCRelayAPI } from "../../../../src/parachain"; +import { BTCCoreAPI, DefaultBTCCoreAPI } from "../../../../src/external/btc-core"; +import { DefaultBTCRelayAPI } from "../../../../src/parachain/btc-relay"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint } from "../../../config"; describe("BTCCore", function () { this.timeout(10000); // API can be slightly slow diff --git a/test/integration/parachain/constants.test.ts b/test/integration/parachain/staging/constants.test.ts similarity index 93% rename from test/integration/parachain/constants.test.ts rename to test/integration/parachain/staging/constants.test.ts index 767d579f2..93c261673 100644 --- a/test/integration/parachain/constants.test.ts +++ b/test/integration/parachain/staging/constants.test.ts @@ -1,8 +1,8 @@ import { ApiPromise } from "@polkadot/api"; import { assert } from "chai"; -import { ConstantsAPI, DefaultConstantsAPI } from "../../../src/parachain/constants"; -import { createPolkadotAPI } from "../../../src/factory"; -import { defaultParachainEndpoint } from "../../config"; +import { ConstantsAPI, DefaultConstantsAPI } from "../../../../src/parachain/constants"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint } from "../../../config"; describe("Constants", function () { this.timeout(10000); // API can be slightly slow diff --git a/test/integration/factory.test.ts b/test/integration/parachain/staging/factory.test.ts similarity index 69% rename from test/integration/factory.test.ts rename to test/integration/parachain/staging/factory.test.ts index 66d3da16b..a7fa48362 100644 --- a/test/integration/factory.test.ts +++ b/test/integration/parachain/staging/factory.test.ts @@ -1,6 +1,6 @@ import { assert } from "chai"; -import { createPolkadotAPI } from "../../src/factory"; -import { defaultParachainEndpoint } from "../config"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint } from "../../../config"; describe("createAPI", () => { diff --git a/test/integration/parachain/staging/issue.test.ts b/test/integration/parachain/staging/issue.test.ts new file mode 100644 index 000000000..84cd06d3f --- /dev/null +++ b/test/integration/parachain/staging/issue.test.ts @@ -0,0 +1,211 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { H256 } from "@polkadot/types/interfaces"; +import { Bytes } from "@polkadot/types/primitive"; +import { DefaultBTCCoreAPI } from "../../../../src/external/btc-core"; +import { DefaultIssueAPI } from "../../../../src/parachain/issue"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { H256Le, PolkaBTC } from "../../../../src/interfaces/default"; +import { btcToSat, dotToPlanck, satToBTC } from "../../../../src/utils"; +import { assert, expect } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; +import * as bitcoin from "bitcoinjs-lib"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; +import Big from "big.js"; +import { issue } from "../../../utils/issue"; + +describe("issue", () => { + let api: ApiPromise; + let issueAPI: DefaultIssueAPI; + let btcCoreAPI: DefaultBTCCoreAPI; + let bitcoinCoreClient: BitcoinCoreClient; + let keyring: Keyring; + + // alice is the root account + let alice: KeyringPair; + + before(async function () { + api = await createPolkadotAPI(defaultParachainEndpoint); + keyring = new Keyring({ type: "sr25519" }); + // Alice is also the root account + alice = keyring.addFromUri("//Alice"); + + btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); + bitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); + + issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); + }); + + after(async () => { + api.disconnect(); + }); + + describe("load requests", () => { + it("should load existing requests", async () => { + keyring = new Keyring({ type: "sr25519" }); + alice = keyring.addFromUri("//Alice"); + issueAPI.setAccount(alice); + + const issueRequests = await issueAPI.list(); + assert.isAtLeast( + issueRequests.length, + 1, + "Error in docker-compose setup. Should have at least 1 issue request" + ); + }); + }); + + describe("request", () => { + it("should fail if no account is set", async () => { + const tmpIssueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); + const amount = api.createType("Balance", 10); + await assert.isRejected(tmpIssueAPI.request(amount)); + }); + + it("should request issue", async () => { + keyring = new Keyring({ type: "sr25519" }); + alice = keyring.addFromUri("//Alice"); + issueAPI.setAccount(alice); + const amount = api.createType("Balance", 100000) as PolkaBTC; + const requestResult = await issueAPI.request(amount); + assert.equal(requestResult.id.length, 32); + + const issueRequest = await issueAPI.getRequestById(requestResult.id); + assert.deepEqual(issueRequest.amount, amount, "Amount different than expected"); + }); + + it("should getGriefingCollateral (rounded)", async () => { + const amountBtc = "0.001"; + const amountAsSatoshiString = btcToSat(amountBtc) as string; + const amountAsSat = api.createType("Balance", amountAsSatoshiString) as PolkaBTC; + const griefingCollateralPlanck = await issueAPI.getGriefingCollateralInPlanck(amountAsSat); + assert.equal(griefingCollateralPlanck.toString(), "1927616"); + }); + }); + + describe("execute", () => { + it("should fail if no account is set", async () => { + const tmpIssueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); + const issueId: H256 = {}; + const txId: H256Le = {}; + const merkleProof: Bytes = {}; + const rawTx: Bytes = {}; + await assert.isRejected(tmpIssueAPI.execute(issueId, txId, merkleProof, rawTx)); + }); + + it("should request and auto-execute issue", async () => { + const amount = "0.001"; + const issueResult = await issue( + api, + btcCoreAPI, + bitcoinCoreClient, + keyring, + amount, + "Alice", + "Charlie", + true, + false + ); + + assert.equal( + satToBTC( + issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() + ), + amount, + "Final balance was not increased by the exact amount specified" + ); + + assert.isTrue( + issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), + "Issue-Redeem were more expensive than 1 DOT" + ); + }).timeout(500000); + + it("should fail to request a value finer than 1 Satoshi", async () => { + const amount = "0.00000121"; + await assert.isRejected( + issue(api, btcCoreAPI, bitcoinCoreClient, keyring, amount, "Alice", "Charlie", true, false) + ); + }).timeout(500000); + + it("should request and auto-execute issue", async () => { + const amount = "0.0000121"; + const issueResult = await issue( + api, + btcCoreAPI, + bitcoinCoreClient, + keyring, + amount, + "Alice", + "Charlie", + true, + false + ); + assert.equal( + satToBTC( + issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() + ), + amount, + "Final balance was not increased by the exact amount specified" + ); + + assert.isTrue( + issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), + "Issue-Redeem were more expensive than 1 DOT" + ); + }).timeout(500000); + + it("should request and manually execute issue", async () => { + const amount = "0.001"; + const issueResult = await issue( + api, + btcCoreAPI, + bitcoinCoreClient, + keyring, + amount, + "Alice", + "Dave", + false, + false + ); + assert.equal( + satToBTC( + issueResult.finalPolkaBtcBalance.sub(issueResult.initialPolkaBtcBalance.toString()).toString() + ), + amount, + "Final balance was not increased by the exact amount specified" + ); + + assert.isTrue( + issueResult.finalDotBalance.sub(issueResult.initialDotBalance).lt(new Big(dotToPlanck("1") as string)), + "Issue-Redeem were more expensive than 1 DOT" + ); + }).timeout(500000); + }); + + describe("fees", () => { + it("should getFeesToPay", async () => { + const amount = "2"; + const feesToPay = await issueAPI.getFeesToPay(amount); + assert.equal(feesToPay, "0.01"); + }); + + it("should getFeeRate", async () => { + const feePercentage = await issueAPI.getFeeRate(); + assert.equal(feePercentage.toString(), "0.005"); + }); + }); + + describe("check getIssuePeriod method ", () => { + it("should getIssuePeriod", async () => { + try { + issueAPI.setAccount(alice); + const period = await issueAPI.getIssuePeriod(); + expect(period.toString()).equal("50"); + } catch (error) { + console.log(error); + } + }); + }); + +}); diff --git a/test/integration/parachain/oracle.test.ts b/test/integration/parachain/staging/oracle.test.ts similarity index 90% rename from test/integration/parachain/oracle.test.ts rename to test/integration/parachain/staging/oracle.test.ts index c4e670bb0..aececd695 100644 --- a/test/integration/parachain/oracle.test.ts +++ b/test/integration/parachain/staging/oracle.test.ts @@ -1,9 +1,9 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; -import { DefaultOracleAPI, OracleAPI } from "../../../src/parachain/oracle"; -import { createPolkadotAPI } from "../../../src/factory"; -import { assert } from "../../chai"; -import { defaultParachainEndpoint } from "../../config"; +import { DefaultOracleAPI, OracleAPI } from "../../../../src/parachain/oracle"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { assert } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; describe("OracleAPI", () => { let api: ApiPromise; diff --git a/test/integration/parachain/redeem.test.ts b/test/integration/parachain/staging/redeem.test.ts similarity index 72% rename from test/integration/parachain/redeem.test.ts rename to test/integration/parachain/staging/redeem.test.ts index 1a026cdf9..e1bc01f51 100644 --- a/test/integration/parachain/redeem.test.ts +++ b/test/integration/parachain/staging/redeem.test.ts @@ -1,20 +1,17 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { Hash } from "@polkadot/types/interfaces"; -import { DefaultRedeemAPI } from "../../../src/parachain/redeem"; -import { createPolkadotAPI } from "../../../src/factory"; -import { Vault } from "../../../src/interfaces/default"; -import { assert } from "../../chai"; -import { defaultParachainEndpoint } from "../../config"; -import { DefaultIssueAPI } from "../../../src/parachain/issue"; -import { btcToSat, stripHexPrefix, satToBTC, Transaction } from "../../../src/utils"; +import { DefaultRedeemAPI } from "../../../../src/parachain/redeem"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { Vault } from "../../../../src/interfaces/default"; +import { assert } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; +import { DefaultIssueAPI } from "../../../../src/parachain/issue"; +import { btcToSat, stripHexPrefix, satToBTC } from "../../../../src/utils"; import * as bitcoin from "bitcoinjs-lib"; -import { DefaultTreasuryAPI } from "../../../src/parachain/treasury"; -import { BitcoinCoreClient } from "../../utils/bitcoin-core-client"; +import { DefaultTreasuryAPI } from "../../../../src/parachain/treasury"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; import BN from "bn.js"; -import Big from "big.js"; -import { issue } from "./issue.test"; -import { DefaultBTCCoreAPI } from "../../../src/external/btc-core"; export type RequestResult = { hash: Hash; vault: Vault }; @@ -22,23 +19,17 @@ describe("redeem", () => { let redeemAPI: DefaultRedeemAPI; let issueAPI: DefaultIssueAPI; let treasuryAPI: DefaultTreasuryAPI; - let btcCoreAPI: DefaultBTCCoreAPI; - let transaction: Transaction; let api: ApiPromise; let keyring: Keyring; // alice is the root account let alice: KeyringPair; let charlie: KeyringPair; - let ferdie: KeyringPair; const randomDecodedAccountId = "0xD5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5D5"; before(async () => { api = await createPolkadotAPI(defaultParachainEndpoint); keyring = new Keyring({ type: "sr25519" }); alice = keyring.addFromUri("//Alice"); - ferdie = keyring.addFromUri("//Ferdie"); - transaction = new Transaction(api); - btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); }); beforeEach(() => { @@ -163,33 +154,4 @@ describe("redeem", () => { }); }); - describe("liquidation redeem", () => { - it("should liquidate a vault that committed theft", async () => { - const vaultToLiquidate = "Bob"; - const aliceBitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); - await issue(api, btcCoreAPI, aliceBitcoinCoreClient, keyring, "0.0001", "Alice", vaultToLiquidate, true, false); - - const vaultBitcoinCoreClient = new BitcoinCoreClient( - "regtest", - "0.0.0.0", - "rpcuser", - "rpcpassword", - "18443", - vaultToLiquidate - ); - - // Steal some bitcoin (spend from the vault's account) - const foreignBitcoinAddress = "bcrt1qefxeckts7tkgz7uach9dnwer4qz5nyehl4sjcc"; - const amount = new Big("0.00001"); - await vaultBitcoinCoreClient.sendToAddress(foreignBitcoinAddress, amount); - await vaultBitcoinCoreClient.mineBlocks(3); - await transaction.waitForEvent(api.events.stakedRelayers.VaultTheft); - - // Burn PolkaBTC for a premium, to restore peg - redeemAPI.setAccount(ferdie); - await redeemAPI.burn(amount); - - // it takes about 15 mins for the theft to be reported - }).timeout(15 * 60000); - }); }); diff --git a/test/integration/parachain/refund.test.ts b/test/integration/parachain/staging/refund.test.ts similarity index 80% rename from test/integration/parachain/refund.test.ts rename to test/integration/parachain/staging/refund.test.ts index 314bb235a..45c9a2f06 100644 --- a/test/integration/parachain/refund.test.ts +++ b/test/integration/parachain/staging/refund.test.ts @@ -1,18 +1,17 @@ import { ApiPromise, Keyring } from "@polkadot/api"; -import { DefaultIssueAPI } from "../../../src/parachain/issue"; -import { DefaultBTCCoreAPI } from "../../../src/external/btc-core"; -import { issue } from "./issue.test"; -import { BitcoinCoreClient } from "../../utils/bitcoin-core-client"; -import { createPolkadotAPI } from "../../../src/factory"; -import { defaultParachainEndpoint } from "../../config"; +import { DefaultIssueAPI } from "../../../../src/parachain/issue"; +import { DefaultBTCCoreAPI } from "../../../../src/external/btc-core"; +import { BitcoinCoreClient } from "../../../utils/bitcoin-core-client"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { defaultParachainEndpoint } from "../../../config"; import * as bitcoin from "bitcoinjs-lib"; -import { DefaultRefundAPI } from "../../../src/parachain/refund"; +import { DefaultRefundAPI } from "../../../../src/parachain/refund"; import { KeyringPair } from "@polkadot/keyring/types"; -import { assert } from "../../chai"; +import { assert } from "../../../chai"; +import { issue } from "../../../utils/issue"; describe("refund", () => { let api: ApiPromise; - let issueAPI: DefaultIssueAPI; let btcCoreAPI: DefaultBTCCoreAPI; let refundAPI: DefaultRefundAPI; let bitcoinCoreClient: BitcoinCoreClient; @@ -26,7 +25,6 @@ describe("refund", () => { alice = keyring.addFromUri("//Alice"); btcCoreAPI = new DefaultBTCCoreAPI("http://0.0.0.0:3002"); bitcoinCoreClient = new BitcoinCoreClient("regtest", "0.0.0.0", "rpcuser", "rpcpassword", "18443", "Alice"); - issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); refundAPI = new DefaultRefundAPI(api, bitcoin.networks.regtest); refundAPI.setAccount(alice); }); diff --git a/test/integration/parachain/staked-relayer.test.ts b/test/integration/parachain/staging/staked-relayer.test.ts similarity index 95% rename from test/integration/parachain/staked-relayer.test.ts rename to test/integration/parachain/staging/staked-relayer.test.ts index b4a4c7a9d..a684b9cfd 100644 --- a/test/integration/parachain/staked-relayer.test.ts +++ b/test/integration/parachain/staging/staked-relayer.test.ts @@ -2,11 +2,11 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { AccountId } from "@polkadot/types/interfaces/runtime"; import BN from "bn.js"; import sinon from "sinon"; -import { DefaultStakedRelayerAPI, StakedRelayerAPI } from "../../../src/parachain/staked-relayer"; -import { createPolkadotAPI } from "../../../src/factory"; -import { StakedRelayer, DOT } from "../../../src/interfaces/default"; -import { assert } from "../../chai"; -import { defaultParachainEndpoint } from "../../config"; +import { DefaultStakedRelayerAPI, StakedRelayerAPI } from "../../../../src/parachain/staked-relayer"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { StakedRelayer, DOT } from "../../../../src/interfaces/default"; +import { assert } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; import * as bitcoin from "bitcoinjs-lib"; import { KeyringPair } from "@polkadot/keyring/types"; import Big from "big.js"; diff --git a/test/integration/parachain/vaults.test.ts b/test/integration/parachain/staging/vaults.test.ts similarity index 92% rename from test/integration/parachain/vaults.test.ts rename to test/integration/parachain/staging/vaults.test.ts index 53e9efd5e..82cfbc22f 100644 --- a/test/integration/parachain/vaults.test.ts +++ b/test/integration/parachain/staging/vaults.test.ts @@ -1,9 +1,9 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; -import { DefaultVaultsAPI } from "../../../src/parachain/vaults"; -import { createPolkadotAPI } from "../../../src/factory"; -import { assert } from "../../chai"; -import { defaultParachainEndpoint } from "../../config"; +import { DefaultVaultsAPI } from "../../../../src/parachain/vaults"; +import { createPolkadotAPI } from "../../../../src/factory"; +import { assert } from "../../../chai"; +import { defaultParachainEndpoint } from "../../../config"; import * as bitcoin from "bitcoinjs-lib"; import Big from "big.js"; import { TypeRegistry } from "@polkadot/types"; @@ -42,7 +42,7 @@ describe("vaultsAPI", () => { }); it("should select random vault for issue", async () => { - const polkaBTCCollateral = api.createType("PolkaBTC", 0); + const polkaBTCCollateral = api.createType("Balance", 0); const randomVault = await vaultsAPI.selectRandomVaultIssue(polkaBTCCollateral); assert.isTrue( randomVault.toHuman() === dave.address || @@ -53,12 +53,12 @@ describe("vaultsAPI", () => { }); it("should fail if no vault for issuing is found", async () => { - const polkaBTCCollateral = api.createType("PolkaBTC", 90000000000); + const polkaBTCCollateral = api.createType("Balance", 90000000000); assert.isRejected(vaultsAPI.selectRandomVaultIssue(polkaBTCCollateral)); }); it("should select random vault for redeem", async () => { - const polkaBTCCollateral = api.createType("PolkaBTC", 0); + const polkaBTCCollateral = api.createType("Balance", 0); const randomVault = await vaultsAPI.selectRandomVaultRedeem(polkaBTCCollateral); assert.isTrue( randomVault.toHuman() === dave.address || @@ -69,7 +69,7 @@ describe("vaultsAPI", () => { }); it("should fail if no vault for redeeming is found", async () => { - const polkaBTCCollateral = api.createType("PolkaBTC", 90000000000); + const polkaBTCCollateral = api.createType("Balance", 90000000000); assert.isRejected(vaultsAPI.selectRandomVaultRedeem(polkaBTCCollateral)); }); diff --git a/test/utils/issue.ts b/test/utils/issue.ts new file mode 100644 index 000000000..b58b97410 --- /dev/null +++ b/test/utils/issue.ts @@ -0,0 +1,110 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import * as bitcoin from "bitcoinjs-lib"; +import { fail } from "assert"; +import { btcToSat, satToBTC, IssueRequestExt } from "../../src"; +import { DefaultBTCCoreAPI } from "../../src/external/btc-core"; +import { DefaultCollateralAPI } from "../../src/parachain/collateral"; +import { IssueRequestResult, DefaultIssueAPI } from "../../src/parachain/issue"; +import { DefaultTreasuryAPI } from "../../src/parachain/treasury"; +import { BitcoinCoreClient } from "./bitcoin-core-client"; +import Big from "big.js"; + +export interface IssueResult { + request: IssueRequestResult; + initialDotBalance: Big; + finalDotBalance: Big; + initialPolkaBtcBalance: Big; + finalPolkaBtcBalance: Big; +} + +export async function issue( + api: ApiPromise, + btcCoreAPI: DefaultBTCCoreAPI, + bitcoinCoreClient: BitcoinCoreClient, + keyring: Keyring, + amount: string, + requesterName: string, + vaultName: string, + autoExecute: boolean, + triggerRefund: boolean +): Promise { + const treasuryAPI = new DefaultTreasuryAPI(api); + const issueAPI = new DefaultIssueAPI(api, bitcoin.networks.regtest); + const collateralAPI = new DefaultCollateralAPI(api); + + const requester = keyring.addFromUri("//" + requesterName); + issueAPI.setAccount(requester); + const requesterAccountId = api.createType("AccountId", requester.address); + const initialBalanceDOT = await collateralAPI.balance(requesterAccountId); + const initialBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); + const blocksToMine = 3; + keyring = new Keyring({ type: "sr25519" }); + const vault = keyring.addFromUri("//" + vaultName); + const vaultAccountId = api.createType("AccountId", vault.address); + + // request issue + let amountAsBtcString = amount; + const amountAsSatoshiString = btcToSat(amountAsBtcString); + if (amountAsSatoshiString === undefined) { + fail(); + } + const amountAsSatoshi = api.createType("Balance", amountAsSatoshiString); + const requestResult = await issueAPI.request(amountAsSatoshi, vaultAccountId); + let issueRequest; + try { + issueRequest = await issueAPI.getRequestById(requestResult.id); + } catch (e) { + // IssueCompleted errors occur when multiple vaults attempt to execute the same request + console.log(e); + } + + amountAsBtcString = satToBTC( + (issueRequest as IssueRequestExt).amount.add((issueRequest as IssueRequestExt).fee).toString() + ); + + if (triggerRefund) { + // Send 1 more Btc than needed + amountAsBtcString = new Big(amountAsBtcString).add(1).toString(); + } + + // send btc tx + const vaultBtcAddress = requestResult.issueRequest.btc_address; + if (vaultBtcAddress === undefined) { + throw new Error("Undefined vault address returned from RequestIssue"); + } + + const txData = await bitcoinCoreClient.sendBtcTxAndMine(vaultBtcAddress, amountAsBtcString, blocksToMine); + + if (autoExecute === false) { + // execute issue, assuming the selected vault has the `--no-issue-execution` flag enabled + const merkleProof = await btcCoreAPI.getMerkleProof(txData.txid); + const parsedIssuedId = api.createType("H256", requestResult.id); + // reverse endianness (expects little-endian) + const parsedTxId = api.createType("H256", "0x" + Buffer.from(txData.txid, "hex").reverse().toString("hex")); + const parsedMerkleProof = api.createType("Bytes", "0x" + merkleProof); + const parsedRawTx = api.createType("Bytes", "0x" + txData.rawTx); + await issueAPI.execute(parsedIssuedId, parsedTxId, parsedMerkleProof, parsedRawTx); + } else { + // wait for vault to execute issue + while (!(await issueAPI.getRequestById(requestResult.id)).status.isCompleted) { + await sleep(1000); + } + } + + // check issuing worked + const finalBalancePolkaBTC = await treasuryAPI.balancePolkaBTC(requesterAccountId); + + const finalBalanceDOT = await collateralAPI.balance(requesterAccountId); + + return { + request: requestResult, + initialDotBalance: new Big(initialBalanceDOT.toString()), + finalDotBalance: new Big(finalBalanceDOT.toString()), + initialPolkaBtcBalance: new Big(initialBalancePolkaBTC.toString()), + finalPolkaBtcBalance: new Big(finalBalancePolkaBTC.toString()), + }; +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} \ No newline at end of file From 52b9e429f7c34f366133cabc64f5a02aa231ad9b Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 13:31:29 +0100 Subject: [PATCH 4/9] chore: Remove factory test --- test/integration/parachain/staging/factory.test.ts | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 test/integration/parachain/staging/factory.test.ts diff --git a/test/integration/parachain/staging/factory.test.ts b/test/integration/parachain/staging/factory.test.ts deleted file mode 100644 index a7fa48362..000000000 --- a/test/integration/parachain/staging/factory.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { assert } from "chai"; -import { createPolkadotAPI } from "../../../../src/factory"; -import { defaultParachainEndpoint } from "../../../config"; - - -describe("createAPI", () => { - it("should connect to parachain", async () => { - const api = await createPolkadotAPI(defaultParachainEndpoint); - assert.isTrue(api.isConnected); - await api.disconnect(); - }); -}); From 9797c77b2f69839c6c38ff8bd0bfa2f0f1412e2f Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 15:04:41 +0100 Subject: [PATCH 5/9] fix(collateral): Use actual BN type --- src/parachain/collateral.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parachain/collateral.ts b/src/parachain/collateral.ts index b0b9dcc1e..b33bec720 100644 --- a/src/parachain/collateral.ts +++ b/src/parachain/collateral.ts @@ -1,8 +1,8 @@ -import { AccountId, Balance as BN } from "@polkadot/types/interfaces/runtime"; +import { AccountId } from "@polkadot/types/interfaces/runtime"; import { ApiPromise } from "@polkadot/api"; import { ACCOUNT_NOT_SET_ERROR_MESSAGE, Transaction } from "../utils"; import { AddressOrPair } from "@polkadot/api/submittable/types"; - +import BN from "bn.js"; /** * @category PolkaBTC Bridge */ From dce5c52b3d2ca7d3cc2b600449c9ecdc80c5b953 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 17:26:22 +0100 Subject: [PATCH 6/9] chore(collateral): Return Big denomination instead of BN --- package.json | 1 + src/parachain/collateral.ts | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index b4ee11bab..5a3182d77 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "test": "run-s build test:*", "test:lint": "eslint src --ext .ts", "test:unit": "mocha test/unit/*.test.ts test/unit/**/*.test.ts", + "test:integration": "mocha test/integration/**/*.test.ts --timeout 10000000", "test:integration:staging": "mocha test/integration/**/staging/*.test.ts --timeout 10000000", "test:integration:release": "mocha test/integration/**/release/*.test.ts --timeout 10000000", "watch:build": "tsc -p tsconfig.json -w", diff --git a/src/parachain/collateral.ts b/src/parachain/collateral.ts index b33bec720..63cb9200f 100644 --- a/src/parachain/collateral.ts +++ b/src/parachain/collateral.ts @@ -1,8 +1,8 @@ import { AccountId } from "@polkadot/types/interfaces/runtime"; import { ApiPromise } from "@polkadot/api"; -import { ACCOUNT_NOT_SET_ERROR_MESSAGE, Transaction } from "../utils"; +import { ACCOUNT_NOT_SET_ERROR_MESSAGE, planckToDOT, Transaction } from "../utils"; import { AddressOrPair } from "@polkadot/api/submittable/types"; -import BN from "bn.js"; +import Big from "big.js"; /** * @category PolkaBTC Bridge */ @@ -15,17 +15,17 @@ export interface CollateralAPI { /** * @returns Total locked collateral */ - totalLocked(): Promise; + totalLocked(): Promise; /** * @param id The ID of an account * @returns The reserved balance of the given account */ - balanceLocked(id: AccountId): Promise; + balanceLocked(id: AccountId): Promise; /** * @param id The ID of an account * @returns The free balance of the given account */ - balance(id: AccountId): Promise; + balance(id: AccountId): Promise; /** * Send a transaction that transfers from the caller's address to another address * @param address The recipient of the transfer @@ -41,21 +41,22 @@ export class DefaultCollateralAPI implements CollateralAPI { this.transaction = new Transaction(api); } - async totalLocked(): Promise { + async totalLocked(): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); - return this.api.query.collateral.totalCollateral.at(head); + const totalLockedBN = await this.api.query.collateral.totalCollateral.at(head); + return new Big(planckToDOT(totalLockedBN.toString())); } - async balanceLocked(id: AccountId): Promise { + async balanceLocked(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); - return account.reserved; + return new Big(planckToDOT(account.reserved.toString())); } - async balance(id: AccountId): Promise { + async balance(id: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const account = await this.api.query.dot.account.at(head, id); - return account.free; + return new Big(planckToDOT(account.free.toString())); } async transfer(address: string, amount: string | number): Promise { From 5b41bf98d60dccff621d41e241745de4cd52050c Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 17:33:00 +0100 Subject: [PATCH 7/9] chore: Re-add `ci:test` to run all tests --- package.json | 1 + test/mock/parachain/collateral.ts | 20 ++++++++------------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 5a3182d77..7e0396e8c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "fix": "run-s fix:*", "fix:prettier": "prettier \"src/**/*.ts\" --write", "fix:lint": "eslint --fix . --ext .ts", + "ci:test": "run-s build test:lint test:unit test:integration", "ci:test:staging": "run-s build test:lint test:unit test:integration:staging", "ci:test:release": "run-s build test:integration:release", "ci:test-with-coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" yarn ci:test", diff --git a/test/mock/parachain/collateral.ts b/test/mock/parachain/collateral.ts index 82302ba94..624933bcb 100644 --- a/test/mock/parachain/collateral.ts +++ b/test/mock/parachain/collateral.ts @@ -1,23 +1,19 @@ -import { AccountId, Balance } from "@polkadot/types/interfaces/runtime"; +import { AccountId } from "@polkadot/types/interfaces/runtime"; import { CollateralAPI } from "../../../src/parachain/collateral"; -import { u128 } from "@polkadot/types/primitive"; -import { TypeRegistry } from "@polkadot/types"; import { AddressOrPair } from "@polkadot/api/submittable/types"; +import Big from "big.js"; export class MockCollateralAPI implements CollateralAPI { - async totalLocked(): Promise { - const registry = new TypeRegistry(); - return new u128(registry, 128); + async totalLocked(): Promise { + return new Big("10"); } - async balanceLocked(_id: AccountId): Promise { - const registry = new TypeRegistry(); - return new u128(registry, 64); + async balanceLocked(_id: AccountId): Promise { + return new Big("1"); } - async balance(_id: AccountId): Promise { - const registry = new TypeRegistry(); - return new u128(registry, 32); + async balance(_id: AccountId): Promise { + return new Big("5"); } async transfer(_address: string, _amount: string | number): Promise { From d19167912c298b865b5466a9a528e52e8cd45b18 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Tue, 6 Apr 2021 19:09:30 +0100 Subject: [PATCH 8/9] fix(clients): APY calculation --- .github/workflows/test-release.yml | 2 +- src/parachain/fee.ts | 5 +++-- src/parachain/staked-relayer.ts | 25 +++++++++++------------ src/parachain/vaults.ts | 29 +++++++++++++-------------- test/mock/parachain/fee.ts | 2 +- test/mock/parachain/staked-relayer.ts | 8 ++++---- test/mock/parachain/vaults.ts | 8 ++++---- 7 files changed, 39 insertions(+), 40 deletions(-) diff --git a/.github/workflows/test-release.yml b/.github/workflows/test-release.yml index 6b3d24767..40a912922 100644 --- a/.github/workflows/test-release.yml +++ b/.github/workflows/test-release.yml @@ -17,4 +17,4 @@ jobs: - name: Run and set up the parachain, oracle, staked relayer and vault run: docker-compose up -d - run: yarn install - - run: yarn ci:test:release + - run: yarn ci:test diff --git a/src/parachain/fee.ts b/src/parachain/fee.ts index c67ddd3be..964619faa 100644 --- a/src/parachain/fee.ts +++ b/src/parachain/fee.ts @@ -26,7 +26,7 @@ export interface FeeAPI { * @param dotToBtcRate Conversion rate * @returns The APY, given the parameters */ - calculateAPY(feesPolkaBTC: string, feesDOT: string, lockedDOT: string, dotToBtcRate: Big): string; + calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big): Promise; /** * @returns The griefing collateral rate for issuing PolkaBTC */ @@ -70,7 +70,8 @@ export class DefaultFeeAPI implements FeeAPI { return new Big(decodeFixedPointType(griefingCollateralRate)); } - calculateAPY(feesPolkaBTC: string, feesDOT: string, lockedDOT: string, dotToBtcRate: Big): string { + async calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big): Promise { + const dotToBtcRate = await this.oracleAPI.getExchangeRate(); const feesPolkaBTCBig = new Big(feesPolkaBTC); const feesPolkaBTCInDot = feesPolkaBTCBig.mul(dotToBtcRate); const totalFees = new Big(feesDOT).add(feesPolkaBTCInDot); diff --git a/src/parachain/staked-relayer.ts b/src/parachain/staked-relayer.ts index 619a86a0a..1dfe1e860 100644 --- a/src/parachain/staked-relayer.ts +++ b/src/parachain/staked-relayer.ts @@ -4,7 +4,7 @@ import { AccountId, BlockNumber, Moment } from "@polkadot/types/interfaces/runti import { ApiPromise } from "@polkadot/api"; import { VaultsAPI, DefaultVaultsAPI } from "./vaults"; import BN from "bn.js"; -import { pagedIterator, decodeFixedPointType, Transaction, ACCOUNT_NOT_SET_ERROR_MESSAGE } from "../utils"; +import { pagedIterator, decodeFixedPointType, Transaction, ACCOUNT_NOT_SET_ERROR_MESSAGE, satToBTC, planckToDOT } from "../utils"; import { Network } from "bitcoinjs-lib"; import Big from "big.js"; import { DefaultOracleAPI, OracleAPI } from "./oracle"; @@ -84,14 +84,14 @@ export interface StakedRelayerAPI { getAllStatusUpdates(): Promise>; /** * @param stakedRelayerId The ID of a staked relayer - * @returns Total rewards in PolkaBTC, denoted in Satoshi, for the given staked relayer + * @returns Total rewards in PolkaBTC for the given staked relayer */ - getFeesPolkaBTC(stakedRelayerId: AccountId): Promise; + getFeesPolkaBTC(stakedRelayerId: AccountId): Promise; /** * @param stakedRelayerId The ID of a staked relayer - * @returns Total rewards in DOT, denoted in Planck, for the given staked relayer + * @returns Total rewards in DOT for the given staked relayer */ - getFeesDOT(stakedRelayerId: AccountId): Promise; + getFeesDOT(stakedRelayerId: AccountId): Promise; /** * Get the total APY for a staked relayer based on the income in PolkaBTC and DOT * divided by the locked DOT. @@ -370,26 +370,25 @@ export class DefaultStakedRelayerAPI implements StakedRelayerAPI { return [...activeStatusUpdates, ...inactiveStatusUpdates]; } - async getFeesPolkaBTC(stakedRelayerId: AccountId): Promise { + async getFeesPolkaBTC(stakedRelayerId: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const fees = await this.api.query.fee.totalRewardsPolkaBTC.at(head, stakedRelayerId); - return fees.toString(); + return new Big(satToBTC(fees.toString())); } - async getFeesDOT(stakedRelayerId: AccountId): Promise { + async getFeesDOT(stakedRelayerId: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); const fees = await this.api.query.fee.totalRewardsDOT.at(head, stakedRelayerId); - return fees.toString(); + return new Big(planckToDOT(fees.toString())); } async getAPY(stakedRelayerId: AccountId): Promise { - const [feesPolkaBTC, feesDOT, dotToBtcRate, lockedDOT] = await Promise.all([ + const [feesPolkaBTC, feesDOT, lockedDOT] = await Promise.all([ await this.getFeesPolkaBTC(stakedRelayerId), await this.getFeesDOT(stakedRelayerId), - await this.oracleAPI.getExchangeRate(), - await (await this.collateralAPI.balanceLocked(stakedRelayerId)).toString(), + await this.collateralAPI.balanceLocked(stakedRelayerId), ]); - return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT, dotToBtcRate); + return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT); } async getSLA(stakedRelayerId: AccountId): Promise { diff --git a/src/parachain/vaults.ts b/src/parachain/vaults.ts index 681930829..73f4fd222 100644 --- a/src/parachain/vaults.ts +++ b/src/parachain/vaults.ts @@ -197,14 +197,14 @@ export interface VaultsAPI { getSecureCollateralThreshold(): Promise; /** * @param vaultId The vault account ID - * @returns The total PolkaBTC reward collected by the vault, denoted in Satoshi + * @returns The total PolkaBTC reward collected by the vault */ - getFeesPolkaBTC(vaultId: AccountId): Promise; + getFeesPolkaBTC(vaultId: AccountId): Promise; /** * @param vaultId The vault account ID - * @returns The total DOT reward collected by the vault, denoted in Planck + * @returns The total DOT reward collected by the vault */ - getFeesDOT(vaultId: AccountId): Promise; + getFeesDOT(vaultId: AccountId): Promise; /** * Get the total APY for a vault based on the income in PolkaBTC and DOT * divided by the locked DOT. @@ -464,8 +464,7 @@ export class DefaultVaultsAPI { } async getPolkaBTCCapacity(): Promise { - const totalLockedDotAsPlanck = await this.collateralAPI.totalLocked(); - const totalLockedDot = new Big(planckToDOT(totalLockedDotAsPlanck.toString())); + const totalLockedDot = await this.collateralAPI.totalLocked(); const oracle = new DefaultOracleAPI(this.api); const exchangeRate = await oracle.getExchangeRate(); const exchangeRateU128 = new Big(exchangeRate); @@ -549,25 +548,25 @@ export class DefaultVaultsAPI { return new Big(decodeFixedPointType(threshold)); } - async getFeesPolkaBTC(vaultId: AccountId): Promise { + async getFeesPolkaBTC(vaultId: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); - const parsedId = this.api.createType("AccountId", vaultId); - return (await this.api.query.fee.totalRewardsPolkaBTC.at(head, parsedId)).toString(); + const feesSatoshi = (await this.api.query.fee.totalRewardsPolkaBTC.at(head, vaultId)).toString(); + return new Big(satToBTC(feesSatoshi)); } - async getFeesDOT(vaultId: AccountId): Promise { + async getFeesDOT(vaultId: AccountId): Promise { const head = await this.api.rpc.chain.getFinalizedHead(); - return (await this.api.query.fee.totalRewardsDOT.at(head, vaultId)).toString(); + const feesPlanck = (await this.api.query.fee.totalRewardsDOT.at(head, vaultId)).toString(); + return new Big(planckToDOT(feesPlanck)); } async getAPY(vaultId: AccountId): Promise { - const [feesPolkaBTC, feesDOT, dotToBtcRate, lockedDOT] = await Promise.all([ + const [feesPolkaBTC, feesDOT, lockedDOT] = await Promise.all([ await this.getFeesPolkaBTC(vaultId), await this.getFeesDOT(vaultId), - await this.oracleAPI.getExchangeRate(), - await (await this.collateralAPI.balanceLocked(vaultId)).toString(), + await this.collateralAPI.balanceLocked(vaultId), ]); - return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT, dotToBtcRate); + return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT); } async getSLA(vaultId: AccountId): Promise { diff --git a/test/mock/parachain/fee.ts b/test/mock/parachain/fee.ts index 5f7f8ecca..2c04107f4 100644 --- a/test/mock/parachain/fee.ts +++ b/test/mock/parachain/fee.ts @@ -17,7 +17,7 @@ export class MockFeeAPI implements FeeAPI { throw new Error("Method not implemented."); } - calculateAPY(_feesPolkaBTC: string, _feesDOT: string, _lockedDOT: string, _dotToBtcRate: Big): string { + calculateAPY(_feesPolkaBTC: Big, _feesDOT: Big, _lockedDOT: Big): Promise { throw new Error("Method not implemented."); } diff --git a/test/mock/parachain/staked-relayer.ts b/test/mock/parachain/staked-relayer.ts index 781d7dee6..a2e04bae4 100644 --- a/test/mock/parachain/staked-relayer.ts +++ b/test/mock/parachain/staked-relayer.ts @@ -148,12 +148,12 @@ export class MockStakedRelayerAPI implements StakedRelayerAPI { return [createStatusUpdate()]; } - async getFeesPolkaBTC(_stakedRelayerId: AccountId): Promise { - return "10.22"; + async getFeesPolkaBTC(_stakedRelayerId: AccountId): Promise { + return new Big("10.22"); } - async getFeesDOT(_stakedRelayerId: AccountId): Promise { - return "10.22"; + async getFeesDOT(_stakedRelayerId: AccountId): Promise { + return new Big("10.22"); } async getAPY(_stakedRelayerId: AccountId): Promise { diff --git a/test/mock/parachain/vaults.ts b/test/mock/parachain/vaults.ts index 371c3d071..e02565313 100644 --- a/test/mock/parachain/vaults.ts +++ b/test/mock/parachain/vaults.ts @@ -188,12 +188,12 @@ export class MockVaultsAPI implements VaultsAPI { return new Big(0); } - async getFeesPolkaBTC(_vaultId: AccountId): Promise { - return "368"; + async getFeesPolkaBTC(_vaultId: AccountId): Promise { + return new Big("368"); } - async getFeesDOT(_vaultId: AccountId): Promise { - return "368"; + async getFeesDOT(_vaultId: AccountId): Promise { + return new Big("368"); } async getAPY(_vaultId: AccountId): Promise { From 83ad194e3b2b5c0acce4f465c102cb7f3335a992 Mon Sep 17 00:00:00 2001 From: Daniel Savu Date: Wed, 7 Apr 2021 16:26:54 +0100 Subject: [PATCH 9/9] chore(fee): Calculate APY using optional exchange rate param --- src/parachain/fee.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/parachain/fee.ts b/src/parachain/fee.ts index 964619faa..8e3c86c91 100644 --- a/src/parachain/fee.ts +++ b/src/parachain/fee.ts @@ -23,10 +23,10 @@ export interface FeeAPI { * @param feesPolkaBTC Satoshi value representing the BTC fees accrued * @param feesDOT Planck value representing the DOT fees accrued * @param lockedDOT Planck value representing the value locked to gain yield - * @param dotToBtcRate Conversion rate + * @param dotToBtcRate (Optional) Conversion rate of the large denominations (DOT/BTC as opposed to Planck/Satoshi) * @returns The APY, given the parameters */ - calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big): Promise; + calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big, dotToBtcRate?: Big): Promise; /** * @returns The griefing collateral rate for issuing PolkaBTC */ @@ -70,14 +70,14 @@ export class DefaultFeeAPI implements FeeAPI { return new Big(decodeFixedPointType(griefingCollateralRate)); } - async calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big): Promise { - const dotToBtcRate = await this.oracleAPI.getExchangeRate(); - const feesPolkaBTCBig = new Big(feesPolkaBTC); - const feesPolkaBTCInDot = feesPolkaBTCBig.mul(dotToBtcRate); - const totalFees = new Big(feesDOT).add(feesPolkaBTCInDot); - const lockedDotBig = new Big(lockedDOT); + async calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big, dotToBtcRate?: Big): Promise { + if(dotToBtcRate === undefined) { + dotToBtcRate = await this.oracleAPI.getExchangeRate(); + } + const feesPolkaBTCInDot = feesPolkaBTC.mul(dotToBtcRate); + const totalFees = feesDOT.add(feesPolkaBTCInDot); // convert to percent - return totalFees.div(lockedDotBig).mul(100).toString(); + return totalFees.div(lockedDOT).mul(100).toString(); } } \ No newline at end of file