diff --git a/.gitignore b/.gitignore index 3ddc0f0a7..31db5cce6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ src/**.js yarn.lock src/interfaces/* !src/interfaces/definitions.ts +!src/interfaces/interbtc-types.ts !src/interfaces/default src/interfaces/default/* !src/interfaces/default/.keep diff --git a/README.md b/README.md index 1e5c071dd..68e089cc3 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ yarn test:unit Note that the parachain needs to be running for all tests to run. ```bash -docker-compose up +docker-compose up -d ``` The default parachain runtime is Kintsugi. @@ -178,9 +178,18 @@ yarn test yarn test:integration ``` -NOTE: While the parachain is starting up, there will be warnings from the integration tests until it can locate the locally running test vaults. Expect the startup to take around 2-3 minutes, and only start the integration tests after that time frame. +NOTE: While the parachain is starting up, there will be warnings from the integration tests until it can locate the locally running test vaults. Expect the startup to take a few seconds, before the integration tests start. -Another option is to check the logs, i.e. for `vault_1` you can use this: +##### Dealing with timeouts during local testing + +At times, when running tests locally, the timeout set in `package.json`'s `jest.testTimeout` setting might not be enough. In that case, you can override the `testTimeout` value (in ms) on the command line: + +```bash +yarn test:integration --testTimeout=900000 +``` + +##### Check service logs in detached mode +To check the logs or the services (for example, `vault_1`) you can use this: ```bash docker-compose logs -f vault_1 diff --git a/package.json b/package.json index 1cae4e972..b1db21b11 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,9 @@ { "name": "@interlay/interbtc-api", - "version": "2.4.3", + "version": "2.5.0", "description": "JavaScript library to interact with interBTC", "main": "build/src/index.js", + "type": "module", "typings": "build/src/index.d.ts", "repository": "https://github.com/interlay/interbtc-api", "license": "Apache-2.0", @@ -26,8 +27,8 @@ "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 node_modules/.bin/polkadot-types-from-defs --package @interlay/interbtc-api/interfaces --input ./src/interfaces --endpoint ./src/json/parachain.json", - "generate:meta": "ts-node node_modules/.bin/polkadot-types-from-chain --package @interlay/interbtc-api/interfaces --endpoint ./src/json/parachain.json --output ./src/interfaces", + "generate:defs": "node --experimental-specifier-resolution=node --loader ts-node/esm node_modules/.bin/polkadot-types-from-defs --package @interlay/interbtc-api/interfaces --input ./src/interfaces --endpoint ./src/json/parachain.json", + "generate:meta": "node --experimental-specifier-resolution=node --loader ts-node/esm node_modules/.bin/polkadot-types-from-chain --package @interlay/interbtc-api/interfaces --endpoint ./src/json/parachain.json --output ./src/interfaces", "hrmp-setup": "ts-node scripts/hrmp-setup", "runtime-upgrade": "ts-node scripts/runtime-upgrade", "xcm-cross-chain-transfer": "ts-node scripts/xcm-cross-chain-transfer", @@ -37,15 +38,16 @@ "undercollateralized-borrowers": "ts-node scripts/get-undercollateralized-borrowers", "test": "run-s build test:*", "test:lint": "eslint src --ext .ts", - "test:unit": "mocha test/unit/*.test.ts test/unit/**/*.test.ts", + "test:unit": "jest test/unit/*.test.ts test/unit/**/*.test.ts", "test:integration": "run-s test:integration:staging", "test:integration:staging": "run-s test:integration:setup test:integration:parallel test:integration:sequential", - "test:integration:setup": "mocha test/integration/**/staging/setup/initialize.test.ts", - "test:integration:parallel": "mocha test/integration/**/staging/*.test.ts --parallel", - "test:integration:sequential": "mocha test/integration/**/staging/sequential/*.test.ts", + "test:integration:setup": "jest test/integration/**/staging/setup/initialize.test.ts", + "test:integration:parallel": "jest test/integration/**/staging/*.test.ts", + "test:integration:sequential": "jest --runInBand test/integration/**/staging/sequential/*.test.ts", "watch:build": "tsc -p tsconfig.json -w", - "watch:test": "mocha --watch test/**/*.test.ts", - "update-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/json/parachain.json" + "watch:test": "jest --watch test/**/*.test.ts", + "update-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/json/parachain.json", + "update-metadata-kintnet": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' https://api-dev-kintsugi.interlay.io/parachain > src/json/parachain.json" }, "engines": { "node": ">=11" @@ -55,7 +57,7 @@ "@interlay/esplora-btc-api": "0.4.0", "@interlay/interbtc-types": "1.13.0", "@interlay/monetary-js": "0.7.3", - "@polkadot/api": "9.14.2", + "@polkadot/api": "10.9.1", "big.js": "6.1.1", "bitcoin-core": "^3.0.0", "bitcoinjs-lib": "^5.2.0", @@ -65,36 +67,29 @@ "regtest-client": "^0.2.0" }, "devDependencies": { - "@polkadot/typegen": "9.14.2", + "@polkadot/typegen": "10.9.1", "@types/big.js": "6.1.2", - "@types/chai": "^4.2.12", - "@types/chai-as-promised": "^7.1.3", - "@types/mocha": "^10.0.1", + "@types/jest": "^29.5.3", "@types/node": "^18.11.9", "@types/shelljs": "0.8.12", - "@types/sinon": "^10.0.15", "@types/yargs": "^17.0.10", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "chai": "^4.2.0", - "chai-as-promised": "^7.1.1", "cli-table3": "0.6.3", "eslint": "^8.41.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-unused-imports": "^2.0.0", "husky": "^8.0.3", - "mocha": "10.2.0", - "nock": "^13.0.4", + "jest": "^29.6.2", "npm-run-all": "^4.1.5", "nyc": "^15.1.0", "prettier": "^3.0.1", "shelljs": "0.8.5", - "sinon": "^15.1.0", - "ts-mock-imports": "^1.3.0", + "ts-jest": "^29.1.1", "ts-node": "10.9.1", "typedoc": "^0.24.7", "typedoc-plugin-markdown": "^3.15.3", - "typescript": "5.0.4", + "typescript": "4.9.5", "yargs": "^17.5.1" }, "resolutions": { @@ -113,13 +108,29 @@ "singleQuote": false, "tabWidth": 4 }, - "mocha": { - "reporter": "spec", - "require": "ts-node/register", - "watch-files": [ - "src/**/*.ts", - "test/**/*.ts" + "jest": { + "moduleNameMapper": { + "^(\\.\\.?\\/.+)\\.js$": "$1" + }, + "testPathIgnorePatterns": [ + "/src" ], - "recursive": true + "preset": "ts-jest", + "testEnvironment": "node", + "modulePathIgnorePatterns": [ + "/build/" + ], + "collectCoverageFrom": [ + "/src/**/*.ts*" + ], + "coveragePathIgnorePatterns": [ + "/node_modules/", + "/build/", + "/src/interfaces/" + ], + "setupFilesAfterEnv": [ + "/test/utils/jestSetupFileAfterEnv.ts" + ], + "testTimeout": 30000 } -} \ No newline at end of file +} diff --git a/src/clients/faucet.ts b/src/clients/faucet.ts index b78ddcbba..aadf74649 100644 --- a/src/clients/faucet.ts +++ b/src/clients/faucet.ts @@ -1,7 +1,7 @@ import { FundAccountJsonRpcRequest } from "../interfaces/default"; import { getAPITypes } from "../factory"; import { TypeRegistry } from "@polkadot/types"; -import { Constructor } from "@polkadot/types/types"; +import { CodecClass } from "@polkadot/types/types"; import { AccountId } from "@polkadot/types/interfaces"; import { JsonRpcClient } from "./client"; import { newCurrencyId } from "../utils"; @@ -15,7 +15,7 @@ export class FaucetClient extends JsonRpcClient { registry: TypeRegistry; constr: { - FundAccountJsonRpcRequest: Constructor; + FundAccountJsonRpcRequest: CodecClass; }; constructor(private api: ApiPromise, url: string) { diff --git a/src/factory.ts b/src/factory.ts index 83f42f9fc..66b1bcb93 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -8,8 +8,6 @@ import { DefinitionRpc, DefinitionRpcSub } from "@polkadot/types/types"; import * as definitions from "./interfaces/definitions"; import { InterBtcApi, DefaultInterBtcApi } from "./interbtc-api"; import { BitcoinNetwork } from "./types"; -import { objectSpread } from "@polkadot/util"; -import { DefinitionCall, DefinitionsCall } from "@polkadot/types/types"; export function createProvider(endpoint: string, autoConnect?: number | false | undefined): ProviderInterface { if (/https?:\/\//.exec(endpoint)) { @@ -35,9 +33,6 @@ export function createSubstrateAPI( types, rpc, noInitWarn: noInitWarn || true, - // manual definition for transactionPaymentApi.queryInfo until polkadot-js/api can be upgraded - // TODO: revert when this work is merged: https://github.com/interlay/interbtc-api/pull/672 - runtime: getRuntimeDefs(), }); } @@ -64,86 +59,3 @@ export function createAPIRegistry(): TypeRegistry { registry.register(getAPITypes()); return registry; } - -const V1_TO_V4_SHARED_PAY: Record = { - query_fee_details: { - description: "The transaction fee details", - params: [ - { - name: "uxt", - type: "Extrinsic" - }, - { - name: "len", - type: "u32" - } - ], - type: "FeeDetails" - } -}; - -const V2_TO_V4_SHARED_PAY: Record = { - query_info: { - description: "The transaction info", - params: [ - { - name: "uxt", - type: "Extrinsic" - }, - { - name: "len", - type: "u32" - } - ], - type: "RuntimeDispatchInfo" - } -}; - -const V3_SHARED_PAY_CALL: Record = { - query_length_to_fee: { - description: "Query the output of the current LengthToFee given some input", - params: [ - { - name: "length", - type: "u32" - } - ], - type: "Balance" - }, - query_weight_to_fee: { - description: "Query the output of the current WeightToFee given some input", - params: [ - { - name: "weight", - type: "Weight" - } - ], - type: "Balance" - } -}; - -export function getRuntimeDefs(): DefinitionsCall { - return { - TransactionPaymentApi: [ - { - // V4 is equivalent to V3 (V4 just dropped all V1 references) - methods: objectSpread( - {}, - V3_SHARED_PAY_CALL, - V2_TO_V4_SHARED_PAY, - V1_TO_V4_SHARED_PAY - ), - version: 4 - }, - { - methods: objectSpread( - {}, - V3_SHARED_PAY_CALL, - V2_TO_V4_SHARED_PAY, - V1_TO_V4_SHARED_PAY - ), - version: 3 - }, - ] - }; -} diff --git a/src/interfaces/definitions.ts b/src/interfaces/definitions.ts index 33a3b92aa..38e154cb5 100644 --- a/src/interfaces/definitions.ts +++ b/src/interfaces/definitions.ts @@ -1,4 +1,14 @@ -import definitions, { RpcFunctionDefinition } from "@interlay/interbtc-types"; +import { RpcFunctionDefinition } from "@interlay/interbtc-types"; +import fs from "fs"; + +// hacky, but cannot import json "the old way" in esnext +const definitionsString = fs.readFileSync("./node_modules/@interlay/interbtc-types/definitions.json", "utf-8"); +const definitions = JSON.parse(definitionsString); + +interface DecoratedRpcFunctionDefinition extends RpcFunctionDefinition { + aliasSection: string; +} + export default { types: definitions.types[0].types, rpc: parseProviderRpcDefinitions(definitions.rpc), @@ -22,7 +32,3 @@ function parseProviderRpcDefinitions( } return parsedDefs; } - -interface DecoratedRpcFunctionDefinition extends RpcFunctionDefinition { - aliasSection: string; -} diff --git a/test/chai.ts b/test/chai.ts deleted file mode 100644 index ec67668e1..000000000 --- a/test/chai.ts +++ /dev/null @@ -1,6 +0,0 @@ -import chai from "chai"; -import chaiAsPromised from "chai-as-promised"; -chai.use(chaiAsPromised); - -export const assert = chai.assert; -export const expect = chai.expect; diff --git a/test/integration/external/staging/electrs.test.ts b/test/integration/external/staging/electrs.test.ts index c41c1bf2a..a8da99ec4 100644 --- a/test/integration/external/staging/electrs.test.ts +++ b/test/integration/external/staging/electrs.test.ts @@ -1,5 +1,5 @@ +import expect from "expect"; import { ApiPromise } from "@polkadot/api"; -import { assert } from "chai"; import { ElectrsAPI, DefaultElectrsAPI } from "../../../../src/external/electrs"; import { createSubstrateAPI } from "../../../../src/factory"; import { @@ -16,12 +16,12 @@ import { BitcoinCoreClient } from "../../../../src/utils/bitcoin-core-client"; import { BitcoinAmount } from "@interlay/monetary-js"; import { makeRandomBitcoinAddress, runWhileMiningBTCBlocks, waitSuccess } from "../../../utils/helpers"; -describe("ElectrsAPI regtest", function () { +describe("ElectrsAPI regtest", () => { let api: ApiPromise; let electrsAPI: ElectrsAPI; let bitcoinCoreClient: BitcoinCoreClient; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); electrsAPI = new DefaultElectrsAPI(ESPLORA_BASE_PATH); bitcoinCoreClient = new BitcoinCoreClient( @@ -34,7 +34,7 @@ describe("ElectrsAPI regtest", function () { ); }); - after(async () => { + afterAll(async () => { await api.disconnect(); }); @@ -45,9 +45,9 @@ describe("ElectrsAPI regtest", function () { const txData = await bitcoinCoreClient.broadcastTx(recipientAddress, amount); const txid = await waitSuccess(() => electrsAPI.getLargestPaymentToRecipientAddressTxId(recipientAddress)); - assert.strictEqual(txid, txData.txid); + expect(txid).toBe(txData.txid); }); - }).timeout(1000 * 10); + }, 1000 * 10); it("should getTxByOpreturn", async () => { await runWhileMiningBTCBlocks(bitcoinCoreClient, async () => { @@ -57,9 +57,9 @@ describe("ElectrsAPI regtest", function () { const txData = await bitcoinCoreClient.broadcastTx(recipientAddress, amount, opReturnValue); const txid = await waitSuccess(() => electrsAPI.getTxIdByOpReturn(opReturnValue, recipientAddress, amount)); - assert.strictEqual(txid, txData.txid); + expect(txid).toBe(txData.txid); }); - }).timeout(1000 * 10); + }, 1000 * 10); it("should use getTxStatus to return correct confirmations", async () => { await runWhileMiningBTCBlocks(bitcoinCoreClient, async () => { @@ -70,19 +70,19 @@ describe("ElectrsAPI regtest", function () { const txData = await bitcoinCoreClient.broadcastTx(recipientAddress, amount, opReturnValue); // transaction in mempool let status = await electrsAPI.getTransactionStatus(txData.txid); - assert.strictEqual(status.confirmations, 0); + expect(status.confirmations).toBe(0); // transaction in the latest block await waitSuccess(async () => { status = await electrsAPI.getTransactionStatus(txData.txid); - assert.strictEqual(status.confirmations, 1); + expect(status.confirmations).toBe(1); }); // transaction in the parent of the latest block await waitSuccess(async () => { status = await electrsAPI.getTransactionStatus(txData.txid); - assert.strictEqual(status.confirmations, 2); + expect(status.confirmations).toBe(2); }); }); - }).timeout(1000 * 60); + }, 1000 * 60); }); diff --git a/test/integration/parachain/staging/btc-relay.test.ts b/test/integration/parachain/staging/btc-relay.test.ts index 861058a29..c8f78d9a6 100644 --- a/test/integration/parachain/staging/btc-relay.test.ts +++ b/test/integration/parachain/staging/btc-relay.test.ts @@ -1,29 +1,28 @@ import { ApiPromise } from "@polkadot/api"; -import { assert } from "chai"; import { createSubstrateAPI } from "../../../../src/factory"; import { PARACHAIN_ENDPOINT } from "../../../config"; import { DefaultInterBtcApi, InterBtcApi } from "../../../../src"; -describe("BTCRelay", function () { +describe("BTCRelay", () => { let api: ApiPromise; let interBtcAPI: InterBtcApi; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); interBtcAPI = new DefaultInterBtcApi(api, "regtest", undefined, "testnet"); }); - after(async () => { + afterAll(async () => { await api.disconnect(); }); it("should getLatestBTCBlockFromBTCRelay", async () => { const latestBTCBlockFromBTCRelay = await interBtcAPI.btcRelay.getLatestBlock(); - assert.isDefined(latestBTCBlockFromBTCRelay); - }).timeout(1500); + expect(latestBTCBlockFromBTCRelay).toBeDefined(); + }, 1500); it("should getLatestBTCBlockHeightFromBTCRelay", async () => { const latestBTCBlockHeightFromBTCRelay = await interBtcAPI.btcRelay.getLatestBlockHeight(); - assert.isDefined(latestBTCBlockHeightFromBTCRelay); - }).timeout(1500); + expect(latestBTCBlockHeightFromBTCRelay).toBeDefined(); + }, 1500); }); diff --git a/test/integration/parachain/staging/constants.test.ts b/test/integration/parachain/staging/constants.test.ts index 365e404e9..64882aa51 100644 --- a/test/integration/parachain/staging/constants.test.ts +++ b/test/integration/parachain/staging/constants.test.ts @@ -1,40 +1,39 @@ import { ApiPromise } from "@polkadot/api"; -import { assert } from "chai"; import { ConstantsAPI, DefaultConstantsAPI } from "../../../../src/parachain/constants"; import { createSubstrateAPI } from "../../../../src/factory"; import { PARACHAIN_ENDPOINT } from "../../../config"; -describe("Constants", function () { +describe("Constants", () => { let api: ApiPromise; let constantAPI: ConstantsAPI; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); constantAPI = new DefaultConstantsAPI(api); }); - after(async () => { + afterAll(async () => { await api.disconnect(); }); describe("getSystemBlockHashCount", () => { it("should sucessfully return", async () => { const returnValue = constantAPI.getSystemBlockHashCount(); - assert.isDefined(returnValue); - }).timeout(500); + expect(returnValue).toBeDefined(); + }, 500); }); describe("getSystemDbWeight", () => { it("should sucessfully return", async () => { const returnValue = constantAPI.getSystemDbWeight(); - assert.isDefined(returnValue); - }).timeout(500); + expect(returnValue).toBeDefined(); + }, 500); }); describe("getTimestampMinimumPeriod", () => { it("should sucessfully return", async () => { const returnValue = constantAPI.getTimestampMinimumPeriod(); - assert.isDefined(returnValue); - }).timeout(500); + expect(returnValue).toBeDefined(); + }, 500); }); }); diff --git a/test/integration/parachain/staging/fee.test.ts b/test/integration/parachain/staging/fee.test.ts index d23cb3471..950ece338 100644 --- a/test/integration/parachain/staging/fee.test.ts +++ b/test/integration/parachain/staging/fee.test.ts @@ -1,5 +1,4 @@ import { ApiPromise, Keyring } from "@polkadot/api"; -import { assert } from "chai"; import Big from "big.js"; import { createSubstrateAPI } from "../../../../src/factory"; @@ -16,7 +15,7 @@ describe("fee", () => { let wrappedCurrency: WrappedCurrency; - before(async function () { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); const keyring = new Keyring({ type: "sr25519" }); const oracleAccount = keyring.addFromUri(ORACLE_URI); @@ -28,13 +27,13 @@ describe("fee", () => { wrappedCurrency = oracleInterBtcAPI.getWrappedCurrency(); }); - after(async () => { - api.disconnect(); + afterAll(async () => { + await api.disconnect(); }); it("should check getReplaceGriefingCollateralRate", async () => { const replaceGriefingCollateralRate = await oracleInterBtcAPI.fee.getReplaceGriefingCollateralRate(); - assert.equal(replaceGriefingCollateralRate.toString(), "0.1"); + expect(replaceGriefingCollateralRate.toString()).toEqual("0.1"); }); it("should getGriefingCollateral for issue", async () => { @@ -48,7 +47,7 @@ describe("fee", () => { GriefingCollateralType.Issue ); console.log(griefingCollateral.toString()); - assert.equal(griefingCollateral.toBig().round(5, 0).toString(), "0.0014"); + expect(griefingCollateral.toBig().round(5, 0).toString()).toEqual("0.0014"); }); }); @@ -62,7 +61,7 @@ describe("fee", () => { amountToReplace, GriefingCollateralType.Replace ); - assert.equal(griefingCollateral.toString(), "2040.35874224"); + expect(griefingCollateral.toString()).toEqual("2040.35874224"); }); }); }); diff --git a/test/integration/parachain/staging/interbtc-api.test.ts b/test/integration/parachain/staging/interbtc-api.test.ts index 255a39ebc..fb5698026 100644 --- a/test/integration/parachain/staging/interbtc-api.test.ts +++ b/test/integration/parachain/staging/interbtc-api.test.ts @@ -1,6 +1,5 @@ import { ApiPromise, Keyring } from "@polkadot/api"; -import { assert } from "../../../chai"; import { createAPIRegistry, createSubstrateAPI, @@ -19,29 +18,29 @@ describe("InterBtcApi", () => { const registry = createAPIRegistry(); let api: ApiPromise; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); interBTC = new DefaultInterBtcApi(api); }); - after(async () => { + afterAll(async () => { await api.disconnect(); }); describe("setAccount", () => { it("should succeed to set KeyringPair", () => { interBTC.setAccount(keyringPair); - assert.isDefined(interBTC.account); + expect(interBTC.account).toBeDefined(); }); it("should succeed to set address with signer", () => { const signer = new SingleAccountSigner(registry, keyringPair); interBTC.setAccount(keyringPair, signer); - assert.isDefined(interBTC.account); + expect(interBTC.account).toBeDefined(); }); it("should fail to set address without signer", () => { - assert.throw(() => interBTC.setAccount(keyringPair.address)); + expect(() => interBTC.setAccount(keyringPair.address)).toThrow(); }); }); @@ -49,7 +48,7 @@ describe("InterBtcApi", () => { it("should remove account after it was set", () => { interBTC.setAccount(keyringPair); interBTC.removeAccount(); - assert.isUndefined(interBTC.account); + expect(interBTC.account).not.toBeDefined(); }); it("should fail to send transaction after account removal", async () => { @@ -61,7 +60,7 @@ describe("InterBtcApi", () => { const aliceAddress = keyring.addFromUri("//Alice").address; const tx = submitExtrinsic(interBTC, interBTC.tokens.transfer(aliceAddress, amount)); // Transfer to Alice should be rejected, since Bob's account was removed. - await assert.isRejected(tx); + await expect(tx).rejects.toThrow(); }); }); }); diff --git a/test/integration/parachain/staging/sequential/amm.partial.ts b/test/integration/parachain/staging/sequential/amm.partial.ts new file mode 100644 index 000000000..252a20f6e --- /dev/null +++ b/test/integration/parachain/staging/sequential/amm.partial.ts @@ -0,0 +1,191 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { InterbtcPrimitivesCurrencyId } from "@polkadot/types/lookup"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; +import { + CurrencyExt, + DefaultInterBtcApi, + DefaultTransactionAPI, + LiquidityPool, + newAccountId, + newCurrencyId, + newMonetaryAmount, +} from "../../../../../src"; +import { makeRandomPolkadotKeyPair, submitExtrinsic } from "../../../../utils/helpers"; +import BN from "bn.js"; +import { AnyNumber } from "@polkadot/types-codec/types"; + +async function setBalance( + api: ApiPromise, + sudoAccount: KeyringPair, + userAccount: KeyringPair, + currencyId: InterbtcPrimitivesCurrencyId, + amountFree: AnyNumber +) { + await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(api.tx.tokens.setBalance(userAccount.address, currencyId, amountFree, 0)) + ); +} + +async function createAndFundPair( + api: ApiPromise, + sudoAccount: KeyringPair, + asset0: InterbtcPrimitivesCurrencyId, + asset1: InterbtcPrimitivesCurrencyId, + amount0: BN, + amount1: BN +) { + await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(api.tx.dexGeneral.createPair(asset0, asset1, 30)) + ); + await setBalance(api, sudoAccount, sudoAccount, asset0, "1152921504606846976"); + await setBalance(api, sudoAccount, sudoAccount, asset1, "1152921504606846976"); + + await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.dexGeneral.addLiquidity(asset0, asset1, amount0, amount1, amount0, amount1, 999999) + ); +} + +export const ammTests = () => { + describe("AMM", () => { + let api: ApiPromise; + let interBtcAPI: DefaultInterBtcApi; + + let lpAccount: KeyringPair; + let sudoAccount: KeyringPair; + + let currency0: CurrencyExt; + let currency1: CurrencyExt; + let asset0: InterbtcPrimitivesCurrencyId; + let asset1: InterbtcPrimitivesCurrencyId; + + beforeAll(async () => { + const keyring = new Keyring({ type: "sr25519" }); + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + + sudoAccount = keyring.addFromUri(SUDO_URI); + lpAccount = makeRandomPolkadotKeyPair(keyring); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", lpAccount, ESPLORA_BASE_PATH); + + currency0 = interBtcAPI.getRelayChainCurrency(); + currency1 = interBtcAPI.getWrappedCurrency(); + asset0 = newCurrencyId(api, currency0); + asset1 = newCurrencyId(api, currency1); + + // fund liquidity provider so they can pay tx fees + await setBalance( + api, + sudoAccount, + lpAccount, + newCurrencyId(api, interBtcAPI.getGovernanceCurrency()), + "1152921504606846976" + ); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + it("should create and get liquidity pool", async () => { + await createAndFundPair(api, sudoAccount, asset0, asset1, new BN(8000000000000000), new BN(2000000000)); + + const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); + expect(liquidityPools).not.toHaveLength(0); + + const lpTokens = await interBtcAPI.amm.getLpTokens(); + expect(liquidityPools).not.toHaveLength(0); + + expect(liquidityPools[0].lpToken).toEqual(lpTokens[0]); + }); + + describe("should add liquidity", () => { + let lpPool: LiquidityPool; + + beforeAll(async () => { + const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); + lpPool = liquidityPools[0]; + + const inputAmount = newMonetaryAmount(1000000000, currency0); + const amounts = lpPool.getLiquidityDepositInputAmounts(inputAmount); + + for (const amount of amounts) { + await setBalance( + api, + sudoAccount, + lpAccount, + newCurrencyId(api, amount.currency), + amount.toString(true) + ); + } + + console.log("Adding liquidity..."); + await submitExtrinsic( + interBtcAPI, + await interBtcAPI.amm.addLiquidity(amounts, lpPool, 0, 999999, lpAccount.address) + ); + }); + + it("should compute liquidity", async () => { + const lpAmounts = await interBtcAPI.amm.getLiquidityProvidedByAccount(newAccountId(api, lpAccount.address)); + expect(lpAmounts).not.toHaveLength(0); + + const poolAmounts = lpPool.getLiquidityWithdrawalPooledCurrencyAmounts(lpAmounts[0] as any); + for (const poolAmount of poolAmounts) { + expect(poolAmount.isZero()).toBe(false); + } + }); + + it("should remove liquidity", async () => { + const lpToken = lpPool.lpToken; + + const lpAmount = newMonetaryAmount(100, lpToken); + await submitExtrinsic( + interBtcAPI, + await interBtcAPI.amm.removeLiquidity(lpAmount, lpPool, 0, 999999, lpAccount.address) + ); + }); + }); + + it("should swap currencies", async () => { + const inputAmount = newMonetaryAmount(1000000000, currency0); + const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); + const trade = interBtcAPI.amm.getOptimalTrade(inputAmount, currency1, liquidityPools); + + await setBalance(api, sudoAccount, lpAccount, asset0, inputAmount.toString(true)); + + const [asset0AccountBefore, asset1AccountBefore] = await Promise.all([ + api.query.tokens.accounts(lpAccount.address, asset0), + api.query.tokens.accounts(lpAccount.address, asset1), + ]); + + expect(trade).toBeDefined(); + + const outputAmount = trade!.getMinimumOutputAmount(0); + await submitExtrinsic(interBtcAPI, interBtcAPI.amm.swap(trade!, outputAmount, lpAccount.address, 999999)); + + const [asset0AccountAfter, asset1AccountAfter] = await Promise.all([ + api.query.tokens.accounts(lpAccount.address, asset0), + api.query.tokens.accounts(lpAccount.address, asset1), + ]); + + expect(asset0AccountAfter.free.toBn().toString()).toBe(asset0AccountBefore.free + .toBn() + .sub(new BN(inputAmount.toString(true))) + .toString() + ); + + expect(asset1AccountAfter.free.toBn().toString()).toBe(asset1AccountBefore.free + .toBn() + .add(new BN(outputAmount.toString(true))) + .toString() + ); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/amm.test.ts b/test/integration/parachain/staging/sequential/amm.test.ts deleted file mode 100644 index 0be841bf2..000000000 --- a/test/integration/parachain/staging/sequential/amm.test.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { assert } from "../../../../chai"; -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { InterbtcPrimitivesCurrencyId } from "@polkadot/types/lookup"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; -import { - CurrencyExt, - DefaultInterBtcApi, - DefaultTransactionAPI, - LiquidityPool, - newAccountId, - newCurrencyId, - newMonetaryAmount, -} from "../../../../../src"; -import { makeRandomPolkadotKeyPair, submitExtrinsic } from "../../../../utils/helpers"; -import BN from "bn.js"; -import { AnyNumber } from "@polkadot/types-codec/types"; - -async function setBalance( - api: ApiPromise, - sudoAccount: KeyringPair, - userAccount: KeyringPair, - currencyId: InterbtcPrimitivesCurrencyId, - amountFree: AnyNumber -) { - await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(api.tx.tokens.setBalance(userAccount.address, currencyId, amountFree, 0)) - ); -} - -async function createAndFundPair( - api: ApiPromise, - sudoAccount: KeyringPair, - asset0: InterbtcPrimitivesCurrencyId, - asset1: InterbtcPrimitivesCurrencyId, - amount0: BN, - amount1: BN -) { - await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(api.tx.dexGeneral.createPair(asset0, asset1, 30)) - ); - await setBalance(api, sudoAccount, sudoAccount, asset0, "1152921504606846976"); - await setBalance(api, sudoAccount, sudoAccount, asset1, "1152921504606846976"); - - await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.dexGeneral.addLiquidity(asset0, asset1, amount0, amount1, amount0, amount1, 999999) - ); -} - -describe("AMM", () => { - let api: ApiPromise; - let interBtcAPI: DefaultInterBtcApi; - - let lpAccount: KeyringPair; - let sudoAccount: KeyringPair; - - let currency0: CurrencyExt; - let currency1: CurrencyExt; - let asset0: InterbtcPrimitivesCurrencyId; - let asset1: InterbtcPrimitivesCurrencyId; - - before(async () => { - const keyring = new Keyring({ type: "sr25519" }); - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - - sudoAccount = keyring.addFromUri(SUDO_URI); - lpAccount = makeRandomPolkadotKeyPair(keyring); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", lpAccount, ESPLORA_BASE_PATH); - - currency0 = interBtcAPI.getRelayChainCurrency(); - currency1 = interBtcAPI.getWrappedCurrency(); - asset0 = newCurrencyId(api, currency0); - asset1 = newCurrencyId(api, currency1); - - // fund liquidity provider so they can pay tx fees - await setBalance( - api, - sudoAccount, - lpAccount, - newCurrencyId(api, interBtcAPI.getGovernanceCurrency()), - "1152921504606846976" - ); - }); - - after(async () => { - return api.disconnect(); - }); - - it("should create and get liquidity pool", async () => { - await createAndFundPair(api, sudoAccount, asset0, asset1, new BN(8000000000000000), new BN(2000000000)); - - const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); - assert.isNotEmpty(liquidityPools, "Should have at least one pool"); - - const lpTokens = await interBtcAPI.amm.getLpTokens(); - assert.isNotEmpty(liquidityPools, "Should have at least one token"); - - assert.deepEqual(liquidityPools[0].lpToken, lpTokens[0]); - }); - - describe("should add liquidity", () => { - let lpPool: LiquidityPool; - - before(async () => { - const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); - lpPool = liquidityPools[0]; - - const inputAmount = newMonetaryAmount(1000000000, currency0); - const amounts = lpPool.getLiquidityDepositInputAmounts(inputAmount); - - for (const amount of amounts) { - await setBalance( - api, - sudoAccount, - lpAccount, - newCurrencyId(api, amount.currency), - amount.toString(true) - ); - } - - console.log("Adding liquidity..."); - await submitExtrinsic( - interBtcAPI, - await interBtcAPI.amm.addLiquidity(amounts, lpPool, 0, 999999, lpAccount.address) - ); - }); - - it("should compute liquidity", async () => { - const lpAmounts = await interBtcAPI.amm.getLiquidityProvidedByAccount(newAccountId(api, lpAccount.address)); - assert.isNotEmpty(lpAmounts, "Should have at least one position"); - - const poolAmounts = lpPool.getLiquidityWithdrawalPooledCurrencyAmounts(lpAmounts[0] as any); - for (const poolAmount of poolAmounts) { - assert.isTrue(!poolAmount.isZero(), "Should compute withdrawal tokens"); - } - }); - - it("should remove liquidity", async () => { - const lpToken = lpPool.lpToken; - - const lpAmount = newMonetaryAmount(100, lpToken); - await submitExtrinsic( - interBtcAPI, - await interBtcAPI.amm.removeLiquidity(lpAmount, lpPool, 0, 999999, lpAccount.address) - ); - }); - }); - - it("should swap currencies", async () => { - const inputAmount = newMonetaryAmount(1000000000, currency0); - const liquidityPools = await interBtcAPI.amm.getLiquidityPools(); - const trade = interBtcAPI.amm.getOptimalTrade(inputAmount, currency1, liquidityPools); - - await setBalance(api, sudoAccount, lpAccount, asset0, inputAmount.toString(true)); - - const [asset0AccountBefore, asset1AccountBefore] = await Promise.all([ - api.query.tokens.accounts(lpAccount.address, asset0), - api.query.tokens.accounts(lpAccount.address, asset1), - ]); - - assert.isDefined(trade, "Did not find trade"); - const outputAmount = trade!.getMinimumOutputAmount(0); - await submitExtrinsic(interBtcAPI, interBtcAPI.amm.swap(trade!, outputAmount, lpAccount.address, 999999)); - - const [asset0AccountAfter, asset1AccountAfter] = await Promise.all([ - api.query.tokens.accounts(lpAccount.address, asset0), - api.query.tokens.accounts(lpAccount.address, asset1), - ]); - - assert.equal( - asset0AccountAfter.free.toBn().toString(), - asset0AccountBefore.free - .toBn() - .sub(new BN(inputAmount.toString(true))) - .toString() - ); - assert.equal( - asset1AccountAfter.free.toBn().toString(), - asset1AccountBefore.free - .toBn() - .add(new BN(outputAmount.toString(true))) - .toString() - ); - }); -}); diff --git a/test/integration/parachain/staging/sequential/asset-registry.partial.ts b/test/integration/parachain/staging/sequential/asset-registry.partial.ts new file mode 100644 index 000000000..e6d0c1eef --- /dev/null +++ b/test/integration/parachain/staging/sequential/asset-registry.partial.ts @@ -0,0 +1,146 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { StorageKey } from "@polkadot/types"; +import { AnyTuple } from "@polkadot/types/types"; +import { AssetId } from "@polkadot/types/interfaces/runtime"; +import { OrmlTraitsAssetRegistryAssetMetadata } from "@polkadot/types/lookup"; + +import { createSubstrateAPI } from "../../../../../src/factory"; +import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; +import { DefaultAssetRegistryAPI, DefaultInterBtcApi, DefaultTransactionAPI } from "../../../../../src"; +import { storageKeyToNthInner, stripHexPrefix } from "../../../../../src/utils"; + +export const assetRegistryTests = () => { + describe("AssetRegistry", () => { + let api: ApiPromise; + let interBtcAPI: DefaultInterBtcApi; + + let sudoAccount: KeyringPair; + + let assetRegistryMetadataPrefix: string; + let registeredKeysBefore: StorageKey[] = []; + + beforeAll(async () => { + const keyring = new Keyring({ type: "sr25519" }); + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + + sudoAccount = keyring.addFromUri(SUDO_URI); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); + + assetRegistryMetadataPrefix = api.query.assetRegistry.metadata.keyPrefix(); + // check which keys exist before the tests + const keys = await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix); + registeredKeysBefore = keys.toArray(); + }); + + afterAll(async () => { + // clean up keys created in tests if necessary + const registeredKeysAfter = (await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix)).toArray(); + + const previousKeyHashes = registeredKeysBefore.map((key) => stripHexPrefix(key.toHex())); + // need to use string comparison since the raw StorageKeys don't play nicely with .filter() + const newKeys = registeredKeysAfter.filter((key) => { + const newKeyHash = stripHexPrefix(key.toHex()); + return !previousKeyHashes.includes(newKeyHash); + }); + + if (newKeys.length > 0) { + // clean up assets registered in test(s) + const deleteKeysCall = api.tx.system.killStorage(newKeys); + await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(deleteKeysCall), + api.events.sudo.Sudid + ); + } + + await api.disconnect(); + }); + + /** + * This test checks that the returned metadata from the chain has all the fields we need to construct + * a `Currency` object. + * To see the fields required, take a look at {@link DefaultAssetRegistryAPI.metadataToCurrency}. + * + * Note: More detailed tests around the internal logic are in the unit tests. + */ + it("should get expected shape of AssetRegistry metadata", async () => { + // check if any assets have been registered + const existingKeys = (await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix)).toArray(); + + if (existingKeys.length === 0) { + // no existing foreign assets; register a new foreign asset for the test + const nextAssetId = (await interBtcAPI.api.query.assetRegistry.lastAssetId()).toNumber() + 1; + + const callToRegister = interBtcAPI.api.tx.assetRegistry.registerAsset( + { + decimals: 6, + name: "Test coin", + symbol: "TSC", + }, + api.createType("u32", nextAssetId) + ); + + // need sudo to add new foreign asset + const result = await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(callToRegister), + api.events.sudo.RegisteredAsset + ); + + expect(result.isCompleted).toBe(true); + } + + // get the metadata for the asset we just registered + const assetRegistryAPI = new DefaultAssetRegistryAPI(api); + const foreignAssetEntries = await assetRegistryAPI.getAssetRegistryEntries(); + const unwrappedMetadataTupleArray = DefaultAssetRegistryAPI.unwrapMetadataFromEntries(foreignAssetEntries); + + type OrmlARAMetadataKey = keyof OrmlTraitsAssetRegistryAssetMetadata; + + // now check that we have the fields we absolutely need on the returned metadata + // check {@link DefaultAssetRegistryAPI.metadataToCurrency} to see which fields are needed. + const requiredFieldClassnames = new Map([ + ["name" as OrmlARAMetadataKey, "Bytes"], + ["symbol" as OrmlARAMetadataKey, "Bytes"], + ["decimals" as OrmlARAMetadataKey, "u32"], + ]); + + for (const [storageKey, metadata] of unwrappedMetadataTupleArray) { + expect(metadata).toBeDefined(); + expect(storageKey).toBeDefined(); + + const storageKeyValue = storageKeyToNthInner(storageKey); + expect(storageKeyValue).toBeDefined(); + + for (const [key, className] of requiredFieldClassnames) { + try { + expect(metadata[key]).toBeDefined(); + } catch(_) { + throw Error(`Expected metadata to have field ${key.toString()}, but it does not.`); + } + + // check type + try { + expect(metadata[key]?.constructor.name).toBe(className); + } catch(_) { + throw Error( + `Expected metadata to have field ${key.toString()} of type ${className}, + but its type is ${metadata[key]?.constructor.name}.` + ); + } + } + } + }); + + // PRECONDITION: This test requires at least one foreign asset set up as collateral currency. + // This should have happened as part of preparations in initialize.test.ts + it("should get at least one collateral foreign asset", async () => { + const collateralForeignAssets = await interBtcAPI.assetRegistry.getCollateralForeignAssets(); + + expect(collateralForeignAssets.length).toBeGreaterThanOrEqual(1); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/asset-registry.test.ts b/test/integration/parachain/staging/sequential/asset-registry.test.ts deleted file mode 100644 index 13e78f272..000000000 --- a/test/integration/parachain/staging/sequential/asset-registry.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { assert } from "../../../../chai"; -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { StorageKey } from "@polkadot/types"; -import { AnyTuple } from "@polkadot/types/types"; -import { AssetId } from "@polkadot/types/interfaces/runtime"; -import { OrmlTraitsAssetRegistryAssetMetadata } from "@polkadot/types/lookup"; - -import { createSubstrateAPI } from "../../../../../src/factory"; -import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; -import { DefaultAssetRegistryAPI, DefaultInterBtcApi, DefaultTransactionAPI } from "../../../../../src"; -import { storageKeyToNthInner, stripHexPrefix } from "../../../../../src/utils"; - -describe("AssetRegistry", () => { - let api: ApiPromise; - let interBtcAPI: DefaultInterBtcApi; - - let sudoAccount: KeyringPair; - - let assetRegistryMetadataPrefix: string; - let registeredKeysBefore: StorageKey[] = []; - - before(async () => { - const keyring = new Keyring({ type: "sr25519" }); - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - - sudoAccount = keyring.addFromUri(SUDO_URI); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); - - assetRegistryMetadataPrefix = api.query.assetRegistry.metadata.keyPrefix(); - // check which keys exist before the tests - const keys = await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix); - registeredKeysBefore = keys.toArray(); - }); - - after(async () => { - // clean up keys created in tests if necessary - const registeredKeysAfter = (await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix)).toArray(); - - const previousKeyHashes = registeredKeysBefore.map((key) => stripHexPrefix(key.toHex())); - // need to use string comparison since the raw StorageKeys don't play nicely with .filter() - const newKeys = registeredKeysAfter.filter((key) => { - const newKeyHash = stripHexPrefix(key.toHex()); - return !previousKeyHashes.includes(newKeyHash); - }); - - if (newKeys.length > 0) { - // clean up assets registered in test(s) - const deleteKeysCall = api.tx.system.killStorage(newKeys); - await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(deleteKeysCall), - api.events.sudo.Sudid - ); - } - - return api.disconnect(); - }); - - /** - * This test checks that the returned metadata from the chain has all the fields we need to construct - * a `Currency` object. - * To see the fields required, take a look at {@link DefaultAssetRegistryAPI.metadataToCurrency}. - * - * Note: More detailed tests around the internal logic are in the unit tests. - */ - it("should get expected shape of AssetRegistry metadata", async () => { - // check if any assets have been registered - const existingKeys = (await interBtcAPI.api.rpc.state.getKeys(assetRegistryMetadataPrefix)).toArray(); - - if (existingKeys.length === 0) { - // no existing foreign assets; register a new foreign asset for the test - const nextAssetId = (await interBtcAPI.api.query.assetRegistry.lastAssetId()).toNumber() + 1; - - const callToRegister = interBtcAPI.api.tx.assetRegistry.registerAsset( - { - decimals: 6, - name: "Test coin", - symbol: "TSC", - }, - api.createType("u32", nextAssetId) - ); - - // need sudo to add new foreign asset - const result = await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(callToRegister), - api.events.sudo.RegisteredAsset - ); - - assert.isTrue(result.isCompleted, "Sudo event to create new foreign asset not found"); - } - - // get the metadata for the asset we just registered - const assetRegistryAPI = new DefaultAssetRegistryAPI(api); - const foreignAssetEntries = await assetRegistryAPI.getAssetRegistryEntries(); - const unwrappedMetadataTupleArray = DefaultAssetRegistryAPI.unwrapMetadataFromEntries(foreignAssetEntries); - - type OrmlARAMetadataKey = keyof OrmlTraitsAssetRegistryAssetMetadata; - - // now check that we have the fields we absolutely need on the returned metadata - // check {@link DefaultAssetRegistryAPI.metadataToCurrency} to see which fields are needed. - const requiredFieldClassnames = new Map([ - ["name" as OrmlARAMetadataKey, "Bytes"], - ["symbol" as OrmlARAMetadataKey, "Bytes"], - ["decimals" as OrmlARAMetadataKey, "u32"], - ]); - - for (const [storageKey, metadata] of unwrappedMetadataTupleArray) { - assert.isDefined(metadata, "Expected metadata to be defined, but it is not."); - assert.isDefined(storageKey, "Expected storage key to be defined, but it is not."); - - const storageKeyValue = storageKeyToNthInner(storageKey); - assert.isDefined(storageKeyValue, "Expected storage key can be decoded but it cannot."); - - for (const [key, className] of requiredFieldClassnames) { - assert.isDefined(metadata[key], `Expected metadata to have field ${key.toString()}, but it does not.`); - - // check type - assert.equal( - metadata[key]?.constructor.name, - className, - `Expected metadata to have field ${key.toString()} of type ${className}, - but its type is ${metadata[key]?.constructor.name}.` - ); - } - } - }); - - // PRECONDITION: This test requires at least one foreign asset set up as collateral currency. - // This should have happened as part of preparations in initialize.test.ts - it("should get at least one collateral foreign asset", async () => { - const collateralForeignAssets = await interBtcAPI.assetRegistry.getCollateralForeignAssets(); - - assert.isAtLeast( - collateralForeignAssets.length, - 1, - "Expected at least one foreign asset that can be used as collateral currency, but found none" - ); - }); -}); diff --git a/test/integration/parachain/staging/sequential/escrow.partial.ts b/test/integration/parachain/staging/sequential/escrow.partial.ts new file mode 100644 index 000000000..eb818dbd9 --- /dev/null +++ b/test/integration/parachain/staging/sequential/escrow.partial.ts @@ -0,0 +1,169 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import BN from "bn.js"; +import Big, { RoundingMode } from "big.js"; +import { SubmittableExtrinsic } from "@polkadot/api/types"; + +import { createSubstrateAPI } from "../../../../../src/factory"; +import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; +import { + DefaultInterBtcApi, + GovernanceCurrency, + InterBtcApi, + newAccountId, + newCurrencyId, + newMonetaryAmount, +} from "../../../../../src"; + +import { setRawStorage } from "../../../../../src/utils/storage"; +import { makeRandomPolkadotKeyPair, submitExtrinsic } from "../../../../utils/helpers"; + +function fundAccountCall(api: InterBtcApi, address: string): SubmittableExtrinsic<"promise"> { + return api.api.tx.tokens.setBalance( + address, + newCurrencyId(api.api, api.getGovernanceCurrency()), + "1152921504606846976", + 0 + ); +} + +// NOTE: we don't test withdraw here because even with instant-seal +// it is significantly slow to produce many blocks +export const escrowTests = () => { + describe("escrow", () => { + let api: ApiPromise; + let interBtcAPI: DefaultInterBtcApi; + + let userAccount1: KeyringPair; + let userAccount2: KeyringPair; + let userAccount3: KeyringPair; + let sudoAccount: KeyringPair; + + let governanceCurrency: GovernanceCurrency; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); + governanceCurrency = interBtcAPI.getGovernanceCurrency(); + + const keyring = new Keyring({ type: "sr25519" }); + sudoAccount = keyring.addFromUri(SUDO_URI); + + userAccount1 = makeRandomPolkadotKeyPair(keyring); + userAccount2 = makeRandomPolkadotKeyPair(keyring); + userAccount3 = makeRandomPolkadotKeyPair(keyring); + + await api.tx.sudo + .sudo( + api.tx.utility.batchAll([ + fundAccountCall(interBtcAPI, userAccount1.address), + fundAccountCall(interBtcAPI, userAccount2.address), + fundAccountCall(interBtcAPI, userAccount3.address), + ]) + ) + .signAndSend(sudoAccount); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + // PRECONDITION: This test must run first, so no tokens are locked. + it("Non-negative voting supply", async () => { + const totalVotingSupply = await interBtcAPI.escrow.totalVotingSupply(); + expect(totalVotingSupply.toString()).toEqual("0"); + }); + + // PRECONDITION: This test must run second, so no tokens are locked. + it("should return 0 reward and apy estimate", async () => { + const rewardsEstimate = await interBtcAPI.escrow.getRewardEstimate(newAccountId(api, userAccount1.address)); + + const expected = new Big(0); + expect(expected.eq(rewardsEstimate.apy)).toBe(true); + expect(rewardsEstimate.amount.isZero()).toBe(true); + }); + + it( + "should compute voting balance, total supply, and total staked balance", + async () => { + const user1Amount = newMonetaryAmount(100, governanceCurrency, true); + const user2Amount = newMonetaryAmount(60, governanceCurrency, true); + + const currentBlockNumber = await interBtcAPI.system.getCurrentBlockNumber(); + const unlockHeightDiff = (await interBtcAPI.escrow.getSpan()).toNumber(); + const stakedTotalBefore = await interBtcAPI.escrow.getTotalStakedBalance(); + + const user1InterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount1, ESPLORA_BASE_PATH); + await submitExtrinsic( + user1InterBtcAPI, + user1InterBtcAPI.escrow.createLock(user1Amount, currentBlockNumber + unlockHeightDiff) + ); + + const votingBalance = await interBtcAPI.escrow.votingBalance( + newAccountId(api, userAccount1.address), + currentBlockNumber + 0.4 * unlockHeightDiff + ); + const votingSupply = await interBtcAPI.escrow.totalVotingSupply(currentBlockNumber + 0.4 * unlockHeightDiff); + expect(votingBalance.toString()).toEqual(votingSupply.toString()); + + // Hardcoded value here to match the parachain + expect(votingSupply.toBig().round(2, RoundingMode.RoundDown).toString()).toEqual("0.62"); + + const firstYearRewards = "125000000000000000"; + const blocksPerYear = 2628000; + const rewardPerBlock = new BN(firstYearRewards).divn(blocksPerYear).abs(); + + await setRawStorage( + api, + api.query.escrowAnnuity.rewardPerBlock.key(), + api.createType("Balance", rewardPerBlock), + sudoAccount + ); + + const account1 = newAccountId(api, userAccount1.address); + + const rewardsEstimate = await interBtcAPI.escrow.getRewardEstimate(account1); + + expect(rewardsEstimate.amount.toBig().gt(0)).toBe(true); + expect(rewardsEstimate.apy.gte(100)).toBe(true); + + // Lock the tokens of a second user, to ensure total voting supply is still correct + const user2InterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount2, ESPLORA_BASE_PATH); + + await submitExtrinsic( + user2InterBtcAPI, + user2InterBtcAPI.escrow.createLock(user2Amount, currentBlockNumber + unlockHeightDiff) + ); + const votingSupplyAfterSecondUser = await interBtcAPI.escrow.totalVotingSupply( + currentBlockNumber + 0.4 * unlockHeightDiff + ); + expect( + votingSupplyAfterSecondUser.toBig().round(2, RoundingMode.RoundDown).toString() + ).toEqual("0.99"); + + const stakedTotalAfter = await interBtcAPI.escrow.getTotalStakedBalance(); + const lockedBalanceTotal = user1Amount.add(user2Amount); + const expectedNewBalance = stakedTotalBefore.add(lockedBalanceTotal); + + expect(stakedTotalAfter.eq(expectedNewBalance)).toBe(true); + } + ); + + it("should increase amount and unlock height", async () => { + const userAmount = newMonetaryAmount(1000, governanceCurrency, true); + const currentBlockNumber = await interBtcAPI.system.getCurrentBlockNumber(); + const unlockHeightDiff = (await interBtcAPI.escrow.getSpan()).toNumber(); + + const user3InterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount3, ESPLORA_BASE_PATH); + await submitExtrinsic( + user3InterBtcAPI, + user3InterBtcAPI.escrow.createLock(userAmount, currentBlockNumber + unlockHeightDiff) + ); + await submitExtrinsic(user3InterBtcAPI, user3InterBtcAPI.escrow.increaseAmount(userAmount)); + await submitExtrinsic( + user3InterBtcAPI, + user3InterBtcAPI.escrow.increaseUnlockHeight(currentBlockNumber + unlockHeightDiff + unlockHeightDiff) + ); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/escrow.test.ts b/test/integration/parachain/staging/sequential/escrow.test.ts deleted file mode 100644 index c65ce4267..000000000 --- a/test/integration/parachain/staging/sequential/escrow.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { assert } from "chai"; -import { KeyringPair } from "@polkadot/keyring/types"; -import BN from "bn.js"; -import Big, { RoundingMode } from "big.js"; -import { SubmittableExtrinsic } from "@polkadot/api/types"; -import { AccountId } from "@polkadot/types/interfaces"; - -import { createSubstrateAPI } from "../../../../../src/factory"; -import { ESPLORA_BASE_PATH, PARACHAIN_ENDPOINT, SUDO_URI } from "../../../../config"; -import { - decodeFixedPointType, - DefaultInterBtcApi, - GovernanceCurrency, - InterBtcApi, - newAccountId, - newCurrencyId, - newMonetaryAmount, -} from "../../../../../src"; - -import { setRawStorage } from "../../../../../src/utils/storage"; -import { makeRandomPolkadotKeyPair, submitExtrinsic } from "../../../../utils/helpers"; - -function fundAccountCall(api: InterBtcApi, address: string): SubmittableExtrinsic<"promise"> { - return api.api.tx.tokens.setBalance( - address, - newCurrencyId(api.api, api.getGovernanceCurrency()), - "1152921504606846976", - 0 - ); -} - -async function getEscrowStake(api: ApiPromise, accountId: AccountId): Promise { - const rawStake = await api.query.escrowRewards.stake([null, accountId]); - return decodeFixedPointType(rawStake); -} - -async function getEscrowTotalStake(api: ApiPromise): Promise { - const rawTotalStake = await api.query.escrowRewards.totalStake(null); - return decodeFixedPointType(rawTotalStake); -} - -async function getEscrowRewardPerToken(api: InterBtcApi): Promise { - const governanceCurrencyId = newCurrencyId(api.api, api.getGovernanceCurrency()); - const rawRewardPerToken = await api.api.query.escrowRewards.rewardPerToken(governanceCurrencyId, null); - return decodeFixedPointType(rawRewardPerToken); -} - -// NOTE: we don't test withdraw here because even with instant-seal -// it is significantly slow to produce many blocks -describe("escrow", () => { - let api: ApiPromise; - let interBtcAPI: DefaultInterBtcApi; - - let userAccount1: KeyringPair; - let userAccount2: KeyringPair; - let userAccount3: KeyringPair; - let sudoAccount: KeyringPair; - - let governanceCurrency: GovernanceCurrency; - - before(async function () { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); - governanceCurrency = interBtcAPI.getGovernanceCurrency(); - - const keyring = new Keyring({ type: "sr25519" }); - sudoAccount = keyring.addFromUri(SUDO_URI); - - userAccount1 = makeRandomPolkadotKeyPair(keyring); - userAccount2 = makeRandomPolkadotKeyPair(keyring); - userAccount3 = makeRandomPolkadotKeyPair(keyring); - - await api.tx.sudo - .sudo( - api.tx.utility.batchAll([ - fundAccountCall(interBtcAPI, userAccount1.address), - fundAccountCall(interBtcAPI, userAccount2.address), - fundAccountCall(interBtcAPI, userAccount3.address), - ]) - ) - .signAndSend(sudoAccount); - }); - - after(async () => { - api.disconnect(); - }); - - // PRECONDITION: This test must run first, so no tokens are locked. - it("Non-negative voting supply", async () => { - const totalVotingSupply = await interBtcAPI.escrow.totalVotingSupply(); - assert.equal( - totalVotingSupply.toString(), - "0", - "Voting supply balance should be zero before any tokens are locked" - ); - }); - - // PRECONDITION: This test must run second, so no tokens are locked. - it("should return 0 reward and apy estimate", async () => { - const rewardsEstimate = await interBtcAPI.escrow.getRewardEstimate(newAccountId(api, userAccount1.address)); - - const expected = new Big(0); - assert.isTrue(expected.eq(rewardsEstimate.apy), `APY should be 0, but is ${rewardsEstimate.apy.toString()}`); - assert.isTrue( - rewardsEstimate.amount.isZero(), - `Rewards should be 0, but are ${rewardsEstimate.amount.toHuman()}` - ); - }); - - it("should compute voting balance, total supply, and total staked balance", async () => { - const user1Amount = newMonetaryAmount(100, governanceCurrency, true); - const user2Amount = newMonetaryAmount(60, governanceCurrency, true); - - const currentBlockNumber = await interBtcAPI.system.getCurrentBlockNumber(); - const unlockHeightDiff = (await interBtcAPI.escrow.getSpan()).toNumber(); - const stakedTotalBefore = await interBtcAPI.escrow.getTotalStakedBalance(); - - interBtcAPI.setAccount(userAccount1); - await submitExtrinsic( - interBtcAPI, - interBtcAPI.escrow.createLock(user1Amount, currentBlockNumber + unlockHeightDiff) - ); - - const votingBalance = await interBtcAPI.escrow.votingBalance( - newAccountId(api, userAccount1.address), - currentBlockNumber + 0.4 * unlockHeightDiff - ); - const votingSupply = await interBtcAPI.escrow.totalVotingSupply(currentBlockNumber + 0.4 * unlockHeightDiff); - assert.equal(votingBalance.toString(), votingSupply.toString()); - - // Hardcoded value here to match the parachain - assert.equal(votingSupply.toBig().round(2, RoundingMode.RoundDown).toString(), "0.62"); - - const firstYearRewards = "125000000000000000"; - const blocksPerYear = 2628000; - const rewardPerBlock = new BN(firstYearRewards).divn(blocksPerYear).abs(); - - await setRawStorage( - api, - api.query.escrowAnnuity.rewardPerBlock.key(), - api.createType("Balance", rewardPerBlock), - sudoAccount - ); - - const account1 = newAccountId(api, userAccount1.address); - - const rewardsEstimate = await interBtcAPI.escrow.getRewardEstimate(account1); - - assert.isTrue( - rewardsEstimate.amount.toBig().gt(0), - `Expected reward to be a positive amount, got ${rewardsEstimate.amount.toString()}` - ); - assert.isTrue( - rewardsEstimate.apy.gte(100), - `Expected more than 100% APY, got ${rewardsEstimate.apy.toString()}` - ); - - // Lock the tokens of a second user, to ensure total voting supply is still correct - interBtcAPI.setAccount(userAccount2); - await submitExtrinsic( - interBtcAPI, - interBtcAPI.escrow.createLock(user2Amount, currentBlockNumber + unlockHeightDiff) - ); - const votingSupplyAfterSecondUser = await interBtcAPI.escrow.totalVotingSupply( - currentBlockNumber + 0.4 * unlockHeightDiff - ); - assert.equal(votingSupplyAfterSecondUser.toBig().round(2, RoundingMode.RoundDown).toString(), "0.99"); - - const stakedTotalAfter = await interBtcAPI.escrow.getTotalStakedBalance(); - const lockedBalanceTotal = user1Amount.add(user2Amount); - const expectedNewBalance = stakedTotalBefore.add(lockedBalanceTotal); - - assert.isTrue( - stakedTotalAfter.eq(expectedNewBalance), - `Expected total staked balance to have increased by locked amounts: ${lockedBalanceTotal.toHuman()}, - but old balance was ${stakedTotalBefore.toHuman()} and new balance is ${stakedTotalAfter.toHuman()}` - ); - }); - - it("should increase amount and unlock height", async () => { - const userAmount = newMonetaryAmount(1000, governanceCurrency, true); - const currentBlockNumber = await interBtcAPI.system.getCurrentBlockNumber(); - const unlockHeightDiff = (await interBtcAPI.escrow.getSpan()).toNumber(); - - interBtcAPI.setAccount(userAccount3); - await submitExtrinsic( - interBtcAPI, - interBtcAPI.escrow.createLock(userAmount, currentBlockNumber + unlockHeightDiff) - ); - await submitExtrinsic(interBtcAPI, interBtcAPI.escrow.increaseAmount(userAmount)); - await submitExtrinsic( - interBtcAPI, - interBtcAPI.escrow.increaseUnlockHeight(currentBlockNumber + unlockHeightDiff + unlockHeightDiff) - ); - }); -}); diff --git a/test/integration/parachain/staging/sequential/index.test.ts b/test/integration/parachain/staging/sequential/index.test.ts new file mode 100644 index 000000000..69afe80ba --- /dev/null +++ b/test/integration/parachain/staging/sequential/index.test.ts @@ -0,0 +1,24 @@ +import { ammTests } from "./amm.partial"; +import { assetRegistryTests } from "./asset-registry.partial"; +import { escrowTests } from "./escrow.partial"; +import { issueTests } from "./issue.partial"; +import { loansTests } from "./loans.partial"; +import { nominationTests } from "./nomination.partial"; +import { oracleTests } from "./oracle.partial"; +import { redeemTests } from "./redeem.partial"; +import { replaceTests } from "./replace.partial"; +import { vaultsTests } from "./vaults.partial"; + +// this forces jest to run the sequential tests in a specific order +// replicated the previous behavior (alphabetic) instead of using +// jest's default order (pretty much files in random order) +ammTests(); +assetRegistryTests(); +escrowTests(); +issueTests(); +loansTests(); +nominationTests(); +oracleTests(); +redeemTests(); +replaceTests(); +vaultsTests(); \ No newline at end of file diff --git a/test/integration/parachain/staging/sequential/issue.partial.ts b/test/integration/parachain/staging/sequential/issue.partial.ts new file mode 100644 index 000000000..73af8bfff --- /dev/null +++ b/test/integration/parachain/staging/sequential/issue.partial.ts @@ -0,0 +1,263 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { + ATOMIC_UNIT, + CollateralCurrencyExt, + currencyIdToMonetaryCurrency, + DefaultInterBtcApi, + getIssueRequestsFromExtrinsicResult, + InterBtcApi, + InterbtcPrimitivesVaultId, + IssueStatus, + newAccountId, + newMonetaryAmount, +} from "../../../../../src/index"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { + USER_1_URI, + VAULT_1_URI, + VAULT_2_URI, + BITCOIN_CORE_HOST, + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_WALLET, + PARACHAIN_ENDPOINT, + ESPLORA_BASE_PATH, +} from "../../../../config"; +import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; +import { issueSingle } from "../../../../../src/utils/issueRedeem"; +import { newVaultId, WrappedCurrency } from "../../../../../src"; +import { + getCorrespondingCollateralCurrenciesForTests, + getIssuableAmounts, + runWhileMiningBTCBlocks, + submitExtrinsic, + sudo, +} from "../../../../utils/helpers"; + +export const issueTests = () => { + describe("issue", () => { + let api: ApiPromise; + let bitcoinCoreClient: BitcoinCoreClient; + let keyring: Keyring; + let userInterBtcAPI: InterBtcApi; + + let userAccount: KeyringPair; + let vault_1: KeyringPair; + let vault_1_ids: Array; + let vault_2: KeyringPair; + let vault_2_ids: Array; + + let wrappedCurrency: WrappedCurrency; + let collateralCurrencies: Array; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + keyring = new Keyring({ type: "sr25519" }); + userAccount = keyring.addFromUri(USER_1_URI); + userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); + collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(userInterBtcAPI.getGovernanceCurrency()); + wrappedCurrency = userInterBtcAPI.getWrappedCurrency(); + + vault_1 = keyring.addFromUri(VAULT_1_URI); + vault_2 = keyring.addFromUri(VAULT_2_URI); + vault_1_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) + ); + vault_2_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency) + ); + + bitcoinCoreClient = new BitcoinCoreClient( + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_HOST, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_WALLET + ); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + it("should request one issue", async () => { + // may fail if the relay isn't fully initialized + const amount = newMonetaryAmount(0.0001, wrappedCurrency, true); + const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); + const requestResults = await getIssueRequestsFromExtrinsicResult( + userInterBtcAPI, + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.issue.request(amount)) + ); + expect(requestResults).toHaveLength(1); + const requestResult = requestResults[0]; + const issueRequest = await userInterBtcAPI.issue.getRequestById(requestResult.id); + + expect(issueRequest.wrappedAmount.toString()).toBe(amount.sub(feesToPay).toString()); + }); + + it("should list existing requests", async () => { + const issueRequests = await userInterBtcAPI.issue.list(); + expect(issueRequests.length).toBeGreaterThanOrEqual(1); + }); + + // FIXME: can we make this test more elegant? i.e. check what is issuable + // by two vaults can request exactly that amount instead of multiplying by 1.1 + it("should batch request across several vaults", async () => { + const requestLimits = await userInterBtcAPI.issue.getRequestLimits(); + const amount = requestLimits.singleVaultMaxIssuable.mul(1.1); + const issueRequests = await getIssueRequestsFromExtrinsicResult( + userInterBtcAPI, + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.issue.request(amount)) + ); + expect(issueRequests).toHaveLength(2); + const issuedAmount1 = issueRequests[0].wrappedAmount; + const issueFee1 = issueRequests[0].bridgeFee; + const issuedAmount2 = issueRequests[1].wrappedAmount; + const issueFee2 = issueRequests[1].bridgeFee; + + expect(issuedAmount1.add(issueFee1).add(issuedAmount2).add(issueFee2).toBig().round(5).toString()) + .toBe(amount.toBig().round(5).toString()); + }); + + it("should request and manually execute issue", async () => { + for (const vault_2_id of vault_2_ids) { + const currencyTicker = (await currencyIdToMonetaryCurrency(api, vault_2_id.currencies.collateral)).ticker; + + const amount = newMonetaryAmount(0.00001, wrappedCurrency, true); + const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); + const oneSatoshi = newMonetaryAmount(1, wrappedCurrency, false); + const issueResult = await issueSingle( + userInterBtcAPI, + bitcoinCoreClient, + userAccount, + amount, + vault_2_id, + false, + false + ); + + // calculate expected final balance and round the fees value as the parachain will do so when calculating fees. + const amtInSatoshi = amount.toBig(ATOMIC_UNIT); + const feesInSatoshiRounded = feesToPay.toBig(ATOMIC_UNIT).round(0); + const expectedFinalBalance = amtInSatoshi + .sub(feesInSatoshiRounded) + .sub(oneSatoshi.toBig(ATOMIC_UNIT)) + .toString(); + + expect( + issueResult.finalWrappedTokenBalance + .sub(issueResult.initialWrappedTokenBalance) + .toBig(ATOMIC_UNIT) + .toString() + ).toBe(expectedFinalBalance); + } + }, 1000 * 60); + + it("should get issueBtcDustValue", async () => { + const dust = await userInterBtcAPI.api.query.issue.issueBtcDustValue(); + expect(dust.toString()).toBe("1000"); + }); + + it("should getFeesToPay", async () => { + const amount = newMonetaryAmount(2, wrappedCurrency, true); + const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); + const feeRate = await userInterBtcAPI.issue.getFeeRate(); + + const expectedFeesInBTC = amount.toBig().toNumber() * feeRate.toNumber(); + + // compare floating point values in BTC, allowing for small delta difference + const decimalsToCheck = 5; + const maxDelta = 10**(-decimalsToCheck); // 0.00001 + // rounded after max delta's decimals, + // probably not needed, but safeguards against Big -> Number conversion having granularity issues. + const differenceRounded = Math.abs(feesToPay.toBig().sub(expectedFeesInBTC).round(decimalsToCheck+2).toNumber()); + expect(differenceRounded).toBeLessThan(maxDelta); + }); + + it("should getFeeRate", async () => { + const feePercentage = await userInterBtcAPI.issue.getFeeRate(); + expect(feePercentage.toNumber()).toBe(0.0015); + }); + + it("should getRequestLimits", async () => { + const requestLimits = await userInterBtcAPI.issue.getRequestLimits(); + const singleMaxIssuable = requestLimits.singleVaultMaxIssuable; + const totalMaxIssuable = requestLimits.totalMaxIssuable; + + const issuableAmounts = await getIssuableAmounts(userInterBtcAPI); + const singleIssueable = issuableAmounts.reduce( + (prev, curr) => (prev > curr ? prev : curr), + newMonetaryAmount(0, wrappedCurrency) + ); + const totalIssuable = issuableAmounts.reduce((prev, curr) => prev.add(curr)); + + try { + expect(singleMaxIssuable.toBig().sub(singleIssueable.toBig()).abs().lte(1)).toBe(true); + } catch(_) { + throw Error(`${singleMaxIssuable.toHuman()} != ${singleIssueable.toHuman()}`); + } + + try { + expect(totalMaxIssuable.toBig().sub(totalIssuable.toBig()).abs().lte(1)).toBe(true); + } catch(_) { + throw Error(`${totalMaxIssuable.toHuman()} != ${totalIssuable.toHuman()}`); + } + }); + + // This test should be kept at the end of the file as it will ban the vault used for issuing + it("should cancel an issue request", async () => { + for (const vault_2_id of vault_2_ids) { + await runWhileMiningBTCBlocks(bitcoinCoreClient, async () => { + const initialIssuePeriod = await userInterBtcAPI.issue.getIssuePeriod(); + await sudo(userInterBtcAPI, async () => { + await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.issue.setIssuePeriod(1)); + }); + try { + // request issue + const amount = newMonetaryAmount(0.0000121, wrappedCurrency, true); + const vaultCollateral = await currencyIdToMonetaryCurrency(api, vault_2_id.currencies.collateral); + const requestResults = await getIssueRequestsFromExtrinsicResult( + userInterBtcAPI, + await submitExtrinsic( + userInterBtcAPI, + await userInterBtcAPI.issue.request( + amount, + newAccountId(api, vault_2.address), + vaultCollateral + ) + ) + ); + expect(requestResults).toHaveLength(1); + const requestResult = requestResults[0]; + + await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.issue.cancel(requestResult.id)); + + const issueRequest = await userInterBtcAPI.issue.getRequestById(requestResult.id); + expect(issueRequest.status).toBe(IssueStatus.Cancelled); + + // Set issue period back to its initial value to minimize side effects. + await sudo(userInterBtcAPI, async () => { + await submitExtrinsic( + userInterBtcAPI, + userInterBtcAPI.issue.setIssuePeriod(initialIssuePeriod) + ); + }); + } catch (e) { + // Set issue period back to its initial value to minimize side effects. + await sudo(userInterBtcAPI, async () => { + await submitExtrinsic( + userInterBtcAPI, + userInterBtcAPI.issue.setIssuePeriod(initialIssuePeriod) + ); + }); + throw e; + } + }); + } + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/issue.test.ts b/test/integration/parachain/staging/sequential/issue.test.ts deleted file mode 100644 index d93c4e5e6..000000000 --- a/test/integration/parachain/staging/sequential/issue.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { - ATOMIC_UNIT, - CollateralCurrencyExt, - currencyIdToMonetaryCurrency, - DefaultInterBtcApi, - getIssueRequestsFromExtrinsicResult, - InterBtcApi, - InterbtcPrimitivesVaultId, - IssueStatus, - newAccountId, - newMonetaryAmount, -} from "../../../../../src/index"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { assert } from "../../../../chai"; -import { - USER_1_URI, - VAULT_1_URI, - VAULT_2_URI, - BITCOIN_CORE_HOST, - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_WALLET, - PARACHAIN_ENDPOINT, - ESPLORA_BASE_PATH, -} from "../../../../config"; -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; -import { issueSingle } from "../../../../../src/utils/issueRedeem"; -import { newVaultId, WrappedCurrency } from "../../../../../src"; -import { - getCorrespondingCollateralCurrenciesForTests, - getIssuableAmounts, - runWhileMiningBTCBlocks, - submitExtrinsic, - sudo, -} from "../../../../utils/helpers"; - -describe("issue", () => { - let api: ApiPromise; - let bitcoinCoreClient: BitcoinCoreClient; - let keyring: Keyring; - let userInterBtcAPI: InterBtcApi; - - let userAccount: KeyringPair; - let vault_1: KeyringPair; - let vault_1_ids: Array; - let vault_2: KeyringPair; - let vault_2_ids: Array; - - let wrappedCurrency: WrappedCurrency; - let collateralCurrencies: Array; - - before(async function () { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - keyring = new Keyring({ type: "sr25519" }); - userAccount = keyring.addFromUri(USER_1_URI); - userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); - collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(userInterBtcAPI.getGovernanceCurrency()); - wrappedCurrency = userInterBtcAPI.getWrappedCurrency(); - - vault_1 = keyring.addFromUri(VAULT_1_URI); - vault_2 = keyring.addFromUri(VAULT_2_URI); - vault_1_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) - ); - vault_2_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency) - ); - - bitcoinCoreClient = new BitcoinCoreClient( - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_HOST, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_WALLET - ); - }); - - after(async () => { - api.disconnect(); - }); - - it("should request one issue", async () => { - // may fail if the relay isn't fully initialized - const amount = newMonetaryAmount(0.0001, wrappedCurrency, true); - const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); - const requestResults = await getIssueRequestsFromExtrinsicResult( - userInterBtcAPI, - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.issue.request(amount)) - ); - assert.equal( - requestResults.length, - 1, - "Created multiple requests instead of one (ensure vault has sufficient collateral)" - ); - const requestResult = requestResults[0]; - const issueRequest = await userInterBtcAPI.issue.getRequestById(requestResult.id); - assert.equal( - issueRequest.wrappedAmount.toString(), - amount.sub(feesToPay).toString(), - "Amount different than expected" - ); - }); - - it("should list existing requests", async () => { - const issueRequests = await userInterBtcAPI.issue.list(); - assert.isAtLeast(issueRequests.length, 1, "Should have at least 1 issue request"); - }); - - // FIXME: can we make this test more elegant? i.e. check what is issuable - // by two vaults can request exactly that amount instead of multiplying by 1.1 - it("should batch request across several vaults", async () => { - const requestLimits = await userInterBtcAPI.issue.getRequestLimits(); - const amount = requestLimits.singleVaultMaxIssuable.mul(1.1); - const issueRequests = await getIssueRequestsFromExtrinsicResult( - userInterBtcAPI, - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.issue.request(amount)) - ); - assert.equal(issueRequests.length, 2, "Created wrong amount of requests, vaults have insufficient collateral"); - const issuedAmount1 = issueRequests[0].wrappedAmount; - const issueFee1 = issueRequests[0].bridgeFee; - const issuedAmount2 = issueRequests[1].wrappedAmount; - const issueFee2 = issueRequests[1].bridgeFee; - assert.equal( - issuedAmount1.add(issueFee1).add(issuedAmount2).add(issueFee2).toBig().round(5).toString(), - amount.toBig().round(5).toString(), - "Issued amount is not equal to requested amount" - ); - }); - - it("should request and manually execute issue", async () => { - for (const vault_2_id of vault_2_ids) { - const currencyTicker = (await currencyIdToMonetaryCurrency(api, vault_2_id.currencies.collateral)).ticker; - - const amount = newMonetaryAmount(0.00001, wrappedCurrency, true); - const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); - const oneSatoshi = newMonetaryAmount(1, wrappedCurrency, false); - const issueResult = await issueSingle( - userInterBtcAPI, - bitcoinCoreClient, - userAccount, - amount, - vault_2_id, - false, - false - ); - - // calculate expected final balance and round the fees value as the parachain will do so when calculating fees. - const amtInSatoshi = amount.toBig(ATOMIC_UNIT); - const feesInSatoshiRounded = feesToPay.toBig(ATOMIC_UNIT).round(0); - const expectedFinalBalance = amtInSatoshi - .sub(feesInSatoshiRounded) - .sub(oneSatoshi.toBig(ATOMIC_UNIT)) - .toString(); - assert.equal( - issueResult.finalWrappedTokenBalance - .sub(issueResult.initialWrappedTokenBalance) - .toBig(ATOMIC_UNIT) - .toString(), - expectedFinalBalance, - `Final balance was not increased by the exact amount specified (collateral: ${currencyTicker})` - ); - } - }).timeout(1000 * 60); - - it("should get issueBtcDustValue", async () => { - const dust = await userInterBtcAPI.api.query.issue.issueBtcDustValue(); - assert.equal(dust.toString(), "1000"); - }); - - it("should getFeesToPay", async () => { - const amount = newMonetaryAmount(2, wrappedCurrency, true); - const feesToPay = await userInterBtcAPI.issue.getFeesToPay(amount); - const feeRate = await userInterBtcAPI.issue.getFeeRate(); - - const expectedFeesInBTC = amount.toBig().toNumber() * feeRate.toNumber(); - // compare floating point values in BTC, allowing for small delta difference - assert.closeTo( - feesToPay.toBig().toNumber(), - expectedFeesInBTC, - 0.00001, - "Calculated fees in BTC do not match expectations" - ); - }); - - it("should getFeeRate", async () => { - const feePercentage = await userInterBtcAPI.issue.getFeeRate(); - assert.equal(feePercentage.toString(), "0.0015"); - }); - - it("should getRequestLimits", async () => { - const requestLimits = await userInterBtcAPI.issue.getRequestLimits(); - const singleMaxIssuable = requestLimits.singleVaultMaxIssuable; - const totalMaxIssuable = requestLimits.totalMaxIssuable; - - const issuableAmounts = await getIssuableAmounts(userInterBtcAPI); - const singleIssueable = issuableAmounts.reduce( - (prev, curr) => (prev > curr ? prev : curr), - newMonetaryAmount(0, wrappedCurrency) - ); - const totalIssuable = issuableAmounts.reduce((prev, curr) => prev.add(curr)); - - assert.isTrue( - singleMaxIssuable.toBig().sub(singleIssueable.toBig()).abs().lte(1), - `${singleMaxIssuable.toHuman()} != ${singleIssueable.toHuman()}` - ); - assert.isTrue( - totalMaxIssuable.toBig().sub(totalIssuable.toBig()).abs().lte(1), - `${totalMaxIssuable.toHuman()} != ${totalIssuable.toHuman()}` - ); - }); - - // This test should be kept at the end of the file as it will ban the vault used for issuing - it("should cancel an issue request", async () => { - for (const vault_2_id of vault_2_ids) { - await runWhileMiningBTCBlocks(bitcoinCoreClient, async () => { - const initialIssuePeriod = await userInterBtcAPI.issue.getIssuePeriod(); - await sudo(userInterBtcAPI, async () => { - await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.issue.setIssuePeriod(1)); - }); - try { - // request issue - const amount = newMonetaryAmount(0.0000121, wrappedCurrency, true); - const vaultCollateral = await currencyIdToMonetaryCurrency(api, vault_2_id.currencies.collateral); - const requestResults = await getIssueRequestsFromExtrinsicResult( - userInterBtcAPI, - await submitExtrinsic( - userInterBtcAPI, - await userInterBtcAPI.issue.request( - amount, - newAccountId(api, vault_2.address), - vaultCollateral - ) - ) - ); - assert.equal(requestResults.length, 1, "Test broken: more than one issue request created"); // sanity check - const requestResult = requestResults[0]; - - await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.issue.cancel(requestResult.id)); - - const issueRequest = await userInterBtcAPI.issue.getRequestById(requestResult.id); - assert.isTrue(issueRequest.status === IssueStatus.Cancelled, "Failed to cancel issue request"); - - // Set issue period back to its initial value to minimize side effects. - await sudo(userInterBtcAPI, async () => { - await submitExtrinsic( - userInterBtcAPI, - userInterBtcAPI.issue.setIssuePeriod(initialIssuePeriod) - ); - }); - } catch (e) { - // Set issue period back to its initial value to minimize side effects. - await sudo(userInterBtcAPI, async () => { - await submitExtrinsic( - userInterBtcAPI, - userInterBtcAPI.issue.setIssuePeriod(initialIssuePeriod) - ); - }); - throw e; - } - }); - } - }); -}); diff --git a/test/integration/parachain/staging/sequential/loans.partial.ts b/test/integration/parachain/staging/sequential/loans.partial.ts new file mode 100644 index 000000000..9cba7e5f9 --- /dev/null +++ b/test/integration/parachain/staging/sequential/loans.partial.ts @@ -0,0 +1,588 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { + CurrencyExt, + currencyIdToMonetaryCurrency, + DefaultInterBtcApi, + DefaultLoansAPI, + DefaultOracleAPI, + DefaultTransactionAPI, + getUnderlyingCurrencyFromLendTokenId, + InterBtcApi, + LendToken, + newAccountId, + newCurrencyId, + newMonetaryAmount, +} from "../../../../../src/index"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { USER_1_URI, USER_2_URI, PARACHAIN_ENDPOINT, ESPLORA_BASE_PATH, SUDO_URI } from "../../../../config"; +import { callWithExchangeRate, includesStringified, submitExtrinsic } from "../../../../utils/helpers"; +import { InterbtcPrimitivesCurrencyId } from "@polkadot/types/lookup"; +import Big from "big.js"; +import { InterBtc, MonetaryAmount } from "@interlay/monetary-js"; +import { AccountId } from "@polkadot/types/interfaces"; + +export const loansTests = () => { + describe("loans", () => { + let api: ApiPromise; + let keyring: Keyring; + let userInterBtcAPI: InterBtcApi; + let user2InterBtcAPI: InterBtcApi; + let sudoInterBtcAPI: InterBtcApi; + let LoansAPI: DefaultLoansAPI; + + let userAccount: KeyringPair; + let user2Account: KeyringPair; + let sudoAccount: KeyringPair; + let userAccountId: AccountId; + let user2AccountId: AccountId; + + let lendTokenId1: InterbtcPrimitivesCurrencyId; + let lendTokenId2: InterbtcPrimitivesCurrencyId; + let underlyingCurrencyId: InterbtcPrimitivesCurrencyId; + let underlyingCurrency: CurrencyExt; + let underlyingCurrencyId2: InterbtcPrimitivesCurrencyId; + let underlyingCurrency2: CurrencyExt; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + keyring = new Keyring({ type: "sr25519" }); + userAccount = keyring.addFromUri(USER_1_URI); + user2Account = keyring.addFromUri(USER_2_URI); + userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); + user2InterBtcAPI = new DefaultInterBtcApi(api, "regtest", user2Account, ESPLORA_BASE_PATH); + + sudoAccount = keyring.addFromUri(SUDO_URI); + sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); + userAccountId = newAccountId(api, userAccount.address); + user2AccountId = newAccountId(api, user2Account.address); + const wrappedCurrency = sudoInterBtcAPI.getWrappedCurrency(); + const oracleAPI = new DefaultOracleAPI(api, wrappedCurrency); + + LoansAPI = new DefaultLoansAPI(api, wrappedCurrency, oracleAPI); + + // Add market for governance currency. + underlyingCurrencyId = sudoInterBtcAPI.api.consts.currency.getNativeCurrencyId; + underlyingCurrency = sudoInterBtcAPI.getGovernanceCurrency(); + + underlyingCurrencyId2 = sudoInterBtcAPI.api.consts.currency.getRelayChainCurrencyId; + underlyingCurrency2 = await currencyIdToMonetaryCurrency(api, underlyingCurrencyId2); + + lendTokenId1 = newCurrencyId(sudoInterBtcAPI.api, { lendToken: { id: 1 } } as LendToken); + lendTokenId2 = newCurrencyId(sudoInterBtcAPI.api, { lendToken: { id: 2 } } as LendToken); + + const percentageToPermill = (percentage: number) => percentage * 10000; + + const marketData = (id: InterbtcPrimitivesCurrencyId) => ({ + collateralFactor: percentageToPermill(50), + liquidationThreshold: percentageToPermill(55), + reserveFactor: percentageToPermill(15), + closeFactor: percentageToPermill(50), + liquidateIncentive: "1100000000000000000", + liquidateIncentiveReservedFactor: percentageToPermill(3), + rateModel: { + Jump: { + baseRate: "20000000000000000", + jumpRate: "100000000000000000", + fullRate: "320000000000000000", + jumpUtilization: percentageToPermill(80), + }, + }, + state: "Pending", + supplyCap: "5000000000000000000000", + borrowCap: "5000000000000000000000", + lendTokenId: id, + }); + + const addMarket1Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( + underlyingCurrencyId, + marketData(lendTokenId1) + ); + const addMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( + underlyingCurrencyId2, + marketData(lendTokenId2) + ); + const activateMarket1Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId); + const activateMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId2); + const addMarkets = sudoInterBtcAPI.api.tx.utility.batchAll([ + addMarket1Extrinsic, + addMarket2Extrinsic, + activateMarket1Extrinsic, + activateMarket2Extrinsic, + ]); + + const result = await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(addMarkets), + api.events.sudo.Sudid + ); + + expect(result.isCompleted).toBe(true); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + afterEach(() => { + // discard any stubbed methods after each test + jest.restoreAllMocks(); + }); + + describe("getLendTokens", () => { + it("should get lend token for each existing market", async () => { + const [markets, lendTokens] = await Promise.all([ + api.query.loans.markets.entries(), + userInterBtcAPI.loans.getLendTokens(), + ]); + + const marketsUnderlyingCurrencyId = markets[0][0].args[0]; + + expect(markets.length).toBe(lendTokens.length); + + expect(marketsUnderlyingCurrencyId.eq(underlyingCurrencyId)).toBe(true); + }); + + it( + "should return LendToken in correct format - 'q' prefix, correct id", + async () => { + // Requires first market to be initialized for governance currency. + const lendTokens = await userInterBtcAPI.loans.getLendTokens(); + const lendToken = lendTokens[0]; + + // Should have same amount of decimals as underlying currency. + expect(lendToken.decimals).toBe(underlyingCurrency.decimals); + + // Should add 'q' prefix. + expect(lendToken.name).toBe(`q${underlyingCurrency.name}`); + expect(lendToken.ticker).toBe(`q${underlyingCurrency.ticker}`); + + expect(lendToken.lendToken.id).toBe(lendTokenId1.asLendToken.toNumber()); + } + ); + + it("should return empty array if no market exists", async () => { + // Mock empty list returned from chain. + jest.spyOn(LoansAPI, "getLoansMarkets").mockClear().mockReturnValue(Promise.resolve([])); + + const lendTokens = await LoansAPI.getLendTokens(); + expect(lendTokens).toHaveLength(0); + }); + }); + + describe("getLendPositionsOfAccount", () => { + let lendAmount: MonetaryAmount; + beforeAll(async () => { + lendAmount = newMonetaryAmount(1, underlyingCurrency, true); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); + }); + + it( + "should get all lend positions of account in correct format", + async () => { + const [lendPosition] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + expect(lendPosition.amount.toString()).toBe(lendAmount.toString()); + expect(lendPosition.amount.currency).toBe(underlyingCurrency); + expect(lendPosition.isCollateral).toBe(false); + // TODO: add tests for more markets + } + ); + + it( + "should get correct data after position is enabled as collateral", + async () => { + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)); + + const [lendPosition] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + expect(lendPosition.isCollateral).toBe(true); + } + ); + + it( + "should get empty array when no lend position exists for account", + async () => { + const lendPositions = await user2InterBtcAPI.loans.getLendPositionsOfAccount(user2AccountId); + + expect(lendPositions).toHaveLength(0); + } + ); + + it.skip("should get correct interest amount", async function () { + // Borrows underlying currency with 2nd user account + const user2LendAmount = newMonetaryAmount(100, underlyingCurrency, true); + const user2BorrowAmount = newMonetaryAmount(20, underlyingCurrency, true); + const user2LendExtrinsic = user2InterBtcAPI.api.tx.loans.mint( + underlyingCurrencyId, + user2LendAmount.toString(true) + ); + const user2CollateralExtrinsic = user2InterBtcAPI.api.tx.loans.depositAllCollateral(underlyingCurrencyId); + const user2BorrowExtrinsic = user2InterBtcAPI.api.tx.loans.borrow( + underlyingCurrencyId, + user2BorrowAmount.toString(true) + ); + + const result1 = await DefaultTransactionAPI.sendLogged( + api, + user2Account, + api.tx.utility.batchAll([user2LendExtrinsic, user2CollateralExtrinsic, user2BorrowExtrinsic]), + api.events.loans.Borrowed + ); + expect(result1.isCompleted).toBe(true); + + // TODO: cannot submit timestamp.set - gettin error + // 'RpcError: 1010: Invalid Transaction: Transaction dispatch is mandatory; transactions may not have mandatory dispatches.' + // Solution: Move APR calculation to separate function and unit test it without using actual parachain value, + // mock the parachain response for this. + + // Manipulates time to accredit interest. + const timestamp1MonthInFuture = Date.now() + 1000 * 60 * 60 * 24 * 30; + const setTimeToFutureExtrinsic = sudoInterBtcAPI.api.tx.timestamp.set(timestamp1MonthInFuture); + + const result2 = await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + api.tx.sudo.sudo(setTimeToFutureExtrinsic), + api.events.sudo.Sudid + ); + expect(result2.isCompleted).toBe(true); + }); + }); + + describe("getUnderlyingCurrencyFromLendTokenId", () => { + it("should return correct underlying currency for lend token", async () => { + const returnedUnderlyingCurrency = await getUnderlyingCurrencyFromLendTokenId(api, lendTokenId1); + + expect(returnedUnderlyingCurrency).toEqual(underlyingCurrency); + }); + + it( + "should throw when lend token id is of non-existing currency", + async () => { + const invalidLendTokenId = (lendTokenId1 = newCurrencyId(sudoInterBtcAPI.api, { + lendToken: { id: 999 }, + } as LendToken)); + + await expect(getUnderlyingCurrencyFromLendTokenId(api, invalidLendTokenId)).rejects.toThrow(); + } + ); + }); + + describe("lend", () => { + it("should lend expected amount of currency to protocol", async () => { + const [{ amount: lendAmountBefore }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + const lendAmount = newMonetaryAmount(100, underlyingCurrency, true); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); + + const [{ amount: lendAmountAfter }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + const actuallyLentAmount = lendAmountAfter.sub(lendAmountBefore); + + // Check that lent amount is same as sent amount + expect(actuallyLentAmount.eq(lendAmount)).toBe(true); + }); + it("should throw if trying to lend from inactive market", async () => { + const inactiveUnderlyingCurrency = InterBtc; + const amount = newMonetaryAmount(1, inactiveUnderlyingCurrency); + const lendPromise = userInterBtcAPI.loans.lend(inactiveUnderlyingCurrency, amount); + + await expect(lendPromise).rejects.toThrow(); + }); + }); + + describe("withdraw", () => { + it("should withdraw part of lent amount", async () => { + const [{ amount: lendAmountBefore }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + const amountToWithdraw = newMonetaryAmount(1, underlyingCurrency, true); + await submitExtrinsic( + userInterBtcAPI, + await userInterBtcAPI.loans.withdraw(underlyingCurrency, amountToWithdraw) + ); + + const [{ amount: lendAmountAfter }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + const actuallyWithdrawnAmount = lendAmountBefore.sub(lendAmountAfter).toBig().round(2); + + try { + expect(actuallyWithdrawnAmount.eq(amountToWithdraw.toBig())).toBe(true); + } catch(_) { + // eslint-disable-next-line max-len + throw Error(`Expected withdrawn amount: ${amountToWithdraw.toHuman()} is different from the actual amount: ${actuallyWithdrawnAmount.toString()}!`); + } + }); + }); + + describe("withdrawAll", () => { + it("should withdraw full amount from lending protocol", async () => { + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.withdrawAll(underlyingCurrency)); + + const lendPositions = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + try { + expect(lendPositions).toHaveLength(0); + } catch(_) { + throw Error("Expected to withdraw full amount and close position!"); + } + }); + }); + + describe("enableAsCollateral", () => { + it("should enable lend position as collateral", async () => { + const lendAmount = newMonetaryAmount(1, underlyingCurrency, true); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)); + const [{ isCollateral }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + expect(isCollateral).toBe(true); + }); + }); + + describe("disableAsCollateral", () => { + it( + "should disable enabled collateral position if there are no borrows", + async () => { + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.disableAsCollateral(underlyingCurrency)); + const [{ isCollateral }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); + + expect(isCollateral).toBe(false); + } + ); + }); + + describe("getLoanAssets", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("should get loan assets in correct format", async () => { + const loanAssets = await userInterBtcAPI.loans.getLoanAssets(); + const underlyingCurrencyLoanAsset = loanAssets[underlyingCurrency.ticker]; + + expect(underlyingCurrencyLoanAsset).toBeDefined(); + expect(underlyingCurrencyLoanAsset.currency).toEqual(underlyingCurrency); + expect(underlyingCurrencyLoanAsset.isActive).toBe(true); + // TODO: add more tests to check data validity + }); + + it("should return empty object if there are no added markets", async () => { + // Mock empty list returned from chain. + jest.spyOn(LoansAPI, "getLoansMarkets").mockClear().mockReturnValue(Promise.resolve([])); + + const loanAssets = await LoansAPI.getLoanAssets(); + expect(JSON.stringify(loanAssets)).toBe("{}"); + }); + }); + + describe("getAccruedRewardsOfAccount", () => { + beforeAll(async () => { + const addRewardExtrinsic = sudoInterBtcAPI.api.tx.loans.addReward("100000000000000"); + const updateRewardSpeedExtrinsic_1 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed( + underlyingCurrencyId, + "1000000000000", + "0" + ); + const updateRewardSpeedExtrinsic_2 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed( + underlyingCurrencyId2, + "0", + "1000000000000" + ); + + const updateRewardSpeed = api.tx.sudo.sudo( + sudoInterBtcAPI.api.tx.utility.batchAll([updateRewardSpeedExtrinsic_1, updateRewardSpeedExtrinsic_2]) + ); + + const rewardExtrinsic = sudoInterBtcAPI.api.tx.utility.batchAll([addRewardExtrinsic, updateRewardSpeed]); + + const result = await DefaultTransactionAPI.sendLogged( + api, + sudoAccount, + rewardExtrinsic, + api.events.sudo.Sudid + ); + + try { + expect(result.isCompleted).toBe(true); + } catch(_) { + throw Error("Sudo event to add rewards not found"); + } + }); + + it("should return correct amount of rewards", async () => { + await submitExtrinsic( + userInterBtcAPI, + await userInterBtcAPI.loans.lend(underlyingCurrency, newMonetaryAmount(1, underlyingCurrency, true)), + false + ); + + const rewards = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId); + + expect(rewards.total.toBig().eq(1)).toBe(true); + + await submitExtrinsic(userInterBtcAPI, { + extrinsic: userInterBtcAPI.api.tx.utility.batchAll([ + ( + await userInterBtcAPI.loans.lend( + underlyingCurrency2, + newMonetaryAmount(0.1, underlyingCurrency2, true) + ) + ).extrinsic, + (await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)).extrinsic, + ( + await userInterBtcAPI.loans.borrow( + underlyingCurrency2, + newMonetaryAmount(0.1, underlyingCurrency2, true) + ) + ).extrinsic, + ]), + event: userInterBtcAPI.api.events.loans.Borrowed, + }); + + const rewardsAfterBorrow = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId); + + expect(rewardsAfterBorrow.total.toBig().eq(2)).toBe(true); + + // repay the loan to clean the state + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.repayAll(underlyingCurrency2)); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.withdrawAll(underlyingCurrency2)); + }); + }); + + describe("claimAllSubsidyRewards", () => { + it("should claim all subsidy rewards", () => { + // TODO + }); + }); + + describe("borrow", () => { + it("should borrow specified amount", async () => { + const lendAmount = newMonetaryAmount(100, underlyingCurrency, true); + const borrowAmount = newMonetaryAmount(1, underlyingCurrency, true); + await submitExtrinsic(user2InterBtcAPI, await user2InterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); + await submitExtrinsic( + user2InterBtcAPI, + await user2InterBtcAPI.loans.enableAsCollateral(underlyingCurrency) + ); + let borrowers = await user2InterBtcAPI.loans.getBorrowerAccountIds(); + + try { + expect(!includesStringified(borrowers, user2AccountId)).toBe(true); + } catch(_) { + throw Error(`Expected ${user2AccountId.toString()} not to be included in the result of \`getBorrowerAccountIds\``); + } + await submitExtrinsic( + user2InterBtcAPI, + await user2InterBtcAPI.loans.borrow(underlyingCurrency, borrowAmount) + ); + borrowers = await user2InterBtcAPI.loans.getBorrowerAccountIds(); + + try { + expect(includesStringified(borrowers, user2AccountId)).toBe(true); + } catch(_) { + throw Error(`Expected ${user2AccountId.toString()} to be included in the result of \`getBorrowerAccountIds\``); + } + + const [{ amount }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount(user2AccountId); + const roundedAmount = amount.toBig().round(2); + + try { + expect(roundedAmount.eq(borrowAmount.toBig())).toBe(true); + } catch(_) { + throw Error(`Expected borrowed amount to equal ${borrowAmount.toString()}, but it is ${amount.toString()}.`); + } + }); + }); + + describe("repay", () => { + it("should repay specified amount", async () => { + const repayAmount = newMonetaryAmount(0.5, underlyingCurrency, true); + const [{ amount: borrowAmountBefore }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount( + user2AccountId + ); + await submitExtrinsic( + user2InterBtcAPI, + await user2InterBtcAPI.loans.repay(underlyingCurrency, repayAmount) + ); + const [{ amount: borrowAmountAfter }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount( + user2AccountId + ); + + const borrowAmountAfterRounded = borrowAmountAfter.toBig().round(2); + const expectedRemainingAmount = borrowAmountBefore.sub(repayAmount); + + expect(borrowAmountAfterRounded.toNumber()).toBe(expectedRemainingAmount.toBig().toNumber()); + }); + }); + + describe("repayAll", () => { + it("should repay whole loan", async () => { + await submitExtrinsic(user2InterBtcAPI, await user2InterBtcAPI.loans.repayAll(underlyingCurrency)); + const borrowPositions = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount(user2AccountId); + + try { + expect(borrowPositions).toHaveLength(0); + } catch(_) { + throw Error(`Expected to repay full borrow position, but positions: ${borrowPositions} were found`); + } + }); + }); + + describe.skip("getBorrowPositionsOfAccount", () => { + beforeAll(async () => { + // TODO + }); + + it("should get borrow positions in correct format", async () => { + // TODO + }); + }); + + // Prerequisites: This test depends on the ones above. User 2 must have already + // deposited funds and enabled them as collateral, so that they can successfully borrow. + describe("liquidateBorrowPosition", () => { + it("should liquidate position when possible", async () => { + // Supply asset by account1, borrow by account2 + const borrowAmount = newMonetaryAmount(10, underlyingCurrency2, true); + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency2, borrowAmount)); + await submitExtrinsic( + user2InterBtcAPI, + await user2InterBtcAPI.loans.borrow(underlyingCurrency2, borrowAmount) + ); + + const exchangeRateValue = new Big(1); + await callWithExchangeRate(sudoInterBtcAPI, underlyingCurrency2, exchangeRateValue, async () => { + const repayAmount = newMonetaryAmount(1, underlyingCurrency2); // repay smallest amount + const undercollateralizedBorrowers = await user2InterBtcAPI.loans.getUndercollateralizedBorrowers(); + + expect(undercollateralizedBorrowers).toHaveLength(1); + expect(undercollateralizedBorrowers[0].accountId.toString()).toBe(user2AccountId.toString()); + await submitExtrinsic( + userInterBtcAPI, + userInterBtcAPI.loans.liquidateBorrowPosition( + user2AccountId, + underlyingCurrency2, + repayAmount, + underlyingCurrency + ) + ); + }); + }); + + it("should throw when no position can be liquidated", async () => { + const repayAmount = newMonetaryAmount(1, underlyingCurrency2, true); // repay smallest amount + + await expect( + submitExtrinsic( + userInterBtcAPI, + userInterBtcAPI.loans.liquidateBorrowPosition( + user2AccountId, + underlyingCurrency2, + repayAmount, + underlyingCurrency + ) + ) + ).rejects.toThrow(); + }); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/loans.test.ts b/test/integration/parachain/staging/sequential/loans.test.ts deleted file mode 100644 index f43a7c3e4..000000000 --- a/test/integration/parachain/staging/sequential/loans.test.ts +++ /dev/null @@ -1,563 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { - CurrencyExt, - currencyIdToMonetaryCurrency, - DefaultInterBtcApi, - DefaultLoansAPI, - DefaultOracleAPI, - DefaultTransactionAPI, - getUnderlyingCurrencyFromLendTokenId, - InterBtcApi, - LendToken, - newAccountId, - newCurrencyId, - newMonetaryAmount, -} from "../../../../../src/index"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { USER_1_URI, USER_2_URI, PARACHAIN_ENDPOINT, ESPLORA_BASE_PATH, SUDO_URI } from "../../../../config"; -import { callWithExchangeRate, includesStringified, submitExtrinsic } from "../../../../utils/helpers"; -import { InterbtcPrimitivesCurrencyId } from "@polkadot/types/lookup"; -import { expect } from "../../../../chai"; -import sinon from "sinon"; -import Big from "big.js"; -import { InterBtc, MonetaryAmount } from "@interlay/monetary-js"; -import { AccountId } from "@polkadot/types/interfaces"; - -describe("Loans", () => { - let api: ApiPromise; - let keyring: Keyring; - let userInterBtcAPI: InterBtcApi; - let user2InterBtcAPI: InterBtcApi; - let sudoInterBtcAPI: InterBtcApi; - let LoansAPI: DefaultLoansAPI; - - let userAccount: KeyringPair; - let user2Account: KeyringPair; - let sudoAccount: KeyringPair; - let userAccountId: AccountId; - let user2AccountId: AccountId; - - let lendTokenId1: InterbtcPrimitivesCurrencyId; - let lendTokenId2: InterbtcPrimitivesCurrencyId; - let underlyingCurrencyId: InterbtcPrimitivesCurrencyId; - let underlyingCurrency: CurrencyExt; - let underlyingCurrencyId2: InterbtcPrimitivesCurrencyId; - let underlyingCurrency2: CurrencyExt; - - before(async function () { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - keyring = new Keyring({ type: "sr25519" }); - userAccount = keyring.addFromUri(USER_1_URI); - user2Account = keyring.addFromUri(USER_2_URI); - userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); - user2InterBtcAPI = new DefaultInterBtcApi(api, "regtest", user2Account, ESPLORA_BASE_PATH); - - sudoAccount = keyring.addFromUri(SUDO_URI); - sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); - userAccountId = newAccountId(api, userAccount.address); - user2AccountId = newAccountId(api, user2Account.address); - const wrappedCurrency = sudoInterBtcAPI.getWrappedCurrency(); - const oracleAPI = new DefaultOracleAPI(api, wrappedCurrency); - - LoansAPI = new DefaultLoansAPI(api, wrappedCurrency, oracleAPI); - - // Add market for governance currency. - underlyingCurrencyId = sudoInterBtcAPI.api.consts.currency.getNativeCurrencyId; - underlyingCurrency = sudoInterBtcAPI.getGovernanceCurrency(); - - underlyingCurrencyId2 = sudoInterBtcAPI.api.consts.currency.getRelayChainCurrencyId; - underlyingCurrency2 = await currencyIdToMonetaryCurrency(api, underlyingCurrencyId2); - - lendTokenId1 = newCurrencyId(sudoInterBtcAPI.api, { lendToken: { id: 1 } } as LendToken); - lendTokenId2 = newCurrencyId(sudoInterBtcAPI.api, { lendToken: { id: 2 } } as LendToken); - - const percentageToPermill = (percentage: number) => percentage * 10000; - - const marketData = (id: InterbtcPrimitivesCurrencyId) => ({ - collateralFactor: percentageToPermill(50), - liquidationThreshold: percentageToPermill(55), - reserveFactor: percentageToPermill(15), - closeFactor: percentageToPermill(50), - liquidateIncentive: "1100000000000000000", - liquidateIncentiveReservedFactor: percentageToPermill(3), - rateModel: { - Jump: { - baseRate: "20000000000000000", - jumpRate: "100000000000000000", - fullRate: "320000000000000000", - jumpUtilization: percentageToPermill(80), - }, - }, - state: "Pending", - supplyCap: "5000000000000000000000", - borrowCap: "5000000000000000000000", - lendTokenId: id, - }); - - const addMarket1Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( - underlyingCurrencyId, - marketData(lendTokenId1) - ); - const addMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( - underlyingCurrencyId2, - marketData(lendTokenId2) - ); - const activateMarket1Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId); - const activateMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId2); - const addMarkets = sudoInterBtcAPI.api.tx.utility.batchAll([ - addMarket1Extrinsic, - addMarket2Extrinsic, - activateMarket1Extrinsic, - activateMarket2Extrinsic, - ]); - - const result = await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(addMarkets), - api.events.sudo.Sudid - ); - expect(result.isCompleted, "Sudo event to create new market not found").to.be.true; - }); - - after(async () => { - api.disconnect(); - }); - - afterEach(() => { - // discard any stubbed methods after each test - sinon.restore(); - }); - - describe("getLendTokens", () => { - it("should get lend token for each existing market", async () => { - const [markets, lendTokens] = await Promise.all([ - api.query.loans.markets.entries(), - userInterBtcAPI.loans.getLendTokens(), - ]); - - const marketsUnderlyingCurrencyId = markets[0][0].args[0]; - - expect(markets.length).to.be.equal(lendTokens.length); - - expect(marketsUnderlyingCurrencyId.eq(underlyingCurrencyId)).to.be.true; - }); - - it("should return LendToken in correct format - 'q' prefix, correct id", async () => { - // Requires first market to be initialized for governance currency. - const lendTokens = await userInterBtcAPI.loans.getLendTokens(); - const lendToken = lendTokens[0]; - - // Should have same amount of decimals as underlying currency. - expect(lendToken.decimals).to.be.eq(underlyingCurrency.decimals); - - // Should add 'q' prefix. - expect(lendToken.name).to.be.eq(`q${underlyingCurrency.name}`); - expect(lendToken.ticker).to.be.eq(`q${underlyingCurrency.ticker}`); - - expect(lendToken.lendToken.id).to.be.eq(lendTokenId1.asLendToken.toNumber()); - }); - - it("should return empty array if no market exists", async () => { - // Mock empty list returned from chain. - sinon.stub(LoansAPI, "getLoansMarkets").returns(Promise.resolve([])); - - const lendTokens = await LoansAPI.getLendTokens(); - expect(lendTokens).to.be.empty; - - sinon.restore(); - sinon.reset(); - }); - }); - - describe("getLendPositionsOfAccount", () => { - let lendAmount: MonetaryAmount; - before(async function () { - lendAmount = newMonetaryAmount(1, underlyingCurrency, true); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); - }); - - it("should get all lend positions of account in correct format", async () => { - const [lendPosition] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - expect(lendPosition.amount.toString()).to.be.equal(lendAmount.toString()); - expect(lendPosition.amount.currency).to.be.equal(underlyingCurrency); - expect(lendPosition.isCollateral).to.be.false; - // TODO: add tests for more markets - }); - - it("should get correct data after position is enabled as collateral", async function () { - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)); - - const [lendPosition] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - expect(lendPosition.isCollateral).to.be.true; - }); - - it("should get empty array when no lend position exists for account", async () => { - const lendPositions = await user2InterBtcAPI.loans.getLendPositionsOfAccount(user2AccountId); - - expect(lendPositions).to.be.empty; - }); - - it.skip("should get correct interest amount", async function () { - // Borrows underlying currency with 2nd user account - const user2LendAmount = newMonetaryAmount(100, underlyingCurrency, true); - const user2BorrowAmount = newMonetaryAmount(20, underlyingCurrency, true); - const user2LendExtrinsic = user2InterBtcAPI.api.tx.loans.mint( - underlyingCurrencyId, - user2LendAmount.toString(true) - ); - const user2CollateralExtrinsic = user2InterBtcAPI.api.tx.loans.depositAllCollateral(underlyingCurrencyId); - const user2BorrowExtrinsic = user2InterBtcAPI.api.tx.loans.borrow( - underlyingCurrencyId, - user2BorrowAmount.toString(true) - ); - - const result1 = await DefaultTransactionAPI.sendLogged( - api, - user2Account, - api.tx.utility.batchAll([user2LendExtrinsic, user2CollateralExtrinsic, user2BorrowExtrinsic]), - api.events.loans.Borrowed - ); - expect(result1.isCompleted, "No event found for depositing collateral"); - - // TODO: cannot submit timestamp.set - gettin error - // 'RpcError: 1010: Invalid Transaction: Transaction dispatch is mandatory; transactions may not have mandatory dispatches.' - // Solution: Move APR calculation to separate function and unit test it without using actual parachain value, - // mock the parachain response for this. - - // Manipulates time to accredit interest. - const timestamp1MonthInFuture = Date.now() + 1000 * 60 * 60 * 24 * 30; - const setTimeToFutureExtrinsic = sudoInterBtcAPI.api.tx.timestamp.set(timestamp1MonthInFuture); - - const result2 = await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - api.tx.sudo.sudo(setTimeToFutureExtrinsic), - api.events.sudo.Sudid - ); - expect(result2.isCompleted, "Sudo event to manipulate time not found").to.be.true; - }); - }); - - describe("getUnderlyingCurrencyFromLendTokenId", () => { - it("should return correct underlying currency for lend token", async () => { - const returnedUnderlyingCurrency = await getUnderlyingCurrencyFromLendTokenId(api, lendTokenId1); - - expect(returnedUnderlyingCurrency).to.deep.equal(underlyingCurrency); - }); - it("should throw when lend token id is of non-existing currency", async () => { - const invalidLendTokenId = (lendTokenId1 = newCurrencyId(sudoInterBtcAPI.api, { - lendToken: { id: 999 }, - } as LendToken)); - - await expect(getUnderlyingCurrencyFromLendTokenId(api, invalidLendTokenId)).to.be.rejected; - }); - }); - - describe("lend", () => { - it("should lend expected amount of currency to protocol", async function () { - const [{ amount: lendAmountBefore }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - const lendAmount = newMonetaryAmount(100, underlyingCurrency, true); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); - - const [{ amount: lendAmountAfter }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - const actuallyLentAmount = lendAmountAfter.sub(lendAmountBefore); - - // Check that lent amount is same as sent amount - expect(actuallyLentAmount.eq(lendAmount)).to.be.true; - }); - it("should throw if trying to lend from inactive market", async () => { - const inactiveUnderlyingCurrency = InterBtc; - const amount = newMonetaryAmount(1, inactiveUnderlyingCurrency); - const lendPromise = userInterBtcAPI.loans.lend(inactiveUnderlyingCurrency, amount); - - await expect(lendPromise).to.be.rejected; - }); - }); - - describe("withdraw", () => { - it("should withdraw part of lent amount", async function () { - const [{ amount: lendAmountBefore }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - const amountToWithdraw = newMonetaryAmount(1, underlyingCurrency, true); - await submitExtrinsic( - userInterBtcAPI, - await userInterBtcAPI.loans.withdraw(underlyingCurrency, amountToWithdraw) - ); - - const [{ amount: lendAmountAfter }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - const actuallyWithdrawnAmount = lendAmountBefore.sub(lendAmountAfter).toBig().round(2); - - expect( - actuallyWithdrawnAmount.eq(amountToWithdraw.toBig()), - // eslint-disable-next-line max-len - `Expected withdrawn amount: ${amountToWithdraw.toHuman()} is different from the actual amount: ${actuallyWithdrawnAmount.toString()}!` - ).to.be.true; - }); - }); - - describe("withdrawAll", () => { - it("should withdraw full amount from lending protocol", async function () { - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.withdrawAll(underlyingCurrency)); - - const lendPositions = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - expect(lendPositions, "Expected to withdraw full amount and close position!").to.be.empty; - }); - }); - - describe("enableAsCollateral", () => { - it("should enable lend position as collateral", async function () { - const lendAmount = newMonetaryAmount(1, underlyingCurrency, true); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)); - const [{ isCollateral }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - expect(isCollateral).to.be.true; - }); - }); - - describe("disableAsCollateral", () => { - it("should disable enabled collateral position if there are no borrows", async function () { - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.disableAsCollateral(underlyingCurrency)); - const [{ isCollateral }] = await userInterBtcAPI.loans.getLendPositionsOfAccount(userAccountId); - - expect(isCollateral).to.be.false; - }); - }); - - describe("getLoanAssets", () => { - it("should get loan assets in correct format", async () => { - const loanAssets = await userInterBtcAPI.loans.getLoanAssets(); - const underlyingCurrencyLoanAsset = loanAssets[underlyingCurrency.ticker]; - - expect(underlyingCurrencyLoanAsset).is.not.undefined; - expect(underlyingCurrencyLoanAsset.currency).to.be.deep.equal(underlyingCurrency); - expect(underlyingCurrencyLoanAsset.isActive).to.be.true; - // TODO: add more tests to check data validity - }); - - it("should return empty object if there are no added markets", async () => { - // Mock empty list returned from chain. - sinon.stub(LoansAPI, "getLoansMarkets").returns(Promise.resolve([])); - - const loanAssets = await LoansAPI.getLoanAssets(); - expect(loanAssets).to.be.empty; - - sinon.restore(); - sinon.reset(); - }); - }); - - describe("getAccruedRewardsOfAccount", () => { - before(async function () { - const addRewardExtrinsic = sudoInterBtcAPI.api.tx.loans.addReward("100000000000000"); - const updateRewardSpeedExtrinsic_1 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed( - underlyingCurrencyId, - "1000000000000", - "0" - ); - const updateRewardSpeedExtrinsic_2 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed( - underlyingCurrencyId2, - "0", - "1000000000000" - ); - - const updateRewardSpeed = api.tx.sudo.sudo( - sudoInterBtcAPI.api.tx.utility.batchAll([updateRewardSpeedExtrinsic_1, updateRewardSpeedExtrinsic_2]) - ); - - const rewardExtrinsic = sudoInterBtcAPI.api.tx.utility.batchAll([addRewardExtrinsic, updateRewardSpeed]); - - const result = await DefaultTransactionAPI.sendLogged( - api, - sudoAccount, - rewardExtrinsic, - api.events.sudo.Sudid - ); - - expect(result.isCompleted, "Sudo event to add rewards not found").to.be.true; - }); - - it("should return correct amount of rewards", async () => { - await submitExtrinsic( - userInterBtcAPI, - await userInterBtcAPI.loans.lend(underlyingCurrency, newMonetaryAmount(1, underlyingCurrency, true)), - false - ); - - const rewards = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId); - - expect(rewards.total.toBig().eq(1)).to.be.true; - - await submitExtrinsic(userInterBtcAPI, { - extrinsic: userInterBtcAPI.api.tx.utility.batchAll([ - ( - await userInterBtcAPI.loans.lend( - underlyingCurrency2, - newMonetaryAmount(0.1, underlyingCurrency2, true) - ) - ).extrinsic, - (await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)).extrinsic, - ( - await userInterBtcAPI.loans.borrow( - underlyingCurrency2, - newMonetaryAmount(0.1, underlyingCurrency2, true) - ) - ).extrinsic, - ]), - event: userInterBtcAPI.api.events.loans.Borrowed, - }); - - const rewardsAfterBorrow = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId); - - expect(rewardsAfterBorrow.total.toBig().eq(2)).to.be.true; - - // repay the loan to clean the state - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.repayAll(underlyingCurrency2)); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.withdrawAll(underlyingCurrency2)); - }); - }); - - describe("claimAllSubsidyRewards", () => { - it("should claim all subsidy rewards", () => { - // TODO - }); - }); - - describe("borrow", () => { - it("should borrow specified amount", async function () { - const lendAmount = newMonetaryAmount(100, underlyingCurrency, true); - const borrowAmount = newMonetaryAmount(1, underlyingCurrency, true); - await submitExtrinsic(user2InterBtcAPI, await user2InterBtcAPI.loans.lend(underlyingCurrency, lendAmount)); - await submitExtrinsic( - user2InterBtcAPI, - await user2InterBtcAPI.loans.enableAsCollateral(underlyingCurrency) - ); - let borrowers = await user2InterBtcAPI.loans.getBorrowerAccountIds(); - expect( - !includesStringified(borrowers, user2AccountId), - `Expected ${user2AccountId.toString()} not to be included in the result of \`getBorrowerAccountIds\`` - ).to.be.true; - await submitExtrinsic( - user2InterBtcAPI, - await user2InterBtcAPI.loans.borrow(underlyingCurrency, borrowAmount) - ); - borrowers = await user2InterBtcAPI.loans.getBorrowerAccountIds(); - expect( - includesStringified(borrowers, user2AccountId), - `Expected ${user2AccountId.toString()} to be included in the result of \`getBorrowerAccountIds\`` - ).to.be.true; - - const [{ amount }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount(user2AccountId); - const roundedAmount = amount.toBig().round(2); - expect( - roundedAmount.eq(borrowAmount.toBig()), - `Expected borrowed amount to equal ${borrowAmount.toString()}, but it is ${amount.toString()}.` - ).to.be.true; - }); - }); - - describe("repay", () => { - it("should repay specified amount", async function () { - const repayAmount = newMonetaryAmount(0.5, underlyingCurrency, true); - const [{ amount: borrowAmountBefore }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount( - user2AccountId - ); - await submitExtrinsic( - user2InterBtcAPI, - await user2InterBtcAPI.loans.repay(underlyingCurrency, repayAmount) - ); - const [{ amount: borrowAmountAfter }] = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount( - user2AccountId - ); - - const borrowAmountAfterRounded = borrowAmountAfter.toBig().round(2); - const expectedRemainingAmount = borrowAmountBefore.sub(repayAmount); - - expect( - borrowAmountAfterRounded.eq(expectedRemainingAmount.toBig()), - `Expected remaining borrow amount to equal ${expectedRemainingAmount.toString()}, but it is ${borrowAmountAfter.toString()}` - ).to.be.true; - }); - }); - - describe("repayAll", () => { - it("should repay whole loan", async function () { - await submitExtrinsic(user2InterBtcAPI, await user2InterBtcAPI.loans.repayAll(underlyingCurrency)); - const borrowPositions = await user2InterBtcAPI.loans.getBorrowPositionsOfAccount(user2AccountId); - - expect( - borrowPositions, - `Expected to repay full borrow position, but positions: ${borrowPositions} were found` - ).to.be.empty; - }); - }); - - describe("getBorrowPositionsOfAccount", () => { - before(async function () { - // TODO - }); - - it("should get borrow positions in correct format", async function () { - // TODO - }); - }); - - // Prerequisites: This test depends on the ones above. User 2 must have already - // deposited funds and enabled them as collateral, so that they can successfully borrow. - describe("liquidateBorrowPosition", () => { - it("should liquidate position when possible", async function () { - // Supply asset by account1, borrow by account2 - const borrowAmount = newMonetaryAmount(10, underlyingCurrency2, true); - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.lend(underlyingCurrency2, borrowAmount)); - await submitExtrinsic( - user2InterBtcAPI, - await user2InterBtcAPI.loans.borrow(underlyingCurrency2, borrowAmount) - ); - - const exchangeRateValue = new Big(1); - await callWithExchangeRate(sudoInterBtcAPI, underlyingCurrency2, exchangeRateValue, async () => { - const repayAmount = newMonetaryAmount(1, underlyingCurrency2); // repay smallest amount - const undercollateralizedBorrowers = await user2InterBtcAPI.loans.getUndercollateralizedBorrowers(); - expect( - undercollateralizedBorrowers.length, - `Expected one undercollateralized borrower, found ${undercollateralizedBorrowers.length}` - ).to.be.eq(1); - expect( - undercollateralizedBorrowers[0].accountId.toString(), - `Expected undercollateralized borrower to be ${user2AccountId.toString()},\ - found ${undercollateralizedBorrowers[0].accountId.toString()}` - ).to.be.eq(user2AccountId.toString()); - await submitExtrinsic( - userInterBtcAPI, - userInterBtcAPI.loans.liquidateBorrowPosition( - user2AccountId, - underlyingCurrency2, - repayAmount, - underlyingCurrency - ) - ); - }); - }); - - it("should throw when no position can be liquidated", async function () { - const repayAmount = newMonetaryAmount(1, underlyingCurrency2, true); // repay smallest amount - - await expect( - submitExtrinsic( - userInterBtcAPI, - userInterBtcAPI.loans.liquidateBorrowPosition( - user2AccountId, - underlyingCurrency2, - repayAmount, - underlyingCurrency - ) - ) - ).to.be.rejected; - }); - }); -}); diff --git a/test/integration/parachain/staging/sequential/nomination.partial.ts b/test/integration/parachain/staging/sequential/nomination.partial.ts new file mode 100644 index 000000000..ebf8a0fa6 --- /dev/null +++ b/test/integration/parachain/staging/sequential/nomination.partial.ts @@ -0,0 +1,194 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import BN from "bn.js"; +import { DefaultInterBtcApi, InterBtcApi, InterbtcPrimitivesVaultId } from "../../../../../src/index"; + +import { + BitcoinCoreClient, + CollateralCurrencyExt, + currencyIdToMonetaryCurrency, + encodeUnsignedFixedPoint, + newAccountId, + newVaultId, + WrappedCurrency, +} from "../../../../../src"; +import { setRawStorage, issueSingle, newMonetaryAmount } from "../../../../../src/utils"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { + SUDO_URI, + USER_1_URI, + VAULT_1_URI, + BITCOIN_CORE_HOST, + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_WALLET, + PARACHAIN_ENDPOINT, + ESPLORA_BASE_PATH, +} from "../../../../config"; +import { + callWith, + getCorrespondingCollateralCurrenciesForTests, + submitExtrinsic, + sudo, +} from "../../../../utils/helpers"; +import { Nomination } from "../../../../../src/parachain/nomination"; + +// TODO: readd this once we want to activate nomination +export const nominationTests = () => { + describe.skip("NominationAPI", () => { + let api: ApiPromise; + let userInterBtcAPI: InterBtcApi; + let sudoInterBtcAPI: InterBtcApi; + let sudoAccount: KeyringPair; + let userAccount: KeyringPair; + let vault_1: KeyringPair; + let vault_1_ids: Array; + + let bitcoinCoreClient: BitcoinCoreClient; + + let wrappedCurrency: WrappedCurrency; + let collateralCurrencies: Array; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + const keyring = new Keyring({ type: "sr25519" }); + sudoAccount = keyring.addFromUri(SUDO_URI); + userAccount = keyring.addFromUri(USER_1_URI); + userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); + sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); + wrappedCurrency = userInterBtcAPI.getWrappedCurrency(); + collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(userInterBtcAPI.getGovernanceCurrency()); + vault_1 = keyring.addFromUri(VAULT_1_URI); + vault_1_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) + ); + + if (!(await sudoInterBtcAPI.nomination.isNominationEnabled())) { + console.log("Enabling nomination..."); + await sudo(sudoInterBtcAPI, async () => { + await submitExtrinsic(sudoInterBtcAPI, sudoInterBtcAPI.nomination.setNominationEnabled(true)); + }); + } + + // The account of a vault from docker-compose + vault_1 = keyring.addFromUri(VAULT_1_URI); + bitcoinCoreClient = new BitcoinCoreClient( + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_HOST, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_WALLET + ); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + it("Should opt a vault in and out of nomination", async () => { + for (const vault_1_id of vault_1_ids) { + await optInWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); + const nominationVaults = await userInterBtcAPI.nomination.listVaults(); + expect(1).toEqual(nominationVaults.length); + expect(vault_1.address).toEqual(nominationVaults.map((v) => v.accountId.toString())[0]); + await optOutWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); + expect(0).toEqual((await userInterBtcAPI.nomination.listVaults()).length); + } + }); + + async function setIssueFee(x: BN) { + await setRawStorage(api, api.query.fee.issueFee.key(), api.createType("UnsignedFixedPoint", x), sudoAccount); + } + + it("Should nominate to and withdraw from a vault", async () => { + for (const vault_1_id of vault_1_ids) { + await optInWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); + const issueFee = await userInterBtcAPI.fee.getIssueFee(); + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); + const nominatorDeposit = newMonetaryAmount(1, collateralCurrency, true); + try { + // Set issue fees to 100% + await setIssueFee(new BN("1000000000000000000")); + const stakingCapacityBeforeNomination = await userInterBtcAPI.vaults.getStakingCapacity( + vault_1_id.accountId, + collateralCurrency + ); + // Deposit + await submitExtrinsic( + userInterBtcAPI, + userInterBtcAPI.nomination.depositCollateral(vault_1_id.accountId, nominatorDeposit) + ); + const stakingCapacityAfterNomination = await userInterBtcAPI.vaults.getStakingCapacity( + vault_1_id.accountId, + collateralCurrency + ); + expect(stakingCapacityBeforeNomination.sub(nominatorDeposit).toString()) + .toEqual(stakingCapacityAfterNomination.toString()); + const nominationPairs = await userInterBtcAPI.nomination.list(); + expect(2).toEqual(nominationPairs.length); + + const userAddress = userAccount.address; + const vault_1Address = vault_1.address; + + const nomination = nominationPairs.find( + (nomination) => userAddress == nomination.nominatorId.toString() + ) as Nomination; + + expect(userAddress).toEqual(nomination.nominatorId.toString()); + expect(vault_1Address).toEqual(nomination.vaultId.accountId.toString()); + + const amountToIssue = newMonetaryAmount(0.00001, wrappedCurrency, true); + await issueSingle(userInterBtcAPI, bitcoinCoreClient, userAccount, amountToIssue, vault_1_id); + const wrappedRewardsBeforeWithdrawal = ( + await userInterBtcAPI.nomination.getNominatorReward( + vault_1_id.accountId, + collateralCurrency, + wrappedCurrency, + newAccountId(api, userAccount.address) + ) + ).toBig(); + expect(wrappedRewardsBeforeWithdrawal.gt(0)).toBe(true); + + // Withdraw Rewards + await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.rewards.withdrawRewards(vault_1_id)); + // Withdraw Collateral + await submitExtrinsic( + userInterBtcAPI, + await userInterBtcAPI.nomination.withdrawCollateral(vault_1_id.accountId, nominatorDeposit) + ); + + const nominatorsAfterWithdrawal = await userInterBtcAPI.nomination.list(); + // The vault always has a "nomination" to itself + expect(1).toEqual(nominatorsAfterWithdrawal.length); + const totalNomination = await userInterBtcAPI.nomination.getTotalNomination( + newAccountId(api, userAccount.address), + await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral) + ); + expect(totalNomination.toString()).toEqual("0"); + } finally { + await setIssueFee(encodeUnsignedFixedPoint(api, issueFee)); + await optOutWithAccount( + vault_1, + await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral) + ); + } + } + }); + + async function optInWithAccount(vaultAccount: KeyringPair, collateralCurrency: CollateralCurrencyExt) { + // will fail if vault is already opted in + await callWith(userInterBtcAPI, vaultAccount, async () => { + await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.nomination.optIn(collateralCurrency)); + }); + } + + async function optOutWithAccount(vaultAccount: KeyringPair, collateralCurrency: CollateralCurrencyExt) { + await callWith(userInterBtcAPI, vaultAccount, async () => { + await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.nomination.optOut(collateralCurrency)); + }); + } + }); +}; diff --git a/test/integration/parachain/staging/sequential/nomination.test.ts b/test/integration/parachain/staging/sequential/nomination.test.ts deleted file mode 100644 index fabf396a4..000000000 --- a/test/integration/parachain/staging/sequential/nomination.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import BN from "bn.js"; -import { DefaultInterBtcApi, InterBtcApi, InterbtcPrimitivesVaultId } from "../../../../../src/index"; - -import { - BitcoinCoreClient, - CollateralCurrencyExt, - currencyIdToMonetaryCurrency, - encodeUnsignedFixedPoint, - newAccountId, - newVaultId, - WrappedCurrency, -} from "../../../../../src"; -import { setRawStorage, issueSingle, newMonetaryAmount } from "../../../../../src/utils"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { assert } from "../../../../chai"; -import { - SUDO_URI, - USER_1_URI, - VAULT_1_URI, - BITCOIN_CORE_HOST, - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_WALLET, - PARACHAIN_ENDPOINT, - ESPLORA_BASE_PATH, -} from "../../../../config"; -import { - callWith, - getCorrespondingCollateralCurrenciesForTests, - submitExtrinsic, - sudo, -} from "../../../../utils/helpers"; -import { Nomination } from "../../../../../src/parachain/nomination"; - -// TODO: readd this once we want to activate nomination -describe.skip("NominationAPI", () => { - let api: ApiPromise; - let userInterBtcAPI: InterBtcApi; - let sudoInterBtcAPI: InterBtcApi; - let sudoAccount: KeyringPair; - let userAccount: KeyringPair; - let vault_1: KeyringPair; - let vault_1_ids: Array; - - let bitcoinCoreClient: BitcoinCoreClient; - - let wrappedCurrency: WrappedCurrency; - let collateralCurrencies: Array; - - before(async () => { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - const keyring = new Keyring({ type: "sr25519" }); - sudoAccount = keyring.addFromUri(SUDO_URI); - userAccount = keyring.addFromUri(USER_1_URI); - userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); - sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); - wrappedCurrency = userInterBtcAPI.getWrappedCurrency(); - collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(userInterBtcAPI.getGovernanceCurrency()); - vault_1 = keyring.addFromUri(VAULT_1_URI); - vault_1_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) - ); - - if (!(await sudoInterBtcAPI.nomination.isNominationEnabled())) { - console.log("Enabling nomination..."); - await sudo(sudoInterBtcAPI, async () => { - await submitExtrinsic(sudoInterBtcAPI, sudoInterBtcAPI.nomination.setNominationEnabled(true)); - }); - } - - // The account of a vault from docker-compose - vault_1 = keyring.addFromUri(VAULT_1_URI); - bitcoinCoreClient = new BitcoinCoreClient( - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_HOST, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_WALLET - ); - }); - - after(() => { - return api.disconnect(); - }); - - it("Should opt a vault in and out of nomination", async () => { - for (const vault_1_id of vault_1_ids) { - await optInWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); - const nominationVaults = await userInterBtcAPI.nomination.listVaults(); - assert.equal(1, nominationVaults.length); - assert.equal(vault_1.address, nominationVaults.map((v) => v.accountId.toString())[0]); - await optOutWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); - assert.equal(0, (await userInterBtcAPI.nomination.listVaults()).length); - } - }); - - async function setIssueFee(x: BN) { - await setRawStorage(api, api.query.fee.issueFee.key(), api.createType("UnsignedFixedPoint", x), sudoAccount); - } - - it("Should nominate to and withdraw from a vault", async () => { - for (const vault_1_id of vault_1_ids) { - await optInWithAccount(vault_1, await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral)); - const issueFee = await userInterBtcAPI.fee.getIssueFee(); - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const nominatorDeposit = newMonetaryAmount(1, collateralCurrency, true); - try { - // Set issue fees to 100% - await setIssueFee(new BN("1000000000000000000")); - const stakingCapacityBeforeNomination = await userInterBtcAPI.vaults.getStakingCapacity( - vault_1_id.accountId, - collateralCurrency - ); - // Deposit - await submitExtrinsic( - userInterBtcAPI, - userInterBtcAPI.nomination.depositCollateral(vault_1_id.accountId, nominatorDeposit) - ); - const stakingCapacityAfterNomination = await userInterBtcAPI.vaults.getStakingCapacity( - vault_1_id.accountId, - collateralCurrency - ); - assert.equal( - stakingCapacityBeforeNomination.sub(nominatorDeposit).toString(), - stakingCapacityAfterNomination.toString(), - "Nomination failed to decrease staking capacity" - ); - const nominationPairs = await userInterBtcAPI.nomination.list(); - assert.equal( - 2, - nominationPairs.length, - "There should be one nomination pair in the system, besides the vault to itself" - ); - - const userAddress = userAccount.address; - const vault_1Address = vault_1.address; - - const nomination = nominationPairs.find( - (nomination) => userAddress == nomination.nominatorId.toString() - ) as Nomination; - - assert.equal(userAddress, nomination.nominatorId.toString()); - assert.equal(vault_1Address, nomination.vaultId.accountId.toString()); - - const amountToIssue = newMonetaryAmount(0.00001, wrappedCurrency, true); - await issueSingle(userInterBtcAPI, bitcoinCoreClient, userAccount, amountToIssue, vault_1_id); - const wrappedRewardsBeforeWithdrawal = ( - await userInterBtcAPI.nomination.getNominatorReward( - vault_1_id.accountId, - collateralCurrency, - wrappedCurrency, - newAccountId(api, userAccount.address) - ) - ).toBig(); - assert.isTrue(wrappedRewardsBeforeWithdrawal.gt(0), "Nominator should receive non-zero wrapped tokens"); - - // Withdraw Rewards - await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.rewards.withdrawRewards(vault_1_id)); - // Withdraw Collateral - await submitExtrinsic( - userInterBtcAPI, - await userInterBtcAPI.nomination.withdrawCollateral(vault_1_id.accountId, nominatorDeposit) - ); - - const nominatorsAfterWithdrawal = await userInterBtcAPI.nomination.list(); - // The vault always has a "nomination" to itself - assert.equal(1, nominatorsAfterWithdrawal.length); - const totalNomination = await userInterBtcAPI.nomination.getTotalNomination( - newAccountId(api, userAccount.address), - await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral) - ); - assert.equal(totalNomination.toString(), "0"); - } finally { - await setIssueFee(encodeUnsignedFixedPoint(api, issueFee)); - await optOutWithAccount( - vault_1, - await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral) - ); - } - } - }); - - async function optInWithAccount(vaultAccount: KeyringPair, collateralCurrency: CollateralCurrencyExt) { - // will fail if vault is already opted in - await callWith(userInterBtcAPI, vaultAccount, async () => { - await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.nomination.optIn(collateralCurrency)); - }); - } - - async function optOutWithAccount(vaultAccount: KeyringPair, collateralCurrency: CollateralCurrencyExt) { - await callWith(userInterBtcAPI, vaultAccount, async () => { - await submitExtrinsic(userInterBtcAPI, userInterBtcAPI.nomination.optOut(collateralCurrency)); - }); - } -}); diff --git a/test/integration/parachain/staging/sequential/oracle.partial.ts b/test/integration/parachain/staging/sequential/oracle.partial.ts new file mode 100644 index 000000000..939bf5ee5 --- /dev/null +++ b/test/integration/parachain/staging/sequential/oracle.partial.ts @@ -0,0 +1,108 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Bitcoin, BitcoinAmount, ExchangeRate } from "@interlay/monetary-js"; + +import { createSubstrateAPI } from "../../../../../src/factory"; +import { ESPLORA_BASE_PATH, ORACLE_URI, PARACHAIN_ENDPOINT } from "../../../../config"; +import { + CollateralCurrencyExt, + DefaultInterBtcApi, + getSS58Prefix, + InterBtcApi, + tokenSymbolToCurrency, +} from "../../../../../src"; +import { + getCorrespondingCollateralCurrenciesForTests, + getExchangeRateValueToSetForTesting, + ORACLE_MAX_DELAY, + submitExtrinsic, +} from "../../../../utils/helpers"; + +export const oracleTests = () => { + describe("OracleAPI", () => { + let api: ApiPromise; + let interBtcAPI: InterBtcApi; + let collateralCurrencies: Array; + let oracleAccount: KeyringPair; + let aliceAccount: KeyringPair; + let bobAccount: KeyringPair; + let charlieAccount: KeyringPair; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + const ss58Prefix = getSS58Prefix(api); + const keyring = new Keyring({ type: "sr25519", ss58Format: ss58Prefix }); + oracleAccount = keyring.addFromUri(ORACLE_URI); + + aliceAccount = keyring.addFromUri("//Alice"); + bobAccount = keyring.addFromUri("//Bob"); + charlieAccount = keyring.addFromUri("//Charlie"); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", oracleAccount, ESPLORA_BASE_PATH); + collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + it("should set exchange rate", async () => { + for (const collateralCurrency of collateralCurrencies) { + const exchangeRateValue = getExchangeRateValueToSetForTesting(collateralCurrency); + const newExchangeRate = new ExchangeRate( + Bitcoin, + collateralCurrency, + exchangeRateValue + ); + await submitExtrinsic(interBtcAPI, interBtcAPI.oracle.setExchangeRate(newExchangeRate)); + } + }); + + it("should convert satoshi to collateral currency", async () => { + for (const collateralCurrency of collateralCurrencies) { + const bitcoinAmount = new BitcoinAmount(100); + const exchangeRate = await interBtcAPI.oracle.getExchangeRate(collateralCurrency); + const expectedCollateral = exchangeRate.toBig().mul(bitcoinAmount.toBig(Bitcoin.decimals)).round(0, 0); + + const collateralAmount = await interBtcAPI.oracle.convertWrappedToCurrency( + bitcoinAmount, + collateralCurrency + ); + expect(collateralAmount.toBig(collateralCurrency.decimals).round(0, 0).toString()).toEqual(expectedCollateral.toString()); + } + }); + + it("should get names by id", async () => { + const expectedSources = new Map(); + expectedSources.set(aliceAccount.address, "Alice"); + expectedSources.set(bobAccount.address, "Bob"); + expectedSources.set(charlieAccount.address, "Charlie"); + const sources = await interBtcAPI.oracle.getSourcesById(); + for (const entry of sources.entries()) { + expect(entry[1]).toEqual(expectedSources.get(entry[0])); + } + }); + + it("should getOnlineTimeout", async () => { + const onlineTimeout = await interBtcAPI.oracle.getOnlineTimeout(); + const expectedOnlineTimeout = ORACLE_MAX_DELAY; + expect(onlineTimeout).toEqual(expectedOnlineTimeout); + }); + + it("should getValidUntil", async () => { + for (const collateralCurrency of collateralCurrencies) { + const validUntil = await interBtcAPI.oracle.getValidUntil(collateralCurrency); + const dateAnHourFromNow = new Date(); + dateAnHourFromNow.setMinutes(dateAnHourFromNow.getMinutes() + 30); + expect(validUntil > dateAnHourFromNow).toBe(true); + } + }); + + it("should be online", async () => { + const relayChainCurrencyId = api.consts.currency.getRelayChainCurrencyId; + const relayChainCurrency = tokenSymbolToCurrency(relayChainCurrencyId.asToken); + + const isOnline = await interBtcAPI.oracle.isOnline(relayChainCurrency); + expect(isOnline).toBe(true); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/oracle.test.ts b/test/integration/parachain/staging/sequential/oracle.test.ts deleted file mode 100644 index ccedf1244..000000000 --- a/test/integration/parachain/staging/sequential/oracle.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { Bitcoin, BitcoinAmount, ExchangeRate } from "@interlay/monetary-js"; - -import { createSubstrateAPI } from "../../../../../src/factory"; -import { assert } from "../../../../chai"; -import { ESPLORA_BASE_PATH, ORACLE_URI, PARACHAIN_ENDPOINT } from "../../../../config"; -import { - CollateralCurrencyExt, - DefaultInterBtcApi, - getSS58Prefix, - InterBtcApi, - tokenSymbolToCurrency, -} from "../../../../../src"; -import { - getCorrespondingCollateralCurrenciesForTests, - getExchangeRateValueToSetForTesting, - ORACLE_MAX_DELAY, - submitExtrinsic, -} from "../../../../utils/helpers"; - -describe("OracleAPI", () => { - let api: ApiPromise; - let interBtcAPI: InterBtcApi; - let collateralCurrencies: Array; - let oracleAccount: KeyringPair; - let aliceAccount: KeyringPair; - let bobAccount: KeyringPair; - let charlieAccount: KeyringPair; - - before(async () => { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - const ss58Prefix = getSS58Prefix(api); - const keyring = new Keyring({ type: "sr25519", ss58Format: ss58Prefix }); - oracleAccount = keyring.addFromUri(ORACLE_URI); - - aliceAccount = keyring.addFromUri("//Alice"); - bobAccount = keyring.addFromUri("//Bob"); - charlieAccount = keyring.addFromUri("//Charlie"); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", oracleAccount, ESPLORA_BASE_PATH); - collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); - }); - - after(() => { - return api.disconnect(); - }); - - it("should set exchange rate", async () => { - for (const collateralCurrency of collateralCurrencies) { - const exchangeRateValue = getExchangeRateValueToSetForTesting(collateralCurrency); - const newExchangeRate = new ExchangeRate( - Bitcoin, - collateralCurrency, - exchangeRateValue - ); - await submitExtrinsic(interBtcAPI, interBtcAPI.oracle.setExchangeRate(newExchangeRate)); - } - }); - - it("should convert satoshi to collateral currency", async () => { - for (const collateralCurrency of collateralCurrencies) { - const bitcoinAmount = new BitcoinAmount(100); - const exchangeRate = await interBtcAPI.oracle.getExchangeRate(collateralCurrency); - const expectedCollateral = exchangeRate.toBig().mul(bitcoinAmount.toBig(Bitcoin.decimals)).round(0, 0); - - const collateralAmount = await interBtcAPI.oracle.convertWrappedToCurrency( - bitcoinAmount, - collateralCurrency - ); - assert.equal( - collateralAmount.toBig(collateralCurrency.decimals).round(0, 0).toString(), - expectedCollateral.toString(), - `Unexpected collateral (${collateralCurrency.ticker}) amount` - ); - } - }); - - it("should get names by id", async () => { - const expectedSources = new Map(); - expectedSources.set(aliceAccount.address, "Alice"); - expectedSources.set(bobAccount.address, "Bob"); - expectedSources.set(charlieAccount.address, "Charlie"); - const sources = await interBtcAPI.oracle.getSourcesById(); - for (const entry of sources.entries()) { - assert.equal(entry[1], expectedSources.get(entry[0])); - } - }); - - it("should getOnlineTimeout", async () => { - const onlineTimeout = await interBtcAPI.oracle.getOnlineTimeout(); - const expectedOnlineTimeout = ORACLE_MAX_DELAY; - assert.equal(onlineTimeout, expectedOnlineTimeout); - }); - - it("should getValidUntil", async () => { - for (const collateralCurrency of collateralCurrencies) { - const validUntil = await interBtcAPI.oracle.getValidUntil(collateralCurrency); - const dateAnHourFromNow = new Date(); - dateAnHourFromNow.setMinutes(dateAnHourFromNow.getMinutes() + 30); - assert.isTrue( - validUntil > dateAnHourFromNow, - `lastExchangeRateTime is older than one hour (${collateralCurrency.ticker})` - ); - } - }); - - it("should be online", async () => { - const relayChainCurrencyId = api.consts.currency.getRelayChainCurrencyId; - const relayChainCurrency = tokenSymbolToCurrency(relayChainCurrencyId.asToken); - - const isOnline = await interBtcAPI.oracle.isOnline(relayChainCurrency); - assert.isTrue(isOnline); - }); -}); diff --git a/test/integration/parachain/staging/sequential/redeem.partial.ts b/test/integration/parachain/staging/sequential/redeem.partial.ts new file mode 100644 index 000000000..668dadd06 --- /dev/null +++ b/test/integration/parachain/staging/sequential/redeem.partial.ts @@ -0,0 +1,142 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { Hash } from "@polkadot/types/interfaces"; +import { + DefaultInterBtcApi, + InterBtcApi, + InterbtcPrimitivesVaultId, + VaultRegistryVault, +} from "../../../../../src/index"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { + BITCOIN_CORE_HOST, + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_USERNAME, + PARACHAIN_ENDPOINT, + BITCOIN_CORE_WALLET, + BITCOIN_CORE_PORT, + USER_1_URI, + VAULT_1_URI, + VAULT_2_URI, + ESPLORA_BASE_PATH, +} from "../../../../config"; +import { issueAndRedeem, newMonetaryAmount } from "../../../../../src/utils"; +import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; +import { newVaultId, WrappedCurrency } from "../../../../../src"; +import { ExecuteRedeem } from "../../../../../src/utils/issueRedeem"; +import { getAUSDForeignAsset, getCorrespondingCollateralCurrenciesForTests } from "../../../../utils/helpers"; + +export type RequestResult = { hash: Hash; vault: VaultRegistryVault }; + +export const redeemTests = () => { + describe("redeem", () => { + let api: ApiPromise; + let keyring: Keyring; + let userAccount: KeyringPair; + let bitcoinCoreClient: BitcoinCoreClient; + let vault_1: KeyringPair; + let vault_2: KeyringPair; + const collateralTickerToVaultIdsMap: Map = new Map(); + + let wrappedCurrency: WrappedCurrency; + + let interBtcAPI: InterBtcApi; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + keyring = new Keyring({ type: "sr25519" }); + userAccount = keyring.addFromUri(USER_1_URI); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); + wrappedCurrency = interBtcAPI.getWrappedCurrency(); + + const collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); + vault_1 = keyring.addFromUri(VAULT_1_URI); + vault_2 = keyring.addFromUri(VAULT_2_URI); + + collateralCurrencies.forEach((collateralCurrency) => { + const vault_1_id = newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency); + const vault_2_id = newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency); + collateralTickerToVaultIdsMap.set(collateralCurrency.ticker, [vault_1_id, vault_2_id]); + }); + + bitcoinCoreClient = new BitcoinCoreClient( + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_HOST, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_WALLET + ); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + it("should issue and request redeem", async () => { + // "usual" scope with hard coded collateral currency (or currencies) + const vaultsInScope = Array.from(collateralTickerToVaultIdsMap.values()); + + // add a pair of aUSD vaults to the list if aUSD has been registered + const aUSD = await getAUSDForeignAsset(interBtcAPI.assetRegistry); + if (aUSD !== undefined) { + const vault_1_id_ausd = newVaultId(api, vault_1.address, aUSD, wrappedCurrency); + const vault_2_id_ausd = newVaultId(api, vault_2.address, aUSD, wrappedCurrency); + vaultsInScope.push([vault_1_id_ausd, vault_2_id_ausd]); + } + + for (const [vault_1_id] of vaultsInScope) { + const issueAmount = newMonetaryAmount(0.00005, wrappedCurrency, true); + const redeemAmount = newMonetaryAmount(0.00003, wrappedCurrency, true); + + await issueAndRedeem( + interBtcAPI, + bitcoinCoreClient, + userAccount, + vault_1_id, + issueAmount, + redeemAmount, + false, + ExecuteRedeem.False + ); + } + }, 1000 * 90); + + it("should load existing redeem requests", async () => { + const redeemRequests = await interBtcAPI.redeem.list(); + expect(redeemRequests.length).toBeGreaterThanOrEqual(1); + }); + + it("should get redeemBtcDustValue", async () => { + const dust = await interBtcAPI.api.query.redeem.redeemBtcDustValue(); + expect(dust.toString()).toEqual("1000"); + }); + + it("should getFeesToPay", async () => { + const amount = newMonetaryAmount(2, wrappedCurrency, true); + const feesToPay = await interBtcAPI.redeem.getFeesToPay(amount); + expect(feesToPay.toString()).toEqual("0.01"); + }); + + it("should getFeeRate", async () => { + const feePercentage = await interBtcAPI.redeem.getFeeRate(); + expect(feePercentage.toString()).toEqual("0.005"); + }); + + it("should getPremiumRedeemFeeRate", async () => { + const premiumRedeemFee = await interBtcAPI.redeem.getPremiumRedeemFeeRate(); + expect(premiumRedeemFee.toString()).toEqual("0.05"); + }); + + it("should getCurrentInclusionFee", async () => { + const currentInclusionFee = await interBtcAPI.redeem.getCurrentInclusionFee(); + expect(!currentInclusionFee.isZero()).toBe(true); + }); + + it("should getDustValue", async () => { + const dustValue = await interBtcAPI.redeem.getDustValue(); + expect(dustValue.toString()).toEqual("0.00001"); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/redeem.test.ts b/test/integration/parachain/staging/sequential/redeem.test.ts deleted file mode 100644 index 66a8636b1..000000000 --- a/test/integration/parachain/staging/sequential/redeem.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { Hash } from "@polkadot/types/interfaces"; -import { - DefaultInterBtcApi, - InterBtcApi, - InterbtcPrimitivesVaultId, - VaultRegistryVault, -} from "../../../../../src/index"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { assert } from "../../../../chai"; -import { - BITCOIN_CORE_HOST, - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_USERNAME, - PARACHAIN_ENDPOINT, - BITCOIN_CORE_WALLET, - BITCOIN_CORE_PORT, - USER_1_URI, - VAULT_1_URI, - VAULT_2_URI, - ESPLORA_BASE_PATH, -} from "../../../../config"; -import { issueAndRedeem, newMonetaryAmount } from "../../../../../src/utils"; -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; -import { newVaultId, WrappedCurrency } from "../../../../../src"; -import { ExecuteRedeem } from "../../../../../src/utils/issueRedeem"; -import { getAUSDForeignAsset, getCorrespondingCollateralCurrenciesForTests } from "../../../../utils/helpers"; - -export type RequestResult = { hash: Hash; vault: VaultRegistryVault }; - -describe("redeem", () => { - let api: ApiPromise; - let keyring: Keyring; - let userAccount: KeyringPair; - let bitcoinCoreClient: BitcoinCoreClient; - let vault_1: KeyringPair; - let vault_2: KeyringPair; - const collateralTickerToVaultIdsMap: Map = new Map(); - - let wrappedCurrency: WrappedCurrency; - - let interBtcAPI: InterBtcApi; - - before(async () => { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - keyring = new Keyring({ type: "sr25519" }); - userAccount = keyring.addFromUri(USER_1_URI); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); - wrappedCurrency = interBtcAPI.getWrappedCurrency(); - - const collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); - vault_1 = keyring.addFromUri(VAULT_1_URI); - vault_2 = keyring.addFromUri(VAULT_2_URI); - - collateralCurrencies.forEach((collateralCurrency) => { - const vault_1_id = newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency); - const vault_2_id = newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency); - collateralTickerToVaultIdsMap.set(collateralCurrency.ticker, [vault_1_id, vault_2_id]); - }); - - bitcoinCoreClient = new BitcoinCoreClient( - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_HOST, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_WALLET - ); - }); - - after(() => { - return api.disconnect(); - }); - - it("should issue and request redeem", async () => { - // "usual" scope with hard coded collateral currency (or currencies) - const vaultsInScope = Array.from(collateralTickerToVaultIdsMap.values()); - - // add a pair of aUSD vaults to the list if aUSD has been registered - const aUSD = await getAUSDForeignAsset(interBtcAPI.assetRegistry); - if (aUSD !== undefined) { - const vault_1_id_ausd = newVaultId(api, vault_1.address, aUSD, wrappedCurrency); - const vault_2_id_ausd = newVaultId(api, vault_2.address, aUSD, wrappedCurrency); - vaultsInScope.push([vault_1_id_ausd, vault_2_id_ausd]); - } - - for (const [vault_1_id] of vaultsInScope) { - const issueAmount = newMonetaryAmount(0.00005, wrappedCurrency, true); - const redeemAmount = newMonetaryAmount(0.00003, wrappedCurrency, true); - - await issueAndRedeem( - interBtcAPI, - bitcoinCoreClient, - userAccount, - vault_1_id, - issueAmount, - redeemAmount, - false, - ExecuteRedeem.False - ); - } - }).timeout(1000 * 90); - - it("should load existing redeem requests", async () => { - const redeemRequests = await interBtcAPI.redeem.list(); - assert.isAtLeast( - redeemRequests.length, - 1, - "Error in initialization setup. Should have at least 1 issue request" - ); - }); - - it("should get redeemBtcDustValue", async () => { - const dust = await interBtcAPI.api.query.redeem.redeemBtcDustValue(); - assert.equal(dust.toString(), "1000"); - }); - - it("should getFeesToPay", async () => { - const amount = newMonetaryAmount(2, wrappedCurrency, true); - const feesToPay = await interBtcAPI.redeem.getFeesToPay(amount); - assert.equal(feesToPay.toString(), "0.01"); - }); - - it("should getFeeRate", async () => { - const feePercentage = await interBtcAPI.redeem.getFeeRate(); - assert.equal(feePercentage.toString(), "0.005"); - }); - - it("should getPremiumRedeemFeeRate", async () => { - const premiumRedeemFee = await interBtcAPI.redeem.getPremiumRedeemFeeRate(); - assert.equal(premiumRedeemFee.toString(), "0.05"); - }); - - it("should getCurrentInclusionFee", async () => { - const currentInclusionFee = await interBtcAPI.redeem.getCurrentInclusionFee(); - assert.isTrue(!currentInclusionFee.isZero()); - }); - - it("should getDustValue", async () => { - const dustValue = await interBtcAPI.redeem.getDustValue(); - assert.equal(dustValue.toString(), "0.00001"); - }); -}); diff --git a/test/integration/parachain/staging/sequential/replace.partial.ts b/test/integration/parachain/staging/sequential/replace.partial.ts new file mode 100644 index 000000000..f86bfaad7 --- /dev/null +++ b/test/integration/parachain/staging/sequential/replace.partial.ts @@ -0,0 +1,176 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { + DefaultInterBtcApi, + InterBtcApi, + InterbtcPrimitivesVaultId, + SLEEP_TIME_MS, + newMonetaryAmount, + sleep, +} from "../../../../../src/index"; + +import { MonetaryAmount } from "@interlay/monetary-js"; +import { ApiTypes, AugmentedEvent } from "@polkadot/api/types"; +import { BlockHash } from "@polkadot/types/interfaces"; +import { FrameSystemEventRecord } from "@polkadot/types/lookup"; +import { WrappedCurrency, currencyIdToMonetaryCurrency, newAccountId, newVaultId } from "../../../../../src"; +import { createSubstrateAPI } from "../../../../../src/factory"; +import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; +import { issueSingle } from "../../../../../src/utils/issueRedeem"; +import { + BITCOIN_CORE_HOST, + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_WALLET, + ESPLORA_BASE_PATH, + PARACHAIN_ENDPOINT, + USER_1_URI, + VAULT_2_URI, + VAULT_3_URI, +} from "../../../../config"; +import { getCorrespondingCollateralCurrenciesForTests, submitExtrinsic } from "../../../../utils/helpers"; + +export const replaceTests = () => { + describe("replace", () => { + let api: ApiPromise; + let bitcoinCoreClient: BitcoinCoreClient; + let keyring: Keyring; + let userAccount: KeyringPair; + let vault_3: KeyringPair; + let vault_3_ids: Array; + let vault_2: KeyringPair; + let vault_2_ids: Array; + let interBtcAPI: InterBtcApi; + + let wrappedCurrency: WrappedCurrency; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + keyring = new Keyring({ type: "sr25519" }); + bitcoinCoreClient = new BitcoinCoreClient( + BITCOIN_CORE_NETWORK, + BITCOIN_CORE_HOST, + BITCOIN_CORE_USERNAME, + BITCOIN_CORE_PASSWORD, + BITCOIN_CORE_PORT, + BITCOIN_CORE_WALLET + ); + + userAccount = keyring.addFromUri(USER_1_URI); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); + wrappedCurrency = interBtcAPI.getWrappedCurrency(); + const collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); + vault_3 = keyring.addFromUri(VAULT_3_URI); + vault_3_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_3.address, collateralCurrency, wrappedCurrency) + ); + vault_2 = keyring.addFromUri(VAULT_2_URI); + vault_2_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency) + ); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + describe("request", () => { + let dustValue: MonetaryAmount; + let feesEstimate: MonetaryAmount; + + beforeAll(async () => { + dustValue = await interBtcAPI.replace.getDustValue(); + feesEstimate = newMonetaryAmount(await interBtcAPI.oracle.getBitcoinFees(), wrappedCurrency, false); + }); + + // TODO: update test once replace protocol changes + // https://github.com/interlay/interbtc/issues/823 + it("should request vault replacement", async () => { + const interBtcAPI = new DefaultInterBtcApi(api, "regtest", vault_3, ESPLORA_BASE_PATH); + for (const vault_3_id of vault_3_ids) { + // try to set value above dust + estimated fees + const issueAmount = dustValue.add(feesEstimate).mul(1.2); + const replaceAmount = dustValue; + await issueSingle(interBtcAPI, bitcoinCoreClient, userAccount, issueAmount, vault_3_id); + + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_3_id.currencies.collateral); + + console.log(`Requesting vault replacement for ${replaceAmount.toString()}`); + const result = await submitExtrinsic( + interBtcAPI, + interBtcAPI.replace.request(replaceAmount, collateralCurrency), + false + ); + const blockHash = result.status.asFinalized; + + // query at included block since it may be accepted after + const apiAt = await api.at(blockHash); + const vault = await apiAt.query.vaultRegistry.vaults(vault_3_id); + const toBeReplaced = vault.unwrap().toBeReplacedTokens.toBn(); + + expect(toBeReplaced.toString()).toEqual(replaceAmount.toString(true)); + + // hacky way to subscribe to events from a previous height + // we can remove this once the request / accept flow is removed + // eslint-disable-next-line no-inner-declarations + async function waitForEvent( + blockHash: BlockHash, + expectedEvent: AugmentedEvent + ): Promise<[FrameSystemEventRecord, BlockHash]> { + let hash = blockHash; + // eslint-disable-next-line no-constant-condition + while (true) { + const header = await api.rpc.chain.getHeader(hash); + const nextHash = await api.rpc.chain.getBlockHash(header.number.toNumber() + 1); + + if (nextHash.isEmpty) { + await sleep(SLEEP_TIME_MS); + continue; + } else { + hash = nextHash; + } + + const apiAt = await api.at(hash); + const events = await apiAt.query.system.events(); + const foundEvent = events.find(({ event }) => expectedEvent.is(event)); + if (foundEvent) { + return [foundEvent, hash]; + } + } + } + + const [acceptReplaceEvent, foundBlockHash] = await waitForEvent( + blockHash, + api.events.replace.AcceptReplace + ); + const requestId = api.createType("Hash", acceptReplaceEvent.event.data[0]); + + const replaceRequest = await interBtcAPI.replace.getRequestById(requestId, foundBlockHash); + expect(replaceRequest.oldVault.accountId.toString()).toEqual(vault_3_id.accountId.toString()); + } + }, 1000 * 30); + }); + + describe("check values, and request statuses", () => { + it("should getDustValue", async () => { + const dustValue = await interBtcAPI.replace.getDustValue(); + expect(dustValue.toString()).toEqual("0.00001"); + }, 500); + + it("should getReplacePeriod", async () => { + const replacePeriod = await interBtcAPI.replace.getReplacePeriod(); + expect(replacePeriod).toBeDefined(); + }, 500); + + it("should list replace request by a vault", async () => { + const vault3Id = newAccountId(api, vault_3.address); + const replaceRequests = await interBtcAPI.replace.mapReplaceRequests(vault3Id); + replaceRequests.forEach((request) => { + expect(request.oldVault.accountId.toString()).toEqual(vault3Id.toString()); + }); + }); + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/replace.test.ts b/test/integration/parachain/staging/sequential/replace.test.ts deleted file mode 100644 index 74cb9d439..000000000 --- a/test/integration/parachain/staging/sequential/replace.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import { - DefaultInterBtcApi, - InterBtcApi, - InterbtcPrimitivesVaultId, - newMonetaryAmount, - sleep, - SLEEP_TIME_MS, -} from "../../../../../src/index"; - -import { BitcoinCoreClient } from "../../../../../src/utils/bitcoin-core-client"; -import { createSubstrateAPI } from "../../../../../src/factory"; -import { - USER_1_URI, - VAULT_2_URI, - BITCOIN_CORE_HOST, - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_WALLET, - PARACHAIN_ENDPOINT, - VAULT_3_URI, - ESPLORA_BASE_PATH, -} from "../../../../config"; -import { assert, expect } from "../../../../chai"; -import { issueSingle } from "../../../../../src/utils/issueRedeem"; -import { currencyIdToMonetaryCurrency, newAccountId, newVaultId, WrappedCurrency } from "../../../../../src"; -import { MonetaryAmount } from "@interlay/monetary-js"; -import { getCorrespondingCollateralCurrenciesForTests, submitExtrinsic } from "../../../../utils/helpers"; -import { BlockHash } from "@polkadot/types/interfaces"; -import { ApiTypes, AugmentedEvent } from "@polkadot/api/types"; -import { FrameSystemEventRecord } from "@polkadot/types/lookup"; - -describe("replace", () => { - let api: ApiPromise; - let bitcoinCoreClient: BitcoinCoreClient; - let keyring: Keyring; - let userAccount: KeyringPair; - let vault_3: KeyringPair; - let vault_3_ids: Array; - let vault_2: KeyringPair; - let vault_2_ids: Array; - let interBtcAPI: InterBtcApi; - - let wrappedCurrency: WrappedCurrency; - - before(async function () { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - keyring = new Keyring({ type: "sr25519" }); - bitcoinCoreClient = new BitcoinCoreClient( - BITCOIN_CORE_NETWORK, - BITCOIN_CORE_HOST, - BITCOIN_CORE_USERNAME, - BITCOIN_CORE_PASSWORD, - BITCOIN_CORE_PORT, - BITCOIN_CORE_WALLET - ); - - userAccount = keyring.addFromUri(USER_1_URI); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount, ESPLORA_BASE_PATH); - wrappedCurrency = interBtcAPI.getWrappedCurrency(); - const collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); - vault_3 = keyring.addFromUri(VAULT_3_URI); - vault_3_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_3.address, collateralCurrency, wrappedCurrency) - ); - vault_2 = keyring.addFromUri(VAULT_2_URI); - vault_2_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_2.address, collateralCurrency, wrappedCurrency) - ); - }); - - after(async () => { - api.disconnect(); - }); - - describe("request", () => { - let dustValue: MonetaryAmount; - let feesEstimate: MonetaryAmount; - - before(async () => { - dustValue = await interBtcAPI.replace.getDustValue(); - feesEstimate = newMonetaryAmount(await interBtcAPI.oracle.getBitcoinFees(), wrappedCurrency, false); - }); - - // TODO: update test once replace protocol changes - // https://github.com/interlay/interbtc/issues/823 - it("should request vault replacement", async () => { - interBtcAPI.setAccount(vault_3); - for (const vault_3_id of vault_3_ids) { - // try to set value above dust + estimated fees - const issueAmount = dustValue.add(feesEstimate).mul(1.2); - const replaceAmount = dustValue; - await issueSingle(interBtcAPI, bitcoinCoreClient, userAccount, issueAmount, vault_3_id); - - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_3_id.currencies.collateral); - - console.log(`Requesting vault replacement for ${replaceAmount.toString()}`); - const result = await submitExtrinsic( - interBtcAPI, - interBtcAPI.replace.request(replaceAmount, collateralCurrency), - false - ); - const blockHash = result.status.asFinalized; - - // query at included block since it may be accepted after - const apiAt = await api.at(blockHash); - const vault = await apiAt.query.vaultRegistry.vaults(vault_3_id); - const toBeReplaced = vault.unwrap().toBeReplacedTokens.toBn(); - - assert.equal(toBeReplaced.toString(), replaceAmount.toString(true)); - - // hacky way to subscribe to events from a previous height - // we can remove this once the request / accept flow is removed - // eslint-disable-next-line no-inner-declarations - async function waitForEvent( - blockHash: BlockHash, - expectedEvent: AugmentedEvent - ): Promise<[FrameSystemEventRecord, BlockHash]> { - let hash = blockHash; - // eslint-disable-next-line no-constant-condition - while (true) { - const header = await api.rpc.chain.getHeader(hash); - const nextHash = await api.rpc.chain.getBlockHash(header.number.toNumber() + 1); - - if (nextHash.isEmpty) { - await sleep(SLEEP_TIME_MS); - continue; - } else { - hash = nextHash; - } - - const apiAt = await api.at(hash); - const events = await apiAt.query.system.events(); - const foundEvent = events.find(({ event }) => expectedEvent.is(event)); - if (foundEvent) { - return [foundEvent, hash]; - } - } - } - - const [acceptReplaceEvent, foundBlockHash] = await waitForEvent( - blockHash, - api.events.replace.AcceptReplace - ); - const requestId = api.createType("Hash", acceptReplaceEvent.event.data[0]); - - const replaceRequest = await interBtcAPI.replace.getRequestById(requestId, foundBlockHash); - assert.equal(replaceRequest.oldVault.accountId.toString(), vault_3_id.accountId.toString()); - } - }).timeout(1000 * 30); - - it("should fail vault replace request if not having enough tokens", async () => { - interBtcAPI.setAccount(vault_2); - for (const vault_2_id of vault_2_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_2_id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - - // fetch tokens held by vault - const tokensInVault = await interBtcAPI.vaults.getIssuedAmount( - newAccountId(api, vault_2.address), - collateralCurrency - ); - - // make sure vault does not hold enough issued tokens to request a replace - const replaceAmount = dustValue.add(tokensInVault); - - const replacePromise = submitExtrinsic( - interBtcAPI, - interBtcAPI.replace.request(replaceAmount, collateralCurrency), - false - ); - expect(replacePromise).to.be.rejectedWith( - Error, - `Expected replace request to fail with Error (${currencyTicker} vault)` - ); - } - }); - }); - - it("should getDustValue", async () => { - const dustValue = await interBtcAPI.replace.getDustValue(); - assert.equal(dustValue.toString(), "0.00001"); - }).timeout(500); - - it("should getReplacePeriod", async () => { - const replacePeriod = await interBtcAPI.replace.getReplacePeriod(); - assert.isDefined(replacePeriod, "Expected replace period to be defined, but was not"); - }).timeout(500); - - it("should list replace request by a vault", async () => { - const vault3Id = newAccountId(api, vault_3.address); - const replaceRequests = await interBtcAPI.replace.mapReplaceRequests(vault3Id); - replaceRequests.forEach((request) => { - assert.deepEqual(request.oldVault.accountId, vault3Id); - }); - }); -}); diff --git a/test/integration/parachain/staging/sequential/vaults.partial.ts b/test/integration/parachain/staging/sequential/vaults.partial.ts new file mode 100644 index 000000000..0cbca1fad --- /dev/null +++ b/test/integration/parachain/staging/sequential/vaults.partial.ts @@ -0,0 +1,347 @@ +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import Big from "big.js"; +import { + DefaultInterBtcApi, + InterBtcApi, + InterbtcPrimitivesVaultId, + currencyIdToMonetaryCurrency, + CollateralCurrencyExt, + VaultStatusExt, + GovernanceCurrency, + AssetRegistryAPI, + DefaultAssetRegistryAPI, +} from "../../../../../src/index"; + +import { createSubstrateAPI } from "../../../../../src/factory"; +import { VAULT_1_URI, VAULT_2_URI, PARACHAIN_ENDPOINT, VAULT_3_URI, ESPLORA_BASE_PATH } from "../../../../config"; +import { newAccountId, WrappedCurrency, newVaultId } from "../../../../../src"; +import { getSS58Prefix, newMonetaryAmount } from "../../../../../src/utils"; +import { + getAUSDForeignAsset, + getCorrespondingCollateralCurrenciesForTests, + getIssuableAmounts, + submitExtrinsic, + vaultStatusToLabel, +} from "../../../../utils/helpers"; + +export const vaultsTests = () => { + describe("vaultsAPI", () => { + let vault_1: KeyringPair; + let vault_1_ids: Array; + let vault_2: KeyringPair; + let vault_3: KeyringPair; + let api: ApiPromise; + + let wrappedCurrency: WrappedCurrency; + let collateralCurrencies: Array; + let governanceCurrency: GovernanceCurrency; + + let interBtcAPI: InterBtcApi; + let assetRegistry: AssetRegistryAPI; + + beforeAll(async () => { + api = await createSubstrateAPI(PARACHAIN_ENDPOINT); + const ss58Prefix = getSS58Prefix(api); + const keyring = new Keyring({ type: "sr25519", ss58Format: ss58Prefix }); + assetRegistry = new DefaultAssetRegistryAPI(api); + interBtcAPI = new DefaultInterBtcApi(api, "regtest", undefined, ESPLORA_BASE_PATH); + + wrappedCurrency = interBtcAPI.getWrappedCurrency(); + governanceCurrency = interBtcAPI.getGovernanceCurrency(); + + collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(governanceCurrency); + const aUSD = await getAUSDForeignAsset(assetRegistry); + if (aUSD !== undefined) { + // also add aUSD collateral vaults if they exist (ie. the foreign asset exists) + collateralCurrencies.push(aUSD); + } + + vault_1 = keyring.addFromUri(VAULT_1_URI); + vault_1_ids = collateralCurrencies.map((collateralCurrency) => + newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) + ); + + vault_2 = keyring.addFromUri(VAULT_2_URI); + vault_3 = keyring.addFromUri(VAULT_3_URI); + }); + + afterAll(async () => { + await api.disconnect(); + }); + + afterEach(() => { + // discard any stubbed methods after each test + jest.restoreAllMocks(); + }); + + function vaultIsATestVault(vaultAddress: string): boolean { + return vaultAddress === vault_2.address || vaultAddress === vault_1.address || vaultAddress === vault_3.address; + } + + it("should get issuable", async () => { + const issuableInterBTC = await interBtcAPI.vaults.getTotalIssuableAmount(); + const issuableAmounts = await getIssuableAmounts(interBtcAPI); + const totalIssuable = issuableAmounts.reduce((prev, curr) => prev.add(curr)); + expect(issuableInterBTC.toBig().sub(totalIssuable.toBig()).abs().lte(1)).toBe(true); + }); + + it("should get the required collateral for the vault", async () => { + for (const vault_1_id of vault_1_ids) { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); + const requiredCollateralForVault = await interBtcAPI.vaults.getRequiredCollateralForVault( + vault_1_id.accountId, + collateralCurrency + ); + + const vault = await interBtcAPI.vaults.get(vault_1_id.accountId, collateralCurrency); + + // The numeric value of the required collateral should be greater than that of issued tokens. + // e.g. we require `0.8096` KSM for `0.00014` kBTC + // edge case: we require 0 KSM for 0 kBTC, so check greater than or equal to + expect(requiredCollateralForVault.toBig().gte(vault.getBackedTokens().toBig())).toBe(true); + } + }); + + // WARNING: this test is not idempotent + // PRECONDITION: vault_1 must have issued some tokens against all collateral currencies + it("should deposit and withdraw collateral", async () => { + const interBtcAPI = new DefaultInterBtcApi(api, "regtest", vault_1, ESPLORA_BASE_PATH); + for (const vault_1_id of vault_1_ids) { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); + const currencyTicker = collateralCurrency.ticker; + + const amount = newMonetaryAmount(100, collateralCurrency, true); + + const collateralizationBeforeDeposit = await interBtcAPI.vaults.getVaultCollateralization( + newAccountId(api, vault_1.address), + collateralCurrency + ); + await submitExtrinsic(interBtcAPI, interBtcAPI.vaults.depositCollateral(amount)); + const collateralizationAfterDeposit = await interBtcAPI.vaults.getVaultCollateralization( + newAccountId(api, vault_1.address), + collateralCurrency + ); + if (collateralizationBeforeDeposit === undefined || collateralizationAfterDeposit == undefined) { + throw Error( + `Collateralization is undefined for vault with collateral currency ${currencyTicker} + - potential cause: the vault may not have any issued tokens secured by ${currencyTicker}` + ); + } + expect(collateralizationAfterDeposit.gt(collateralizationBeforeDeposit)).toBe(true); + + await submitExtrinsic(interBtcAPI, await interBtcAPI.vaults.withdrawCollateral(amount)); + const collateralizationAfterWithdrawal = await interBtcAPI.vaults.getVaultCollateralization( + newAccountId(api, vault_1.address), + collateralCurrency + ); + if (collateralizationAfterWithdrawal === undefined) { + throw Error(`Collateralization is undefined for vault with collateral currency ${currencyTicker}`); + } + expect(collateralizationAfterDeposit.gt(collateralizationAfterWithdrawal)).toBe(true); + expect(collateralizationBeforeDeposit.toString()).toEqual(collateralizationAfterWithdrawal.toString()); + } + }); + + it("should getLiquidationCollateralThreshold", async () => { + for (const collateralCurrency of collateralCurrencies) { + const currencyTicker = collateralCurrency.ticker; + + const threshold = await interBtcAPI.vaults.getLiquidationCollateralThreshold(collateralCurrency); + try { + expect(threshold.gt(0)).toBe(true); + } catch(_) { + throw Error(`Liqduiation collateral threshold for ${currencyTicker} was ${threshold.toString()}, expected: 0`); + } + } + }); + + it("should getPremiumRedeemThreshold", async () => { + for (const collateralCurrency of collateralCurrencies) { + const currencyTicker = collateralCurrency.ticker; + const threshold = await interBtcAPI.vaults.getPremiumRedeemThreshold(collateralCurrency); + + try { + expect(threshold.gt(0)).toBe(true); + } catch(_) { + throw Error(`Premium redeem threshold for ${currencyTicker} was ${threshold.toString()}, expected: 0`); + } + } + }); + + it("should select random vault for issue", async () => { + const randomVault = await interBtcAPI.vaults.selectRandomVaultIssue(newMonetaryAmount(0, wrappedCurrency)); + expect(vaultIsATestVault(randomVault.accountId.toHuman())).toBe(true); + }); + + it("should fail if no vault for issuing is found", async () => { + await expect(interBtcAPI.vaults.selectRandomVaultIssue(newMonetaryAmount(9000000, wrappedCurrency, true))).rejects.toThrow(); + }); + + it("should select random vault for redeem", async () => { + const randomVault = await interBtcAPI.vaults.selectRandomVaultRedeem(newMonetaryAmount(0, wrappedCurrency)); + expect(vaultIsATestVault(randomVault.accountId.toHuman())).toBe(true); + }); + + it("should fail if no vault for redeeming is found", async () => { + const amount = newMonetaryAmount(9000000, wrappedCurrency, true); + await expect(interBtcAPI.vaults.selectRandomVaultRedeem(amount)).rejects.toThrow(); + }); + + it("should get the issuable InterBtc for a vault", async () => { + for (const vault_1_id of vault_1_ids) { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); + const currencyTicker = collateralCurrency.ticker; + + const vault = await interBtcAPI.vaults.get(vault_1_id.accountId, collateralCurrency); + const issuableTokens = await vault.getIssuableTokens(); + + try { + expect(issuableTokens.gt(newMonetaryAmount(0, wrappedCurrency))).toBe(true); + } catch(_) { + throw Error(`Issuable tokens should be greater than 0 (${currencyTicker} vault)`); + } + } + }); + + it("should get the issuable InterBtc", async () => { + const issuableInterBtc = await interBtcAPI.vaults.getTotalIssuableAmount(); + expect(issuableInterBtc.gt(newMonetaryAmount(0, wrappedCurrency))).toBe(true); + }); + + it("should getFees", async () => { + const vaultIdsInScope = vault_1_ids; + let countSkippedVaults = 0; + let countVaultsWithNonZeroWrappedRewards = 0; + + for (const vaultId of vaultIdsInScope) { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vaultId.currencies.collateral); + const wrappedCurrency = await currencyIdToMonetaryCurrency(api, vaultId.currencies.wrapped); + const currencyTicker = collateralCurrency.ticker; + + const vault = await interBtcAPI.vaults.get(vaultId.accountId, collateralCurrency); + const issueableTokens = await vault.getIssuableTokens(); + const issuedTokens = vault.issuedTokens; + const totalTokensCapacity = issuedTokens.toBig().add(issueableTokens.toBig()); + if (totalTokensCapacity.eq(0)) { + // no token capacity => no rewards => nothing to check + countSkippedVaults++; + continue; + } + + const feesWrapped = await interBtcAPI.vaults.getWrappedReward( + vaultId.accountId, + collateralCurrency, + wrappedCurrency + ); + + try { + expect(feesWrapped.gte(newMonetaryAmount(0, wrappedCurrency))).toBe(true); + } catch(_) { + // eslint-disable-next-line max-len + throw Error(`Fees (wrapped reward) should be greater than or equal to 0 (${currencyTicker} vault, account id ${vaultId.accountId.toString()}), but was: ${feesWrapped.toHuman()}`); + } + + if (feesWrapped.gt(newMonetaryAmount(0, wrappedCurrency))) { + // we will check that at least one return was greater than zero + countVaultsWithNonZeroWrappedRewards++; + } + + const govTokenReward = await interBtcAPI.vaults.getGovernanceReward( + vaultId.accountId, + collateralCurrency, + governanceCurrency + ); + + try { + expect(govTokenReward.gte(newMonetaryAmount(0, governanceCurrency))).toBe(true); + } catch(_) { + // eslint-disable-next-line max-len + throw Error(`Governance reward should be greater than or equal to 0 (${currencyTicker} vault, account id ${vaultId.accountId.toString()}), but was: ${feesWrapped.toHuman()}`); + } + } + // make sure not every vault has been skipped (due to no issued tokens) + try { + expect(countSkippedVaults).not.toEqual(vaultIdsInScope.length); + } catch(_) { + // eslint-disable-next-line max-len + throw Error(`Unexpected test behavior: skipped all ${vaultIdsInScope.length} vaults in the test; all vaults lacking capacity (issued + issuable > 0)`); + } + + // make sure at least one vault is receiving wrapped rewards greater than zero + try { + expect(countVaultsWithNonZeroWrappedRewards).toBeGreaterThan(0); + } catch(_) { + // eslint-disable-next-line max-len + throw Error(`Unexpected test behavior: none of the ${vaultIdsInScope.length} vaults in the test have received more than 0 wrapped token rewards`); + } + }); + + it("should getAPY", async () => { + for (const vault_1_id of vault_1_ids) { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); + const currencyTicker = collateralCurrency.ticker; + const accountId = newAccountId(api, vault_1.address); + + const apy = await interBtcAPI.vaults.getAPY(accountId, collateralCurrency); + const apyBig = new Big(apy); + const apyBenchmark = new Big("0"); + try { + expect(apyBig.gte(apyBenchmark)).toBe(true); + } catch(_) { + throw Error(`APY should be greater than or equal to ${apyBenchmark.toString()}, + but was ${apyBig.toString()} (${currencyTicker} vault)`); + } + } + }); + + it("should getPunishmentFee", async () => { + const punishmentFee = await interBtcAPI.vaults.getPunishmentFee(); + expect(punishmentFee.toString()).toEqual("0.1"); + }); + + it("should get vault list", async () => { + const vaults = (await interBtcAPI.vaults.list()).map((vault) => vault.id.toHuman()); + expect(vaults.length).toBeGreaterThan(0); + }); + + it("should disable and enable issuing with vault", async () => { + const interBtcAPI = new DefaultInterBtcApi(api, "regtest", vault_1, ESPLORA_BASE_PATH); + + const assertVaultStatus = async (id: InterbtcPrimitivesVaultId, expectedStatus: VaultStatusExt) => { + const collateralCurrency = await currencyIdToMonetaryCurrency(api, id.currencies.collateral); + const currencyTicker = collateralCurrency.ticker; + const { status } = await interBtcAPI.vaults.get(id.accountId, collateralCurrency); + const assertionMessage = `Vault with id ${id.toString()} (collateral: ${currencyTicker}) was expected to have + status: ${vaultStatusToLabel(expectedStatus)}, but got status: ${vaultStatusToLabel(status)}`; + + try { + expect(status === expectedStatus).toBe(true); + } catch(_) { + throw Error(assertionMessage); + } + }; + const ACCEPT_NEW_ISSUES = true; + const REJECT_NEW_ISSUES = false; + + for (const vault_1_id of vault_1_ids) { + // Check that vault 1 is active. + await assertVaultStatus(vault_1_id, VaultStatusExt.Active); + // Disables vault 1 which is active. + await submitExtrinsic( + interBtcAPI, + await interBtcAPI.vaults.toggleIssueRequests(vault_1_id, REJECT_NEW_ISSUES) + ); + // Check that vault 1 is inactive. + await assertVaultStatus(vault_1_id, VaultStatusExt.Inactive); + // Re-enable issuing with vault 1. + await submitExtrinsic( + interBtcAPI, + await interBtcAPI.vaults.toggleIssueRequests(vault_1_id, ACCEPT_NEW_ISSUES) + ); + // Check that vault 1 is again active. + await assertVaultStatus(vault_1_id, VaultStatusExt.Active); + } + }); + }); +}; diff --git a/test/integration/parachain/staging/sequential/vaults.test.ts b/test/integration/parachain/staging/sequential/vaults.test.ts deleted file mode 100644 index 97e4f5f97..000000000 --- a/test/integration/parachain/staging/sequential/vaults.test.ts +++ /dev/null @@ -1,370 +0,0 @@ -import { ApiPromise, Keyring } from "@polkadot/api"; -import { KeyringPair } from "@polkadot/keyring/types"; -import Big from "big.js"; -import { - DefaultInterBtcApi, - InterBtcApi, - InterbtcPrimitivesVaultId, - currencyIdToMonetaryCurrency, - CollateralCurrencyExt, - VaultStatusExt, - GovernanceCurrency, - AssetRegistryAPI, - DefaultAssetRegistryAPI, -} from "../../../../../src/index"; - -import { createSubstrateAPI } from "../../../../../src/factory"; -import { assert } from "../../../../chai"; -import { VAULT_1_URI, VAULT_2_URI, PARACHAIN_ENDPOINT, VAULT_3_URI, ESPLORA_BASE_PATH } from "../../../../config"; -import { newAccountId, WrappedCurrency, newVaultId } from "../../../../../src"; -import { getSS58Prefix, newMonetaryAmount } from "../../../../../src/utils"; -import { - getAUSDForeignAsset, - getCorrespondingCollateralCurrenciesForTests, - getIssuableAmounts, - submitExtrinsic, - vaultStatusToLabel, -} from "../../../../utils/helpers"; -import sinon from "sinon"; - -describe("vaultsAPI", () => { - let vault_1: KeyringPair; - let vault_1_ids: Array; - let vault_2: KeyringPair; - let vault_3: KeyringPair; - let api: ApiPromise; - - let wrappedCurrency: WrappedCurrency; - let collateralCurrencies: Array; - let governanceCurrency: GovernanceCurrency; - - let interBtcAPI: InterBtcApi; - let assetRegistry: AssetRegistryAPI; - - before(async () => { - api = await createSubstrateAPI(PARACHAIN_ENDPOINT); - const ss58Prefix = getSS58Prefix(api); - const keyring = new Keyring({ type: "sr25519", ss58Format: ss58Prefix }); - assetRegistry = new DefaultAssetRegistryAPI(api); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", undefined, ESPLORA_BASE_PATH); - - wrappedCurrency = interBtcAPI.getWrappedCurrency(); - governanceCurrency = interBtcAPI.getGovernanceCurrency(); - - collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(governanceCurrency); - const aUSD = await getAUSDForeignAsset(assetRegistry); - if (aUSD !== undefined) { - // also add aUSD collateral vaults if they exist (ie. the foreign asset exists) - collateralCurrencies.push(aUSD); - } - - vault_1 = keyring.addFromUri(VAULT_1_URI); - vault_1_ids = collateralCurrencies.map((collateralCurrency) => - newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency) - ); - - vault_2 = keyring.addFromUri(VAULT_2_URI); - vault_3 = keyring.addFromUri(VAULT_3_URI); - }); - - after(() => { - return api.disconnect(); - }); - - afterEach(() => { - // discard any stubbed methods after each test - sinon.restore(); - }); - - function vaultIsATestVault(vaultAddress: string): boolean { - return vaultAddress === vault_2.address || vaultAddress === vault_1.address || vaultAddress === vault_3.address; - } - - it("should get issuable", async () => { - const issuableInterBTC = await interBtcAPI.vaults.getTotalIssuableAmount(); - const issuableAmounts = await getIssuableAmounts(interBtcAPI); - const totalIssuable = issuableAmounts.reduce((prev, curr) => prev.add(curr)); - assert.isTrue( - issuableInterBTC.toBig().sub(totalIssuable.toBig()).abs().lte(1), - `${issuableInterBTC.toHuman()} != ${totalIssuable.toHuman()}` - ); - }); - - it("should get the required collateral for the vault", async () => { - for (const vault_1_id of vault_1_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const requiredCollateralForVault = await interBtcAPI.vaults.getRequiredCollateralForVault( - vault_1_id.accountId, - collateralCurrency - ); - - const vault = await interBtcAPI.vaults.get(vault_1_id.accountId, collateralCurrency); - - // The numeric value of the required collateral should be greater than that of issued tokens. - // e.g. we require `0.8096` KSM for `0.00014` kBTC - // edge case: we require 0 KSM for 0 kBTC, so check greater than or equal to - assert.isTrue( - requiredCollateralForVault.toBig().gte(vault.getBackedTokens().toBig()), - `Expect required collateral (${requiredCollateralForVault.toHuman()}) - to be greater than or equal to backed tokens (${vault.getBackedTokens().toHuman()})` - ); - } - }); - - // WARNING: this test is not idempotent - // PRECONDITION: vault_1 must have issued some tokens against all collateral currencies - it("should deposit and withdraw collateral", async () => { - const prevAccount = interBtcAPI.account; - for (const vault_1_id of vault_1_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - - interBtcAPI.setAccount(vault_1); - const amount = newMonetaryAmount(100, collateralCurrency, true); - - const collateralizationBeforeDeposit = await interBtcAPI.vaults.getVaultCollateralization( - newAccountId(api, vault_1.address), - collateralCurrency - ); - await submitExtrinsic(interBtcAPI, interBtcAPI.vaults.depositCollateral(amount)); - const collateralizationAfterDeposit = await interBtcAPI.vaults.getVaultCollateralization( - newAccountId(api, vault_1.address), - collateralCurrency - ); - if (collateralizationBeforeDeposit === undefined || collateralizationAfterDeposit == undefined) { - assert.fail( - `Collateralization is undefined for vault with collateral currency ${currencyTicker} - - potential cause: the vault may not have any issued tokens secured by ${currencyTicker}` - ); - return; - } - assert.isTrue( - collateralizationAfterDeposit.gt(collateralizationBeforeDeposit), - `Depositing did not increase collateralization (${currencyTicker} vault), - expected ${collateralizationAfterDeposit} greater than ${collateralizationBeforeDeposit}` - ); - - await submitExtrinsic(interBtcAPI, await interBtcAPI.vaults.withdrawCollateral(amount)); - const collateralizationAfterWithdrawal = await interBtcAPI.vaults.getVaultCollateralization( - newAccountId(api, vault_1.address), - collateralCurrency - ); - if (collateralizationAfterWithdrawal === undefined) { - assert.fail(`Collateralization is undefined for vault with collateral currency ${currencyTicker}`); - return; - } - assert.isTrue( - collateralizationAfterDeposit.gt(collateralizationAfterWithdrawal), - `Withdrawing did not decrease collateralization (${currencyTicker} vault), expected - ${collateralizationAfterDeposit} greater than ${collateralizationAfterWithdrawal}` - ); - assert.equal( - collateralizationBeforeDeposit.toString(), - collateralizationAfterWithdrawal.toString(), - `Collateralization after identical deposit and withdrawal changed (${currencyTicker} vault)` - ); - } - if (prevAccount) { - interBtcAPI.setAccount(prevAccount); - } - }); - - it("should getLiquidationCollateralThreshold", async () => { - for (const collateralCurrency of collateralCurrencies) { - const currencyTicker = collateralCurrency.ticker; - - const threshold = await interBtcAPI.vaults.getLiquidationCollateralThreshold(collateralCurrency); - assert.isTrue( - threshold.gt(0), - `Expected liquidation threshold for ${currencyTicker} to be greater than 0, but was ${threshold.toString()}` - ); - } - }); - - it("should getPremiumRedeemThreshold", async () => { - for (const collateralCurrency of collateralCurrencies) { - const currencyTicker = collateralCurrency.ticker; - - const threshold = await interBtcAPI.vaults.getPremiumRedeemThreshold(collateralCurrency); - assert.isTrue( - threshold.gt(0), - `Expected premium redeem threshold for ${currencyTicker} to be greater than 0, but was ${threshold.toString()}` - ); - } - }); - - it("should select random vault for issue", async () => { - const randomVault = await interBtcAPI.vaults.selectRandomVaultIssue(newMonetaryAmount(0, wrappedCurrency)); - assert.isTrue(vaultIsATestVault(randomVault.accountId.toHuman())); - }); - - it("should fail if no vault for issuing is found", async () => { - assert.isRejected(interBtcAPI.vaults.selectRandomVaultIssue(newMonetaryAmount(9000000, wrappedCurrency, true))); - }); - - it("should select random vault for redeem", async () => { - const randomVault = await interBtcAPI.vaults.selectRandomVaultRedeem(newMonetaryAmount(0, wrappedCurrency)); - assert.isTrue(vaultIsATestVault(randomVault.accountId.toHuman())); - }); - - it("should fail if no vault for redeeming is found", async () => { - const amount = newMonetaryAmount(9000000, wrappedCurrency, true); - assert.isRejected(interBtcAPI.vaults.selectRandomVaultRedeem(amount)); - }); - - it("should fail to get vault collateralization for vault with zero collateral", async () => { - for (const vault_1_id of vault_1_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - - const vault1Id = newAccountId(api, vault_1.address); - assert.isRejected( - interBtcAPI.vaults.getVaultCollateralization(vault1Id, collateralCurrency), - `Collateralization should not be available (${currencyTicker} vault)` - ); - } - }); - - it("should get the issuable InterBtc for a vault", async () => { - for (const vault_1_id of vault_1_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - - const vault = await interBtcAPI.vaults.get(vault_1_id.accountId, collateralCurrency); - const issuableTokens = await vault.getIssuableTokens(); - assert.isTrue( - issuableTokens.gt(newMonetaryAmount(0, wrappedCurrency)), - `Issuable tokens should be greater than 0 (${currencyTicker} vault)` - ); - } - }); - - it("should get the issuable InterBtc", async () => { - const issuableInterBtc = await interBtcAPI.vaults.getTotalIssuableAmount(); - assert.isTrue(issuableInterBtc.gt(newMonetaryAmount(0, wrappedCurrency))); - }); - - it("should getFees", async () => { - const vaultIdsInScope = vault_1_ids; - let countSkippedVaults = 0; - let countVaultsWithNonZeroWrappedRewards = 0; - - for (const vaultId of vaultIdsInScope) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vaultId.currencies.collateral); - const wrappedCurrency = await currencyIdToMonetaryCurrency(api, vaultId.currencies.wrapped); - const currencyTicker = collateralCurrency.ticker; - - const vault = await interBtcAPI.vaults.get(vaultId.accountId, collateralCurrency); - const issueableTokens = await vault.getIssuableTokens(); - const issuedTokens = vault.issuedTokens; - const totalTokensCapacity = issuedTokens.toBig().add(issueableTokens.toBig()); - if (totalTokensCapacity.eq(0)) { - // no token capacity => no rewards => nothing to check - countSkippedVaults++; - continue; - } - - const feesWrapped = await interBtcAPI.vaults.getWrappedReward( - vaultId.accountId, - collateralCurrency, - wrappedCurrency - ); - assert.isTrue( - feesWrapped.gte(newMonetaryAmount(0, wrappedCurrency)), - // eslint-disable-next-line max-len - `Fees (wrapped reward) should be greater than or equal to 0 (${currencyTicker} vault, account id ${vaultId.accountId.toString()}), but was: ${feesWrapped.toHuman()}` - ); - - if (feesWrapped.gt(newMonetaryAmount(0, wrappedCurrency))) { - // we will check that at least one return was greater than zero - countVaultsWithNonZeroWrappedRewards++; - } - - const govTokenReward = await interBtcAPI.vaults.getGovernanceReward( - vaultId.accountId, - collateralCurrency, - governanceCurrency - ); - assert.isTrue( - govTokenReward.gte(newMonetaryAmount(0, governanceCurrency)), - // eslint-disable-next-line max-len - `Governance reward should be greater than or equal to 0 (${currencyTicker} vault, account id ${vaultId.accountId.toString()}), but was: ${feesWrapped.toHuman()}` - ); - } - // make sure not every vault has been skipped (due to no issued tokens) - assert.notEqual( - countSkippedVaults, - vaultIdsInScope.length, - // eslint-disable-next-line max-len - `Unexpected test behavior: skipped all ${vaultIdsInScope.length} vaults in the test; all vaults lacking capacity (issued + issuable > 0)` - ); - - // make sure at least one vault is receiving wrapped rewards greater than zero - assert.isAbove( - countVaultsWithNonZeroWrappedRewards, - 0, - // eslint-disable-next-line max-len - `Unexpected test behavior: none of the ${vaultIdsInScope.length} vaults in the test have received more than 0 wrapped token rewards` - ); - }); - - it("should getAPY", async () => { - for (const vault_1_id of vault_1_ids) { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, vault_1_id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - const accountId = newAccountId(api, vault_1.address); - - const apy = await interBtcAPI.vaults.getAPY(accountId, collateralCurrency); - const apyBig = new Big(apy); - const apyBenchmark = new Big("0"); - assert.isTrue( - apyBig.gte(apyBenchmark), - `APY should be greater than or equal to ${apyBenchmark.toString()}, - but was ${apyBig.toString()} (${currencyTicker} vault)` - ); - } - }); - - it("should getPunishmentFee", async () => { - const punishmentFee = await interBtcAPI.vaults.getPunishmentFee(); - assert.equal(punishmentFee.toString(), "0.1"); - }); - - it("should get vault list", async () => { - const vaults = (await interBtcAPI.vaults.list()).map((vault) => vault.id.toHuman()); - assert.isAbove(vaults.length, 0, "Vault list should not be empty"); - }); - - it("should disable and enable issuing with vault", async () => { - const assertVaultStatus = async (id: InterbtcPrimitivesVaultId, expectedStatus: VaultStatusExt) => { - const collateralCurrency = await currencyIdToMonetaryCurrency(api, id.currencies.collateral); - const currencyTicker = collateralCurrency.ticker; - const { status } = await interBtcAPI.vaults.get(id.accountId, collateralCurrency); - const assertionMessage = `Vault with id ${id.toString()} (collateral: ${currencyTicker}) was expected to have - status: ${vaultStatusToLabel(expectedStatus)}, but got status: ${vaultStatusToLabel(status)}`; - - assert.isTrue(status === expectedStatus, assertionMessage); - }; - const ACCEPT_NEW_ISSUES = true; - const REJECT_NEW_ISSUES = false; - - for (const vault_1_id of vault_1_ids) { - // Check that vault 1 is active. - await assertVaultStatus(vault_1_id, VaultStatusExt.Active); - // Disables vault 1 which is active. - await submitExtrinsic( - interBtcAPI, - await interBtcAPI.vaults.toggleIssueRequests(vault_1_id, REJECT_NEW_ISSUES) - ); - // Check that vault 1 is inactive. - await assertVaultStatus(vault_1_id, VaultStatusExt.Inactive); - // Re-enable issuing with vault 1. - await submitExtrinsic( - interBtcAPI, - await interBtcAPI.vaults.toggleIssueRequests(vault_1_id, ACCEPT_NEW_ISSUES) - ); - // Check that vault 1 is again active. - await assertVaultStatus(vault_1_id, VaultStatusExt.Active); - } - }); -}); diff --git a/test/integration/parachain/staging/setup/initialize.test.ts b/test/integration/parachain/staging/setup/initialize.test.ts index 96acebfa4..5c4c65d7e 100644 --- a/test/integration/parachain/staging/setup/initialize.test.ts +++ b/test/integration/parachain/staging/setup/initialize.test.ts @@ -2,7 +2,6 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { AccountId } from "@polkadot/types/interfaces"; import { Bitcoin, ExchangeRate, Kintsugi, Kusama, MonetaryAmount, Polkadot } from "@interlay/monetary-js"; -import { assert } from "chai"; import Big from "big.js"; import { @@ -99,7 +98,7 @@ describe("Initialize parachain state", () => { } } - before(async function () { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); const ss58Prefix = getSS58Prefix(api); keyring = new Keyring({ type: "sr25519", ss58Format: ss58Prefix }); @@ -152,51 +151,46 @@ describe("Initialize parachain state", () => { ); }); - after(async () => { - api.disconnect(); + afterAll(async () => { + await api.disconnect(); }); - it("should set the stable confirmations and ready the BTC-Relay", async () => { - // Speed up the process by only requiring 0 parachain and 0 bitcoin confirmations - const stableBitcoinConfirmationsToSet = 0; - const stableParachainConfirmationsToSet = 0; - let [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ - userInterBtcAPI.btcRelay.getStableBitcoinConfirmations(), - userInterBtcAPI.btcRelay.getStableParachainConfirmations(), - ]); - - if (stableBitcoinConfirmations != 0 || stableParachainConfirmations != 0) { - console.log("Initializing stable block confirmations..."); - await setRawStorage( - api, - api.query.btcRelay.stableBitcoinConfirmations.key(), - api.createType("u32", stableBitcoinConfirmationsToSet), - sudoAccount - ); - await setRawStorage( - api, - api.query.btcRelay.stableParachainConfirmations.key(), - api.createType("u32", stableParachainConfirmationsToSet), - sudoAccount - ); - await bitcoinCoreClient.mineBlocks(3); - - [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ + it( + "should set the stable confirmations and ready the BTC-Relay", + async () => { + // Speed up the process by only requiring 0 parachain and 0 bitcoin confirmations + const stableBitcoinConfirmationsToSet = 0; + const stableParachainConfirmationsToSet = 0; + let [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ userInterBtcAPI.btcRelay.getStableBitcoinConfirmations(), userInterBtcAPI.btcRelay.getStableParachainConfirmations(), ]); + + if (stableBitcoinConfirmations != 0 || stableParachainConfirmations != 0) { + console.log("Initializing stable block confirmations..."); + await setRawStorage( + api, + api.query.btcRelay.stableBitcoinConfirmations.key(), + api.createType("u32", stableBitcoinConfirmationsToSet), + sudoAccount + ); + await setRawStorage( + api, + api.query.btcRelay.stableParachainConfirmations.key(), + api.createType("u32", stableParachainConfirmationsToSet), + sudoAccount + ); + await bitcoinCoreClient.mineBlocks(3); + + [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ + userInterBtcAPI.btcRelay.getStableBitcoinConfirmations(), + userInterBtcAPI.btcRelay.getStableParachainConfirmations(), + ]); + } + expect(stableBitcoinConfirmationsToSet).toEqual(stableBitcoinConfirmations); + expect(stableParachainConfirmationsToSet).toEqual(stableParachainConfirmations); } - assert.equal( - stableBitcoinConfirmationsToSet, - stableBitcoinConfirmations, - "Setting the Bitcoin confirmations failed" - ); - assert.equal( - stableParachainConfirmationsToSet, - stableParachainConfirmations, - "Setting the Parachain confirmations failed" - ); - }); + ); it("should fund vault annuity account", async () => { // get address in with local prefix @@ -234,18 +228,14 @@ describe("Initialize parachain state", () => { sudoInterBtcAPI.api.tx.sudo.sudo(callToRegister), api.events.assetRegistry.RegisteredAsset ); - assert.isTrue(result.isCompleted, "Sudo event to register new foreign asset not found"); + expect(result.isCompleted).toBe(true); } const currencies = await sudoInterBtcAPI.assetRegistry.getForeignAssets(); - assert.isAtLeast( - currencies.length, - 1, - `Expected at least one foreign asset registered, but found ${currencies.length}` - ); + expect(currencies.length).toBeGreaterThanOrEqual(1); aUSD = await getAUSDForeignAsset(sudoInterBtcAPI.assetRegistry); - assert.isDefined(aUSD, "aUSD not found after registration"); + expect(aUSD).toBeDefined(); }); it("should set oracle value expiry to a longer period", async () => { @@ -294,7 +284,7 @@ describe("Initialize parachain state", () => { // just check that this is set since we medianize results getFeeEstimate = await sudoInterBtcAPI.oracle.getBitcoinFees(); } - assert.isDefined(getFeeEstimate); + expect(getFeeEstimate).toBeDefined(); }); it("should update vault annuity rewards", async () => { @@ -302,7 +292,7 @@ describe("Initialize parachain state", () => { // so the test doesn't need to wait for transfer to settle const updateRewardsExtrinsic = api.tx.vaultAnnuity.updateRewards(); const hash = await api.tx.sudo.sudo(updateRewardsExtrinsic).signAndSend(sudoAccount); - assert.isNotEmpty(hash); + expect(hash).not.toHaveLength(0); }); it("should enable vault nomination", async () => { @@ -311,96 +301,99 @@ describe("Initialize parachain state", () => { await submitExtrinsic(sudoInterBtcAPI, sudoInterBtcAPI.nomination.setNominationEnabled(true)); isNominationEnabled = await sudoInterBtcAPI.nomination.isNominationEnabled(); } - assert.isTrue(isNominationEnabled); + expect(isNominationEnabled).toBe(true); }); - it("should set collateral ceiling and thresholds for aUSD", async function () { - // only get aUSD if it hasn't been set yet (e.g. when running this test in isolation) - !aUSD ? (aUSD = await getAUSDForeignAsset(sudoInterBtcAPI.assetRegistry)) : undefined; + it( + "should set collateral ceiling and thresholds for aUSD", + async () => { + // only get aUSD if it hasn't been set yet (e.g. when running this test in isolation) + !aUSD ? (aUSD = await getAUSDForeignAsset(sudoInterBtcAPI.assetRegistry)) : undefined; - if (aUSD === undefined) { - // no point in completing this if aUSD is not registered - this.skip(); - } + if (aUSD === undefined) { + // no point in completing this if aUSD is not registered + return; + } - // (unsafely) get first collateral currency's ceiling and thresholds - const existingCollCcy = getCorrespondingCollateralCurrenciesForTests( - userInterBtcAPI.getGovernanceCurrency() - )[0]; - const existingCcyPair = newVaultCurrencyPair(api, existingCollCcy, sudoInterBtcAPI.getWrappedCurrency()); - // borrow values from existing currency pair - const [optionCeilValue, existingSecureThreshold, existingPremiumThreshold, existingLiquidationThreshold] = - await Promise.all([ - sudoInterBtcAPI.api.query.vaultRegistry.systemCollateralCeiling(existingCcyPair), - sudoInterBtcAPI.vaults.getSecureCollateralThreshold(existingCollCcy), - sudoInterBtcAPI.vaults.getPremiumRedeemThreshold(existingCollCcy), - sudoInterBtcAPI.vaults.getLiquidationCollateralThreshold(existingCollCcy), - ]); - // boldly assume the value is set - const existingCeiling = optionCeilValue.unwrap(); + // (unsafely) get first collateral currency's ceiling and thresholds + const existingCollCcy = getCorrespondingCollateralCurrenciesForTests( + userInterBtcAPI.getGovernanceCurrency() + )[0]; + const existingCcyPair = newVaultCurrencyPair(api, existingCollCcy, sudoInterBtcAPI.getWrappedCurrency()); + // borrow values from existing currency pair + const [optionCeilValue, existingSecureThreshold, existingPremiumThreshold, existingLiquidationThreshold] = + await Promise.all([ + sudoInterBtcAPI.api.query.vaultRegistry.systemCollateralCeiling(existingCcyPair), + sudoInterBtcAPI.vaults.getSecureCollateralThreshold(existingCollCcy), + sudoInterBtcAPI.vaults.getPremiumRedeemThreshold(existingCollCcy), + sudoInterBtcAPI.vaults.getLiquidationCollateralThreshold(existingCollCcy), + ]); + // boldly assume the value is set + const existingCeiling = optionCeilValue.unwrap(); + + // encode thresholds + const encodedSecThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingSecureThreshold)); + const encodedPremThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingPremiumThreshold)); + const encodedLiqThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingLiquidationThreshold)); - // encode thresholds - const encodedSecThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingSecureThreshold)); - const encodedPremThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingPremiumThreshold)); - const encodedLiqThresh = encodeUnsignedFixedPoint(sudoInterBtcAPI.api, new Big(existingLiquidationThreshold)); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const aUsdCcyPair = newVaultCurrencyPair(api, aUSD!, sudoInterBtcAPI.getWrappedCurrency()); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const aUsdCcyPair = newVaultCurrencyPair(api, aUSD!, sudoInterBtcAPI.getWrappedCurrency()); + // set the collateral ceiling + const setCeilingExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setSystemCollateralCeiling( + aUsdCcyPair, + existingCeiling + ); - // set the collateral ceiling - const setCeilingExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setSystemCollateralCeiling( - aUsdCcyPair, - existingCeiling - ); + // set normal threshold + const setThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setSecureCollateralThreshold( + aUsdCcyPair, + encodedSecThresh + ); - // set normal threshold - const setThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setSecureCollateralThreshold( - aUsdCcyPair, - encodedSecThresh - ); + // set premium threshold + const setPremiumThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setPremiumRedeemThreshold( + aUsdCcyPair, + encodedPremThresh + ); - // set premium threshold - const setPremiumThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setPremiumRedeemThreshold( - aUsdCcyPair, - encodedPremThresh - ); + // set liquidation threshold + const setLiquidationThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setLiquidationCollateralThreshold( + aUsdCcyPair, + encodedLiqThresh + ); - // set liquidation threshold - const setLiquidationThresholdExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setLiquidationCollateralThreshold( - aUsdCcyPair, - encodedLiqThresh - ); + // set minimum collateral required + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const minimumCollateral = new MonetaryAmount(aUSD!, 100); + const minimumCollateralToSet = api.createType("u128", minimumCollateral.toString(true)); + const setMinimumCollateralExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setMinimumCollateral( + aUsdCcyPair.collateral, + minimumCollateralToSet + ); - // set minimum collateral required - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const minimumCollateral = new MonetaryAmount(aUSD!, 100); - const minimumCollateralToSet = api.createType("u128", minimumCollateral.toString(true)); - const setMinimumCollateralExtrinsic = sudoInterBtcAPI.api.tx.vaultRegistry.setMinimumCollateral( - aUsdCcyPair.collateral, - minimumCollateralToSet - ); + // batch all + const batch = api.tx.utility.batchAll([ + setCeilingExtrinsic, + setThresholdExtrinsic, + setPremiumThresholdExtrinsic, + setLiquidationThresholdExtrinsic, + setMinimumCollateralExtrinsic, + ]); + const tx = api.tx.sudo.sudo(batch); - // batch all - const batch = api.tx.utility.batchAll([ - setCeilingExtrinsic, - setThresholdExtrinsic, - setPremiumThresholdExtrinsic, - setLiquidationThresholdExtrinsic, - setMinimumCollateralExtrinsic, - ]); - const tx = api.tx.sudo.sudo(batch); - - const result = await DefaultTransactionAPI.sendLogged(api, sudoAccount, tx, api.events.sudo.Sudid); - assert.isTrue(result.isCompleted, "Cannot find Sudid event for setting thresholds in batch"); - }); + const result = await DefaultTransactionAPI.sendLogged(api, sudoAccount, tx, api.events.sudo.Sudid); + expect(result.isCompleted).toBe(true); + } + ); - it("should fund and register aUSD foreign asset vaults", async function () { + it("should fund and register aUSD foreign asset vaults", async () => { // only get aUSD if it hasn't been set yet (e.g. when running this test in isolation) !aUSD ? (aUSD = await getAUSDForeignAsset(sudoInterBtcAPI.assetRegistry)) : undefined; if (aUSD === undefined) { // no point in completing this if aUSD is not registered - this.skip(); + return; } // assign locally to make TS understand it isn't undefined @@ -420,7 +413,7 @@ describe("Initialize parachain state", () => { api.tx.sudo.sudo(api.tx.tokens.setBalance(vaultAccountId, newCurrencyId(api, aUsd), 100000000000, 0)), api.events.tokens.BalanceSet ); - assert.isTrue(result.isCompleted, `Cannot find BalanceSet event for ${vaultAccountId}`); + expect(result.isCompleted).toBe(true); // register the aUSD vault const vaultInterBtcApi = new DefaultInterBtcApi(api, "regtest", vaultKeyringPair, ESPLORA_BASE_PATH); @@ -431,19 +424,16 @@ describe("Initialize parachain state", () => { ); // sanity check the collateral computation const actualCollateralAmount = await vaultInterBtcApi.vaults.getCollateral(vaultAccountId, aUsd); - assert.equal(actualCollateralAmount.toString(), collateralAmount.toString()); + expect(actualCollateralAmount.toString()).toEqual(collateralAmount.toString()); } }); - it("should return aUSD among the collateral currencies", async function () { + it("should return aUSD among the collateral currencies", async () => { // run this check a few tests after it has been registered to avoid needing to wait for // block finalizations const collateralCurrencies = await getCollateralCurrencies(userInterBtcAPI.api); - assert.isDefined( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - collateralCurrencies.find((currency) => currency.ticker === aUSD!.ticker), - "Expected to find aUSD within the array of collateral currencies, but did not" - ); + expect(// eslint-disable-next-line @typescript-eslint/no-non-null-assertion + collateralCurrencies.find((currency) => currency.ticker === aUSD!.ticker)).toBeDefined(); }); }); diff --git a/test/integration/parachain/staging/system.test.ts b/test/integration/parachain/staging/system.test.ts index bb3994382..ecc707c96 100644 --- a/test/integration/parachain/staging/system.test.ts +++ b/test/integration/parachain/staging/system.test.ts @@ -2,7 +2,6 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { createSubstrateAPI } from "../../../../src/factory"; -import { assert } from "../../../chai"; import { SUDO_URI, PARACHAIN_ENDPOINT, ESPLORA_BASE_PATH } from "../../../config"; import { BLOCK_TIME_SECONDS, DefaultInterBtcApi, InterBtcApi } from "../../../../src"; @@ -12,20 +11,20 @@ describe("systemAPI", () => { let interBtcAPI: InterBtcApi; let keyring: Keyring; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); keyring = new Keyring({ type: "sr25519" }); sudoAccount = keyring.addFromUri(SUDO_URI); interBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount, ESPLORA_BASE_PATH); }); - after(async () => { - api.disconnect(); + afterAll(async () => { + await api.disconnect(); }); it("should getCurrentBlockNumber", async () => { const currentBlockNumber = await interBtcAPI.system.getCurrentBlockNumber(); - assert.isDefined(currentBlockNumber); + expect(currentBlockNumber).toBeDefined(); }); it("should getFutureBlockNumber", async () => { @@ -35,16 +34,13 @@ describe("systemAPI", () => { interBtcAPI.system.getFutureBlockNumber(approximately10BlocksTime), ]); - assert.isAtLeast(futureBlockNumber, currentBlockNumber + 9); - assert.isAtMost(futureBlockNumber, currentBlockNumber + 11); + expect(futureBlockNumber).toBeGreaterThanOrEqual(currentBlockNumber + 9); + expect(futureBlockNumber).toBeLessThanOrEqual(currentBlockNumber + 11); }); it("should get paymentInfo", async () => { const tx = api.tx.system.remark(""); - assert.isTrue(tx.hasPaymentInfo); - await assert.isFulfilled( - tx.paymentInfo(sudoAccount), - "Expected payment info for extrinsic" - ); + expect(tx.hasPaymentInfo).toBe(true); + await expect(tx.paymentInfo(sudoAccount)).resolves.toBeDefined(); }); }); diff --git a/test/integration/parachain/staging/tokens.test.ts b/test/integration/parachain/staging/tokens.test.ts index 9b55c3c73..0ef5117bf 100644 --- a/test/integration/parachain/staging/tokens.test.ts +++ b/test/integration/parachain/staging/tokens.test.ts @@ -2,7 +2,6 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { createSubstrateAPI } from "../../../../src/factory"; -import { assert } from "../../../chai"; import { USER_1_URI, USER_2_URI, PARACHAIN_ENDPOINT, ESPLORA_BASE_PATH } from "../../../config"; import { ATOMIC_UNIT, @@ -22,7 +21,7 @@ describe("TokensAPI", () => { let interBtcAPI: InterBtcApi; let collateralCurrencies: Array; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); const keyring = new Keyring({ type: "sr25519" }); user1Account = keyring.addFromUri(USER_1_URI); @@ -31,8 +30,8 @@ describe("TokensAPI", () => { collateralCurrencies = getCorrespondingCollateralCurrenciesForTests(interBtcAPI.getGovernanceCurrency()); }); - after(() => { - return api.disconnect(); + afterAll(async () => { + await api.disconnect(); }); it("should subscribe to balance updates", async () => { @@ -65,28 +64,28 @@ describe("TokensAPI", () => { interBtcAPI, interBtcAPI.tokens.transfer(user2Account.address, amountToUpdateUser2sAccountBy) ); - assert.equal(updatedAccount, user2Account.address); + expect(updatedAccount).toEqual(user2Account.address); const expectedUser2BalanceAfterFirstTransfer = new ChainBalance( currency, user2BalanceBeforeTransfer.free.add(amountToUpdateUser2sAccountBy).toBig(ATOMIC_UNIT), user2BalanceBeforeTransfer.transferable.add(amountToUpdateUser2sAccountBy).toBig(ATOMIC_UNIT), user2BalanceBeforeTransfer.reserved.toBig(ATOMIC_UNIT) ); - assert.equal(updatedBalance.toString(), expectedUser2BalanceAfterFirstTransfer.toString()); + expect(updatedBalance.toString()).toEqual(expectedUser2BalanceAfterFirstTransfer.toString()); // Send the second transfer, expect the callback to be called with correct values await submitExtrinsic( interBtcAPI, interBtcAPI.tokens.transfer(user2Account.address, amountToUpdateUser2sAccountBy) ); - assert.equal(updatedAccount, user2Account.address); + expect(updatedAccount).toEqual(user2Account.address); const expectedUser2BalanceAfterSecondTransfer = new ChainBalance( currency, expectedUser2BalanceAfterFirstTransfer.free.add(amountToUpdateUser2sAccountBy).toBig(ATOMIC_UNIT), expectedUser2BalanceAfterFirstTransfer.transferable.add(amountToUpdateUser2sAccountBy).toBig(ATOMIC_UNIT), expectedUser2BalanceAfterFirstTransfer.reserved.toBig(ATOMIC_UNIT) ); - assert.equal(updatedBalance.toString(), expectedUser2BalanceAfterSecondTransfer.toString()); + expect(updatedBalance.toString()).toEqual(expectedUser2BalanceAfterSecondTransfer.toString()); // TODO: Commented out because it blocks release, fix. // Fails because it conflicts with the escrowAPI test: diff --git a/test/integration/parachain/staging/utils.test.ts b/test/integration/parachain/staging/utils.test.ts index acaef069c..3535ecab6 100644 --- a/test/integration/parachain/staging/utils.test.ts +++ b/test/integration/parachain/staging/utils.test.ts @@ -4,7 +4,6 @@ import { bnToHex } from "@polkadot/util"; import BN from "bn.js"; import { createSubstrateAPI } from "../../../../src/factory"; -import { assert } from "../../../chai"; import { PARACHAIN_ENDPOINT } from "../../../config"; import { stripHexPrefix } from "../../../../src/utils"; @@ -19,41 +18,32 @@ export function blake2_128Concat(data: `0x${string}`): string { describe("Utils", () => { let api: ApiPromise; - before(async () => { + beforeAll(async () => { api = await createSubstrateAPI(PARACHAIN_ENDPOINT); }); - after(() => { - return api.disconnect(); + afterAll(async () => { + await api.disconnect(); }); it("should encode and decode exchange rate", async () => { const value = api.createType("UnsignedFixedPoint", "0x00000000000000000001000000000000"); - assert.equal( - api.createType("UnsignedFixedPoint", value.toString()).toHex(true), - value.toHex(true), - ); + expect(api.createType("UnsignedFixedPoint", value.toString()).toHex(true)).toEqual(value.toHex(true)); }); it("should encode storage key", async () => { - assert.equal( - getStorageKey("Oracle", "MaxDelay"), - api.query.oracle.maxDelay.key(), - ); - assert.equal( - getStorageKey("AssetRegistry", "Metadata"), - api.query.assetRegistry.metadata.keyPrefix(), - ) + expect(getStorageKey("Oracle", "MaxDelay")).toEqual(api.query.oracle.maxDelay.key()); + expect(getStorageKey("AssetRegistry", "Metadata")).toEqual(api.query.assetRegistry.metadata.keyPrefix()); }); it("should encode storage value", async () => { const value = new BN(100); const data32 = bnToHex(value, { bitLength: 32, isLe: true }); const codec32 = api.createType("u32", value); - assert.equal(data32, codec32.toHex(true)); + expect(data32).toEqual(codec32.toHex(true)); const data64 = bnToHex(value, { bitLength: 64, isLe: true }); const codec64 = api.createType("u64", value); - assert.equal(data64, codec64.toHex(true)); + expect(data64).toEqual(codec64.toHex(true)); }); }); diff --git a/test/unit/factory.test.ts b/test/unit/factory.test.ts index f96a4cd55..8123138c5 100644 --- a/test/unit/factory.test.ts +++ b/test/unit/factory.test.ts @@ -1,4 +1,3 @@ -import { assert } from "chai"; import { createProvider } from "../../src/factory"; import { WsProvider, HttpProvider } from "@polkadot/rpc-provider"; @@ -7,14 +6,14 @@ describe("createProvider", () => { it("should support HTTP endpoints", () => { for (const endpoint of ["http://example.com", "https://example.com"]) { const httpProvider = createProvider(endpoint); - assert.instanceOf(httpProvider, HttpProvider); + expect(httpProvider).toBeInstanceOf(HttpProvider); } }); it("should support Websocket endpoints", () => { for (const endpoint of ["ws://example.com", "wss://example.com"]) { const wsProvider = createProvider(endpoint, false); - assert.instanceOf(wsProvider, WsProvider); + expect(wsProvider).toBeInstanceOf(WsProvider); } }); }); diff --git a/test/unit/mocks/vaultsTestMocks.ts b/test/unit/mocks/vaultsTestMocks.ts index 609d39656..fffbddd06 100644 --- a/test/unit/mocks/vaultsTestMocks.ts +++ b/test/unit/mocks/vaultsTestMocks.ts @@ -1,5 +1,4 @@ import Big, { BigSource } from "big.js"; -import sinon from "sinon"; import { CollateralCurrencyExt, CurrencyExt, @@ -27,58 +26,54 @@ export const MOCKED_SEND_LOGGED_ERR_MSG = "mocked sendLogged rejection"; /** * * @returns Two mock account IDs, one for the nominator and one for the vault - * @param sinon The sinon sandbox to use for mocking * @param vaultsApi The vaults API used to call the method under test - * @param stubbedTransactionApi The stubbed transaction API (called internally) + * @param transactionApi The transaction API (mocked/called internally) * @param isAccountIdUndefined if true, mocks that transactionApi.getAccountId() returns an undefined account ID * @param doesSendLoggedReject if true, mocks that transactionApi.sendLogged() rejects * @returns the mocked extrinsic to return when transactionApi.sendLogged() is finally called */ export const prepareRegisterNewCollateralVaultMocks = ( - sinon: sinon.SinonSandbox, vaultsApi: DefaultVaultsAPI, - stubbedTransactionApi: sinon.SinonStubbedInstance, + transactionApi: DefaultTransactionAPI, isAccountIdUndefined?: boolean, doesSendLoggedReject?: boolean ): SubmittableExtrinsic<"promise", ISubmittableResult> | null => { if (isAccountIdUndefined) { - stubbedTransactionApi.getAccount.returns(undefined); + jest.spyOn(transactionApi, "getAccount").mockClear().mockReturnValue(undefined); return null; } // mock getting a valid (ie. has been set) account id const vaultAccountId = createMockAccountId("0x0123456789012345678901234567890123456789012345678901234567890123"); - stubbedTransactionApi.getAccount.returns(vaultAccountId); + jest.spyOn(transactionApi, "getAccount").mockClear().mockReturnValue(vaultAccountId); // mock api returns to be able to call sendLogged const mockSubmittableExtrinsic = >{}; - sinon.stub(vaultsApi, "buildRegisterVaultExtrinsic").returns(mockSubmittableExtrinsic); + jest.spyOn(vaultsApi, "buildRegisterVaultExtrinsic").mockClear().mockReturnValue(mockSubmittableExtrinsic); const fakeEvent = >{}; - sinon.stub(vaultsApi, "getRegisterVaultEvent").returns(fakeEvent); + jest.spyOn(vaultsApi, "getRegisterVaultEvent").mockClear().mockReturnValue(fakeEvent); if (doesSendLoggedReject) { - stubbedTransactionApi.sendLogged.rejects(new Error(MOCKED_SEND_LOGGED_ERR_MSG)); + jest.spyOn(transactionApi, "sendLogged").mockClear().mockReturnValue(Promise.reject(new Error(MOCKED_SEND_LOGGED_ERR_MSG))); return null; } - stubbedTransactionApi.sendLogged.resolves(); + jest.spyOn(transactionApi, "sendLogged").mockClear().mockResolvedValue(undefined as never); return mockSubmittableExtrinsic; }; /** * Helper function to mock calls outside of the function backingCollateralProportion - * @param sinon The sinon sandbox to use for mocking * @param vaultsApi The vaults API used to call the method under test - * @param stubbedRewardsApi The stubbed rewards API (called internally) + * @param rewardsApi The rewards API (mocked/called internally) * @param nominatorCollateralStakedAmount The mocked nominator's collateral staked amount * @param vaultBackingCollateralAmount The mocked vault's backing collateral amount * @param collateralCurrency The collateral currency for the amounts mocked * @returns Two mock account IDs, one for the nominator and one for the vault */ export const prepareBackingCollateralProportionMocks = ( - sinon: sinon.SinonSandbox, vaultsApi: DefaultVaultsAPI, - stubbedRewardsApi: sinon.SinonStubbedInstance, + rewardsApi: DefaultRewardsAPI, nominatorCollateralStakedAmount: Big, vaultBackingCollateralAmount: Big, collateralCurrency: CollateralCurrencyExt @@ -88,10 +83,9 @@ export const prepareBackingCollateralProportionMocks = ( // prepare mocks const mockVault = createMockVaultWithBacking(vaultBackingCollateralAmount, collateralCurrency); - mockVaultsApiGetMethod(sinon, vaultsApi, mockVault); + mockVaultsApiGetMethod(vaultsApi, mockVault); mockComputeCollateralInStakingPoolMethod( - sinon, - stubbedRewardsApi, + rewardsApi, nominatorCollateralStakedAmount, collateralCurrency ); @@ -118,23 +112,22 @@ export const createMockAccountId = (someString: string): AccountId => { /** * Mock RewardsAPI.computeCollateralInStakingPool to return a specific collateral amount - * @param sinon The sinon sandbox to use for mocking - * @param stubbedRewardsApi A stubbed rewards API to add the mocked bahvior to + * @param rewardsApi The rewards API to add the mocked bahvior to * @param amount The mocked return amount * @param currency The currency of the mocked return amount */ export const mockComputeCollateralInStakingPoolMethod = ( - sinon: sinon.SinonSandbox, - stubbedRewardsApi: sinon.SinonStubbedInstance, + rewardsApi: DefaultRewardsAPI, amount: BigSource, currency: CurrencyExt ): void => { // don't care what the inner method returns as we mock the outer one const tempId = {}; - sinon.stub(allThingsEncoding, "newVaultId").returns(tempId); + jest.spyOn(allThingsEncoding, "newVaultId").mockClear().mockReturnValue(tempId); - // the actual mock that matters - stubbedRewardsApi.computeCollateralInStakingPool.resolves(newMonetaryAmount(amount, currency) as never); + jest.spyOn(rewardsApi, "computeCollateralInStakingPool") + .mockClear() + .mockReturnValue(Promise.resolve(newMonetaryAmount(amount, currency))); }; /** @@ -152,21 +145,18 @@ export const createMockVaultWithBacking = (amount: BigSource, collateralCurrency /** * Mock the return value of VaultsAPI.get - * @param sinon The sinon sandbox to use for mocking * @param vaultsApi The vaultsAPI instance for which to mock .get() methed * @param vault The vault to return */ export const mockVaultsApiGetMethod = ( - sinon: sinon.SinonSandbox, vaultsApi: DefaultVaultsAPI, vault: VaultExt ): void => { // make VaultAPI.get() return a mocked vault - sinon.stub(vaultsApi, "get").returns(Promise.resolve(vault)); + jest.spyOn(vaultsApi, "get").mockClear().mockReturnValue(Promise.resolve(vault)); }; export const prepareLiquidationRateMocks = ( - sinon: sinon.SinonSandbox, vaultsApi: DefaultVaultsAPI, mockIssuedTokensNumber: BigSource, mockCollateralTokensNumber: BigSource, @@ -180,13 +170,13 @@ export const prepareLiquidationRateMocks = ( }; // mock this.get return mock vault - mockVaultsApiGetMethod(sinon, vaultsApi, mockVault); + mockVaultsApiGetMethod(vaultsApi, mockVault); // mock this.getLiquidationCollateralThreshold return value // if we have less than 2x collateral (in BTC) compared to BTC, we need to liquidate - sinon.stub(vaultsApi, "getLiquidationCollateralThreshold").returns(Promise.resolve(Big(mockLiquidationThreshold))); + jest.spyOn(vaultsApi, "getLiquidationCollateralThreshold").mockClear().mockReturnValue(Promise.resolve(Big(mockLiquidationThreshold))); // mock this.getCollateral return value const mockCollateral = new MonetaryAmount(collateralCurrency, mockCollateralTokensNumber); - sinon.stub(vaultsApi, "getCollateral").returns(Promise.resolve(mockCollateral)); + jest.spyOn(vaultsApi, "getCollateral").mockClear().mockReturnValue(Promise.resolve(mockCollateral)); }; diff --git a/test/unit/parachain/asset-registry.test.ts b/test/unit/parachain/asset-registry.test.ts index d6bae63f5..7e5dd29eb 100644 --- a/test/unit/parachain/asset-registry.test.ts +++ b/test/unit/parachain/asset-registry.test.ts @@ -1,7 +1,4 @@ -import { expect } from "../../chai"; -import sinon from "sinon"; -import { ApiPromise } from "@polkadot/api"; -import { StorageKey, u32 } from "@polkadot/types"; +import { StorageKey, u32, TypeRegistry } from "@polkadot/types"; import { InterbtcPrimitivesCurrencyId, InterbtcPrimitivesVaultCurrencyPair, @@ -12,7 +9,8 @@ import { AssetRegistryMetadataTuple } from "../../../src/parachain/asset-registr import * as allThingsEncoding from "../../../src/utils/encoding"; describe("DefaultAssetRegistryAPI", () => { - let api: ApiPromise; + // let api: ApiPromise; + let registry: TypeRegistry; let assetRegistryApi: DefaultAssetRegistryAPI; let mockMetadata: OrmlTraitsAssetRegistryAssetMetadata; let mockStorageKey: StorageKey<[u32]>; @@ -27,15 +25,12 @@ describe("DefaultAssetRegistryAPI", () => { coingeckoId: "mock-coin-one", }; - before(() => { - api = new ApiPromise(); - // disconnect immediately to avoid printing errors - // we only need the instance to create variables - api.disconnect(); + beforeAll(() => { + registry = new TypeRegistry(undefined); // register just enough from OrmlTraitsAssetRegistryAssetMetadata to construct // meaningful representations for our tests - api.registerTypes({ + registry.register({ OrmlTraitsAssetRegistryAssetMetadata: { name: "Bytes", symbol: "Bytes", @@ -51,84 +46,75 @@ describe("DefaultAssetRegistryAPI", () => { }); beforeEach(() => { - assetRegistryApi = new DefaultAssetRegistryAPI(api); + // anything calling the api should have been mocked, so pass null + assetRegistryApi = new DefaultAssetRegistryAPI(null as never); // reset to base values mockMetadata = { - name: api.createType("Bytes", mockMetadataValues.name), - symbol: api.createType("Bytes", mockMetadataValues.symbol), - decimals: api.createType("u32", mockMetadataValues.decimals), - existentialDeposit: api.createType("u128", mockMetadataValues.existentialDeposit), - additional: api.createType("InterbtcPrimitivesCustomMetadata", { - feePerSecond: api.createType("u128", mockMetadataValues.feesPerMinute), - coingeckoId: api.createType("Bytes", mockMetadataValues.coingeckoId), + name: registry.createType("Bytes", mockMetadataValues.name), + symbol: registry.createType("Bytes", mockMetadataValues.symbol), + decimals: registry.createType("u32", mockMetadataValues.decimals), + existentialDeposit: registry.createType("u128", mockMetadataValues.existentialDeposit), + additional: registry.createType("InterbtcPrimitivesCustomMetadata", { + feePerSecond: registry.createType("u128", mockMetadataValues.feesPerMinute), + coingeckoId: registry.createType("Bytes", mockMetadataValues.coingeckoId), }), } as OrmlTraitsAssetRegistryAssetMetadata; // mock return type of storageKeyToNthInner method which only works correctly in integration tests - const mockedReturn = api.createType("AssetId", mockStorageKeyValue); - sinon.stub(allThingsEncoding, "storageKeyToNthInner").returns(mockedReturn); + const mockedReturn = registry.createType("AssetId", mockStorageKeyValue); + jest.spyOn(allThingsEncoding, "storageKeyToNthInner").mockClear().mockReturnValue(mockedReturn); }); afterEach(() => { - sinon.restore(); - sinon.reset(); + jest.restoreAllMocks(); }); describe("getForeignAssets", () => { - it("should return empty list if chain returns no foreign assets", async () => { - // mock empty list returned from chain - sinon.stub(assetRegistryApi, "getAssetRegistryEntries").returns(Promise.resolve([])); - - const actual = await assetRegistryApi.getForeignAssets(); - expect(actual).to.be.empty; - }); + it( + "should return empty list if chain returns no foreign assets", + async () => { + // mock empty list returned from chain + jest.spyOn(assetRegistryApi, "getAssetRegistryEntries").mockClear().mockResolvedValue([]); + + const actual = await assetRegistryApi.getForeignAssets(); + expect(actual).toHaveLength(0); + } + ); - it("should ignore empty optionals in foreign assets data from chain", async () => { - const chainDataReturned: AssetRegistryMetadataTuple[] = [ - // one "good" returned value - [mockStorageKey, api.createType("Option", mockMetadata)], - // one empty option - [mockStorageKey, api.createType("Option", undefined)], - ]; + it( + "should ignore empty optionals in foreign assets data from chain", + async () => { + const chainDataReturned: AssetRegistryMetadataTuple[] = [ + // one "good" returned value + [mockStorageKey, registry.createType("Option", mockMetadata)], + // one empty option + [mockStorageKey, registry.createType("Option", undefined)], + ]; - sinon.stub(assetRegistryApi, "getAssetRegistryEntries").returns(Promise.resolve(chainDataReturned)); + jest.spyOn(assetRegistryApi, "getAssetRegistryEntries").mockClear().mockResolvedValue(chainDataReturned); - const actual = await assetRegistryApi.getForeignAssets(); + const actual = await assetRegistryApi.getForeignAssets(); - expect(actual).to.have.lengthOf(1, `Expected only one currency to be returned, but got ${actual.length}`); + expect(actual).toHaveLength(1); - const actualCurrency = actual[0]; - expect(actualCurrency.ticker).to.equal( - mockMetadataValues.symbol, - `Expected the returned currency ticker to be ${mockMetadataValues.symbol}, but it was ${actualCurrency.ticker}` - ); - }); + const actualCurrency = actual[0]; + expect(actualCurrency.ticker).toBe(mockMetadataValues.symbol); + } + ); }); describe("unwrapMetadataFromEntries", () => { it("should convert foreign asset metadata to currency", async () => { const actual = DefaultAssetRegistryAPI.metadataTupleToForeignAsset([mockStorageKey, mockMetadata]); - expect(actual.ticker).to.equal( - mockMetadataValues.symbol, - `Expected currency ticker to be ${mockMetadataValues.symbol}, but was ${actual.ticker}` - ); - - expect(actual.name).to.equal( - mockMetadataValues.name, - `Expected currency name to be ${mockMetadataValues.name}, but was ${actual.name}` - ); - - expect(actual.decimals).to.equal( - mockMetadataValues.decimals, - `Expected currency base to be ${mockMetadataValues.decimals}, but was ${actual.decimals}` - ); - - expect(actual.foreignAsset.coingeckoId).to.equal( - mockMetadataValues.coingeckoId, - `Expected coingecko id to be ${mockMetadataValues.coingeckoId}, but was ${actual.foreignAsset.coingeckoId}` - ); + expect(actual.ticker).toBe(mockMetadataValues.symbol); + + expect(actual.name).toBe(mockMetadataValues.name); + + expect(actual.decimals).toBe(mockMetadataValues.decimals); + + expect(actual.foreignAsset.coingeckoId).toBe(mockMetadataValues.coingeckoId); }); }); @@ -141,77 +127,81 @@ describe("DefaultAssetRegistryAPI", () => { ]; const prepareMocks = ( - sinon: sinon.SinonSandbox, assetRegistryApi: DefaultAssetRegistryAPI, allForeignAssets: ForeignAsset[], collateralCeilingCurrencyPairs?: InterbtcPrimitivesVaultCurrencyPair[] ) => { - sinon.stub(assetRegistryApi, "getForeignAssets").returns(Promise.resolve(allForeignAssets)); + jest.spyOn(assetRegistryApi, "getForeignAssets").mockClear().mockResolvedValue(allForeignAssets); // this return does not matter since individual tests mock extractCollateralCeilingEntryKeys // which returns the actual values of interest - sinon.stub(assetRegistryApi, "getSystemCollateralCeilingEntries").returns(Promise.resolve([])); + jest.spyOn(assetRegistryApi, "getSystemCollateralCeilingEntries").mockClear().mockResolvedValue([]); if (collateralCeilingCurrencyPairs !== undefined) { - sinon - .stub(assetRegistryApi, "extractCollateralCeilingEntryKeys") - .returns(collateralCeilingCurrencyPairs); + jest.spyOn(assetRegistryApi, "extractCollateralCeilingEntryKeys").mockClear() + .mockReturnValue(collateralCeilingCurrencyPairs); } }; it("should return empty array if there are no foreign assets", async () => { - prepareMocks(sinon, assetRegistryApi, []); + prepareMocks(assetRegistryApi, []); const actual = await assetRegistryApi.getCollateralForeignAssets(); - expect(actual).to.be.empty; + expect(actual).toHaveLength(0); }); - it("should return empty array if there are no foreign assets with a collateral ceiling set", async () => { - prepareMocks(sinon, assetRegistryApi, mockForeignAssets, []); + it( + "should return empty array if there are no foreign assets with a collateral ceiling set", + async () => { + prepareMocks(assetRegistryApi, mockForeignAssets, []); - const actual = await assetRegistryApi.getCollateralForeignAssets(); - expect(actual).to.be.empty; - }); - - it("should return only foreign assets, not tokens with collateral ceilings set", async () => { - // pick an asset id that we expect to get returned - const expectedForeignAssetId = mockForeignAssets[0].foreignAsset.id; - - // only bother mocking collateral currencies, the wrapped side is ignored - const mockCurrencyPairs = [ - { - // mocked foreign asset collateral - collateral: { - isForeignAsset: true, - isToken: false, - asForeignAsset: api.createType("u32", expectedForeignAssetId), - type: "ForeignAsset", + const actual = await assetRegistryApi.getCollateralForeignAssets(); + expect(actual).toHaveLength(0); + } + ); + + it( + "should return only foreign assets, not tokens with collateral ceilings set", + async () => { + // pick an asset id that we expect to get returned + const expectedForeignAssetId = mockForeignAssets[0].foreignAsset.id; + + // only bother mocking collateral currencies, the wrapped side is ignored + const mockCurrencyPairs = [ + { + // mocked foreign asset collateral + collateral: { + isForeignAsset: true, + isToken: false, + asForeignAsset: registry.createType("u32", expectedForeignAssetId), + type: "ForeignAsset", + }, }, - }, - { - // mocked token collateral (ie. not foreign asset) - collateral: { - isForeignAsset: false, - isToken: true, - // logically inconsistent (but trying to trick into having a valid result if this is used when it shouldn't) - asForeignAsset: api.createType( - "u32", - mockForeignAssets[mockForeignAssets.length - 1].foreignAsset.id - ), - type: "Token", + { + // mocked token collateral (ie. not foreign asset) + collateral: { + isForeignAsset: false, + isToken: true, + // logically inconsistent (but trying to trick into having a valid result if this is used when it shouldn't) + asForeignAsset: registry.createType( + "u32", + mockForeignAssets[mockForeignAssets.length - 1].foreignAsset.id + ), + type: "Token", + }, }, - }, - ]; + ]; - prepareMocks(sinon, assetRegistryApi, mockForeignAssets, mockCurrencyPairs); + prepareMocks(assetRegistryApi, mockForeignAssets, mockCurrencyPairs); - const actual = await assetRegistryApi.getCollateralForeignAssets(); + const actual = await assetRegistryApi.getCollateralForeignAssets(); - // expect one returned value - expect(actual).to.have.lengthOf(1); + // expect one returned value + expect(actual).toHaveLength(1); - const actualAssetId = actual[0].foreignAsset.id; - expect(actualAssetId).to.be.eq(expectedForeignAssetId); - }); + const actualAssetId = actual[0].foreignAsset.id; + expect(actualAssetId).toBe(expectedForeignAssetId); + } + ); }); }); diff --git a/test/unit/parachain/loans.test.ts b/test/unit/parachain/loans.test.ts index 06bb2fe68..f825cafc4 100644 --- a/test/unit/parachain/loans.test.ts +++ b/test/unit/parachain/loans.test.ts @@ -1,5 +1,4 @@ /* eslint-disable max-len */ -import { ApiPromise } from "@polkadot/api"; import { BorrowPosition, CurrencyExt, @@ -10,40 +9,33 @@ import { TickerToData, newMonetaryAmount, } from "../../../src/"; -import { getAPITypes } from "../../../src/factory"; import Big from "big.js"; -import { expect } from "chai"; import { Bitcoin, ExchangeRate, InterBtc, Interlay, KBtc, MonetaryAmount, Polkadot } from "@interlay/monetary-js"; describe("DefaultLoansAPI", () => { - let api: ApiPromise; let loansApi: DefaultLoansAPI; const wrappedCurrency = InterBtc; const testGovernanceCurrency = Interlay; const testRelayCurrency = Polkadot; - before(() => { - api = new ApiPromise(); - // disconnect immediately to avoid printing errors - // we only need the instance to create variables - api.disconnect(); - api.registerTypes(getAPITypes()); + afterAll(() => { + jest.resetAllMocks(); }); beforeEach(() => { - const oracleAPI = new DefaultOracleAPI(api, wrappedCurrency); - loansApi = new DefaultLoansAPI(api, KBtc, oracleAPI); + const oracleAPI = new DefaultOracleAPI(null as never, wrappedCurrency); + loansApi = new DefaultLoansAPI(null as never, KBtc, oracleAPI); }); - describe("getLendPositionsOfAccount", () => { + describe.skip("getLendPositionsOfAccount", () => { // TODO: add tests }); - describe("getBorrowPositionsOfAccount", () => { + describe.skip("getBorrowPositionsOfAccount", () => { // TODO: add tests }); - describe("getLoanAssets", () => { + describe.skip("getLoanAssets", () => { // TODO: add tests }); @@ -67,8 +59,8 @@ describe("DefaultLoansAPI", () => { testExchangeRate ); - expect(actualTotalLiquidity.toString()).to.be.eq(expectedTotalLiquidityAmount.toString()); - expect(actualAvailableCapacity.toString()).to.be.eq(expectedAvailableCapacityAmount.toString()); + expect(actualTotalLiquidity.toString()).toBe(expectedTotalLiquidityAmount.toString()); + expect(actualAvailableCapacity.toString()).toBe(expectedAvailableCapacityAmount.toString()); }); it("should return zero total liquidity if exchange rate is zero", () => { @@ -80,7 +72,7 @@ describe("DefaultLoansAPI", () => { zeroExchangeRate ); - expect(actualTotalLiquidity.toBig().toNumber()).to.eq(0); + expect(actualTotalLiquidity.toBig().toNumber()).toBe(0); }); it("should return zero total liquidity if total issuance is zero", () => { @@ -92,21 +84,24 @@ describe("DefaultLoansAPI", () => { testExchangeRate ); - expect(actualTotalLiquidity.toBig().toNumber()).to.eq(0); + expect(actualTotalLiquidity.toBig().toNumber()).toBe(0); }); - it("should return zero available capacity if borrow is equal to issuance times exchange rate", () => { - const borrowAll = testTotalIssuance.mul(testExchangeRate); - const borrowAllAtomicAmount = borrowAll.toBig(0); - const [_, actualAvailableCapacity] = loansApi._calculateLiquidityAndCapacityAmounts( - testUnderlying, - testIssuanceAtomicAmount, - borrowAllAtomicAmount, - testExchangeRate - ); - - expect(actualAvailableCapacity.toBig().toNumber()).to.eq(0); - }); + it( + "should return zero available capacity if borrow is equal to issuance times exchange rate", + () => { + const borrowAll = testTotalIssuance.mul(testExchangeRate); + const borrowAllAtomicAmount = borrowAll.toBig(0); + const [, actualAvailableCapacity] = loansApi._calculateLiquidityAndCapacityAmounts( + testUnderlying, + testIssuanceAtomicAmount, + borrowAllAtomicAmount, + testExchangeRate + ); + + expect(actualAvailableCapacity.toBig().toNumber()).toBe(0); + } + ); }); describe("_getSubsidyReward", () => { @@ -117,19 +112,22 @@ describe("DefaultLoansAPI", () => { const actualReward = loansApi._getSubsidyReward(Big(testAmountAtomic), testGovernanceCurrency); - expect(actualReward).to.not.be.null; - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - expect(expectedAmount.toBig().eq(actualReward!.toBig())).to.be.eq( - true, - `Expected total amount (atomic value) to equal ${expectedAmount.toString(true)} - but was: ${actualReward?.toString(true)}` - ); - expect(actualReward?.currency).to.eq(testGovernanceCurrency); + expect(actualReward).not.toBeNull(); + + try { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(expectedAmount.toBig().eq(actualReward!.toBig())).toBe(true); + } catch(_) { + throw Error( + `Expected total amount (atomic value) to equal ${expectedAmount.toString(true)} + but was: ${actualReward?.toString(true)}` + ); + } + expect(actualReward?.currency).toBe(testGovernanceCurrency); }); it("should return null if the amount is zero", () => { - expect(loansApi._getSubsidyReward(Big(0), testGovernanceCurrency)).to.be.null; + expect(loansApi._getSubsidyReward(Big(0), testGovernanceCurrency)).toBeNull(); }); }); @@ -218,24 +216,39 @@ describe("DefaultLoansAPI", () => { loanAssets ); - expect( - totalLentBtc.toBig().eq(expectedTotalLentBtc), - `Total lent amount: ${totalLentBtc - .toBig() - .toString()} doesn't match expected amount ${expectedTotalLentBtc.toString()}` - ).to.be.true; - expect( - totalBorrowedBtc.toBig().eq(expectedTotalBorrowedBtc), - `Total borrowed amount: ${totalBorrowedBtc.toString()} doesn't match expected amount ${expectedTotalBorrowedBtc.toString()}` - ).to.be.true; - expect( - totalCollateralBtc.toBig().eq(expectedTotalLentBtc), - `Collateral amount: ${totalCollateralBtc.toString()} doesn't match expected amount ${expectedTotalLentBtc.toString()}` - ).to.be.true; - expect( - borrowLimitBtc.toBig().eq(expectedBorrowLimitBtc), - `Borrow limit amount: ${borrowLimitBtc.toString()} doesn't match expected amount ${expectedBorrowLimitBtc.toString()}` - ).to.be.true; + try { + expect(totalLentBtc.toBig().eq(expectedTotalLentBtc)).toBe(true); + } catch(_) { + throw Error( + `Total lent amount: ${totalLentBtc + .toBig() + .toString()} doesn't match expected amount ${expectedTotalLentBtc.toString()}` + ); + } + + try { + expect(totalBorrowedBtc.toBig().eq(expectedTotalBorrowedBtc)).toBe(true); + } catch(_) { + throw Error( + `Total borrowed amount: ${totalBorrowedBtc.toString()} doesn't match expected amount ${expectedTotalBorrowedBtc.toString()}` + ); + } + + try { + expect(totalCollateralBtc.toBig().eq(expectedTotalLentBtc)).toBe(true); + } catch(_) { + throw Error( + `Collateral amount: ${totalCollateralBtc.toString()} doesn't match expected amount ${expectedTotalLentBtc.toString()}` + ); + } + + try { + expect(borrowLimitBtc.toBig().eq(expectedBorrowLimitBtc)).toBe(true); + } catch(_) { + throw Error( + `Borrow limit amount: ${borrowLimitBtc.toString()} doesn't match expected amount ${expectedBorrowLimitBtc.toString()}` + ); + } }); it("should compute correct LTV and average thresholds", () => { @@ -284,31 +297,42 @@ describe("DefaultLoansAPI", () => { const expectedAverageLiquidationThreshold = totalLiquidationThresholdAdjustedCollateralAmountBtc.div(totalCollateralAmountBtc); - expect(ltv.eq(expectedLtv), `LTV: ${ltv.toString()} does not match expected LTV: ${expectedLtv.toString()}`) - .to.be.true; - expect( - collateralThresholdWeightedAverage.eq(expectedAverageCollateralThreshold), - `Average collateral threshold: ${collateralThresholdWeightedAverage.toString()} does not match expected threshold: ${expectedAverageCollateralThreshold.toString()}` - ).to.be.true; - expect( - liquidationThresholdWeightedAverage.eq(expectedAverageLiquidationThreshold), - `Average liquidation threshold: ${liquidationThresholdWeightedAverage.toString()} does not match expected threshold: ${expectedAverageLiquidationThreshold.toString()}` - ).to.be.true; + try { + expect(ltv.eq(expectedLtv)).toBe(true); + } catch(_) { + throw Error(`LTV: ${ltv.toString()} does not match expected LTV: ${expectedLtv.toString()}`); + } + + try { + expect(collateralThresholdWeightedAverage.eq(expectedAverageCollateralThreshold)).toBe(true); + } catch(_) { + throw Error( + `Average collateral threshold: ${collateralThresholdWeightedAverage.toString()} does not match expected threshold: ${expectedAverageCollateralThreshold.toString()}` + ); + } + + try { + expect(liquidationThresholdWeightedAverage.eq(expectedAverageLiquidationThreshold)).toBe(true); + } catch(_) { + throw Error( + `Average liquidation threshold: ${liquidationThresholdWeightedAverage.toString()} does not match expected threshold: ${expectedAverageLiquidationThreshold.toString()}` + ); + } }); it("should not throw when there are no positions", () => { - expect(() => loansApi.getLendingStats([], [], loanAssets)).to.not.throw; + expect(() => loansApi.getLendingStats([], [], loanAssets)).not.toThrow(); }); it("should not throw when there are no borrow positions", () => { const lendPositions = [mockLendPosition(new MonetaryAmount(testGovernanceCurrency, 1))]; - expect(() => loansApi.getLendingStats(lendPositions, [], loanAssets)).to.not.throw; + expect(() => loansApi.getLendingStats(lendPositions, [], loanAssets)).not.toThrow(); }); it("should throw when loan assets are empty", () => { const lendPositions = [mockLendPosition(new MonetaryAmount(testGovernanceCurrency, 1))]; const borrowPositions = [mockBorrowPosition(new MonetaryAmount(testGovernanceCurrency, 0.1))]; - expect(() => loansApi.getLendingStats(lendPositions, borrowPositions, {})).to.throw; + expect(() => loansApi.getLendingStats(lendPositions, borrowPositions, {})).toThrow(); }); }); }); diff --git a/test/unit/parachain/redeem.test.ts b/test/unit/parachain/redeem.test.ts index 70ad7ac84..e5f61fc8a 100644 --- a/test/unit/parachain/redeem.test.ts +++ b/test/unit/parachain/redeem.test.ts @@ -1,26 +1,37 @@ -import { expect } from "../../chai"; -import sinon from "sinon"; import { DefaultRedeemAPI, DefaultVaultsAPI, VaultsAPI } from "../../../src"; import { newMonetaryAmount } from "../../../src/utils"; -import { ExchangeRate, KBtc, Kintsugi } from "@interlay/monetary-js"; +import { KBtc, Kintsugi } from "@interlay/monetary-js"; import Big from "big.js"; import { NO_LIQUIDATION_VAULT_FOUND_REJECTION } from "../../../src/parachain/vaults"; describe("DefaultRedeemAPI", () => { + // instances will be fully/partially mocked where needed + let vaultsApi: DefaultVaultsAPI; let redeemApi: DefaultRedeemAPI; - let stubbedVaultsApi: sinon.SinonStubbedInstance; beforeEach(() => { // only mock/stub what we really need // add more if/when needed - stubbedVaultsApi = sinon.createStubInstance(DefaultVaultsAPI); + vaultsApi = new DefaultVaultsAPI( + null as any, + null as any, + null as any, + null as any, + null as any, + null as any, + null as any, + null as any, + null as any, + null as any + ); + redeemApi = new DefaultRedeemAPI( null as any, null as any, null as any, null as any, - stubbedVaultsApi as VaultsAPI, + vaultsApi as VaultsAPI, null as any, null as any, null as any @@ -28,14 +39,12 @@ describe("DefaultRedeemAPI", () => { }); afterEach(() => { - sinon.restore(); - sinon.reset(); + jest.restoreAllMocks(); }); describe("getBurnExchangeRate", () => { afterEach(() => { - sinon.restore(); - sinon.reset(); + jest.restoreAllMocks(); }); it("should reject if burnable amount is zero", async () => { @@ -47,10 +56,10 @@ describe("DefaultRedeemAPI", () => { collateral: newMonetaryAmount(10, Kintsugi), }; - // stub internal call to reutn our mocked vault - stubbedVaultsApi.getLiquidationVault.withArgs(sinon.match.any).resolves(mockVaultExt as any); + // stub internal call to return our mocked vault + jest.spyOn(vaultsApi, "getLiquidationVault").mockClear().mockResolvedValue(mockVaultExt as any); - await expect(redeemApi.getBurnExchangeRate(Kintsugi)).to.be.rejectedWith("no burnable tokens"); + await expect(redeemApi.getBurnExchangeRate(Kintsugi)).rejects.toThrow("no burnable tokens"); }); it("should return an exchange rate", async () => { @@ -61,12 +70,11 @@ describe("DefaultRedeemAPI", () => { collateral: newMonetaryAmount(100, Kintsugi), }; - // stub internal call to reutn our mocked vault - stubbedVaultsApi.getLiquidationVault.withArgs(sinon.match.any).resolves(mockVaultExt as any); + // stub internal call to return our mocked vault + jest.spyOn(vaultsApi, "getLiquidationVault").mockClear().mockResolvedValue(mockVaultExt as any); const exchangeRate = await redeemApi.getBurnExchangeRate(Kintsugi); - expect(exchangeRate).to.be.an.instanceof(ExchangeRate); - expect(exchangeRate.rate.toNumber()).to.be.greaterThan(0); + expect(exchangeRate.rate.toNumber()).toBeGreaterThan(0); }); it("should return a specific exchange rate for given values", async () => { @@ -85,37 +93,40 @@ describe("DefaultRedeemAPI", () => { // bring the numbers back down into a easier readable range of a JS number (expect 3.9127) const testMultiplier = 0.00001; - // stub internal call to reutn our mocked vault - stubbedVaultsApi.getLiquidationVault.withArgs(sinon.match.any).resolves(mockVaultExt as any); + // stub internal call to return our mocked vault + jest.spyOn(vaultsApi, "getLiquidationVault").mockClear().mockResolvedValue(mockVaultExt as any); const exchangeRate = await redeemApi.getBurnExchangeRate(Kintsugi); - expect(exchangeRate).to.be.an.instanceof(ExchangeRate); - expect(exchangeRate.rate.mul(testMultiplier).toNumber()).to.be.closeTo( - expectedExchangeRate.mul(testMultiplier).toNumber(), - 0.000001 - ); + + const delta = Math.abs(exchangeRate.rate.mul(testMultiplier).toNumber() - expectedExchangeRate.mul(testMultiplier).toNumber()); + expect(delta).toBeLessThan(0.000001); }); }); describe("getMaxBurnableTokens", () => { afterEach(() => { - sinon.restore(); - sinon.reset(); + jest.restoreAllMocks(); }); - it("should return zero if getLiquidationVault rejects with no liquidation vault message", async () => { - // stub internal call to return no liquidation vault - stubbedVaultsApi.getLiquidationVault.withArgs(sinon.match.any).returns(Promise.reject(NO_LIQUIDATION_VAULT_FOUND_REJECTION)); + it( + "should return zero if getLiquidationVault rejects with no liquidation vault message", + async () => { + // stub internal call to return no liquidation vault + jest.spyOn(vaultsApi, "getLiquidationVault").mockClear().mockRejectedValue(NO_LIQUIDATION_VAULT_FOUND_REJECTION); - const actualValue = await redeemApi.getMaxBurnableTokens(Kintsugi); - expect(actualValue.toBig().toNumber()).to.be.eq(0); - }); + const actualValue = await redeemApi.getMaxBurnableTokens(Kintsugi); + expect(actualValue.toBig().toNumber()).toBe(0); + } + ); - it("should propagate rejection if getLiquidationVault rejects with other message", async () => { - // stub internal call to return no liquidation vault - stubbedVaultsApi.getLiquidationVault.withArgs(sinon.match.any).returns(Promise.reject("foobar happened here")); + it( + "should propagate rejection if getLiquidationVault rejects with other message", + async () => { + // stub internal call to return no liquidation vault + jest.spyOn(vaultsApi, "getLiquidationVault").mockClear().mockRejectedValue("foobar happened here"); - await expect(redeemApi.getMaxBurnableTokens(Kintsugi)).to.be.rejectedWith("foobar happened here"); - }); + await expect(redeemApi.getMaxBurnableTokens(Kintsugi)).rejects.toEqual("foobar happened here"); + } + ); }); }); diff --git a/test/unit/parachain/vaults.test.ts b/test/unit/parachain/vaults.test.ts index 25a5a746a..adf9d0806 100644 --- a/test/unit/parachain/vaults.test.ts +++ b/test/unit/parachain/vaults.test.ts @@ -1,6 +1,4 @@ -import { assert, expect } from "../../chai"; import Big from "big.js"; -import sinon from "sinon"; import { DefaultRewardsAPI, DefaultTransactionAPI, DefaultVaultsAPI } from "../../../src"; import { newMonetaryAmount } from "../../../src/utils"; import { KBtc, Kusama } from "@interlay/monetary-js"; @@ -11,16 +9,28 @@ import { } from "../mocks/vaultsTestMocks"; describe("DefaultVaultsAPI", () => { + // apis will be mocked fully/partially as needed + let transactionApi: DefaultTransactionAPI; + let rewardsApi: DefaultRewardsAPI; let vaultsApi: DefaultVaultsAPI; + + // alice + const aliceAccount = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"; + const testCollateralCurrency = Kusama; const testWrappedCurrency = KBtc; - let stubbedRewardsApi: sinon.SinonStubbedInstance; - let stubbedTransactionApi: sinon.SinonStubbedInstance; - beforeEach(async () => { - stubbedRewardsApi = sinon.createStubInstance(DefaultRewardsAPI); - stubbedTransactionApi = sinon.createStubInstance(DefaultTransactionAPI); + transactionApi = new DefaultTransactionAPI( + null as any, + aliceAccount + ); + + rewardsApi = new DefaultRewardsAPI( + null as any, + testWrappedCurrency + ); + vaultsApi = new DefaultVaultsAPI( null as any, null as any, @@ -29,75 +39,73 @@ describe("DefaultVaultsAPI", () => { null as any, null as any, null as any, - stubbedRewardsApi, + rewardsApi, null as any, - stubbedTransactionApi + transactionApi ); }); afterEach(() => { - sinon.restore(); - sinon.reset(); + jest.restoreAllMocks(); }); describe("backingCollateralProportion", () => { - it("should return 0 if nominator and vault have zero collateral", async () => { - // prepare mocks - const { nominatorId, vaultId } = prepareBackingCollateralProportionMocks( - sinon, - vaultsApi, - stubbedRewardsApi, - new Big(0), - new Big(0), - testCollateralCurrency - ); - - // do the thing - const proportion = await vaultsApi.backingCollateralProportion( - vaultId, - nominatorId, - testCollateralCurrency - ); - - // check result - const expectedProportion = new Big(0); - assert.equal( - proportion.toString(), - expectedProportion.toString(), - `Expected actual proportion to be ${expectedProportion.toString()} but it was ${proportion.toString()}` - ); - }); - - it("should reject if nominator has collateral, but vault has zero collateral", async () => { - // prepare mocks - const nominatorAmount = new Big(1); - const vaultAmount = new Big(0); - const { nominatorId, vaultId } = prepareBackingCollateralProportionMocks( - sinon, - vaultsApi, - stubbedRewardsApi, - nominatorAmount, - vaultAmount, - testCollateralCurrency - ); + it( + "should return 0 if nominator and vault have zero collateral", + async () => { + // prepare mocks + const { nominatorId, vaultId } = prepareBackingCollateralProportionMocks( + vaultsApi, + rewardsApi, + new Big(0), + new Big(0), + testCollateralCurrency + ); + + // do the thing + const proportion = await vaultsApi.backingCollateralProportion( + vaultId, + nominatorId, + testCollateralCurrency + ); + + // check result + const expectedProportion = new Big(0); + expect(proportion.toString()).toEqual(expectedProportion.toString()); + } + ); - // do & check - const proportionPromise = vaultsApi.backingCollateralProportion( - vaultId, - nominatorId, - testCollateralCurrency - ); - expect(proportionPromise).to.be.rejectedWith(Error); - }); + it( + "should reject if nominator has collateral, but vault has zero collateral", + async () => { + // prepare mocks + const nominatorAmount = new Big(1); + const vaultAmount = new Big(0); + const { nominatorId, vaultId } = prepareBackingCollateralProportionMocks( + vaultsApi, + rewardsApi, + nominatorAmount, + vaultAmount, + testCollateralCurrency + ); + + // do & check + const proportionPromise = vaultsApi.backingCollateralProportion( + vaultId, + nominatorId, + testCollateralCurrency + ); + await expect(proportionPromise).rejects.toThrow(Error); + } + ); it("should calculate expected proportion", async () => { // prepare mocks const nominatorAmount = new Big(1); const vaultAmount = new Big(2); const { nominatorId, vaultId } = prepareBackingCollateralProportionMocks( - sinon, vaultsApi, - stubbedRewardsApi, + rewardsApi, nominatorAmount, vaultAmount, testCollateralCurrency @@ -112,22 +120,17 @@ describe("DefaultVaultsAPI", () => { // check result const expectedProportion = nominatorAmount.div(vaultAmount); - assert.equal( - proportion.toString(), - expectedProportion.toString(), - `Expected actual proportion to be ${expectedProportion.toString()} but it was ${proportion.toString()}` - ); + expect(proportion.toString()).toEqual(expectedProportion.toString()); }); }); describe("registerNewCollateralVault", () => { const testCollateralAmount = newMonetaryAmount(new Big(30), testCollateralCurrency); it("should reject if transaction API account id is not set", async () => { - prepareRegisterNewCollateralVaultMocks(sinon, vaultsApi, stubbedTransactionApi, true); + prepareRegisterNewCollateralVaultMocks(vaultsApi, transactionApi, true); const registerVaultCall = () => vaultsApi.registerNewCollateralVault(testCollateralAmount); - // check for partial string here - expect(registerVaultCall).to.throw("account must be set"); + expect(registerVaultCall).toThrow(Error); }); }); @@ -141,7 +144,6 @@ describe("DefaultVaultsAPI", () => { const expectedLiquidationExchangeRate = 1.5; prepareLiquidationRateMocks( - sinon, vaultsApi, mockIssuedTokens, mockCollateralTokens, @@ -156,8 +158,8 @@ describe("DefaultVaultsAPI", () => { testCollateralCurrency ); - expect(actualRate).to.not.be.undefined; - expect(actualRate?.toNumber()).eq(expectedLiquidationExchangeRate); + expect(actualRate).toBeDefined(); + expect(actualRate?.toNumber()).toBe(expectedLiquidationExchangeRate); }); it("should return undefined if vault has no issued tokens", async () => { @@ -166,7 +168,6 @@ describe("DefaultVaultsAPI", () => { const mockLiquidationThreshold = 2; prepareLiquidationRateMocks( - sinon, vaultsApi, mockIssuedTokens, mockCollateralTokens, @@ -181,7 +182,7 @@ describe("DefaultVaultsAPI", () => { testCollateralCurrency ); - expect(actualRate).to.be.undefined; + expect(actualRate).toBeUndefined(); }); it("should return undefined if liquidation rate is zero", async () => { @@ -190,7 +191,6 @@ describe("DefaultVaultsAPI", () => { const mockLiquidationThreshold = 0; prepareLiquidationRateMocks( - sinon, vaultsApi, mockIssuedTokens, mockCollateralTokens, @@ -205,7 +205,7 @@ describe("DefaultVaultsAPI", () => { testCollateralCurrency ); - expect(actualRate).to.be.undefined; + expect(actualRate).toBeUndefined(); }); }); }); diff --git a/test/unit/utils/bitcoin.test.ts b/test/unit/utils/bitcoin.test.ts index 715acaf43..6190183ab 100644 --- a/test/unit/utils/bitcoin.test.ts +++ b/test/unit/utils/bitcoin.test.ts @@ -1,12 +1,10 @@ /* eslint-disable max-len */ import { TypeRegistry } from "@polkadot/types"; import { BitcoinMerkleProof, decodeBtcAddress, encodeBtcAddress, getTxProof } from "../../../src/utils"; -import { assert } from "../../chai"; import * as bitcoinjs from "bitcoinjs-lib"; import { BitcoinAddress } from "@polkadot/types/lookup"; import { getAPITypes } from "../../../src/factory"; import { H160, H256 } from "@polkadot/types/interfaces/runtime"; -import sinon from "sinon"; import { DefaultElectrsAPI } from "../../../src/external"; describe("Bitcoin", () => { @@ -20,56 +18,54 @@ describe("Bitcoin", () => { return registry.createType("H160", hash); }; - before(() => { + beforeAll(() => { registry = new TypeRegistry(); registry.register(getAPITypes()); }); describe("decodeBtcAddress", () => { it("should get correct hash for p2pkh address", () => { - assert.deepEqual(decodeBtcAddress("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH", bitcoinjs.networks.bitcoin), { + expect( + decodeBtcAddress("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH", bitcoinjs.networks.bitcoin) + ).toEqual({ p2pkh: "0x751e76e8199196d454941c45d1b3a323f1433bd6", }); }); it("should get correct hash for p2sh address", () => { - assert.deepEqual(decodeBtcAddress("3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN", bitcoinjs.networks.bitcoin), { + expect( + decodeBtcAddress("3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN", bitcoinjs.networks.bitcoin) + ).toEqual({ p2sh: "0xbcfeb728b584253d5f3f70bcb780e9ef218a68f4", }); }); it("should get correct hash for p2wpkh address", () => { // compare hex because type lib has trouble with this enum - assert.deepEqual( - decodeBtcAddress("bcrt1qjvmc5dtm4qxgtug8faa5jdedlyq4v76ngpqgrl", bitcoinjs.networks.regtest), - { - p2wpkhv0: "0x93378a357ba80c85f1074f7b49372df901567b53", - } - ); - assert.deepEqual( - decodeBtcAddress("tb1q45uq0q4v22fspeg3xjnkgf8a0v7pqgjks4k6sz", bitcoinjs.networks.testnet), - { - p2wpkhv0: "0xad380782ac529300e51134a76424fd7b3c102256", - } - ); - assert.deepEqual( - decodeBtcAddress("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", bitcoinjs.networks.bitcoin), - { - p2wpkhv0: "0xe8df018c7e326cc253faac7e46cdc51e68542c42", - } - ); + expect( + decodeBtcAddress("bcrt1qjvmc5dtm4qxgtug8faa5jdedlyq4v76ngpqgrl", bitcoinjs.networks.regtest) + ).toEqual({ + p2wpkhv0: "0x93378a357ba80c85f1074f7b49372df901567b53", + }); + expect( + decodeBtcAddress("tb1q45uq0q4v22fspeg3xjnkgf8a0v7pqgjks4k6sz", bitcoinjs.networks.testnet) + ).toEqual({ + p2wpkhv0: "0xad380782ac529300e51134a76424fd7b3c102256", + }); + expect( + decodeBtcAddress("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", bitcoinjs.networks.bitcoin) + ).toEqual({ + p2wpkhv0: "0xe8df018c7e326cc253faac7e46cdc51e68542c42", + }); }); it("should get correct hash for p2wsh address", () => { - assert.deepEqual( - decodeBtcAddress( - "bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul", - bitcoinjs.networks.bitcoin - ), - { - p2wshv0: "0xf513a6b2a03f11e15f921c0345bee24c63fffe638a73eca823ec58aaec5f38c6", - } - ); + expect(decodeBtcAddress( + "bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul", + bitcoinjs.networks.bitcoin + )).toEqual({ + p2wshv0: "0xf513a6b2a03f11e15f921c0345bee24c63fffe638a73eca823ec58aaec5f38c6", + }); }); }); @@ -82,7 +78,7 @@ describe("Bitcoin", () => { }; const encodedAddress = encodeBtcAddress(mockAddress, bitcoinjs.networks.bitcoin); - assert.equal(encodedAddress, "1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"); + expect(encodedAddress).toEqual("1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH"); }); it("should encode correct p2sh address from hash", () => { @@ -93,7 +89,7 @@ describe("Bitcoin", () => { }; const encodedAddress = encodeBtcAddress(mockAddress, bitcoinjs.networks.bitcoin); - assert.equal(encodedAddress, "3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN"); + expect(encodedAddress).toEqual("3JvL6Ymt8MVWiCNHC7oWU6nLeHNJKLZGLN"); }); it("should encode correct p2wpkh address from hash", () => { @@ -104,7 +100,7 @@ describe("Bitcoin", () => { }; const encodedAddress = encodeBtcAddress(mockAddress, bitcoinjs.networks.regtest); - assert.equal(encodedAddress, "bcrt1qjvmc5dtm4qxgtug8faa5jdedlyq4v76ngpqgrl"); + expect(encodedAddress).toEqual("bcrt1qjvmc5dtm4qxgtug8faa5jdedlyq4v76ngpqgrl"); }); it("should encode correct p2wsh address from hash", () => { @@ -115,17 +111,18 @@ describe("Bitcoin", () => { }; const encodedAddress = encodeBtcAddress(mockAddress, bitcoinjs.networks.bitcoin); - assert.equal(encodedAddress, "bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul"); + expect(encodedAddress).toEqual("bc1q75f6dv4q8ug7zhujrsp5t0hzf33lllnr3fe7e2pra3v24mzl8rrqtp3qul"); }); }); describe("getTxProof", () => { const mockElectrsGetParsedExecutionParameters = (merkleProofHex: string, txHex: string) => { - const stubbedElectrsApi = sinon.createStubInstance(DefaultElectrsAPI); + const mockedElectrsApi = new DefaultElectrsAPI("mainnet"); + const [proof, tx] = [BitcoinMerkleProof.fromHex(merkleProofHex), bitcoinjs.Transaction.fromHex(txHex)]; - stubbedElectrsApi.getParsedExecutionParameters.withArgs(sinon.match.any).resolves([proof, tx]); - stubbedElectrsApi.getCoinbaseTxId.withArgs(sinon.match.any).resolves(tx.getId()); - return stubbedElectrsApi; + jest.spyOn(mockedElectrsApi, "getParsedExecutionParameters").mockClear().mockResolvedValue([proof, tx]); + jest.spyOn(mockedElectrsApi, "getCoinbaseTxId").mockClear().mockResolvedValue(tx.getId()); + return mockedElectrsApi; }; it("should parse proof and transactions correctly", async () => { @@ -161,13 +158,13 @@ describe("Bitcoin", () => { userTxProof: { merkleProof, transaction, txEncodedLen }, } = await getTxProof(stubbedElectrsApi, ""); - assert.equal(merkleProof.transactionsCount, expectedMerkleProof.txCount); - assert.equal(merkleProof.flagBits.length, expectedMerkleProof.flagBitsCount); - assert.equal(merkleProof.hashes.length, expectedMerkleProof.hashCount); - assert.equal(JSON.stringify(transaction.inputs), JSON.stringify(expectedTxIns)); - assert.equal(JSON.stringify(transaction.outputs), JSON.stringify(expectedTxOuts)); - assert.deepEqual(transaction.lockAt, expectedTxLockTime); - assert.equal(txEncodedLen, expectedLengthBound); + expect(merkleProof.transactionsCount).toEqual(expectedMerkleProof.txCount); + expect(merkleProof.flagBits.length).toEqual(expectedMerkleProof.flagBitsCount); + expect(merkleProof.hashes.length).toEqual(expectedMerkleProof.hashCount); + expect(JSON.stringify(transaction.inputs)).toEqual(JSON.stringify(expectedTxIns)); + expect(JSON.stringify(transaction.outputs)).toEqual(JSON.stringify(expectedTxOuts)); + expect(transaction.lockAt).toEqual(expectedTxLockTime); + expect(txEncodedLen).toEqual(expectedLengthBound); }); it("should parse coinbase transaction correctly", async () => { @@ -180,7 +177,7 @@ describe("Bitcoin", () => { const { userTxProof: { transaction }, } = await getTxProof(stubbedElectrsApi, ""); - assert.equal(transaction.inputs[0].source, "coinbase"); + expect(transaction.inputs[0].source).toEqual("coinbase"); }); it("should parse timestamp locktime", async () => { @@ -195,7 +192,7 @@ describe("Bitcoin", () => { const { userTxProof: { transaction }, } = await getTxProof(stubbedElectrsApi, ""); - assert.deepEqual(transaction.lockAt, expectedLocktime); + expect(transaction.lockAt).toEqual(expectedLocktime); }); }); }); diff --git a/test/unit/utils/encoding.test.ts b/test/unit/utils/encoding.test.ts index b9c34f319..dc97351db 100644 --- a/test/unit/utils/encoding.test.ts +++ b/test/unit/utils/encoding.test.ts @@ -1,5 +1,4 @@ import { TypeRegistry } from "@polkadot/types"; -import { assert } from "../../chai"; import { getAPITypes } from "../../../src/factory"; import { RedeemStatus } from "../../../src/types"; import { @@ -22,7 +21,7 @@ describe("Encoding", () => { return new (registry.createClass("H256Le"))(registry, hash) as H256Le; }; - before(() => { + beforeAll(() => { registry = new TypeRegistry(); registry.register(getAPITypes()); }); @@ -30,14 +29,14 @@ describe("Encoding", () => { it("should encode / decode same block hash as H256Le", () => { const blockHashHexLE = "0x9067166e896765258f6636a082abad6953f17a0e8dc21fc4f85648ceeedbda69"; const blockHash = createH256Le(blockHashHexLE); - return assert.equal(blockHash.toHex(), blockHashHexLE); + return expect(blockHash.toHex()).toEqual(blockHashHexLE); }); it("should strip prefix", () => { const blockHashHexBEWithPrefix = "0x5499ac3ca3ddf563ace6b6a56ec2e8bdc5f796bef249445c36d90a69d0757d4c"; const blockHashHexBEWithoutPrefix = "5499ac3ca3ddf563ace6b6a56ec2e8bdc5f796bef249445c36d90a69d0757d4c"; - assert.equal(stripHexPrefix(blockHashHexBEWithPrefix), blockHashHexBEWithoutPrefix); - assert.equal(stripHexPrefix(blockHashHexBEWithoutPrefix), blockHashHexBEWithoutPrefix); + expect(stripHexPrefix(blockHashHexBEWithPrefix)).toEqual(blockHashHexBEWithoutPrefix); + expect(stripHexPrefix(blockHashHexBEWithoutPrefix)).toEqual(blockHashHexBEWithoutPrefix); }); it("should reverse endianness from le to be", () => { @@ -46,13 +45,13 @@ describe("Encoding", () => { const blockHash = createH256Le(blockHashHexLE); const result = uint8ArrayToString(reverseEndianness(blockHash)); - return assert.equal(result, stripHexPrefix(blockHashHexBE)); + return expect(result).toEqual(stripHexPrefix(blockHashHexBE)); }); it("should reverse endianness hex", () => { const blockHashHexLE = "0x9067166e896765258f6636a082abad6953f17a0e8dc21fc4f85648ceeedbda69"; const blockHashHexBE = "0x69dadbeece4856f8c41fc28d0e7af15369adab82a036668f256567896e166790"; - return assert.equal(reverseEndiannessHex(blockHashHexLE), stripHexPrefix(blockHashHexBE)); + return expect(reverseEndiannessHex(blockHashHexLE)).toEqual(stripHexPrefix(blockHashHexBE)); }); describe("parseRedeemRequestStatus", () => { @@ -77,8 +76,12 @@ describe("Encoding", () => { }; }; - const assertEqualPretty = (expected: RedeemStatus, actual: RedeemStatus): void => { - assert.equal(actual, expected, `Expected '${RedeemStatus[expected]}' but was '${RedeemStatus[actual]}'`); + const expectEqualPretty = (expected: RedeemStatus, actual: RedeemStatus): void => { + try { + expect(actual).toBe(expected); + } catch(_) { + throw Error(`Expected '${RedeemStatus[expected]}' but was '${RedeemStatus[actual]}'`); + } }; it("should correctly parse completed status", () => { @@ -87,7 +90,7 @@ describe("Encoding", () => { const actualStatus = parseRedeemRequestStatus(mockRequest, 42, 42); - assertEqualPretty(actualStatus, expectedStatus); + expectEqualPretty(actualStatus, expectedStatus); }); it("should correctly parse reimbursed status", () => { @@ -96,7 +99,7 @@ describe("Encoding", () => { const actualStatus = parseRedeemRequestStatus(mockRequest, 42, 42); - assertEqualPretty(actualStatus, expectedStatus); + expectEqualPretty(actualStatus, expectedStatus); }); it("should correctly parse retried status", () => { @@ -105,7 +108,7 @@ describe("Encoding", () => { const actualStatus = parseRedeemRequestStatus(mockRequest, 42, 42); - assertEqualPretty(actualStatus, expectedStatus); + expectEqualPretty(actualStatus, expectedStatus); }); describe("should correctly parse expired status", () => { @@ -115,33 +118,39 @@ describe("Encoding", () => { const mockInternalPendingStatus = buildMockStatus("Pending"); const expectedStatus = RedeemStatus.Expired; - it("when global redeem period is greater than request period and less than opentime + period", () => { - const globalRedeemPeriod = currentBlock - 5; - const requestPeriod = globalRedeemPeriod - 3; - // preconditions - assert.isAbove(globalRedeemPeriod, requestPeriod, "Precondition failed: fix test setup"); - assert.isBelow(globalRedeemPeriod, currentBlock, "Precondition failed: fix test setup"); + it( + "when global redeem period is greater than request period and less than opentime + period", + () => { + const globalRedeemPeriod = currentBlock - 5; + const requestPeriod = globalRedeemPeriod - 3; + // preconditions + expect(globalRedeemPeriod).toBeGreaterThan(requestPeriod); + expect(globalRedeemPeriod).toBeLessThan(currentBlock); - const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); + const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); - const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); + const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); - assertEqualPretty(actualStatus, expectedStatus); - }); + expectEqualPretty(actualStatus, expectedStatus); + } + ); - it("when request period is greater than global redeem period and less than opentime + period", () => { - const requestPeriod = currentBlock - 3; - const globalRedeemPeriod = requestPeriod - 5; - // preconditions - assert.isAbove(requestPeriod, globalRedeemPeriod, "Precondition failed: fix test setup"); - assert.isBelow(requestPeriod, currentBlock, "Precondition failed: fix test setup"); + it( + "when request period is greater than global redeem period and less than opentime + period", + () => { + const requestPeriod = currentBlock - 3; + const globalRedeemPeriod = requestPeriod - 5; + // preconditions + expect(requestPeriod).toBeGreaterThan(globalRedeemPeriod); + expect(requestPeriod).toBeLessThan(currentBlock); - const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); + const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); - const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); + const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); - assertEqualPretty(actualStatus, expectedStatus); - }); + expectEqualPretty(actualStatus, expectedStatus); + } + ); }); describe("should correctly parse pending status", () => { @@ -154,16 +163,13 @@ describe("Encoding", () => { const globalRedeemPeriod = 50; const requestPeriod = 25; // preconditions - assert.isTrue( - opentimeBlock + Math.max(requestPeriod, globalRedeemPeriod) > currentBlock, - "Precondition failed: fix test setup" - ); + expect(opentimeBlock + Math.max(requestPeriod, globalRedeemPeriod) > currentBlock).toBe(true); const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); - assertEqualPretty(actualStatus, expectedStatus); + expectEqualPretty(actualStatus, expectedStatus); }); it("when opentime + period is equal to current block count", () => { @@ -171,16 +177,15 @@ describe("Encoding", () => { // anything less than above const requestPeriod = globalRedeemPeriod - 1; // preconditions - assert.isTrue( - opentimeBlock + Math.max(requestPeriod, globalRedeemPeriod) == currentBlock, - "Precondition failed: fix test setup" - ); + expect( + opentimeBlock + Math.max(requestPeriod, globalRedeemPeriod) == currentBlock + ).toBe(true); const mockRequest = buildMockRedeemRequest(mockInternalPendingStatus, opentimeBlock, requestPeriod); const actualStatus = parseRedeemRequestStatus(mockRequest, globalRedeemPeriod, currentBlock); - assertEqualPretty(actualStatus, expectedStatus); + expectEqualPretty(actualStatus, expectedStatus); }); }); }); diff --git a/test/utils/helpers.ts b/test/utils/helpers.ts index 5cb0c7306..9cc7f4fee 100644 --- a/test/utils/helpers.ts +++ b/test/utils/helpers.ts @@ -25,7 +25,6 @@ import { storageKeyToNthInner, } from "../../src/utils"; import { SUDO_URI } from "../config"; -import { expect } from "chai"; import { ISubmittableResult } from "@polkadot/types/types"; export const SLEEP_TIME_MS = 1000; @@ -78,7 +77,11 @@ export async function callWithExchangeRate( api.tx.sudo.sudo(removeAllOraclesExtrinsic), api.events.sudo.Sudid ); - expect(txResult1.isCompleted, "Sudo event to remove authorized oracles not found").to.be.true; + try { + expect(txResult1.isCompleted).toBe(true); + } catch(_) { + throw Error("Sudo event to remove authorized oracles not found"); + } // Change Exchange rate storage for currency. const exchangeRateOracleKey = createExchangeRateOracleKey(api, currency); @@ -114,7 +117,7 @@ export async function callWithExchangeRate( api.tx.sudo.sudo(restoreAllOraclesExtrinsic), api.events.sudo.Sudid ); - expect(txResult2.isCompleted, "Sudo event to remove authorized oracles not found").to.be.true; + expect(txResult2.isCompleted).toBe(true); } return result; diff --git a/test/utils/jestSetupFileAfterEnv.ts b/test/utils/jestSetupFileAfterEnv.ts new file mode 100644 index 000000000..80d949c4c --- /dev/null +++ b/test/utils/jestSetupFileAfterEnv.ts @@ -0,0 +1,18 @@ +// This file exists to override jest specific setup defaults + +import console from "console"; + +/** + * Replace jest's expanded logger the "stock" console to reduce verbosity of logging. + * Needs `import console from "console"` to work. + */ +// global.console = console; + +/** + * Replace the logger with a silent spy, suppressing console.log outputs. + */ +global.console = { + ...console, + log: jest.fn(), + debug: jest.fn() +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0dc391854..301907bd6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,11 @@ { + "ts-node": { + "experimentalSpecifierResolution": "node" + }, "compilerOptions": { "skipLibCheck": true, - "target": "es6", - "module": "commonjs", + "target": "ES6", + "module": "ES6", "outDir": "build", "declaration": true, "strict": true,