From dde1d78280d5b16f05745b4915e0dbfc869ae7e8 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 28 Feb 2023 17:19:56 +0000 Subject: [PATCH 01/19] wip: TDD for lending-liquidator, upgrade to latest interbtc-api --- bots/bridge-tester/.env.local | 2 - bots/bridge-tester/package.json | 4 +- bots/bridge-tester/src/index.ts | 7 +- bots/bridge-tester/src/issue.ts | 27 +- bots/bridge-tester/src/redeem.ts | 24 +- bots/bridge-tester/tsconfig.json | 2 +- bots/lending-liquidator/package.json | 48 + bots/lending-liquidator/src/index.ts | 0 bots/lending-liquidator/test/config.ts | 7 + .../test/integration/liquidate.test.ts | 144 +++ bots/lending-liquidator/test/utils.ts | 26 + bots/lending-liquidator/tsconfig.json | 19 + tsconfig.json | 2 +- yarn.lock | 909 +++++++++--------- 14 files changed, 725 insertions(+), 496 deletions(-) create mode 100644 bots/lending-liquidator/package.json create mode 100644 bots/lending-liquidator/src/index.ts create mode 100644 bots/lending-liquidator/test/config.ts create mode 100644 bots/lending-liquidator/test/integration/liquidate.test.ts create mode 100644 bots/lending-liquidator/test/utils.ts create mode 100644 bots/lending-liquidator/tsconfig.json diff --git a/bots/bridge-tester/.env.local b/bots/bridge-tester/.env.local index c788d58..5407130 100644 --- a/bots/bridge-tester/.env.local +++ b/bots/bridge-tester/.env.local @@ -7,6 +7,4 @@ export BITCOIN_RPC_PORT="18443" export ISSUE_TOP_UP_AMOUNT="0.1" export REDEEM_ADDRESS="bcrt1qujs29q4gkyn2uj6y570xl460p4y43ruayxu8ry" export PARACHAIN_URL="ws://0.0.0.0:9944" -export STATS_URL="http://0.0.0.0:3007" -export FAUCET_URL="http://0.0.0.0:3036" export BITCOIN_RPC_WALLET="Alice" \ No newline at end of file diff --git a/bots/bridge-tester/package.json b/bots/bridge-tester/package.json index 82cfde9..4f11b3a 100644 --- a/bots/bridge-tester/package.json +++ b/bots/bridge-tester/package.json @@ -20,8 +20,8 @@ "test:integration": "mocha test/**/*.test.ts --timeout 10000000" }, "dependencies": { - "@interlay/interbtc-api": "1.8.3", - "@interlay/monetary-js": "0.5.3", + "@interlay/interbtc-api": "1.21.0", + "@interlay/monetary-js": "0.7.0", "@types/big.js": "6.1.2", "@types/node": "^14.14.31", "@types/underscore": "^1.11.2", diff --git a/bots/bridge-tester/src/index.ts b/bots/bridge-tester/src/index.ts index ab73ada..6bb3b78 100644 --- a/bots/bridge-tester/src/index.ts +++ b/bots/bridge-tester/src/index.ts @@ -2,6 +2,7 @@ import { BitcoinNetwork, createInterBtcApi, InterBtcApi, + newMonetaryAmount, sleep, } from "@interlay/interbtc-api"; import { KeyringPair } from "@polkadot/keyring/types"; @@ -11,8 +12,8 @@ import { cryptoWaitReady } from "@polkadot/util-crypto"; import { MS_IN_AN_HOUR } from "./consts"; import { Issue } from "./issue"; import { Redeem } from "./redeem"; -import { BitcoinAmount } from "@interlay/monetary-js"; import logger from "./logger"; +import { Bitcoin } from "@interlay/monetary-js"; const yargs = require("yargs/yargs"); const { hideBin } = require("yargs/helpers"); @@ -100,7 +101,7 @@ async function heartbeats( ); const redeem = new Redeem( interBtcApi, - BitcoinAmount.from.BTC(process.env.ISSUE_TOP_UP_AMOUNT) + newMonetaryAmount(process.env.ISSUE_TOP_UP_AMOUNT, Bitcoin, true) ); await redeem.performHeartbeatRedeems( account, @@ -151,7 +152,7 @@ async function main(inputFlag: InputFlag, requestWaitingTime: number) { } case InputFlag.heartbeats: { heartbeats(account, process.env.REDEEM_ADDRESS as string); - setInterval(heartbeats, requestWaitingTime, account); + setInterval(heartbeats, requestWaitingTime, account, process.env.REDEEM_ADDRESS as string); break; } } diff --git a/bots/bridge-tester/src/issue.ts b/bots/bridge-tester/src/issue.ts index 71e99be..28cdc15 100644 --- a/bots/bridge-tester/src/issue.ts +++ b/bots/bridge-tester/src/issue.ts @@ -5,13 +5,10 @@ import { BitcoinNetwork, InterbtcPrimitivesVaultId, WrappedCurrency, - CurrencyUnit, newMonetaryAmount, - getCorrespondingCollateralCurrency, - CollateralUnit, encodeVaultId, } from "@interlay/interbtc-api"; -import { BitcoinUnit, Currency, MonetaryAmount } from "@interlay/monetary-js"; +import { Currency, MonetaryAmount } from "@interlay/monetary-js"; import { KeyringPair } from "@polkadot/keyring/types"; import Big from "big.js"; import _ from "underscore"; @@ -23,7 +20,7 @@ import { sleep, waitForEmptyMempool } from "./utils"; export class Issue { interBtcApi: InterBtcApi; private redeemDustValue: - | MonetaryAmount + | MonetaryAmount | undefined; constructor(interBtc: InterBtcApi) { @@ -37,9 +34,7 @@ export class Issue { "AccountId", requester.address ); - const collateralCurrency = getCorrespondingCollateralCurrency( - this.interBtcApi.getGovernanceCurrency() - ) as Currency; + const collateralCurrency = this.interBtcApi.api.consts.currency.getRelayChainCurrencyId; const balance = await this.interBtcApi.tokens.balance( collateralCurrency, requesterAccountId @@ -69,7 +64,7 @@ export class Issue { async requestAndExecuteIssue( requester: KeyringPair, - amount: MonetaryAmount, + amount: MonetaryAmount, bitcoinCoreClient: BitcoinCoreClient, vaultId?: InterbtcPrimitivesVaultId ): Promise { @@ -91,7 +86,7 @@ export class Issue { } async getCachedRedeemDustValue(): Promise< - MonetaryAmount + MonetaryAmount > { if (!this.redeemDustValue) { this.redeemDustValue = await this.interBtcApi.redeem.getDustValue(); @@ -99,14 +94,14 @@ export class Issue { return this.redeemDustValue; } - increaseByFiftyPercent( - x: MonetaryAmount, U> - ): MonetaryAmount, U> { + increaseByFiftyPercent( + x: MonetaryAmount + ): MonetaryAmount { return x.mul(new Big(15)).div(new Big(10)); } async getAmountToIssue(): Promise< - MonetaryAmount + MonetaryAmount > { const redeemDustValue = await this.getCachedRedeemDustValue(); // We need to account for redeem fees to redeem later @@ -161,9 +156,9 @@ export class Issue { for (const vault of vaults) { try { logger.info( - `Issuing ${amountToIssue.toString(amountToIssue.currency.base)} ${ + `Issuing ${amountToIssue.toString(false)} ${ amountToIssue.currency.ticker - } with vault ID ${encodeVaultId(vault.id)}` + } with vault ID ${encodeVaultId(this.interBtcApi.assetRegistry, this.interBtcApi.loans, vault.id)}` ); this.requestAndExecuteIssue( account, diff --git a/bots/bridge-tester/src/redeem.ts b/bots/bridge-tester/src/redeem.ts index 3ac338e..e5512db 100644 --- a/bots/bridge-tester/src/redeem.ts +++ b/bots/bridge-tester/src/redeem.ts @@ -8,7 +8,7 @@ import { newMonetaryAmount, encodeVaultId, } from "@interlay/interbtc-api"; -import { BitcoinUnit, MonetaryAmount } from "@interlay/monetary-js"; +import { MonetaryAmount } from "@interlay/monetary-js"; import { KeyringPair } from "@polkadot/keyring/types"; import { H256 } from "@polkadot/types/interfaces"; import Big from "big.js"; @@ -22,20 +22,20 @@ export class Redeem { vaultHeartbeats = new Map(); issue: Issue; private redeemDustValue: - | MonetaryAmount + | MonetaryAmount | undefined; interBtc: InterBtcApi; expiredRedeemRequests: H256[] = []; constructor( interBtc: InterBtcApi, - private issueTopUpAmount: MonetaryAmount + private issueTopUpAmount: MonetaryAmount ) { this.issue = new Issue(interBtc); this.interBtc = interBtc; } async getCachedRedeemDustValue(): Promise< - MonetaryAmount + MonetaryAmount > { if (!this.redeemDustValue) { this.redeemDustValue = await this.interBtc.redeem.getDustValue(); @@ -44,14 +44,14 @@ export class Redeem { } increaseByThirtyPercent( - x: MonetaryAmount - ): MonetaryAmount { + x: MonetaryAmount + ): MonetaryAmount { return x.mul(new Big(13)).div(new Big(10)); } async getMinimumBalanceForHeartbeat( vaultCount?: number - ): Promise> { + ): Promise> { if (!this.interBtc.vaults) { logger.error("Parachain not connected"); return newMonetaryAmount(0, this.interBtc.getWrappedCurrency()); @@ -69,7 +69,7 @@ export class Redeem { } async getMinRedeemableAmount(): Promise< - MonetaryAmount + MonetaryAmount > { const redeemDustValue = await this.getCachedRedeemDustValue(); const bitcoinNetworkFees = @@ -228,6 +228,8 @@ export class Redeem { if (issuedTokens.gte(amountToRedeem)) { logger.info( `Redeeming ${amountToRedeem.toHuman()} out of ${issuedTokens.toHuman()} from vault ID ${encodeVaultId( + this.interBtc.assetRegistry, + this.interBtc.loans, vault.id )}` ); @@ -239,7 +241,11 @@ export class Redeem { logger.info( `Requested redeem: ${ requestResult.id - } from vault ID ${encodeVaultId(vault.id)}` + } from vault ID ${encodeVaultId( + this.interBtc.assetRegistry, + this.interBtc.loans, + vault.id + )}` ); // TODO: Uncomment once redeems are executed quickly by vaults. // const redeemRequestId = requestResult.id.toString(); diff --git a/bots/bridge-tester/tsconfig.json b/bots/bridge-tester/tsconfig.json index 4e0e8a0..6f329aa 100644 --- a/bots/bridge-tester/tsconfig.json +++ b/bots/bridge-tester/tsconfig.json @@ -8,7 +8,7 @@ "strict": true, "noImplicitAny": true, "esModuleInterop": true, - "baseUrl": "./bots", + "baseUrl": "./", "resolveJsonModule": true, // Generate files for debugging "sourceMap": true, diff --git a/bots/lending-liquidator/package.json b/bots/lending-liquidator/package.json new file mode 100644 index 0000000..9850ef1 --- /dev/null +++ b/bots/lending-liquidator/package.json @@ -0,0 +1,48 @@ + +{ + "name": "lending-liquidator", + "version": "0.0.0", + "description": "Bot for liquidating undercollateralized borrowers on the Interlay and Kintsugi networks", + "main": "build/index.js", + "typings": "build/index.d.ts", + "repository": "https://github.com/interlay/bots/lending-liquidator", + "author": "Interlay", + "license": "Apache-2.0", + "engines": { + "node": ">=11" + }, + "engineStrict": true, + + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node ./build/index.js", + "live": "ts-node src/index.ts", + "test": "run-s build test:*", + "test:integration": "mocha test/**/*.test.ts --timeout 10000000" + }, + "dependencies": { + "@interlay/interbtc-api": "1.21.0", + "@interlay/monetary-js": "0.7.0", + "@types/big.js": "6.1.2", + "@types/node": "^14.14.31", + "@types/underscore": "^1.11.2", + "@types/yargs": "^16.0.1", + "big.js": "6.1.1", + "bitcoinjs-lib": "^5.2.0", + "dotenv": "^10.0.0", + "npm-run-all": "^4.1.5", + "typescript": "^4.3.2", + "underscore": "^1.13.1", + "yargs": "^17.0.1", + "pino": "^7.5.1" + }, + "mocha": { + "reporter": "spec", + "require": "ts-node/register", + "watch-files": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "recursive": true + } +} \ No newline at end of file diff --git a/bots/lending-liquidator/src/index.ts b/bots/lending-liquidator/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/bots/lending-liquidator/test/config.ts b/bots/lending-liquidator/test/config.ts new file mode 100644 index 0000000..3553fe0 --- /dev/null +++ b/bots/lending-liquidator/test/config.ts @@ -0,0 +1,7 @@ +export const DEFAULT_PARACHAIN_ENDPOINT = "ws://127.0.0.1:9944"; +export const DEFAULT_SUDO_URI = "//Alice"; +export const DEFAULT_USER_1_URI = "//Dave"; +export const DEFAULT_USER_2_URI = "//Eve"; + +// approximate time per block in ms +export const APPROX_BLOCK_TIME_MS = 12 * 1000; diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts new file mode 100644 index 0000000..ee3e13f --- /dev/null +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -0,0 +1,144 @@ +import { createSubstrateAPI, CurrencyExt, DefaultInterBtcApi, DefaultTransactionAPI, InterBtcApi, InterbtcPrimitivesCurrencyId, newAccountId, newCurrencyId, LendToken, newMonetaryAmount, currencyIdToMonetaryCurrency } from "@interlay/interbtc-api"; +import { ApiPromise, Keyring } from "@polkadot/api"; +import { KeyringPair } from "@polkadot/keyring/types"; +import { AccountId } from "@polkadot/types/interfaces"; +import { expect } from "chai"; + +import { APPROX_BLOCK_TIME_MS, DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config"; +import { setExchangeRate } from "../utils"; + +describe("liquidate", () => { + const approx10Blocks = 10 * APPROX_BLOCK_TIME_MS; + let api: ApiPromise; + let keyring: Keyring; + let userInterBtcAPI: InterBtcApi; + let user2InterBtcAPI: InterBtcApi; + let sudoInterBtcAPI: InterBtcApi; + + let userAccount: KeyringPair; + let user2Account: KeyringPair; + let sudoAccount: KeyringPair; + let userAccountId: AccountId; + + let lendTokenId1: InterbtcPrimitivesCurrencyId; + let lendTokenId2: InterbtcPrimitivesCurrencyId; + let lendTokenId3: InterbtcPrimitivesCurrencyId; + let underlyingCurrencyId1: InterbtcPrimitivesCurrencyId; + let underlyingCurrency1: CurrencyExt; + let underlyingCurrencyId2: InterbtcPrimitivesCurrencyId; + let underlyingCurrency2: CurrencyExt; + let underlyingCurrencyId3: InterbtcPrimitivesCurrencyId; + let underlyingCurrency3: CurrencyExt; + + before(async function () { + this.timeout(approx10Blocks); + + api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); + keyring = new Keyring({ type: "sr25519" }); + userAccount = keyring.addFromUri(DEFAULT_USER_1_URI); + user2Account = keyring.addFromUri(DEFAULT_USER_2_URI); + userInterBtcAPI = new DefaultInterBtcApi(api, "regtest", userAccount); + user2InterBtcAPI = new DefaultInterBtcApi(api, "regtest", user2Account); + + sudoAccount = keyring.addFromUri(DEFAULT_SUDO_URI); + sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount); + userAccountId = newAccountId(api, userAccount.address); + lendTokenId1 = newCurrencyId(api, { lendToken: { id: 1 } } as LendToken); + lendTokenId2 = newCurrencyId(api, { lendToken: { id: 2 } } as LendToken); + lendTokenId3 = newCurrencyId(api, { lendToken: { id: 3 } } as LendToken); + + underlyingCurrencyId1 = api.consts.escrowRewards.getNativeCurrencyId; + currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId1); + underlyingCurrencyId2 = api.consts.currency.getRelayChainCurrencyId; + underlyingCurrencyId3 = api.consts.currency.getWrappedCurrencyId; + + 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( + underlyingCurrencyId1, + marketData(lendTokenId1) + ); + const addMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( + underlyingCurrencyId2, + marketData(lendTokenId2) + ); + const addMarket3Extrinsic = sudoInterBtcAPI.api.tx.loans.addMarket( + underlyingCurrencyId3, + marketData(lendTokenId3) + ); + const activateMarket1Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId1); + const activateMarket2Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId2); + const activateMarket3Extrinsic = sudoInterBtcAPI.api.tx.loans.activateMarket(underlyingCurrencyId3); + const addMarkets = sudoInterBtcAPI.api.tx.utility.batchAll([ + addMarket1Extrinsic, + addMarket2Extrinsic, + addMarket3Extrinsic, + activateMarket1Extrinsic, + activateMarket2Extrinsic, + activateMarket3Extrinsic, + ]); + + const [eventFound] = await Promise.all([ + DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.sudo.Sudid, approx10Blocks), + api.tx.sudo.sudo(addMarkets).signAndSend(sudoAccount), + ]); + expect( + eventFound, + `Sudo event to create new market not found - timed out after ${approx10Blocks} ms` + ).to.be.true; + }); + + after(async () => { + api.disconnect(); + }); + + it("should lend expected amount of currency to protocol", async function () { + this.timeout(approx10Blocks); + + const depositAmount = newMonetaryAmount(1000, underlyingCurrency1, true); + const borrowAmount1 = newMonetaryAmount(100, underlyingCurrency2, true); + const borrowAmount2 = newMonetaryAmount(100, underlyingCurrency3, true); + + await userInterBtcAPI.loans.lend(underlyingCurrency1, depositAmount); + await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency1); + + await userInterBtcAPI.loans.borrow(borrowAmount1.currency, borrowAmount1); + await userInterBtcAPI.loans.borrow(borrowAmount2.currency, borrowAmount2); + + // start liquidation listener + // TODO!: start the bot listener logic + const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.sudo.Sudid, approx10Blocks), + + // crash the collateral exchange rate + const newExchangeRate = "0x00000000000000000001000000000000"; + await setExchangeRate(sudoInterBtcAPI, depositAmount.currency, newExchangeRate); + + // expect liquidation event to happen + const liquidationOccured = await liquidationEventFoundPromise; + expect( + liquidationOccured, + `Expected the bot to have liquidated user ${userAccountId.toString()}, in collateral currency ${depositAmount.currency.ticker}` + ).to.be.true; + }); +}); diff --git a/bots/lending-liquidator/test/utils.ts b/bots/lending-liquidator/test/utils.ts new file mode 100644 index 0000000..15f2277 --- /dev/null +++ b/bots/lending-liquidator/test/utils.ts @@ -0,0 +1,26 @@ + +import { CurrencyExt, InterBtcApi, storageKeyToNthInner, getStorageMapItemKey, createExchangeRateOracleKey, setStorageAtKey } from "@interlay/interbtc-api"; + +export async function setExchangeRate( + sudoInterBtcAPI: InterBtcApi, + currency: CurrencyExt, + newExchangeRateHex: `0x${string}` +): Promise { + const { account: sudoAccount, api } = sudoInterBtcAPI; + if (!sudoAccount) { + throw new Error("callWithExchangeRate: sudo account is not set."); + } + // Remove authorized oracle to make sure price won't be fed. + const authorizedOracles = await api.query.oracle.authorizedOracles.entries(); + const authorizedOraclesAccountIds = authorizedOracles.map(([key]) => storageKeyToNthInner(key)); + const removeAllOraclesExtrinsic = api.tx.utility.batchAll( + authorizedOraclesAccountIds.map((accountId) => api.tx.oracle.removeAuthorizedOracle(accountId)) + ); + await api.tx.sudo.sudo(removeAllOraclesExtrinsic).signAndSend(sudoAccount); + + // Change Exchange rate storage for currency. + const exchangeRateOracleKey = createExchangeRateOracleKey(api, currency); + + const exchangeRateStorageKey = getStorageMapItemKey("Oracle", "Aggregate", exchangeRateOracleKey.toHex()); + await setStorageAtKey(sudoInterBtcAPI.api, exchangeRateStorageKey, newExchangeRateHex, sudoAccount); +} diff --git a/bots/lending-liquidator/tsconfig.json b/bots/lending-liquidator/tsconfig.json new file mode 100644 index 0000000..6f329aa --- /dev/null +++ b/bots/lending-liquidator/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "skipLibCheck": true, + "target": "es6", + "module": "commonjs", + "outDir": "build", + "declaration": true, + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "baseUrl": "./", + "resolveJsonModule": true, + // Generate files for debugging + "sourceMap": true, + "lib": ["ES2019"], + }, + "include": ["src"], + "exclude": ["./node_modules/*"] +} diff --git a/tsconfig.json b/tsconfig.json index 8662d3b..1de776c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,7 @@ "strict": true, "noImplicitAny": true, "esModuleInterop": true, - "baseUrl": "./bots", + "baseUrl": "./", "resolveJsonModule": true, // Generate files for debugging "sourceMap": true, diff --git a/yarn.lock b/yarn.lock index d89cdff..e37ab2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,12 +2,12 @@ # yarn lockfile v1 -"@babel/runtime@^7.17.0", "@babel/runtime@^7.17.8": - version "7.17.8" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.8.tgz#3e56e4aff81befa55ac3ac6a0967349fd1c5bca2" - integrity sha512-dQpEpK0O9o6lj6oPu0gRDbbnk+4LeHlNcBpspf6Olzt3GIX4P1lWF1gS+pHLDFlaJvbR6q7jCfQ08zA4QJBnmA== +"@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7": + version "7.21.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== dependencies: - regenerator-runtime "^0.13.4" + regenerator-runtime "^0.13.11" "@interlay/esplora-btc-api@0.4.0": version "0.4.0" @@ -16,16 +16,15 @@ dependencies: axios "^0.21.1" -"@interlay/interbtc-api@1.8.3": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-1.8.3.tgz#f1d86d9ff2a617b00fac80cd5ac22ea2cacb3a94" - integrity sha512-U9gU4nJ0A8Yp62EZsvvL/OefrfKMi4gpLU0+NLgbPeNMg+bPAuLe7bZaDxOtkrelO7olQPBHTVFeT6ZCtIln0g== +"@interlay/interbtc-api@1.21.0": + version "1.21.0" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-1.21.0.tgz#1b53c6794f1f729fcbd8e6aae0ef7098b72b07c7" + integrity sha512-sQ3rM+k+j1kwxithrgY3v9TESIKap04ihcm7C5bPtWUwwyEiBLRq273+hq/exfiUIYW1hapBX9quIxgjw3vkrA== dependencies: "@interlay/esplora-btc-api" "0.4.0" - "@interlay/interbtc-types" "1.5.10" - "@interlay/monetary-js" "0.5.3" - "@polkadot/api" "7.7.1" - "@types/big.js" "6.1.2" + "@interlay/interbtc-types" "1.11.2" + "@interlay/monetary-js" "0.7.0" + "@polkadot/api" "9.11.1" big.js "6.1.1" bitcoin-core "^3.0.0" bitcoinjs-lib "^5.2.0" @@ -33,376 +32,398 @@ cross-fetch "^3.0.6" isomorphic-fetch "^3.0.0" regtest-client "^0.2.0" - sinon "^9.0.3" - ts-mock-imports "^1.3.0" -"@interlay/interbtc-types@1.5.10": - version "1.5.10" - resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.5.10.tgz#8ff123e7886df06776e090deacca5c1b84ecf315" - integrity sha512-7pUzNKmNeHi74uJl9WBoYzwyKh8+X18wZz9MVDvx58SgkDiEjUpyeafjhcmVhJptaB+OwPJ1V76RDbJRxr0T4w== +"@interlay/interbtc-types@1.11.2": + version "1.11.2" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.11.2.tgz#e856b70bdc8ca8905ca841fc7181a60418959a91" + integrity sha512-38kEIPtn6xKQchA992sZvk43aCBBk0snYiBg3JRsrARiIOEd1v452f+QWHv7L7dxFAfUtOJaw5MHPNl6c46NYQ== -"@interlay/monetary-js@0.5.3": - version "0.5.3" - resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.5.3.tgz#57e7232394b0ddce483fe73d768b61ad0d3f2fae" - integrity sha512-d3RWoskAT+dWOOxD9ae0RKa6UeR7pFlk1ax7IAOIvrBMnvYOR2nGzVGGsbRhAkmJvk5oKErbUm6RIykH5f7R9A== +"@interlay/monetary-js@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.0.tgz#7142f7d2b86acf77d8524cee22b4ae2c5ba2d3a6" + integrity sha512-vxUcrl2Kum4W2bPt9Lz0WM/pPZEjOrwBtMEgNBAqBtBROXWeOi5jdwQeUwNSr2jqxv0g3V0mVyCVqOxy00iiQg== dependencies: "@types/big.js" "6.1.2" big.js "6.1.1" typescript "^4.3.2" -"@noble/hashes@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.0.0.tgz#d5e38bfbdaba174805a4e649f13be9a9ed3351ae" - integrity sha512-DZVbtY62kc3kkBtMHqwCOfXrT/hnoORy5BJ4+HU1IR59X0KWAOqsfzQPcUl/lQLlG7qXbe/fZ3r/emxtAl+sqg== - -"@noble/secp256k1@1.5.5": - version "1.5.5" - resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3" - integrity sha512-sZ1W6gQzYnu45wPrWx8D3kwI2/U29VYTx9OjbDAd7jwRItJ0cSTMPRL/C8AWZFn9kWFLQGqEXVEE86w4Z8LpIQ== - -"@polkadot/api-augment@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-7.7.1.tgz#8412a09db75da7993419284f4bc708421d321f03" - integrity sha512-M8c38haMh39K2pg3wRE4azpEr3hkaKV8dTlXG6whDmpDsPGTLa/wE9rCUzduRXTLpUZYBUCH529+bJ5LVwmSNQ== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/api-base" "7.7.1" - "@polkadot/rpc-augment" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/types-augment" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/util" "^8.3.3" - -"@polkadot/api-base@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-7.7.1.tgz#02e799a5e05474e1b008b4f706eb4ec0b793f5f4" - integrity sha512-gzgETQrgjMOqcQj7dwsiqVxmlUQEMH89ome0qargrJWiwbFUeceOlvt47E/WGG6/1oWUl3SMZcNRLYANME93ag== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/rpc-core" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/util" "^8.3.3" - rxjs "^7.5.2" - -"@polkadot/api-derive@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-7.7.1.tgz#2ede88baac97b38ccca86575b13cd284a399e442" - integrity sha512-1dBj+vtVk+XAcvwC6BE+eUdVnbcBteWDXzBxrIuzT8NlkAReIq+74+bOEZH7GDcGMorTR5s+Z3FZD1ElmUiUNQ== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/api" "7.7.1" - "@polkadot/api-augment" "7.7.1" - "@polkadot/api-base" "7.7.1" - "@polkadot/rpc-core" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/util" "^8.3.3" - "@polkadot/util-crypto" "^8.3.3" - rxjs "^7.5.2" - -"@polkadot/api@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-7.7.1.tgz#a4a1738473005bcde0f21239ba08ae22e3d548f4" - integrity sha512-wWwtVgRz1Lc7HY0evRnzKZNaRBIo818V16fZ+S73zfIMxvDmO2zbfui9scIlpi4WwL2K3YQbznOwBlWeHH92LA== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/api-augment" "7.7.1" - "@polkadot/api-base" "7.7.1" - "@polkadot/api-derive" "7.7.1" - "@polkadot/keyring" "^8.3.3" - "@polkadot/rpc-augment" "7.7.1" - "@polkadot/rpc-core" "7.7.1" - "@polkadot/rpc-provider" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/types-augment" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/types-create" "7.7.1" - "@polkadot/types-known" "7.7.1" - "@polkadot/util" "^8.3.3" - "@polkadot/util-crypto" "^8.3.3" +"@noble/hashes@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" + integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== + +"@noble/secp256k1@1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" + integrity sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw== + +"@polkadot/api-augment@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-augment/-/api-augment-9.11.1.tgz#1d1fff15e256f5a5c80047db9b56e839c7af0515" + integrity sha512-heQFPjliNAOPNkHu01Fm+8i8aNynL7JUfHrfkbb1zTFAW6dN2x3SfrsaV7UpzJCK2/aghY9LOpayehZvFSy3og== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/api-base" "9.11.1" + "@polkadot/rpc-augment" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/types-augment" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/util" "^10.2.3" + +"@polkadot/api-base@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-base/-/api-base-9.11.1.tgz#f9240200a736c929e9b99d8e735c67bc21c175d9" + integrity sha512-4z4ttJKD3mPD/khPjr3CtolxtV+gbXJtSJLMMFIoNkLd3TnC7cqzerWJoLnQatPQMWgyH9byXGqnAgYaJlOiiQ== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/rpc-core" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/util" "^10.2.3" + rxjs "^7.8.0" + +"@polkadot/api-derive@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-9.11.1.tgz#c03f0668d9d4fe3a3b5bf3c6a329cb83b427fd30" + integrity sha512-xQRNBvciqEgW3TB3XJlYkL8NgoUEI/fYlEVIbkm+CjS5+M/p+GFm+Reog0rwSIip8JeGL5OTOQVOt4GL99rb2A== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/api" "9.11.1" + "@polkadot/api-augment" "9.11.1" + "@polkadot/api-base" "9.11.1" + "@polkadot/rpc-core" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/util" "^10.2.3" + "@polkadot/util-crypto" "^10.2.3" + rxjs "^7.8.0" + +"@polkadot/api@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-9.11.1.tgz#e39353e70c10baf3af70798e1cecfee4126c766d" + integrity sha512-g1xpXpwtxQ1nlvESmolxVqXQmRq6FbGrVZmhA9w4EvkEV1PXUbngz/yCJi52RlGeYNRhv6d7SwvlWLHm80Fu9Q== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/api-augment" "9.11.1" + "@polkadot/api-base" "9.11.1" + "@polkadot/api-derive" "9.11.1" + "@polkadot/keyring" "^10.2.3" + "@polkadot/rpc-augment" "9.11.1" + "@polkadot/rpc-core" "9.11.1" + "@polkadot/rpc-provider" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/types-augment" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/types-create" "9.11.1" + "@polkadot/types-known" "9.11.1" + "@polkadot/util" "^10.2.3" + "@polkadot/util-crypto" "^10.2.3" eventemitter3 "^4.0.7" - rxjs "^7.5.2" - -"@polkadot/keyring@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-8.6.1.tgz#ab1d4e31268317daabf7fe0d951254b770ba194c" - integrity sha512-Ra3L6JwMMnlnmpGIOanMT7XqWtqp2gXpGEmA6eE9bL9xUo6e3RPudFncYrWizUjzbYXnaO4bFExRcp2RIBuvWA== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/util" "8.6.1" - "@polkadot/util-crypto" "8.6.1" - -"@polkadot/networks@8.6.1", "@polkadot/networks@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-8.6.1.tgz#dd3f51a9f861ed9c81da8a87845fb1f152f55255" - integrity sha512-NG59Oc6KqbGbQiF6nb8uD16VVAgMFcAI5RVJWE5yN4dwh3eVdKemIezSii5Pc9sxBUP/SrBDf6KAI84TmsXUUA== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/util" "8.6.1" - "@substrate/ss58-registry" "^1.17.0" - -"@polkadot/rpc-augment@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-7.7.1.tgz#2e4ff7a77c4839fe6d3fb4e26c6c9fa689f72d40" - integrity sha512-Hwa6qjxHSuzjyYxa2gOpIoN0PWhWDLEOxpHFZxwNoCkmGtcIpNwWOtK1chZWEl9Zd1Yq+JLIEoimNN+2ok/3cA== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/rpc-core" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/util" "^8.3.3" - -"@polkadot/rpc-core@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-7.7.1.tgz#a51adeaebced83515354a26bb28676f0bb11f8d9" - integrity sha512-vJuuTbmGKEcjJY3JIjMirLESVjnHBGajtTaLtlcwBcp8Jgj/fBi+FoL3gcpZS4BR87/qVe35aoNO2hDhU9DUJA== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/rpc-augment" "7.7.1" - "@polkadot/rpc-provider" "7.7.1" - "@polkadot/types" "7.7.1" - "@polkadot/util" "^8.3.3" - rxjs "^7.5.2" - -"@polkadot/rpc-provider@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-7.7.1.tgz#51dd5f941ec8029af91139b0d60e43f1ca387f14" - integrity sha512-PPAMjQgV4pA/l+L+mk93dvmzwMA2tZSzVf0DjsldD0vi5Oydo0Sw8I4YEsueTnHme0PadIZXUzWEonG81klAlw== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/keyring" "^8.3.3" - "@polkadot/types" "7.7.1" - "@polkadot/types-support" "7.7.1" - "@polkadot/util" "^8.3.3" - "@polkadot/util-crypto" "^8.3.3" - "@polkadot/x-fetch" "^8.3.3" - "@polkadot/x-global" "^8.3.3" - "@polkadot/x-ws" "^8.3.3" + rxjs "^7.8.0" + +"@polkadot/keyring@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-10.4.2.tgz#793377fdb9076df0af771df11388faa6be03c70d" + integrity sha512-7iHhJuXaHrRTG6cJDbZE9G+c1ts1dujp0qbO4RfAPmT7YUvphHvAtCKueN9UKPz5+TYDL+rP/jDEaSKU8jl/qQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/util" "10.4.2" + "@polkadot/util-crypto" "10.4.2" + +"@polkadot/networks@10.4.2", "@polkadot/networks@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-10.4.2.tgz#d7878c6aad8173c800a21140bfe5459261724456" + integrity sha512-FAh/znrEvWBiA/LbcT5GXHsCFUl//y9KqxLghSr/CreAmAergiJNT0MVUezC7Y36nkATgmsr4ylFwIxhVtuuCw== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/util" "10.4.2" + "@substrate/ss58-registry" "^1.38.0" + +"@polkadot/rpc-augment@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-augment/-/rpc-augment-9.11.1.tgz#2f2398fd8abcf3dc0ef4b39f98f67d4a33c312f8" + integrity sha512-LZU73uSlsv8qwq6LWxv+jmkbvBnFowfoX3Q4SRrjhOTzvo4Z62CkH/fnBsKE5FwB+baJmsL1eU6H/V8VtHoHJA== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/rpc-core" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/util" "^10.2.3" + +"@polkadot/rpc-core@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-9.11.1.tgz#9d957b58edfdf2c24e445b5843205b77fca67132" + integrity sha512-OnV9Vms7CfgAvLWKm6uZCnoUDj75k2E/JaguOC2AeII4LzA4CrM2CRgLrAiaiQIQivupPxr7GjdempWdGSB/6Q== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/rpc-augment" "9.11.1" + "@polkadot/rpc-provider" "9.11.1" + "@polkadot/types" "9.11.1" + "@polkadot/util" "^10.2.3" + rxjs "^7.8.0" + +"@polkadot/rpc-provider@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-9.11.1.tgz#e09b5d24dd4b4edcfe82efc64889bec8fb8ab5db" + integrity sha512-KjUGi9yPaMb00HZVTTtjv/zQBqh8Uf6vHOEQjwM8gZi51qIgs5MMJRyiv0yFnkPtu1wNsw12SI1py0rpQT2NOA== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/keyring" "^10.2.3" + "@polkadot/types" "9.11.1" + "@polkadot/types-support" "9.11.1" + "@polkadot/util" "^10.2.3" + "@polkadot/util-crypto" "^10.2.3" + "@polkadot/x-fetch" "^10.2.3" + "@polkadot/x-global" "^10.2.3" + "@polkadot/x-ws" "^10.2.3" eventemitter3 "^4.0.7" - mock-socket "^9.1.2" - nock "^13.2.4" - -"@polkadot/types-augment@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-7.7.1.tgz#8c180c31e4cb1ca17232c1ca606185a7d8205389" - integrity sha512-NsPJgD85UUIOao2rtY9jCzt2+MdZsn2qhBSphnqQqS3yOQKoonaHojCsRKt39raQ0UNST+At5zL1+HdcKP53EQ== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/types" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/util" "^8.3.3" - -"@polkadot/types-codec@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-7.7.1.tgz#d1ad3343d67449b79d2ccde113662ec0d80bb53b" - integrity sha512-NqxQGpfagc+51TGBo8lrmEJox3cZwx6lSeoPnGUqQBcOhqjaJ8EPEAaFHc0TTc22/sLs1qYqBc+cz+9UXKAEOg== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/util" "^8.3.3" - -"@polkadot/types-create@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-7.7.1.tgz#28b9bd3858845626c8ddb00b4b12bb5945a82fdc" - integrity sha512-T7iyu0u1Ji3ErA3YcY+WuI83TMcnvhTYJXs+OmGksWa64MQAiWs2pkYNmS4ieArUW3vsel5nv9noBzyl2MAocQ== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/types-codec" "7.7.1" - "@polkadot/util" "^8.3.3" - -"@polkadot/types-known@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-7.7.1.tgz#dbf621f03ba37b5c693a5b2193056c3bbb5a386b" - integrity sha512-NtRPOwHwKCJhzYGBWq/8n9pPalWY3AAx9jNE3TXAfxOCwVelZaXhC9TZ8iXkzZUixG1pJ+5KInXbjDeGfGpi/g== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/networks" "^8.3.3" - "@polkadot/types" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/types-create" "7.7.1" - "@polkadot/util" "^8.3.3" - -"@polkadot/types-support@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-7.7.1.tgz#bbd36a00a75c68cb332bd489a229144b0df55cfc" - integrity sha512-JQmplnNGML43sE+dArlnzEbDMvpo0TkOMvpM8VflThjabG1MhCFuZdiOSm0WQ/iz4wm+3KihOKGzR6TPpGMKOA== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/util" "^8.3.3" - -"@polkadot/types@7.7.1": - version "7.7.1" - resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-7.7.1.tgz#1e0b5ef0e94bd64ff3e1d6a39ff92d421be877fa" - integrity sha512-koJfoKvSiwpZKkHZrOpelb+JxCf+0iGzVOt0hj0plOK5oOOSJd0Elpy1dc8QQEJUC3V/pNdAGTsV6dvqi8rnAg== - dependencies: - "@babel/runtime" "^7.17.0" - "@polkadot/keyring" "^8.3.3" - "@polkadot/types-augment" "7.7.1" - "@polkadot/types-codec" "7.7.1" - "@polkadot/types-create" "7.7.1" - "@polkadot/util" "^8.3.3" - "@polkadot/util-crypto" "^8.3.3" - rxjs "^7.5.2" - -"@polkadot/util-crypto@8.6.1", "@polkadot/util-crypto@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-8.6.1.tgz#b030598931c061b3ef7584524dab3dab3e97b1cd" - integrity sha512-+eAKZM7aUIPKLuVl8C2UYauZ0kza0Lq3n3TkVS32bjXmS/Lo+qeMqj8jBSLD8a5BApDUiCL1j+L9cA/wq8pvIQ== - dependencies: - "@babel/runtime" "^7.17.8" - "@noble/hashes" "1.0.0" - "@noble/secp256k1" "1.5.5" - "@polkadot/networks" "8.6.1" - "@polkadot/util" "8.6.1" - "@polkadot/wasm-crypto" "^5.0.1" - "@polkadot/x-bigint" "8.6.1" - "@polkadot/x-randomvalues" "8.6.1" - "@scure/base" "1.0.0" + mock-socket "^9.1.5" + nock "^13.2.9" + optionalDependencies: + "@substrate/connect" "0.7.18" + +"@polkadot/types-augment@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-augment/-/types-augment-9.11.1.tgz#40c493b797a4f1a653166ee3068f28153f94b021" + integrity sha512-SKafiDAfh+hDY6fz9fkuXa37/aqb+UMNv26FTfAscJJFrKxAG4QSXbcxyvWefGvMB2S7gJ59e3D33FfsuuySJA== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/types" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/util" "^10.2.3" + +"@polkadot/types-codec@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-codec/-/types-codec-9.11.1.tgz#f5463d42fea858b48ff2c5716b077f62fc5ef133" + integrity sha512-RrY7+E9SapXsAjnARQqBIsCrPrCZrw3sfvAYCPGULB56j0HyX+Dut/45QBrWUiITeLdbUsbSAP5bLkQoUZOANQ== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/util" "^10.2.3" + "@polkadot/x-bigint" "^10.2.3" + +"@polkadot/types-create@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-create/-/types-create-9.11.1.tgz#73e9438c54ddda95189bbb348a6734eaa053007d" + integrity sha512-y8E7rx5ZJNWtPKrrUVT7s79i+ehffMNV95DTEHtSVizFfYVXEYZuOI0/AqK2vqR93uolth18GxbcRCRZwZGFow== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/types-codec" "9.11.1" + "@polkadot/util" "^10.2.3" + +"@polkadot/types-known@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-known/-/types-known-9.11.1.tgz#82665c57aeda2ec00bc7e6a1cf0e5a642b0665aa" + integrity sha512-DZOXhQ5ST565FijTQn4T5GglsD/g2lnOrgylCHG/U00ppN/5GJYedQZU5qQ8t4uhdBj2EVlkB7PflRNZqfkJ3w== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/networks" "^10.2.3" + "@polkadot/types" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/types-create" "9.11.1" + "@polkadot/util" "^10.2.3" + +"@polkadot/types-support@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types-support/-/types-support-9.11.1.tgz#0ed6f173568df191bfc9d3b72e77574082e3cedb" + integrity sha512-Z6NdqqLxezZBIYmNVVETwKo5r8WCCnvEzd0p/AxVGRsfkQ/Y82+GbynEgczMG7N8/cnWE0AOpDtONlVIodVHMw== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/util" "^10.2.3" + +"@polkadot/types@9.11.1": + version "9.11.1" + resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-9.11.1.tgz#d889be948baba1db0032af2a1b8d94777281dd1b" + integrity sha512-3uo4eoNtqjxqRudyNzGEVhQnvkAdqy5iOJZVsEKXP9/eyRupWbAHsZGxmYBuvmRIL+6Bucv/rnd4/YSTAJH7VQ== + dependencies: + "@babel/runtime" "^7.20.7" + "@polkadot/keyring" "^10.2.3" + "@polkadot/types-augment" "9.11.1" + "@polkadot/types-codec" "9.11.1" + "@polkadot/types-create" "9.11.1" + "@polkadot/util" "^10.2.3" + "@polkadot/util-crypto" "^10.2.3" + rxjs "^7.8.0" + +"@polkadot/util-crypto@10.4.2", "@polkadot/util-crypto@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-10.4.2.tgz#871fb69c65768bd48c57bb5c1f76a85d979fb8b5" + integrity sha512-RxZvF7C4+EF3fzQv8hZOLrYCBq5+wA+2LWv98nECkroChY3C2ZZvyWDqn8+aonNULt4dCVTWDZM0QIY6y4LUAQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@noble/hashes" "1.2.0" + "@noble/secp256k1" "1.7.1" + "@polkadot/networks" "10.4.2" + "@polkadot/util" "10.4.2" + "@polkadot/wasm-crypto" "^6.4.1" + "@polkadot/x-bigint" "10.4.2" + "@polkadot/x-randomvalues" "10.4.2" + "@scure/base" "1.1.1" ed2curve "^0.3.0" tweetnacl "^1.0.3" -"@polkadot/util@8.6.1", "@polkadot/util@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-8.6.1.tgz#48216218ed643401e66512579f95223cac15037b" - integrity sha512-/I0DDo/32wUH6A5qC7SPSSpzHfgZUaRkmWIzovCsIddY389S+C7lI1Ow/A0vhr1bHWrI0L5sV5IJfNpUOQfaSQ== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-bigint" "8.6.1" - "@polkadot/x-global" "8.6.1" - "@polkadot/x-textdecoder" "8.6.1" - "@polkadot/x-textencoder" "8.6.1" - "@types/bn.js" "^5.1.0" - bn.js "^5.2.0" - ip-regex "^4.3.0" - -"@polkadot/wasm-crypto-asmjs@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-5.0.1.tgz#0b9f5018c6f3b8f7ba1a6084ac5fc0c9dc951423" - integrity sha512-AskQIC/Eu7kD8E2lV2I0nx5dCOJc+V5zitzDsGPHvIx9mtm87rjSHbR/pGNu2NwVxJhVMEoFjGxKA7KSwrefhA== - dependencies: - "@babel/runtime" "^7.17.8" - -"@polkadot/wasm-crypto-wasm@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-5.0.1.tgz#9c011e8c6c28b9cce2994cdd5743f8720a912fff" - integrity sha512-X0najgKPvsB3Jmx1+4EuS1c9ra9YN9m+Eo7CpQmRP3LLdcV0AfRd7PT4B8po5lZ2/a3ftTdkxOxnsqdvjb5qwA== - dependencies: - "@babel/runtime" "^7.17.8" - -"@polkadot/wasm-crypto@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-5.0.1.tgz#e9e95b1147db341e2765f34639ea686be2d2cb30" - integrity sha512-/FzetOeC4rPKt5LO3CHPxtbsk+ps9BE19KkbfoRYOphPNgRd/Rq9in0BuGwsCWayfDGdhkYq+wxixPRZQ5A8LQ== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/wasm-crypto-asmjs" "^5.0.1" - "@polkadot/wasm-crypto-wasm" "^5.0.1" - -"@polkadot/x-bigint@8.6.1": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-8.6.1.tgz#0d404307faab90f348d97dd181e0275067bf77e9" - integrity sha512-k8FiamE5t8WSSVGaFTMdpkyCwvpq8rq0FfGq82bZabkzmEps1lIj8h5nNwwsPHleO55LtVIMYvZZ4VR3fth+xA== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" - -"@polkadot/x-fetch@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-8.6.1.tgz#c664801082ec129207b8b673b0346bf4e561f446" - integrity sha512-NBZi4MMfeach/tUFQCG0VXGsdbGaFsCWGabalYAeHRweERmh0kE0r6AECdp3YSQJTl7eVTiMX7gr1qZLUDgfcg== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" - "@types/node-fetch" "^2.6.1" - node-fetch "^2.6.7" - -"@polkadot/x-global@8.6.1", "@polkadot/x-global@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-8.6.1.tgz#0e7ba5238c20e0ed6c75b8e9b916cb8ca2c950aa" - integrity sha512-08ePLMEh8MVNk+jvl82L/CRwfIutwbpziC1XhgM+/iJShac7kEqI1ZL6F91sHp6+mFLm8OtyL4NJuPxglchppQ== - dependencies: - "@babel/runtime" "^7.17.8" - -"@polkadot/x-randomvalues@8.6.1": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-8.6.1.tgz#526bf55b3eb504efd4f8ed4c8d6dead33b2db0c2" - integrity sha512-F0mQ4D8IOMKftvZIaAPgthhg93C/wfjfgRvaxg8dsGz34+xBNGopHvzG0o7AbBmI2DBgzXd1nQrokYTwv7OfhA== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" - -"@polkadot/x-textdecoder@8.6.1": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-8.6.1.tgz#2bddda36d541d491b608098aad7c844fed384685" - integrity sha512-AIVZRpujjlzSqr51XW0JnX9O3K753R7YnPxseztHwqAREvY1HP7Jm9hDBk415qf8IcB2Ttm1fzpwphKu6/nTwA== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" - -"@polkadot/x-textencoder@8.6.1": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-8.6.1.tgz#d7f177194184784a78ca85b0a79f0f2684da9b38" - integrity sha512-16Poz8vpHgMDe7QeQ1J49P547okfWUO/CfHLVz9GRgz4SVFPqFGmZ2bfQlWI9TfaYlyw7MjGW5Qq3VijO2vBbw== - dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" - -"@polkadot/x-ws@^8.3.3": - version "8.6.1" - resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-8.6.1.tgz#b5e22ac6ac7cf065b9d5f21656595e1eb816dada" - integrity sha512-h5XZxb5h0SEqdtEIYf7kAta0fnOgGVJ0nsxiqI/KKLso2HA1AsZLvj1t9eQbn1FIyFSgA50mlA++iJQNgX9kvA== +"@polkadot/util@10.4.2", "@polkadot/util@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-10.4.2.tgz#df41805cb27f46b2b4dad24c371fa2a68761baa1" + integrity sha512-0r5MGICYiaCdWnx+7Axlpvzisy/bi1wZGXgCSw5+ZTyPTOqvsYRqM2X879yxvMsGfibxzWqNzaiVjToz1jvUaA== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-bigint" "10.4.2" + "@polkadot/x-global" "10.4.2" + "@polkadot/x-textdecoder" "10.4.2" + "@polkadot/x-textencoder" "10.4.2" + "@types/bn.js" "^5.1.1" + bn.js "^5.2.1" + +"@polkadot/wasm-bridge@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-6.4.1.tgz#e97915dd67ba543ec3381299c2a5b9330686e27e" + integrity sha512-QZDvz6dsUlbYsaMV5biZgZWkYH9BC5AfhT0f0/knv8+LrbAoQdP3Asbvddw8vyU9sbpuCHXrd4bDLBwUCRfrBQ== + dependencies: + "@babel/runtime" "^7.20.6" + +"@polkadot/wasm-crypto-asmjs@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-6.4.1.tgz#3cc76bbda5ea4a7a860982c64f9565907b312253" + integrity sha512-UxZTwuBZlnODGIQdCsE2Sn/jU0O2xrNQ/TkhRFELfkZXEXTNu4lw6NpaKq7Iey4L+wKd8h4lT3VPVkMcPBLOvA== + dependencies: + "@babel/runtime" "^7.20.6" + +"@polkadot/wasm-crypto-init@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-6.4.1.tgz#4d9ab0030db52cf177bf707ef8e77aa4ca721668" + integrity sha512-1ALagSi/nfkyFaH6JDYfy/QbicVbSn99K8PV9rctDUfxc7P06R7CoqbjGQ4OMPX6w1WYVPU7B4jPHGLYBlVuMw== + dependencies: + "@babel/runtime" "^7.20.6" + "@polkadot/wasm-bridge" "6.4.1" + "@polkadot/wasm-crypto-asmjs" "6.4.1" + "@polkadot/wasm-crypto-wasm" "6.4.1" + +"@polkadot/wasm-crypto-wasm@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-6.4.1.tgz#97180f80583b18f6a13c1054fa5f7e8da40b1028" + integrity sha512-3VV9ZGzh0ZY3SmkkSw+0TRXxIpiO0nB8lFwlRgcwaCihwrvLfRnH9GI8WE12mKsHVjWTEVR3ogzILJxccAUjDA== + dependencies: + "@babel/runtime" "^7.20.6" + "@polkadot/wasm-util" "6.4.1" + +"@polkadot/wasm-crypto@^6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-6.4.1.tgz#79310e23ad1ca62362ba893db6a8567154c2536a" + integrity sha512-FH+dcDPdhSLJvwL0pMLtn/LIPd62QDPODZRCmDyw+pFjLOMaRBc7raomWUOqyRWJTnqVf/iscc2rLVLNMyt7ag== + dependencies: + "@babel/runtime" "^7.20.6" + "@polkadot/wasm-bridge" "6.4.1" + "@polkadot/wasm-crypto-asmjs" "6.4.1" + "@polkadot/wasm-crypto-init" "6.4.1" + "@polkadot/wasm-crypto-wasm" "6.4.1" + "@polkadot/wasm-util" "6.4.1" + +"@polkadot/wasm-util@6.4.1": + version "6.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-6.4.1.tgz#74aecc85bec427a9225d9874685944ea3dc3ab76" + integrity sha512-Uwo+WpEsDmFExWC5kTNvsVhvqXMZEKf4gUHXFn4c6Xz4lmieRT5g+1bO1KJ21pl4msuIgdV3Bksfs/oiqMFqlw== + dependencies: + "@babel/runtime" "^7.20.6" + +"@polkadot/x-bigint@10.4.2", "@polkadot/x-bigint@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-10.4.2.tgz#7eb2ec732259df48b5a00f07879a1331e05606ec" + integrity sha512-awRiox+/XSReLzimAU94fPldowiwnnMUkQJe8AebYhNocAj6SJU00GNoj6j6tAho6yleOwrTJXZaWFBaQVJQNg== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" + +"@polkadot/x-fetch@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-fetch/-/x-fetch-10.4.2.tgz#bc6ba70de71a252472fbe36180511ed920e05f05" + integrity sha512-Ubb64yaM4qwhogNP+4mZ3ibRghEg5UuCYRMNaCFoPgNAY8tQXuDKrHzeks3+frlmeH9YRd89o8wXLtWouwZIcw== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" + "@types/node-fetch" "^2.6.2" + node-fetch "^3.3.0" + +"@polkadot/x-global@10.4.2", "@polkadot/x-global@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-10.4.2.tgz#5662366e3deda0b4c8f024b2d902fa838f9e60a4" + integrity sha512-g6GXHD/ykZvHap3M6wh19dO70Zm43l4jEhlxf5LtTo5/0/UporFCXr2YJYZqfbn9JbQwl1AU+NroYio+vtJdiA== + dependencies: + "@babel/runtime" "^7.20.13" + +"@polkadot/x-randomvalues@10.4.2": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-10.4.2.tgz#895f1220d5a4522a83d8d5014e3c1e03b129893e" + integrity sha512-mf1Wbpe7pRZHO0V3V89isPLqZOy5XGX2bCqsfUWHgb1NvV1MMx5TjVjdaYyNlGTiOkAmJKlOHshcfPU2sYWpNg== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" + +"@polkadot/x-textdecoder@10.4.2": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-10.4.2.tgz#93202f3e5ad0e7f75a3fa02d2b8a3343091b341b" + integrity sha512-d3ADduOKUTU+cliz839+KCFmi23pxTlabH7qh7Vs1GZQvXOELWdqFOqakdiAjtMn68n1KVF4O14Y+OUm7gp/zA== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" + +"@polkadot/x-textencoder@10.4.2": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-10.4.2.tgz#cd2e6c8a66b0b400a73f0164e99c510fb5c83501" + integrity sha512-mxcQuA1exnyv74Kasl5vxBq01QwckG088lYjc3KwmND6+pPrW2OWagbxFX5VFoDLDAE+UJtnUHsjdWyOTDhpQA== + dependencies: + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" + +"@polkadot/x-ws@^10.2.3": + version "10.4.2" + resolved "https://registry.yarnpkg.com/@polkadot/x-ws/-/x-ws-10.4.2.tgz#4e9d88f37717570ccf942c6f4f63b06260f45025" + integrity sha512-3gHSTXAWQu1EMcMVTF5QDKHhEHzKxhAArweEyDXE7VsgKUP/ixxw4hVZBrkX122iI5l5mjSiooRSnp/Zl3xqDQ== dependencies: - "@babel/runtime" "^7.17.8" - "@polkadot/x-global" "8.6.1" + "@babel/runtime" "^7.20.13" + "@polkadot/x-global" "10.4.2" "@types/websocket" "^1.0.5" websocket "^1.0.34" -"@scure/base@1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.0.0.tgz#109fb595021de285f05a7db6806f2f48296fcee7" - integrity sha512-gIVaYhUsy+9s58m/ETjSJVKHhKTBMmcRb9cEV5/5dwvfDlfORjKrFsDeDHWRrm6RjcPvCLZFwGJjAjLj1gg4HA== +"@scure/base@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" + integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== -"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.1": - version "1.8.3" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d" - integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== - dependencies: - type-detect "4.0.8" +"@substrate/connect-extension-protocol@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@substrate/connect-extension-protocol/-/connect-extension-protocol-1.0.1.tgz#fa5738039586c648013caa6a0c95c43265dbe77d" + integrity sha512-161JhCC1csjH3GE5mPLEd7HbWtwNSPJBg3p1Ksz9SFlTzj/bgEwudiRN2y5i0MoLGCIJRYKyKGMxVnd29PzNjg== -"@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" - integrity sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA== +"@substrate/connect@0.7.18": + version "0.7.18" + resolved "https://registry.yarnpkg.com/@substrate/connect/-/connect-0.7.18.tgz#ed4a95a4f5f60132dd26dbaf98820044b622763e" + integrity sha512-T1CaZJhe+uaeyM/cBdmD/oMWnaGf+tJdfG+3Os4H5YR0NVKXWsHpSfBryBP5wEce2hQhRiNnzQ+9ny8siKqRgg== dependencies: - "@sinonjs/commons" "^1.7.0" + "@substrate/connect-extension-protocol" "^1.0.1" + "@substrate/smoldot-light" "0.7.9" + eventemitter3 "^4.0.7" -"@sinonjs/samsam@^5.3.1": - version "5.3.1" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-5.3.1.tgz#375a45fe6ed4e92fca2fb920e007c48232a6507f" - integrity sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg== +"@substrate/smoldot-light@0.7.9": + version "0.7.9" + resolved "https://registry.yarnpkg.com/@substrate/smoldot-light/-/smoldot-light-0.7.9.tgz#68449873a25558e547e9468289686ee228a9930f" + integrity sha512-HP8iP7sFYlpSgjjbo0lqHyU+gu9lL2hbDNce6dWk5/10mFFF9jKIFGfui4zCecUY808o/Go9pan/31kMJoLbug== dependencies: - "@sinonjs/commons" "^1.6.0" - lodash.get "^4.4.2" - type-detect "^4.0.8" - -"@sinonjs/text-encoding@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" - integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== + pako "^2.0.4" + ws "^8.8.1" -"@substrate/ss58-registry@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.17.0.tgz#a6a50dbef67da0114aff7cdae7c6eec685c5983b" - integrity sha512-YdQOxCtEZLnYZFg/zSzfROYtvIs5+iLD7p/VHoll7AVEhrPAmxgF5ggMDB2Dass7dfwABVx7heATbPFNg95Q8w== +"@substrate/ss58-registry@^1.38.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.39.0.tgz#eb916ff5fea7fa02e77745823fde21af979273d2" + integrity sha512-qZYpuE6n+mwew+X71dOur/CbMXj6rNW27o63JeJwdQH/GvcSKm3JLNhd+bGzwUKg0D/zD30Qc6p4JykArzM+tA== "@types/big.js@6.1.2": version "6.1.2" resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.1.2.tgz#68a952b629a6aaa2b5855a2f63363d1e77f6dd91" integrity sha512-h24JIZ52rvSvi2jkpYDk2yLH99VzZoCJiSfDWwjst7TwJVuXN61XVCUlPCzRl7mxKEMsGf8z42Q+J4TZwU3z2w== -"@types/bn.js@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68" - integrity sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA== +"@types/bn.js@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.1.tgz#b51e1b55920a4ca26e9285ff79936bbdec910682" + integrity sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g== dependencies: "@types/node" "*" @@ -429,10 +450,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.3.tgz#bbeb55fbc73f28ea6de601fbfa4613f58d785323" integrity sha512-ekGvFhFgrc2zYQoX4JeZPmVzZxw6Dtllga7iGHzfbYIYkAMUx/sAFP2GdFpLff+vdHXu5fl7WX9AT+TtqYcsyw== -"@types/node-fetch@^2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975" - integrity sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA== +"@types/node-fetch@^2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" + integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== dependencies: "@types/node" "*" form-data "^3.0.0" @@ -707,7 +728,7 @@ bitcoinjs-lib@^5.2.0: varuint-bitcoin "^1.0.4" wif "^2.0.1" -bn.js@4.12.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^5.2.0: +bn.js@4.12.0, bn.js@^4.11.8, bn.js@^4.11.9, bn.js@^5.2.1: version "4.12.0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== @@ -961,6 +982,11 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +data-uri-to-buffer@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" + integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== + debug@4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -1026,7 +1052,7 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -diff@^4.0.1, diff@^4.0.2: +diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -1215,6 +1241,14 @@ fast-redact@^3.0.0: resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.1.tgz#790fcff8f808c2e12fabbfb2be5cb2deda448fa0" integrity sha512-odVmjC8x8jNeMZ3C+rPMESzXVSEU8tSWSHv9HFxP2mm89G/1WwqhrerJDQm9Zus8X6aoRgQDThKqptdNA6bt+A== +fetch-blob@^3.1.2, fetch-blob@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" + integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== + dependencies: + node-domexception "^1.0.0" + web-streams-polyfill "^3.0.3" + file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" @@ -1268,6 +1302,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formdata-polyfill@^4.0.10: + version "4.0.10" + resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" + integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== + dependencies: + fetch-blob "^3.1.2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1480,11 +1521,6 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -ip-regex@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" - integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -1607,11 +1643,6 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= -isarray@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" - integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= - isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1679,11 +1710,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -just-extend@^4.0.2: - version "4.2.1" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" - integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -1701,17 +1727,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - -lodash.set@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" - integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM= - -lodash@^4.0.0: +lodash@^4.0.0, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -1819,10 +1835,10 @@ mocha@^8.1.3: yargs-parser "20.2.4" yargs-unparser "2.0.0" -mock-socket@^9.1.2: - version "9.1.2" - resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.2.tgz#cce6cf2193aada937ba41de3288c5c1922fbd571" - integrity sha512-XKZkCnQ9ISOlTnaPg4LYYSMj7+6i78HyadYzLA5JM4465ibLdjappZD9Csnqc3Tfzep/eEK/LCJ29BTaLHoB1A== +mock-socket@^9.1.5: + version "9.2.1" + resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.2.1.tgz#cc9c0810aa4d0afe02d721dcb2b7e657c00e2282" + integrity sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag== moment@^2.19.3: version "2.29.1" @@ -1878,38 +1894,34 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nise@^4.0.4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/nise/-/nise-4.1.0.tgz#8fb75a26e90b99202fa1e63f448f58efbcdedaf6" - integrity sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA== - dependencies: - "@sinonjs/commons" "^1.7.0" - "@sinonjs/fake-timers" "^6.0.0" - "@sinonjs/text-encoding" "^0.7.1" - just-extend "^4.0.2" - path-to-regexp "^1.7.0" - -nock@^13.2.4: - version "13.2.4" - resolved "https://registry.yarnpkg.com/nock/-/nock-13.2.4.tgz#43a309d93143ee5cdcca91358614e7bde56d20e1" - integrity sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug== +nock@^13.2.9: + version "13.3.0" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.3.0.tgz#b13069c1a03f1ad63120f994b04bfd2556925768" + integrity sha512-HHqYQ6mBeiMc+N038w8LkMpDCRquCHWeNmN3v6645P3NhN2+qXOBqvPqo7Rt1VyCMzKhJ733wZqw5B7cQVFNPg== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash.set "^4.3.2" + lodash "^4.17.21" propagate "^2.0.0" +node-domexception@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" + integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== + node-fetch@2.6.1, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== +node-fetch@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" + integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA== dependencies: - whatwg-url "^5.0.0" + data-uri-to-buffer "^4.0.0" + fetch-blob "^3.1.4" + formdata-polyfill "^4.0.10" node-gyp-build@^4.2.0: version "4.2.3" @@ -1997,6 +2009,11 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -2025,13 +2042,6 @@ path-parse@^1.0.6, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -2174,10 +2184,10 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -regenerator-runtime@^0.13.4: - version "0.13.9" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" - integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== regtest-client@^0.2.0: version "0.2.0" @@ -2251,10 +2261,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" -rxjs@^7.5.2: - version "7.5.5" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.5.tgz#2ebad89af0f560f460ad5cc4213219e1f7dd4e9f" - integrity sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw== +rxjs@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" + integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== dependencies: tslib "^2.1.0" @@ -2333,18 +2343,6 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -sinon@^9.0.3: - version "9.2.4" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-9.2.4.tgz#e55af4d3b174a4443a8762fa8421c2976683752b" - integrity sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg== - dependencies: - "@sinonjs/commons" "^1.8.1" - "@sinonjs/fake-timers" "^6.0.1" - "@sinonjs/samsam" "^5.3.1" - diff "^4.0.2" - nise "^4.0.4" - supports-color "^7.1.0" - sonic-boom@^2.2.1: version "2.6.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-2.6.0.tgz#8786fc78be07c18a90381acd816d1d4afe3537a2" @@ -2558,16 +2556,6 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -ts-mock-imports@^1.3.0: - version "1.3.7" - resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.7.tgz#8c3210a641f40fd5cadbd1c9c88574b51df59bde" - integrity sha512-zy4B3QSGaOhjaX9j0h9YKwM1oHG4Kd1KIUJBeXlXIQrFnATNLgh4+NyRcaAHsPeqwe3TWeRtHXkNXPxySEKk3w== - ts-node@^9.0.0: version "9.1.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" @@ -2602,7 +2590,7 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@^4.0.0, type-detect@^4.0.5: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -2697,10 +2685,10 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= +web-streams-polyfill@^3.0.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" + integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== websocket@^1.0.34: version "1.0.34" @@ -2719,14 +2707,6 @@ whatwg-fetch@^3.4.1: resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -2785,6 +2765,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +ws@^8.8.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.1.tgz#c51e583d79140b5e42e39be48c934131942d4a8f" + integrity sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew== + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" From 88b80fa9f0c3ef29be9e227c0bba4f2a9ccd9a8d Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 1 Mar 2023 12:41:09 +0000 Subject: [PATCH 02/19] fix: tests crash only when expected --- .../test/_initialization/initialize.test.ts | 9 +++---- .../test/integration/redeem.test.ts | 7 ++--- .../test/integration/liquidate.test.ts | 26 +++++++++++++++---- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/bots/bridge-tester/test/_initialization/initialize.test.ts b/bots/bridge-tester/test/_initialization/initialize.test.ts index 1fa1485..2f41562 100644 --- a/bots/bridge-tester/test/_initialization/initialize.test.ts +++ b/bots/bridge-tester/test/_initialization/initialize.test.ts @@ -14,8 +14,7 @@ import { newMonetaryAmount, InterbtcPrimitivesVaultId, newVaultId, - CollateralCurrency, - getCorrespondingCollateralCurrency, + CollateralCurrencyExt, } from "@interlay/interbtc-api"; import { DEFAULT_PARACHAIN_ENDPOINT, @@ -33,7 +32,7 @@ describe.skip("Initialize parachain state", () => { let keyring: Keyring; let interBtcAPI: InterBtcApi; let wrappedCurrency: WrappedCurrency; - let collateralCurrency: CollateralCurrency; + let collateralCurrency: CollateralCurrencyExt; let vault_id: InterbtcPrimitivesVaultId; let alice: KeyringPair; @@ -62,9 +61,7 @@ describe.skip("Initialize parachain state", () => { ); interBtcAPI = new DefaultInterBtcApi(api, "regtest", alice); wrappedCurrency = interBtcAPI.getWrappedCurrency(); - collateralCurrency = getCorrespondingCollateralCurrency( - interBtcAPI.getGovernanceCurrency() - ); + collateralCurrency = interBtcAPI.api.consts.currency.getRelayChainCurrencyId; vault_id = newVaultId( api, charlie_stash.address, diff --git a/bots/bridge-tester/test/integration/redeem.test.ts b/bots/bridge-tester/test/integration/redeem.test.ts index 17e5698..67ada54 100644 --- a/bots/bridge-tester/test/integration/redeem.test.ts +++ b/bots/bridge-tester/test/integration/redeem.test.ts @@ -7,11 +7,12 @@ import { InterBtcApi, DefaultInterBtcApi, WrappedCurrency, + newMonetaryAmount, } from "@interlay/interbtc-api"; import { DEFAULT_ISSUE_TOP_UP_AMOUNT, DEFAULT_PARACHAIN_ENDPOINT } from "../config"; import chai from "chai"; import logger from "../../src/logger"; -import { BitcoinAmount } from "@interlay/monetary-js"; +import { Bitcoin } from "@interlay/monetary-js"; let produceBlocksFlag = false; @@ -78,7 +79,7 @@ describe.skip("redeem", () => { it("should perform heartbeat redeems", async () => { redeem = new Redeem( interBtcAPI, - BitcoinAmount.from.BTC(DEFAULT_ISSUE_TOP_UP_AMOUNT) + newMonetaryAmount(DEFAULT_ISSUE_TOP_UP_AMOUNT, Bitcoin, true) ); await redeem.performHeartbeatRedeems( alice, @@ -97,7 +98,7 @@ describe.skip("redeem", () => { it("should issue tokens to be able to redeem", async () => { redeem = new Redeem( interBtcAPI, - BitcoinAmount.from.BTC(DEFAULT_ISSUE_TOP_UP_AMOUNT) + newMonetaryAmount(DEFAULT_ISSUE_TOP_UP_AMOUNT, Bitcoin, true) ); const tokenBalance = await interBtcAPI.tokens.balance( wrappedCurrency, diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index ee3e13f..2689a61 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -17,8 +17,9 @@ describe("liquidate", () => { let userAccount: KeyringPair; let user2Account: KeyringPair; - let sudoAccount: KeyringPair; let userAccountId: AccountId; + let sudoAccount: KeyringPair; + let sudoAccountId: AccountId; let lendTokenId1: InterbtcPrimitivesCurrencyId; let lendTokenId2: InterbtcPrimitivesCurrencyId; @@ -41,6 +42,7 @@ describe("liquidate", () => { user2InterBtcAPI = new DefaultInterBtcApi(api, "regtest", user2Account); sudoAccount = keyring.addFromUri(DEFAULT_SUDO_URI); + sudoAccountId = newAccountId(api, sudoAccount.address); sudoInterBtcAPI = new DefaultInterBtcApi(api, "regtest", sudoAccount); userAccountId = newAccountId(api, userAccount.address); lendTokenId1 = newCurrencyId(api, { lendToken: { id: 1 } } as LendToken); @@ -48,9 +50,11 @@ describe("liquidate", () => { lendTokenId3 = newCurrencyId(api, { lendToken: { id: 3 } } as LendToken); underlyingCurrencyId1 = api.consts.escrowRewards.getNativeCurrencyId; - currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId1); + underlyingCurrency1 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId1); underlyingCurrencyId2 = api.consts.currency.getRelayChainCurrencyId; + underlyingCurrency2 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId2); underlyingCurrencyId3 = api.consts.currency.getWrappedCurrencyId; + underlyingCurrency3 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId3); const percentageToPermill = (percentage: number) => percentage * 10000; @@ -114,21 +118,33 @@ describe("liquidate", () => { }); it("should lend expected amount of currency to protocol", async function () { - this.timeout(approx10Blocks); + this.timeout(20 * approx10Blocks); const depositAmount = newMonetaryAmount(1000, underlyingCurrency1, true); const borrowAmount1 = newMonetaryAmount(100, underlyingCurrency2, true); - const borrowAmount2 = newMonetaryAmount(100, underlyingCurrency3, true); + const borrowAmount2 = newMonetaryAmount(1, underlyingCurrency3, true); await userInterBtcAPI.loans.lend(underlyingCurrency1, depositAmount); await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency1); + // Deposit cash in the currencies to be borrowed by the user + await sudoInterBtcAPI.loans.lend(borrowAmount1.currency, borrowAmount1); + // Mint some `borrowAmount2.currency` to the sudo account to ensure the tx works + await sudoInterBtcAPI.tokens.setBalance( + sudoAccountId, + borrowAmount2, + borrowAmount2.withAmount(0) + ); + await sudoInterBtcAPI.loans.lend(borrowAmount2.currency, borrowAmount2); + + // Borrow await userInterBtcAPI.loans.borrow(borrowAmount1.currency, borrowAmount1); await userInterBtcAPI.loans.borrow(borrowAmount2.currency, borrowAmount2); // start liquidation listener + // lendingLiquidationChecker(user2InterBtcAPI) // TODO!: start the bot listener logic - const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.sudo.Sudid, approx10Blocks), + const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); // crash the collateral exchange rate const newExchangeRate = "0x00000000000000000001000000000000"; From 292d075238b6cd0d6cde28b4cf5d6dae807552cf Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 1 Mar 2023 12:51:12 +0000 Subject: [PATCH 03/19] fix: temporarily add sleep to liquidation tests setup --- .../lending-liquidator/test/integration/liquidate.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index 2689a61..5202906 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -1,4 +1,4 @@ -import { createSubstrateAPI, CurrencyExt, DefaultInterBtcApi, DefaultTransactionAPI, InterBtcApi, InterbtcPrimitivesCurrencyId, newAccountId, newCurrencyId, LendToken, newMonetaryAmount, currencyIdToMonetaryCurrency } from "@interlay/interbtc-api"; +import { createSubstrateAPI, CurrencyExt, DefaultInterBtcApi, DefaultTransactionAPI, InterBtcApi, InterbtcPrimitivesCurrencyId, newAccountId, newCurrencyId, LendToken, newMonetaryAmount, currencyIdToMonetaryCurrency, sleep, SLEEP_TIME_MS } from "@interlay/interbtc-api"; import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { AccountId } from "@polkadot/types/interfaces"; @@ -32,7 +32,11 @@ describe("liquidate", () => { let underlyingCurrency3: CurrencyExt; before(async function () { - this.timeout(approx10Blocks); + // Sleep for 30s while the parachain is registering + // TODO: Check the chain state for when the parachain has already produces a few blocks + const sleepTimeMs = 30_000; + await sleep(sleepTimeMs); + this.timeout(approx10Blocks + sleepTimeMs); api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); keyring = new Keyring({ type: "sr25519" }); From 0c8bb5e41286ab3ef47f9cbf833ab21cf1bfd381 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 1 Mar 2023 16:40:50 +0000 Subject: [PATCH 04/19] fix(liquidator): wait for market registration in tests --- .../test/integration/liquidate.test.ts | 12 +++------ bots/lending-liquidator/test/utils.ts | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index 5202906..dd93354 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -5,7 +5,7 @@ import { AccountId } from "@polkadot/types/interfaces"; import { expect } from "chai"; import { APPROX_BLOCK_TIME_MS, DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config"; -import { setExchangeRate } from "../utils"; +import { setExchangeRate, waitForNthBlock, waitRegisteredLendingMarkets } from "../utils"; describe("liquidate", () => { const approx10Blocks = 10 * APPROX_BLOCK_TIME_MS; @@ -32,13 +32,8 @@ describe("liquidate", () => { let underlyingCurrency3: CurrencyExt; before(async function () { - // Sleep for 30s while the parachain is registering - // TODO: Check the chain state for when the parachain has already produces a few blocks - const sleepTimeMs = 30_000; - await sleep(sleepTimeMs); - this.timeout(approx10Blocks + sleepTimeMs); - api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); + await waitForNthBlock(api); keyring = new Keyring({ type: "sr25519" }); userAccount = keyring.addFromUri(DEFAULT_USER_1_URI); user2Account = keyring.addFromUri(DEFAULT_USER_2_URI); @@ -108,13 +103,14 @@ describe("liquidate", () => { ]); const [eventFound] = await Promise.all([ - DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.sudo.Sudid, approx10Blocks), + DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.ActivatedMarket, approx10Blocks), api.tx.sudo.sudo(addMarkets).signAndSend(sudoAccount), ]); expect( eventFound, `Sudo event to create new market not found - timed out after ${approx10Blocks} ms` ).to.be.true; + await waitRegisteredLendingMarkets(api); }); after(async () => { diff --git a/bots/lending-liquidator/test/utils.ts b/bots/lending-liquidator/test/utils.ts index 15f2277..c070a90 100644 --- a/bots/lending-liquidator/test/utils.ts +++ b/bots/lending-liquidator/test/utils.ts @@ -1,5 +1,7 @@ -import { CurrencyExt, InterBtcApi, storageKeyToNthInner, getStorageMapItemKey, createExchangeRateOracleKey, setStorageAtKey } from "@interlay/interbtc-api"; +import { CurrencyExt, InterBtcApi, storageKeyToNthInner, getStorageMapItemKey, createExchangeRateOracleKey, setStorageAtKey, sleep, SLEEP_TIME_MS } from "@interlay/interbtc-api"; + +import { ApiPromise } from "@polkadot/api"; export async function setExchangeRate( sudoInterBtcAPI: InterBtcApi, @@ -24,3 +26,25 @@ export async function setExchangeRate( const exchangeRateStorageKey = getStorageMapItemKey("Oracle", "Aggregate", exchangeRateOracleKey.toHex()); await setStorageAtKey(sudoInterBtcAPI.api, exchangeRateStorageKey, newExchangeRateHex, sudoAccount); } + +export async function waitForNthBlock(api: ApiPromise, n: number = 2): Promise { + while (true) { + const currentBlockNo = await api.query.system.number(); + if (currentBlockNo.toNumber() >= n) { + return; + } + console.log(`Waiting for ${n} blocks to be produced... Current block is ${currentBlockNo}`); + await sleep(SLEEP_TIME_MS); + } +} + +export async function waitRegisteredLendingMarkets(api: ApiPromise): Promise { + while (true) { + const currentBlockNo = await api.query.loans.markets.entries(); + if (currentBlockNo.length > 0) { + return; + } + console.log(`Waiting for lending markets to be registered`); + await sleep(SLEEP_TIME_MS); + } +} \ No newline at end of file From 1eb5da17b83fa26efdad4c583261ae0747ee6d2c Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Thu, 2 Mar 2023 17:48:21 +0000 Subject: [PATCH 05/19] feat(liquidator): start adding liquidation logic --- bots/lending-liquidator/src/consts.ts | 3 ++ bots/lending-liquidator/src/liquidate.ts | 57 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 bots/lending-liquidator/src/consts.ts create mode 100644 bots/lending-liquidator/src/liquidate.ts diff --git a/bots/lending-liquidator/src/consts.ts b/bots/lending-liquidator/src/consts.ts new file mode 100644 index 0000000..0398545 --- /dev/null +++ b/bots/lending-liquidator/src/consts.ts @@ -0,0 +1,3 @@ +import { InterBtc, Interlay, KBtc, Kintsugi, Kusama, Polkadot } from "@interlay/monetary-js"; + +export const NATIVE_CURRENCIES = [Kintsugi, Kusama, KBtc, Interlay, Polkadot, InterBtc]; \ No newline at end of file diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts new file mode 100644 index 0000000..783ff54 --- /dev/null +++ b/bots/lending-liquidator/src/liquidate.ts @@ -0,0 +1,57 @@ +import { ChainBalance, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount } from "@interlay/interbtc-api"; +import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; +import { NATIVE_CURRENCIES } from "./consts"; + +function referencePrice(balance: MonetaryAmount, rate: ExchangeRate): MonetaryAmount { + // Convert to the reference currency (BTC) + return rate.toBase(balance) +} + +async function start(interBtcApi: InterBtcApi): Promise { + const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); + let chainAssets = [...NATIVE_CURRENCIES, ...foreignAssets]; + if (!interBtcApi.account) { + return Promise.reject("No account set for the lending-liquidator"); + } + const accountId = newAccountId(interBtcApi.api, interBtcApi.account.toString()); + await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { + + const [balances, oraclePrices, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ + Promise.all(chainAssets.map((asset) => interBtcApi.tokens.balance(asset, accountId))), + Promise.all(chainAssets.map((asset) => interBtcApi.oracle.getExchangeRate(asset))), + interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.assetRegistry.getForeignAssets() + ]); + + const balancesAndPrices: Map]> = new Map(); + chainAssets + .forEach( + (v, index) => + balancesAndPrices.set(v, [balances[index], oraclePrices[index]]) + ); + + // TODO: refactor to a `strategy(...)` function that takes balances, prices, and undercollateralized borrowers + // and returns amountToRepay and collateralToLiquidate + + let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); + let maxRepayment: MonetaryAmount; + let collateralToLiquidate: CurrencyExt; + undercollateralizedBorrowers.forEach((position) => { + position.borrowPositions.forEach((loan) => { + if (balancesAndPrices.has(loan.amount.currency)) { + const [balance, rate] = balancesAndPrices.get(loan.amount.currency) as [ChainBalance, ExchangeRate]; + const repayableAmount = loan.amount.min(balance.free); + const referenceDebt = referencePrice(repayableAmount, rate); + if (referenceDebt.gt(maxRepayableLoan)) { + maxRepayableLoan = referenceDebt; + maxRepayment = repayableAmount; + } + + } + }) + }); + + chainAssets = [...NATIVE_CURRENCIES, ...foreignAssets]; + console.log(`Scanned block: #${header.number}`); + }); +} From ce5e5c0e929c8e888a683f20f354de0915e7dd97 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 7 Mar 2023 18:26:14 +0000 Subject: [PATCH 06/19] feat(liquidator): first liquidation strategy implementation --- bots/lending-liquidator/src/liquidate.ts | 118 ++++++++++++++++------- 1 file changed, 84 insertions(+), 34 deletions(-) diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 783ff54..3f105e0 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,57 +1,107 @@ -import { ChainBalance, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount } from "@interlay/interbtc-api"; +import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; +import { AccountId } from "@polkadot/types/interfaces"; import { NATIVE_CURRENCIES } from "./consts"; -function referencePrice(balance: MonetaryAmount, rate: ExchangeRate): MonetaryAmount { +type CollateralAndValue = { + collateral: CollateralPosition, + referenceValue: MonetaryAmount +} + +function referencePrice(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { + if (!rate) { + return new MonetaryAmount(balance.currency, 0); + } // Convert to the reference currency (BTC) return rate.toBase(balance) } +function findHighestValueCollateral(positions: CollateralPosition[], rates: Map>): CollateralAndValue | undefined { + // It should be impossible to have no collateral currency locked, but just in case + if (positions.length == 0) { + return undefined; + } + const defaultValue = { + collateral: positions[0], + referenceValue: referencePrice(positions[0].amount, rates.get(positions[0].amount.currency)) + }; + return positions.reduce( + (previous, current) => { + const currentReferencePrice = referencePrice(current.amount, rates.get(current.amount.currency)); + if (previous.collateral.amount.gt(currentReferencePrice)) { + return previous; + } + return { + collateral: current, + referenceValue: currentReferencePrice + } + }, + defaultValue + ); +} + +function liquidationStrategy( + interBtcApi: InterBtcApi, + chainAssets: Set, + liquidatorBalance: Map, + oracleRates: Map>, + undercollateralizedBorrowers: UndercollateralizedPosition[] +): [MonetaryAmount, CurrencyExt, AccountId] | undefined { + let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); + let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; + undercollateralizedBorrowers.forEach((position) => { + const highestValueCollateral = findHighestValueCollateral(position.collateralPositions, oracleRates); + if (!highestValueCollateral) { + return; + } + position.borrowPositions.forEach((loan) => { + if (chainAssets.has(loan.accumulatedDebt.currency)) { + const balance = liquidatorBalance.get(loan.accumulatedDebt.currency) as ChainBalance; + const rate = oracleRates.get(loan.accumulatedDebt.currency) as ExchangeRate; + const repayableAmount = loan.accumulatedDebt.min(balance.free); + // TODO: Take close factor into account when consider the collateral's reference value + const referenceRepayable = referencePrice(repayableAmount, rate).min(highestValueCollateral.referenceValue); + if (referenceRepayable.gt(maxRepayableLoan)) { + maxRepayableLoan = referenceRepayable; + result = [repayableAmount, highestValueCollateral.collateral.amount.currency, position.accountId]; + } + } + }) + }); + return result; +} + async function start(interBtcApi: InterBtcApi): Promise { const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); - let chainAssets = [...NATIVE_CURRENCIES, ...foreignAssets]; + let chainAssets = new Set([...NATIVE_CURRENCIES, ...foreignAssets]); if (!interBtcApi.account) { return Promise.reject("No account set for the lending-liquidator"); } const accountId = newAccountId(interBtcApi.api, interBtcApi.account.toString()); await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { - const [balances, oraclePrices, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ - Promise.all(chainAssets.map((asset) => interBtcApi.tokens.balance(asset, accountId))), - Promise.all(chainAssets.map((asset) => interBtcApi.oracle.getExchangeRate(asset))), + const liquidatorBalance: Map = new Map(); + const oracleRates: Map> = new Map(); + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ + Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), + Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), interBtcApi.loans.getUndercollateralizedBorrowers(), interBtcApi.assetRegistry.getForeignAssets() ]); - const balancesAndPrices: Map]> = new Map(); - chainAssets - .forEach( - (v, index) => - balancesAndPrices.set(v, [balances[index], oraclePrices[index]]) - ); - - // TODO: refactor to a `strategy(...)` function that takes balances, prices, and undercollateralized borrowers - // and returns amountToRepay and collateralToLiquidate + if (undercollateralizedBorrowers.length > 0) { + const [amountToRepay, collateralToLiquidate, borrower] = liquidationStrategy( + interBtcApi, + chainAssets, + liquidatorBalance, + oracleRates, + undercollateralizedBorrowers + ) as [MonetaryAmount, CurrencyExt, AccountId]; + await interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate); + } - let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); - let maxRepayment: MonetaryAmount; - let collateralToLiquidate: CurrencyExt; - undercollateralizedBorrowers.forEach((position) => { - position.borrowPositions.forEach((loan) => { - if (balancesAndPrices.has(loan.amount.currency)) { - const [balance, rate] = balancesAndPrices.get(loan.amount.currency) as [ChainBalance, ExchangeRate]; - const repayableAmount = loan.amount.min(balance.free); - const referenceDebt = referencePrice(repayableAmount, rate); - if (referenceDebt.gt(maxRepayableLoan)) { - maxRepayableLoan = referenceDebt; - maxRepayment = repayableAmount; - } - - } - }) - }); - - chainAssets = [...NATIVE_CURRENCIES, ...foreignAssets]; + // Add any new foreign assets to `chainAssets` + chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); console.log(`Scanned block: #${header.number}`); }); } From fdc8ab9bd322cd75478702bc1819203475b5355f Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 8 Mar 2023 18:06:49 +0000 Subject: [PATCH 07/19] feat(liquidator): strategy improvements, use instant seal --- bots/lending-liquidator/src/consts.ts | 3 +- bots/lending-liquidator/src/index.ts | 1 + bots/lending-liquidator/src/liquidate.ts | 69 +++++++---- bots/lending-liquidator/test/config.ts | 3 - .../test/integration/liquidate.test.ts | 16 +-- docker-compose.yml | 108 ++++++++++++++++++ docker/oracle-config.json | 32 ++++++ docker/vault_1-keyfile.json | 3 + docker/vault_2-keyfile.json | 3 + docker/vault_3-keyfile.json | 3 + scripts/docker-setup.sh | 12 -- scripts/setup-parachain-docker.ts | 17 --- 12 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 docker-compose.yml create mode 100644 docker/oracle-config.json create mode 100644 docker/vault_1-keyfile.json create mode 100644 docker/vault_2-keyfile.json create mode 100644 docker/vault_3-keyfile.json delete mode 100755 scripts/docker-setup.sh delete mode 100644 scripts/setup-parachain-docker.ts diff --git a/bots/lending-liquidator/src/consts.ts b/bots/lending-liquidator/src/consts.ts index 0398545..79cba87 100644 --- a/bots/lending-liquidator/src/consts.ts +++ b/bots/lending-liquidator/src/consts.ts @@ -1,3 +1,4 @@ import { InterBtc, Interlay, KBtc, Kintsugi, Kusama, Polkadot } from "@interlay/monetary-js"; -export const NATIVE_CURRENCIES = [Kintsugi, Kusama, KBtc, Interlay, Polkadot, InterBtc]; \ No newline at end of file +// approximate time per block in ms +export const APPROX_BLOCK_TIME_MS = 12 * 1000; \ No newline at end of file diff --git a/bots/lending-liquidator/src/index.ts b/bots/lending-liquidator/src/index.ts index e69de29..2f8d57f 100644 --- a/bots/lending-liquidator/src/index.ts +++ b/bots/lending-liquidator/src/index.ts @@ -0,0 +1 @@ +export { startLiquidator } from "./liquidate"; diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 3f105e0..4f55f63 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,7 +1,7 @@ -import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition } from "@interlay/interbtc-api"; +import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; -import { NATIVE_CURRENCIES } from "./consts"; +import { APPROX_BLOCK_TIME_MS } from "./consts"; type CollateralAndValue = { collateral: CollateralPosition, @@ -28,7 +28,7 @@ function findHighestValueCollateral(positions: CollateralPosition[], rates: Map< return positions.reduce( (previous, current) => { const currentReferencePrice = referencePrice(current.amount, rates.get(current.amount.currency)); - if (previous.collateral.amount.gt(currentReferencePrice)) { + if (previous.referenceValue.gt(currentReferencePrice)) { return previous; } return { @@ -42,7 +42,6 @@ function findHighestValueCollateral(positions: CollateralPosition[], rates: Map< function liquidationStrategy( interBtcApi: InterBtcApi, - chainAssets: Set, liquidatorBalance: Map, oracleRates: Map>, undercollateralizedBorrowers: UndercollateralizedPosition[] @@ -55,10 +54,15 @@ function liquidationStrategy( return; } position.borrowPositions.forEach((loan) => { - if (chainAssets.has(loan.accumulatedDebt.currency)) { - const balance = liquidatorBalance.get(loan.accumulatedDebt.currency) as ChainBalance; - const rate = oracleRates.get(loan.accumulatedDebt.currency) as ExchangeRate; - const repayableAmount = loan.accumulatedDebt.min(balance.free); + const totalDebt = loan.amount.add(loan.accumulatedDebt); + console.log("debt currency", totalDebt.currency); + console.log("highest value collateral ", highestValueCollateral.collateral.amount.currency.ticker, highestValueCollateral.referenceValue.toHuman()); + if (liquidatorBalance.has(totalDebt.currency)) { + const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; + console.log("free bot balance ", balance.free.toHuman()); + console.log("borrower debt", totalDebt.toHuman()); + const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; + const repayableAmount = totalDebt.min(balance.free); // TODO: Take close factor into account when consider the collateral's reference value const referenceRepayable = referencePrice(repayableAmount, rate).min(highestValueCollateral.referenceValue); if (referenceRepayable.gt(maxRepayableLoan)) { @@ -71,37 +75,52 @@ function liquidationStrategy( return result; } -async function start(interBtcApi: InterBtcApi): Promise { +export async function startLiquidator(interBtcApi: InterBtcApi): Promise { + console.log("Starting lending liquidator..."); const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); - let chainAssets = new Set([...NATIVE_CURRENCIES, ...foreignAssets]); - if (!interBtcApi.account) { + + let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] + let chainAssets = new Set([...nativeCurrency, ...foreignAssets]); + if (interBtcApi.account == undefined) { return Promise.reject("No account set for the lending-liquidator"); } - const accountId = newAccountId(interBtcApi.api, interBtcApi.account.toString()); + const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account); + console.log("Listening to new blocks..."); await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { - + console.log(`Scanning block: #${header.number}`); const liquidatorBalance: Map = new Map(); const oracleRates: Map> = new Map(); - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ - Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), - Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), - interBtcApi.loans.getUndercollateralizedBorrowers(), - interBtcApi.assetRegistry.getForeignAssets() - ]); - - if (undercollateralizedBorrowers.length > 0) { - const [amountToRepay, collateralToLiquidate, borrower] = liquidationStrategy( + console.log("awaiting big promise"); + try { + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ + Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), + Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), + interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.assetRegistry.getForeignAssets() + ]); + + console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); + const potentialLiquidation = liquidationStrategy( interBtcApi, - chainAssets, liquidatorBalance, oracleRates, undercollateralizedBorrowers ) as [MonetaryAmount, CurrencyExt, AccountId]; - await interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate); + if (potentialLiquidation) { + const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; + console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); + // Either our liquidation will go through, or someone else's will + await Promise.all([ + DefaultTransactionAPI.waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), + interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) + ]); + } + + } catch (error) { + console.log("found an error: ", error); } // Add any new foreign assets to `chainAssets` chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); - console.log(`Scanned block: #${header.number}`); }); } diff --git a/bots/lending-liquidator/test/config.ts b/bots/lending-liquidator/test/config.ts index 3553fe0..095dfa8 100644 --- a/bots/lending-liquidator/test/config.ts +++ b/bots/lending-liquidator/test/config.ts @@ -2,6 +2,3 @@ export const DEFAULT_PARACHAIN_ENDPOINT = "ws://127.0.0.1:9944"; export const DEFAULT_SUDO_URI = "//Alice"; export const DEFAULT_USER_1_URI = "//Dave"; export const DEFAULT_USER_2_URI = "//Eve"; - -// approximate time per block in ms -export const APPROX_BLOCK_TIME_MS = 12 * 1000; diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index dd93354..b6b1eca 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -4,8 +4,10 @@ import { KeyringPair } from "@polkadot/keyring/types"; import { AccountId } from "@polkadot/types/interfaces"; import { expect } from "chai"; -import { APPROX_BLOCK_TIME_MS, DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config"; -import { setExchangeRate, waitForNthBlock, waitRegisteredLendingMarkets } from "../utils"; +import { DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAULT_USER_2_URI } from "../config"; +import { setExchangeRate, waitRegisteredLendingMarkets } from "../utils"; +import { startLiquidator } from "../../src"; +import { APPROX_BLOCK_TIME_MS } from "../../src/consts"; describe("liquidate", () => { const approx10Blocks = 10 * APPROX_BLOCK_TIME_MS; @@ -33,7 +35,6 @@ describe("liquidate", () => { before(async function () { api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); - await waitForNthBlock(api); keyring = new Keyring({ type: "sr25519" }); userAccount = keyring.addFromUri(DEFAULT_USER_1_URI); user2Account = keyring.addFromUri(DEFAULT_USER_2_URI); @@ -141,13 +142,14 @@ describe("liquidate", () => { await userInterBtcAPI.loans.borrow(borrowAmount1.currency, borrowAmount1); await userInterBtcAPI.loans.borrow(borrowAmount2.currency, borrowAmount2); - // start liquidation listener - // lendingLiquidationChecker(user2InterBtcAPI) - // TODO!: start the bot listener logic + // Start liquidation listener + // Do not `await` so it runs in the background + startLiquidator(sudoInterBtcAPI); + const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); // crash the collateral exchange rate - const newExchangeRate = "0x00000000000000000001000000000000"; + const newExchangeRate = "0x00000000000000000000001000000000"; await setExchangeRate(sudoInterBtcAPI, depositAmount.currency, newExchangeRate); // expect liquidation event to happen diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2af20e0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,108 @@ +version: "3.8" +services: + interbtc: + image: "interlayhq/interbtc:1.21.10" + command: + - --rpc-external + - --ws-external + - --rpc-methods=unsafe + - --dev + - --instant-seal + ports: + - "9933:9933" + - "9944:9944" + bitcoind: + image: "ruimarinho/bitcoin-core:22" + command: + - -regtest + - -server + - -rpcbind=0.0.0.0 + - -rpcallowip=0.0.0.0/0 + - -rpcuser=rpcuser + - -rpcpassword=rpcpassword + - -fallbackfee=0.0002 + ports: + - "18443:18443" + bitcoin-cli: + image: "ruimarinho/bitcoin-core:22" + command: + - /bin/sh + - -c + - | + bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword createwallet Alice + ALICE_ADDRESS=$$(bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword -rpcwallet=Alice getnewaddress) + # coins need 100 confirmations to be spendable + bitcoin-cli -regtest -rpcconnect=bitcoind -rpcwait -rpcuser=rpcuser -rpcpassword=rpcpassword generatetoaddress 101 $${ALICE_ADDRESS} + electrs: + image: "interlayhq/electrs:latest" + command: + - electrs + - -vvvv + - --network + - regtest + - --jsonrpc-import + - --cors + - "*" + - --cookie + - "rpcuser:rpcpassword" + - --daemon-rpc-addr + - "bitcoind:18443" + - --http-addr + - "[::0]:3002" + - --index-unspendables + ports: + - "3002:3002" + restart: always + oracle: + image: "interlayhq/interbtc-clients:oracle-parachain-metadata-kintsugi-testnet-1.19.7" + command: + - oracle-parachain-metadata-kintsugi-testnet + - --keyring=bob + - --btc-parachain-url=ws://interbtc:9944 + environment: + RUST_LOG: info + volumes: + - ./docker/oracle-config.json:/oracle-config.json + vault_1: + image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7" + command: + - vault-parachain-metadata-kintsugi-testnet + - --keyfile=/keyfile.json + - --keyname=vault_1 + - --auto-register=KSM=10000000000000 + - --auto-register=KINT=180000000000000 + - --btc-parachain-url=ws://interbtc:9944 + - --bitcoin-relay-start-height=1 + environment: &client-env + BITCOIN_RPC_URL: http://bitcoind:18443 + BITCOIN_RPC_USER: rpcuser + BITCOIN_RPC_PASS: rpcpassword + RUST_LOG: info + volumes: + - ./docker/vault_1-keyfile.json:/keyfile.json + vault_2: + image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7" + command: + - vault-parachain-metadata-kintsugi-testnet + - --keyfile=/keyfile.json + - --keyname=vault_2 + - --auto-register=KSM=10000000000000 + - --auto-register=KINT=180000000000000 + - --btc-parachain-url=ws://interbtc:9944 + - --bitcoin-relay-start-height=1 + environment: *client-env + volumes: + - ./docker/vault_2-keyfile.json:/keyfile.json + vault_3: + image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-testnet-1.19.7" + command: + - vault-parachain-metadata-kintsugi-testnet + - --keyfile=/keyfile.json + - --keyname=vault_3 + - --auto-register=KSM=10000000000000 + - --auto-register=KINT=180000000000000 + - --btc-parachain-url=ws://interbtc:9944 + - --bitcoin-relay-start-height=1 + environment: *client-env + volumes: + - ./docker/vault_3-keyfile.json:/keyfile.json \ No newline at end of file diff --git a/docker/oracle-config.json b/docker/oracle-config.json new file mode 100644 index 0000000..7867c05 --- /dev/null +++ b/docker/oracle-config.json @@ -0,0 +1,32 @@ +{ + "currencies": { + "BTC": { + "name": "Bitcoin", + "decimals": 8 + }, + "KINT": { + "name": "Kintsugi", + "decimals": 12 + }, + "KSM": { + "name": "Kusama", + "decimals": 12 + } + }, + "prices": [ + { + "pair": [ + "BTC", + "KINT" + ], + "value": 230 + }, + { + "pair": [ + "BTC", + "KSM" + ], + "value": 230 + } + ] +} \ No newline at end of file diff --git a/docker/vault_1-keyfile.json b/docker/vault_1-keyfile.json new file mode 100644 index 0000000..d2bd34e --- /dev/null +++ b/docker/vault_1-keyfile.json @@ -0,0 +1,3 @@ +{ + "vault_1": "//Charlie//stash" +} \ No newline at end of file diff --git a/docker/vault_2-keyfile.json b/docker/vault_2-keyfile.json new file mode 100644 index 0000000..c3c0bcd --- /dev/null +++ b/docker/vault_2-keyfile.json @@ -0,0 +1,3 @@ +{ + "vault_2": "//Dave//stash" +} \ No newline at end of file diff --git a/docker/vault_3-keyfile.json b/docker/vault_3-keyfile.json new file mode 100644 index 0000000..7376a26 --- /dev/null +++ b/docker/vault_3-keyfile.json @@ -0,0 +1,3 @@ +{ + "vault_3": "//Eve//stash" +} \ No newline at end of file diff --git a/scripts/docker-setup.sh b/scripts/docker-setup.sh deleted file mode 100755 index 9047106..0000000 --- a/scripts/docker-setup.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -if ! [ -d "local-setup" ] -then - git clone https://github.com/interlay/parachain-launch/ - cd parachain-launch && yarn install - yarn start generate --config=configs/kintsugi.yml --servicesPath=configs/kintsugi-services.yml --yes --output=local-setup - mv local-setup ../ - cd ../ - rm -rf parachain-launch -fi - -cd local-setup && docker-compose up --build --detach diff --git a/scripts/setup-parachain-docker.ts b/scripts/setup-parachain-docker.ts deleted file mode 100644 index 8f0ac79..0000000 --- a/scripts/setup-parachain-docker.ts +++ /dev/null @@ -1,17 +0,0 @@ -import shell from "shelljs"; - -const exec = (cmd: string, fatal = true) => { - console.log(`$ ${cmd}`); - const res = shell.exec(cmd); - if (res.code !== 0) { - console.error("Error: Command failed with code", res.code); - console.log(res); - if (fatal) { - process.exit(1); - } - } - return res; -}; - -exec("chmod +x ./scripts/docker-setup.sh"); -exec("./scripts/docker-setup.sh"); From 79700fb593557d4e7280b891c49ae7af5e9ff08b Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Thu, 9 Mar 2023 15:25:41 +0000 Subject: [PATCH 08/19] feat(liquidator): use close factor --- .../test/_initialization/initialize.test.ts | 95 ++++++++++--------- bots/bridge-tester/test/config.ts | 2 + bots/lending-liquidator/src/liquidate.ts | 57 ++++++----- .../test/integration/liquidate.test.ts | 2 +- docker-compose.yml | 2 +- 5 files changed, 84 insertions(+), 74 deletions(-) diff --git a/bots/bridge-tester/test/_initialization/initialize.test.ts b/bots/bridge-tester/test/_initialization/initialize.test.ts index 2f41562..821e5d8 100644 --- a/bots/bridge-tester/test/_initialization/initialize.test.ts +++ b/bots/bridge-tester/test/_initialization/initialize.test.ts @@ -1,11 +1,10 @@ import { ApiPromise, Keyring } from "@polkadot/api"; import { KeyringPair } from "@polkadot/keyring/types"; import { assert } from "chai"; -import BN from "bn.js"; import { BitcoinCoreClient, - setNumericStorage, + initializeStableConfirmations, issueSingle, InterBtcApi, DefaultInterBtcApi, @@ -24,19 +23,21 @@ import { DEFAULT_BITCOIN_CORE_PASSWORD, DEFAULT_BITCOIN_CORE_PORT, DEFAULT_BITCOIN_CORE_WALLET, + DEFAULT_SUDO_URI, + DEFAULT_USER_1_URI, } from "../config"; describe.skip("Initialize parachain state", () => { let api: ApiPromise; let bitcoinCoreClient: BitcoinCoreClient; let keyring: Keyring; - let interBtcAPI: InterBtcApi; + let userInterBtcApi: InterBtcApi; let wrappedCurrency: WrappedCurrency; let collateralCurrency: CollateralCurrencyExt; let vault_id: InterbtcPrimitivesVaultId; - let alice: KeyringPair; - let bob: KeyringPair; + let sudoAccount: KeyringPair; + let user1Account: KeyringPair; let charlie_stash: KeyringPair; function sleep(ms: number): Promise { @@ -47,9 +48,9 @@ describe.skip("Initialize parachain state", () => { api = await createSubstrateAPI(DEFAULT_PARACHAIN_ENDPOINT); keyring = new Keyring({ type: "sr25519" }); // Alice is also the root account - alice = keyring.addFromUri("//Alice"); - bob = keyring.addFromUri("//Bob"); + user1Account = keyring.addFromUri(DEFAULT_USER_1_URI); charlie_stash = keyring.addFromUri("//Charlie//stash"); + sudoAccount = keyring.addFromUri(DEFAULT_SUDO_URI); bitcoinCoreClient = new BitcoinCoreClient( DEFAULT_BITCOIN_CORE_NETWORK, @@ -59,9 +60,9 @@ describe.skip("Initialize parachain state", () => { DEFAULT_BITCOIN_CORE_PORT, DEFAULT_BITCOIN_CORE_WALLET ); - interBtcAPI = new DefaultInterBtcApi(api, "regtest", alice); - wrappedCurrency = interBtcAPI.getWrappedCurrency(); - collateralCurrency = interBtcAPI.api.consts.currency.getRelayChainCurrencyId; + userInterBtcApi = new DefaultInterBtcApi(api, "regtest", user1Account); + wrappedCurrency = userInterBtcApi.getWrappedCurrency(); + collateralCurrency = userInterBtcApi.api.consts.currency.getRelayChainCurrencyId; vault_id = newVaultId( api, charlie_stash.address, @@ -76,47 +77,49 @@ describe.skip("Initialize parachain state", () => { 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; - await setNumericStorage( - api, - "BTCRelay", - "StableBitcoinConfirmations", - new BN(stableBitcoinConfirmationsToSet), - alice - ); - await setNumericStorage( - api, - "BTCRelay", - "StableParachainConfirmations", - new BN(stableParachainConfirmationsToSet), - alice - ); - const stableBitcoinConfirmations = - await interBtcAPI.btcRelay.getStableBitcoinConfirmations(); - assert.equal( - stableBitcoinConfirmationsToSet, - stableBitcoinConfirmations, - "Setting the Bitcoin confirmations failed" - ); - const stableParachainConfirmations = - await interBtcAPI.btcRelay.getStableParachainConfirmations(); - assert.equal( - stableParachainConfirmationsToSet, - stableParachainConfirmations, - "Setting the Parachain confirmations failed" - ); - await bitcoinCoreClient.mineBlocks(3); - }); + 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) { + await initializeStableConfirmations( + api, + { + bitcoinConfirmations: stableBitcoinConfirmationsToSet, + parachainConfirmations: stableParachainConfirmationsToSet, + }, + sudoAccount, + bitcoinCoreClient + ); + [stableBitcoinConfirmations, stableParachainConfirmations] = await Promise.all([ + userInterBtcApi.btcRelay.getStableBitcoinConfirmations(), + userInterBtcApi.btcRelay.getStableParachainConfirmations(), + ]); + } + assert.equal( + stableBitcoinConfirmationsToSet, + stableBitcoinConfirmations, + "Setting the Bitcoin confirmations failed" + ); + assert.equal( + stableParachainConfirmationsToSet, + stableParachainConfirmations, + "Setting the Parachain confirmations failed" + ); + }); + it("should issue 0.1 InterBTC", async () => { const wrappedToIssue = newMonetaryAmount(0.00007, wrappedCurrency, true); await issueSingle( - interBtcAPI, + userInterBtcApi, bitcoinCoreClient, - alice, + user1Account, wrappedToIssue, vault_id ); diff --git a/bots/bridge-tester/test/config.ts b/bots/bridge-tester/test/config.ts index c9e484e..f5568ea 100644 --- a/bots/bridge-tester/test/config.ts +++ b/bots/bridge-tester/test/config.ts @@ -6,3 +6,5 @@ export const DEFAULT_BITCOIN_CORE_PASSWORD = "rpcpassword"; export const DEFAULT_BITCOIN_CORE_PORT = "18443"; export const DEFAULT_BITCOIN_CORE_WALLET = "Alice"; export const DEFAULT_ISSUE_TOP_UP_AMOUNT = "0.1"; +export const DEFAULT_SUDO_URI = "//Alice"; +export const DEFAULT_USER_1_URI = "//Dave"; diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 4f55f63..672f8ee 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,14 +1,14 @@ -import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, newAccountId, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI } from "@interlay/interbtc-api"; +import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI, decodePermill } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; import { APPROX_BLOCK_TIME_MS } from "./consts"; type CollateralAndValue = { - collateral: CollateralPosition, + collateral: MonetaryAmount, referenceValue: MonetaryAmount } -function referencePrice(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { +function referenceValue(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { if (!rate) { return new MonetaryAmount(balance.currency, 0); } @@ -16,24 +16,30 @@ function referencePrice(balance: MonetaryAmount, rate: ExchangeRate return rate.toBase(balance) } -function findHighestValueCollateral(positions: CollateralPosition[], rates: Map>): CollateralAndValue | undefined { +function findHighestValueCollateral( + positions: CollateralPosition[], + rates: Map> +): CollateralAndValue | undefined { // It should be impossible to have no collateral currency locked, but just in case if (positions.length == 0) { return undefined; } const defaultValue = { - collateral: positions[0], - referenceValue: referencePrice(positions[0].amount, rates.get(positions[0].amount.currency)) + collateral: positions[0].amount, + referenceValue: referenceValue( + newMonetaryAmount(0, positions[0].amount.currency), + rates.get(positions[0].amount.currency) + ) }; return positions.reduce( (previous, current) => { - const currentReferencePrice = referencePrice(current.amount, rates.get(current.amount.currency)); - if (previous.referenceValue.gt(currentReferencePrice)) { + const liquidatableValue = referenceValue(current.amount, rates.get(current.amount.currency)); + if (previous.referenceValue.gt(liquidatableValue)) { return previous; } return { - collateral: current, - referenceValue: currentReferencePrice + collateral: current.amount, + referenceValue: liquidatableValue } }, defaultValue @@ -44,7 +50,8 @@ function liquidationStrategy( interBtcApi: InterBtcApi, liquidatorBalance: Map, oracleRates: Map>, - undercollateralizedBorrowers: UndercollateralizedPosition[] + undercollateralizedBorrowers: UndercollateralizedPosition[], + markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; @@ -55,19 +62,17 @@ function liquidationStrategy( } position.borrowPositions.forEach((loan) => { const totalDebt = loan.amount.add(loan.accumulatedDebt); - console.log("debt currency", totalDebt.currency); - console.log("highest value collateral ", highestValueCollateral.collateral.amount.currency.ticker, highestValueCollateral.referenceValue.toHuman()); if (liquidatorBalance.has(totalDebt.currency)) { + const loansMarket = markets.get(totalDebt.currency) as LoansMarket; + const closeFactor = decodePermill(loansMarket.closeFactor); const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; - console.log("free bot balance ", balance.free.toHuman()); - console.log("borrower debt", totalDebt.toHuman()); const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; - const repayableAmount = totalDebt.min(balance.free); - // TODO: Take close factor into account when consider the collateral's reference value - const referenceRepayable = referencePrice(repayableAmount, rate).min(highestValueCollateral.referenceValue); + // Can only repay a fraction of the total debt, defined by the `closeFactor` + const repayableAmount = totalDebt.mul(closeFactor).min(balance.free); + const referenceRepayable = referenceValue(repayableAmount, rate).min(highestValueCollateral.referenceValue); if (referenceRepayable.gt(maxRepayableLoan)) { maxRepayableLoan = referenceRepayable; - result = [repayableAmount, highestValueCollateral.collateral.amount.currency, position.accountId]; + result = [repayableAmount, highestValueCollateral.collateral.currency, position.accountId]; } } }) @@ -90,12 +95,12 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { console.log(`Scanning block: #${header.number}`); const liquidatorBalance: Map = new Map(); const oracleRates: Map> = new Map(); - console.log("awaiting big promise"); try { - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, foreignAssets] = await Promise.all([ + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.loans.getLoansMarkets(), interBtcApi.assetRegistry.getForeignAssets() ]); @@ -104,7 +109,8 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { interBtcApi, liquidatorBalance, oracleRates, - undercollateralizedBorrowers + undercollateralizedBorrowers, + new Map(marketsArray) ) as [MonetaryAmount, CurrencyExt, AccountId]; if (potentialLiquidation) { const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; @@ -115,12 +121,11 @@ export async function startLiquidator(interBtcApi: InterBtcApi): Promise { interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) ]); } - + + // Add any new foreign assets to `chainAssets` + chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); } catch (error) { console.log("found an error: ", error); } - - // Add any new foreign assets to `chainAssets` - chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); }); } diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index b6b1eca..dae9458 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -149,7 +149,7 @@ describe("liquidate", () => { const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); // crash the collateral exchange rate - const newExchangeRate = "0x00000000000000000000001000000000"; + const newExchangeRate = "0x00000000000000000000100000000000"; await setExchangeRate(sudoInterBtcAPI, depositAmount.currency, newExchangeRate); // expect liquidation event to happen diff --git a/docker-compose.yml b/docker-compose.yml index 2af20e0..b2e12f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -105,4 +105,4 @@ services: - --bitcoin-relay-start-height=1 environment: *client-env volumes: - - ./docker/vault_3-keyfile.json:/keyfile.json \ No newline at end of file + - ./docker/vault_3-keyfile.json:/keyfile.json From b2a092f75e94f3634b20f676f7f48b8b1915bd6e Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Fri, 10 Mar 2023 17:29:59 +0000 Subject: [PATCH 09/19] feat(liquidator): attempt at graceful termination on api disconnect --- bots/bridge-tester/src/issue.ts | 2 +- bots/bridge-tester/src/utils.ts | 9 -- .../test/_initialization/initialize.test.ts | 21 ++-- bots/lending-liquidator/src/liquidate.ts | 114 ++++++++++-------- bots/lending-liquidator/src/utils.ts | 45 +++++++ .../test/integration/liquidate.test.ts | 9 +- bots/lending-liquidator/test/utils.ts | 5 +- 7 files changed, 132 insertions(+), 73 deletions(-) create mode 100644 bots/lending-liquidator/src/utils.ts diff --git a/bots/bridge-tester/src/issue.ts b/bots/bridge-tester/src/issue.ts index 28cdc15..f893a43 100644 --- a/bots/bridge-tester/src/issue.ts +++ b/bots/bridge-tester/src/issue.ts @@ -15,7 +15,7 @@ import _ from "underscore"; import { LOAD_TEST_ISSUE_AMOUNT } from "./consts"; import logger from "./logger"; -import { sleep, waitForEmptyMempool } from "./utils"; +import { sleep } from "./utils"; export class Issue { interBtcApi: InterBtcApi; diff --git a/bots/bridge-tester/src/utils.ts b/bots/bridge-tester/src/utils.ts index 3ab6de9..ebcb9be 100644 --- a/bots/bridge-tester/src/utils.ts +++ b/bots/bridge-tester/src/utils.ts @@ -1,12 +1,3 @@ -import { BitcoinCoreClient } from "@interlay/interbtc-api"; - -export async function waitForEmptyMempool( - bitcoinCoreClient: BitcoinCoreClient -): Promise { - while ((await bitcoinCoreClient.getMempoolInfo()).size === 0) { - await sleep(1000); - } -} export async function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/bots/bridge-tester/test/_initialization/initialize.test.ts b/bots/bridge-tester/test/_initialization/initialize.test.ts index 821e5d8..8944021 100644 --- a/bots/bridge-tester/test/_initialization/initialize.test.ts +++ b/bots/bridge-tester/test/_initialization/initialize.test.ts @@ -4,7 +4,6 @@ import { assert } from "chai"; import { BitcoinCoreClient, - initializeStableConfirmations, issueSingle, InterBtcApi, DefaultInterBtcApi, @@ -14,6 +13,7 @@ import { InterbtcPrimitivesVaultId, newVaultId, CollateralCurrencyExt, + setRawStorage, } from "@interlay/interbtc-api"; import { DEFAULT_PARACHAIN_ENDPOINT, @@ -87,15 +87,20 @@ describe.skip("Initialize parachain state", () => { ]); if (stableBitcoinConfirmations != 0 || stableParachainConfirmations != 0) { - await initializeStableConfirmations( + console.log("Initializing stable block confirmations..."); + await setRawStorage( api, - { - bitcoinConfirmations: stableBitcoinConfirmationsToSet, - parachainConfirmations: stableParachainConfirmationsToSet, - }, - sudoAccount, - bitcoinCoreClient + 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(), diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 672f8ee..d7e2767 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -2,6 +2,7 @@ import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; import { APPROX_BLOCK_TIME_MS } from "./consts"; +import { waitForEvent } from "./utils"; type CollateralAndValue = { collateral: MonetaryAmount, @@ -78,54 +79,71 @@ function liquidationStrategy( }) }); return result; -} - -export async function startLiquidator(interBtcApi: InterBtcApi): Promise { - console.log("Starting lending liquidator..."); - const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); - - let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] - let chainAssets = new Set([...nativeCurrency, ...foreignAssets]); - if (interBtcApi.account == undefined) { - return Promise.reject("No account set for the lending-liquidator"); } - const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account); - console.log("Listening to new blocks..."); - await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { - console.log(`Scanning block: #${header.number}`); - const liquidatorBalance: Map = new Map(); - const oracleRates: Map> = new Map(); - try { - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ - Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), - Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), - interBtcApi.loans.getUndercollateralizedBorrowers(), - interBtcApi.loans.getLoansMarkets(), - interBtcApi.assetRegistry.getForeignAssets() - ]); - - console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); - const potentialLiquidation = liquidationStrategy( - interBtcApi, - liquidatorBalance, - oracleRates, - undercollateralizedBorrowers, - new Map(marketsArray) - ) as [MonetaryAmount, CurrencyExt, AccountId]; - if (potentialLiquidation) { - const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; - console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); - // Either our liquidation will go through, or someone else's will - await Promise.all([ - DefaultTransactionAPI.waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), - interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) - ]); - } - - // Add any new foreign assets to `chainAssets` - chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); - } catch (error) { - console.log("found an error: ", error); + + export async function startLiquidator(interBtcApi: InterBtcApi): Promise { + console.log("Starting lending liquidator..."); + const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); + + let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] + let chainAssets = new Set([...nativeCurrency, ...foreignAssets]); + if (interBtcApi.account == undefined) { + return Promise.reject("No account set for the lending-liquidator"); } - }); + const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account); + console.log("Listening to new blocks..."); + let flagPromiseResolve: () => void; + const flagPromise = new Promise((resolve) => flagPromiseResolve = () => { resolve(); }) + // The block subscription is a Promise that never resolves + const subscriptionPromise = new Promise(() => { + interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { + console.log(`Scanning block: #${header.number}`); + const liquidatorBalance: Map = new Map(); + const oracleRates: Map> = new Map(); + try { + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ + Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), + Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), + interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.loans.getLoansMarkets(), + interBtcApi.assetRegistry.getForeignAssets() + ]); + + console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); + const potentialLiquidation = liquidationStrategy( + interBtcApi, + liquidatorBalance, + oracleRates, + undercollateralizedBorrowers, + new Map(marketsArray) + ) as [MonetaryAmount, CurrencyExt, AccountId]; + if (potentialLiquidation) { + const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; + console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); + // Either our liquidation will go through, or someone else's will + await Promise.all([ + waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), + interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) + ]); + } + + // Add any new foreign assets to `chainAssets` + chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); + } catch (error) { + if (interBtcApi.api.isConnected) { + console.log(error); + } else { + flagPromiseResolve(); + } + } + }); + }); + await Promise.race([ + flagPromise, + subscriptionPromise + ]); + // TODO: investigate why the process doesn't gracefully terminate here even though `Promise.race` finished + // Likely because of a `setTimeout` created by polkadot-js + // Kill the process for now + process.exit(0); } diff --git a/bots/lending-liquidator/src/utils.ts b/bots/lending-liquidator/src/utils.ts new file mode 100644 index 0000000..391f300 --- /dev/null +++ b/bots/lending-liquidator/src/utils.ts @@ -0,0 +1,45 @@ +import type { AnyTuple } from "@polkadot/types/types"; +import { ApiPromise } from "@polkadot/api"; +import { AugmentedEvent, ApiTypes } from "@polkadot/api/types"; +import { EventRecord } from "@polkadot/types/interfaces/system"; + +export async function waitForEvent( + api: ApiPromise, + event: AugmentedEvent, + timeoutMs: number +): Promise { + // Use this function with a timeout. + // Unless the awaited event occurs, this Promise will never resolve. + let timeoutHandle: NodeJS.Timeout; + const timeoutPromise = new Promise((_, reject) => { + timeoutHandle = setTimeout(() => reject(), timeoutMs); + }); + + await Promise.race([ + new Promise((resolve, _reject) => { + api.query.system.events((eventsVec) => { + const events = eventsVec.toArray(); + if (doesArrayContainEvent(events, event)) { + resolve(); + } + }); + }), + timeoutPromise, + ]).then((_) => { + clearTimeout(timeoutHandle); + }); + + return true; +} + +function doesArrayContainEvent( + events: EventRecord[], + eventType: AugmentedEvent +): boolean { + for (const { event } of events) { + if (eventType.is(event)) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index dae9458..f8eeb77 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -8,6 +8,7 @@ import { DEFAULT_PARACHAIN_ENDPOINT, DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, DEFAU import { setExchangeRate, waitRegisteredLendingMarkets } from "../utils"; import { startLiquidator } from "../../src"; import { APPROX_BLOCK_TIME_MS } from "../../src/consts"; +import { waitForEvent } from "../../src/utils"; describe("liquidate", () => { const approx10Blocks = 10 * APPROX_BLOCK_TIME_MS; @@ -104,7 +105,7 @@ describe("liquidate", () => { ]); const [eventFound] = await Promise.all([ - DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.ActivatedMarket, approx10Blocks), + waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.ActivatedMarket, approx10Blocks), api.tx.sudo.sudo(addMarkets).signAndSend(sudoAccount), ]); expect( @@ -115,10 +116,10 @@ describe("liquidate", () => { }); after(async () => { - api.disconnect(); + await api.disconnect(); }); - it("should lend expected amount of currency to protocol", async function () { + it("should liquidate undercollateralized borrower", async function () { this.timeout(20 * approx10Blocks); const depositAmount = newMonetaryAmount(1000, underlyingCurrency1, true); @@ -146,7 +147,7 @@ describe("liquidate", () => { // Do not `await` so it runs in the background startLiquidator(sudoInterBtcAPI); - const liquidationEventFoundPromise = DefaultTransactionAPI.waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); + const liquidationEventFoundPromise = waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); // crash the collateral exchange rate const newExchangeRate = "0x00000000000000000000100000000000"; diff --git a/bots/lending-liquidator/test/utils.ts b/bots/lending-liquidator/test/utils.ts index c070a90..8747fff 100644 --- a/bots/lending-liquidator/test/utils.ts +++ b/bots/lending-liquidator/test/utils.ts @@ -1,5 +1,5 @@ -import { CurrencyExt, InterBtcApi, storageKeyToNthInner, getStorageMapItemKey, createExchangeRateOracleKey, setStorageAtKey, sleep, SLEEP_TIME_MS } from "@interlay/interbtc-api"; +import { CurrencyExt, InterBtcApi, storageKeyToNthInner, createExchangeRateOracleKey, setStorageAtKey, sleep, SLEEP_TIME_MS } from "@interlay/interbtc-api"; import { ApiPromise } from "@polkadot/api"; @@ -22,8 +22,7 @@ export async function setExchangeRate( // Change Exchange rate storage for currency. const exchangeRateOracleKey = createExchangeRateOracleKey(api, currency); - - const exchangeRateStorageKey = getStorageMapItemKey("Oracle", "Aggregate", exchangeRateOracleKey.toHex()); + const exchangeRateStorageKey = sudoInterBtcAPI.api.query.oracle.aggregate.key(exchangeRateOracleKey); await setStorageAtKey(sudoInterBtcAPI.api, exchangeRateStorageKey, newExchangeRateHex, sudoAccount); } From 5b85b1de10be811375884f516fe479897364c18d Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 14 Mar 2023 13:16:41 +0000 Subject: [PATCH 10/19] fix: update usage of docker compose --- .github/workflows/test.yml | 2 +- README.md | 2 +- package.json | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b13163f..806ccc7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,5 +18,5 @@ jobs: username: ${{ secrets.GITLAB_USERNAME }} password: ${{ secrets.GITLAB_TOKEN }} - name: Run and set up the parachain, oracle, staked relayer and vault - run: yarn install && yarn docker-parachain-start + run: docker-compose up -d - run: yarn install && yarn test diff --git a/README.md b/README.md index bc194fe..3065e86 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Repo with agents that perform actions in response to on-chain states. Includes a ## Testing ```bash yarn install -yarn docker-parachain-start +docker-compose up # open a new terminal... yarn test ``` \ No newline at end of file diff --git a/package.json b/package.json index 2c2a50f..35feb21 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,7 @@ "scripts": { "test": "yarn workspaces run test", - "build": "yarn workspaces run build", - "docker-parachain-start": "ts-node scripts/setup-parachain-docker.ts", - "docker-parachain-stop": "docker-compose -f local-setup/docker-compose.yml down -v" + "build": "yarn workspaces run build" }, "devDependencies": { "@types/chai": "^4.2.16", From 919a09f08099716592ac6a29a96feb32dd15986e Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 14 Mar 2023 16:09:02 +0000 Subject: [PATCH 11/19] chore(loans): upgrade `interbtc-api` dependency --- .../test/_initialization/initialize.test.ts | 8 +++-- bots/lending-liquidator/package.json | 6 ++-- .../test/integration/liquidate.test.ts | 6 ++-- yarn.lock | 31 +++++++++++++++++++ 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/bots/bridge-tester/test/_initialization/initialize.test.ts b/bots/bridge-tester/test/_initialization/initialize.test.ts index 8944021..381d116 100644 --- a/bots/bridge-tester/test/_initialization/initialize.test.ts +++ b/bots/bridge-tester/test/_initialization/initialize.test.ts @@ -12,8 +12,7 @@ import { newMonetaryAmount, InterbtcPrimitivesVaultId, newVaultId, - CollateralCurrencyExt, - setRawStorage, + CollateralCurrencyExt } from "@interlay/interbtc-api"; import { DEFAULT_PARACHAIN_ENDPOINT, @@ -26,6 +25,7 @@ import { DEFAULT_SUDO_URI, DEFAULT_USER_1_URI, } from "../config"; +import { u32 } from "@polkadot/types-codec"; describe.skip("Initialize parachain state", () => { let api: ApiPromise; @@ -130,3 +130,7 @@ describe.skip("Initialize parachain state", () => { ); }); }); +function setRawStorage(api: ApiPromise, arg1: string, arg2: u32, sudoAccount: KeyringPair) { + throw new Error("Function not implemented."); +} + diff --git a/bots/lending-liquidator/package.json b/bots/lending-liquidator/package.json index 9850ef1..b601445 100644 --- a/bots/lending-liquidator/package.json +++ b/bots/lending-liquidator/package.json @@ -21,8 +21,7 @@ "test:integration": "mocha test/**/*.test.ts --timeout 10000000" }, "dependencies": { - "@interlay/interbtc-api": "1.21.0", - "@interlay/monetary-js": "0.7.0", + "@interlay/interbtc-api": "2.0.1", "@types/big.js": "6.1.2", "@types/node": "^14.14.31", "@types/underscore": "^1.11.2", @@ -36,6 +35,9 @@ "yargs": "^17.0.1", "pino": "^7.5.1" }, + "resolutions": { + "bn.js": "4.12.0" + }, "mocha": { "reporter": "spec", "require": "ts-node/register", diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index f8eeb77..2a64201 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -51,11 +51,11 @@ describe("liquidate", () => { lendTokenId3 = newCurrencyId(api, { lendToken: { id: 3 } } as LendToken); underlyingCurrencyId1 = api.consts.escrowRewards.getNativeCurrencyId; - underlyingCurrency1 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId1); + underlyingCurrency1 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.api, underlyingCurrencyId1); underlyingCurrencyId2 = api.consts.currency.getRelayChainCurrencyId; - underlyingCurrency2 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId2); + underlyingCurrency2 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.api, underlyingCurrencyId2); underlyingCurrencyId3 = api.consts.currency.getWrappedCurrencyId; - underlyingCurrency3 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.assetRegistry, sudoInterBtcAPI.loans, underlyingCurrencyId3); + underlyingCurrency3 = await currencyIdToMonetaryCurrency(sudoInterBtcAPI.api, underlyingCurrencyId3); const percentageToPermill = (percentage: number) => percentage * 10000; diff --git a/yarn.lock b/yarn.lock index e37ab2b..a2319a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -33,11 +33,33 @@ isomorphic-fetch "^3.0.0" regtest-client "^0.2.0" +"@interlay/interbtc-api@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-api/-/interbtc-api-2.0.1.tgz#f1dae8047eca82bee8b86d0ffdc23cc1cae644ef" + integrity sha512-+C+o+wO7QPZP6nqRJzplEejSOzYfBg8GIQaEoumLQYeqQr1lUSJHBULgI2CcT4tzGnxL3iJS6wEeVS9R7di35g== + dependencies: + "@interlay/esplora-btc-api" "0.4.0" + "@interlay/interbtc-types" "1.12.0" + "@interlay/monetary-js" "0.7.2" + "@polkadot/api" "9.11.1" + big.js "6.1.1" + bitcoin-core "^3.0.0" + bitcoinjs-lib "^5.2.0" + bn.js "4.12.0" + cross-fetch "^3.0.6" + isomorphic-fetch "^3.0.0" + regtest-client "^0.2.0" + "@interlay/interbtc-types@1.11.2": version "1.11.2" resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.11.2.tgz#e856b70bdc8ca8905ca841fc7181a60418959a91" integrity sha512-38kEIPtn6xKQchA992sZvk43aCBBk0snYiBg3JRsrARiIOEd1v452f+QWHv7L7dxFAfUtOJaw5MHPNl6c46NYQ== +"@interlay/interbtc-types@1.12.0": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@interlay/interbtc-types/-/interbtc-types-1.12.0.tgz#07dc8e15690292387124dbc2bbb7bf5bc8b68001" + integrity sha512-ELJa2ftIbe8Ds2ejS7kO5HumN9EB5l2OBi3Qsy5iHJsHKq2HtXfFoKnW38HarM6hADrWG+e/yNGHSKJIJzEZuA== + "@interlay/monetary-js@0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.0.tgz#7142f7d2b86acf77d8524cee22b4ae2c5ba2d3a6" @@ -47,6 +69,15 @@ big.js "6.1.1" typescript "^4.3.2" +"@interlay/monetary-js@0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@interlay/monetary-js/-/monetary-js-0.7.2.tgz#a54a315b60be12f5b1a9c31f0d71d5e8ee7ba174" + integrity sha512-SqNKJKBEstXuLnzqWi+ON+9ImrNVlKqZpXfiTtT3bcvxqh4jnVsGbYVe2I0FqDWtilCWq6/1RjKkpaSG2dvJhA== + dependencies: + "@types/big.js" "6.1.2" + big.js "6.1.1" + typescript "^4.3.2" + "@noble/hashes@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" From aa8f5a93fb60eab1b7dd1895ed9379c586049435 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Tue, 14 Mar 2023 17:15:09 +0000 Subject: [PATCH 12/19] feat(liquidator): run from CLI --- bots/lending-liquidator/.env.local | 2 + bots/lending-liquidator/.env.testnet | 2 + bots/lending-liquidator/README.md | 27 +++++++ bots/lending-liquidator/src/index.ts | 30 +++++++- bots/lending-liquidator/src/liquidate.ts | 92 ++++++++++++++---------- 5 files changed, 113 insertions(+), 40 deletions(-) create mode 100644 bots/lending-liquidator/.env.local create mode 100644 bots/lending-liquidator/.env.testnet create mode 100644 bots/lending-liquidator/README.md diff --git a/bots/lending-liquidator/.env.local b/bots/lending-liquidator/.env.local new file mode 100644 index 0000000..90c4b7f --- /dev/null +++ b/bots/lending-liquidator/.env.local @@ -0,0 +1,2 @@ +export LENDING_LIQUIDATOR_ACCOUNT="//Alice" +export PARACHAIN_URL="ws://0.0.0.0:9944" \ No newline at end of file diff --git a/bots/lending-liquidator/.env.testnet b/bots/lending-liquidator/.env.testnet new file mode 100644 index 0000000..87c8831 --- /dev/null +++ b/bots/lending-liquidator/.env.testnet @@ -0,0 +1,2 @@ +export LENDING_LIQUIDATOR_ACCOUNT="" +export PARACHAIN_URL="wss://api-dev-kintsugi.interlay.io/parachain" \ No newline at end of file diff --git a/bots/lending-liquidator/README.md b/bots/lending-liquidator/README.md new file mode 100644 index 0000000..fb7a738 --- /dev/null +++ b/bots/lending-liquidator/README.md @@ -0,0 +1,27 @@ +# lending-liquidator + +Liquidation bot for the lending protocol on the Kintsugi and Interlay parachains. The bot only liquidates loans that can be repaid directly with its balance (swapping on the DEX is not yet supported). + +## Kintsugi Testnet + +```shell +git clone https://github.com/interlay/bots +yarn install +docker-compose up + +# In a different terminal: +source .env.testnet +yarn workspace lending-liquidator live +``` + +## Local Testnet + +```shell +git clone https://github.com/interlay/bots +yarn install +docker-compose up + +# In a different terminal: +source .env.local +yarn workspace lending-liquidator live +``` diff --git a/bots/lending-liquidator/src/index.ts b/bots/lending-liquidator/src/index.ts index 2f8d57f..e1931f7 100644 --- a/bots/lending-liquidator/src/index.ts +++ b/bots/lending-liquidator/src/index.ts @@ -1 +1,29 @@ -export { startLiquidator } from "./liquidate"; +import { createInterBtcApi } from "@interlay/interbtc-api"; +import { Keyring } from "@polkadot/api"; +import { cryptoWaitReady } from "@polkadot/util-crypto"; +import { startLiquidator } from "./liquidate"; +export { startLiquidator }; + +async function main() { + if (!process.env.LENDING_LIQUIDATOR_ACCOUNT || !process.env.PARACHAIN_URL) { + Promise.reject( + "`PARACHAIN_URL` and `LENDING_LIQUIDATOR_ACCOUNT` environment variables not set" + ); + } + + await cryptoWaitReady(); + let keyring = new Keyring({ type: "sr25519" }); + let account = keyring.addFromUri(`${process.env.LENDING_LIQUIDATOR_ACCOUNT}`); + console.log(`Bot account: ${account.address}`); + const interBtcApi = await createInterBtcApi( + process.env.PARACHAIN_URL as string, + undefined, + account + ); + await startLiquidator(interBtcApi) +} + +main().catch((err) => { + console.error("Error during bot operation:"); + console.error(err); +}); diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index d7e2767..d27a03f 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,6 +1,8 @@ import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI, decodePermill } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; +import { AddressOrPair } from "@polkadot/api/types"; + import { APPROX_BLOCK_TIME_MS } from "./consts"; import { waitForEvent } from "./utils"; @@ -57,12 +59,16 @@ function liquidationStrategy( let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; undercollateralizedBorrowers.forEach((position) => { + // Among the collateral currencies locked by this borrower, which one is worth the most? const highestValueCollateral = findHighestValueCollateral(position.collateralPositions, oracleRates); if (!highestValueCollateral) { return; } + // Among the borrowed currencies of this borrower, which one accepts the biggest repayment? + // (i.e. is worth the most after considering the close factor) position.borrowPositions.forEach((loan) => { const totalDebt = loan.amount.add(loan.accumulatedDebt); + // If this loan can be repaid using the lending-liquidator's balance if (liquidatorBalance.has(totalDebt.currency)) { const loansMarket = markets.get(totalDebt.currency) as LoansMarket; const closeFactor = decodePermill(loansMarket.closeFactor); @@ -81,16 +87,51 @@ function liquidationStrategy( return result; } + async function checkForLiquidations(interBtcApi: InterBtcApi, chainAssets: Set): Promise { + const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account as AddressOrPair); + const liquidatorBalance: Map = new Map(); + const oracleRates: Map> = new Map(); + + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ + Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), + Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), + interBtcApi.loans.getUndercollateralizedBorrowers(), + interBtcApi.loans.getLoansMarkets(), + interBtcApi.assetRegistry.getForeignAssets() + ]); + + console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); + // Run the liquidation strategy to find the most profitable liquidation + const potentialLiquidation = liquidationStrategy( + interBtcApi, + liquidatorBalance, + oracleRates, + undercollateralizedBorrowers, + new Map(marketsArray) + ) as [MonetaryAmount, CurrencyExt, AccountId]; + if (potentialLiquidation) { + const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; + console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); + // Either our liquidation will go through, or someone else's will + await Promise.all([ + waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), + interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) + ]); + } + + // Add any new foreign assets to `chainAssets` + chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); + } + export async function startLiquidator(interBtcApi: InterBtcApi): Promise { + if (interBtcApi.account == undefined) { + return Promise.reject("No account set for the lending-liquidator"); + } console.log("Starting lending liquidator..."); + const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); - let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] let chainAssets = new Set([...nativeCurrency, ...foreignAssets]); - if (interBtcApi.account == undefined) { - return Promise.reject("No account set for the lending-liquidator"); - } - const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account); console.log("Listening to new blocks..."); let flagPromiseResolve: () => void; const flagPromise = new Promise((resolve) => flagPromiseResolve = () => { resolve(); }) @@ -98,44 +139,17 @@ function liquidationStrategy( const subscriptionPromise = new Promise(() => { interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { console.log(`Scanning block: #${header.number}`); - const liquidatorBalance: Map = new Map(); - const oracleRates: Map> = new Map(); - try { - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ - Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), - Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), - interBtcApi.loans.getUndercollateralizedBorrowers(), - interBtcApi.loans.getLoansMarkets(), - interBtcApi.assetRegistry.getForeignAssets() - ]); - - console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); - const potentialLiquidation = liquidationStrategy( - interBtcApi, - liquidatorBalance, - oracleRates, - undercollateralizedBorrowers, - new Map(marketsArray) - ) as [MonetaryAmount, CurrencyExt, AccountId]; - if (potentialLiquidation) { - const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; - console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); - // Either our liquidation will go through, or someone else's will - await Promise.all([ - waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), - interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) - ]); - } - - // Add any new foreign assets to `chainAssets` - chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); - } catch (error) { + checkForLiquidations(interBtcApi, chainAssets).catch((reason) => { if (interBtcApi.api.isConnected) { - console.log(error); + console.error(reason); } else { + // If the provider connection is closed, terminate the bot + // Used in the test to shut the bot down + console.log("Terminating..."); flagPromiseResolve(); } - } + + }); }); }); await Promise.race([ From 9f1f5042a74aa6d567d7e5050515834c55c85b88 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:02:06 +0000 Subject: [PATCH 13/19] feat(liquidator): improve bot termination logic, fix some review commens --- bots/bridge-tester/package.json | 2 +- bots/lending-liquidator/package.json | 4 +- bots/lending-liquidator/src/index.ts | 5 +- bots/lending-liquidator/src/liquidate.ts | 64 ++++++++----------- .../test/integration/liquidate.test.ts | 6 +- bots/lending-liquidator/test/utils.ts | 2 +- 6 files changed, 37 insertions(+), 46 deletions(-) diff --git a/bots/bridge-tester/package.json b/bots/bridge-tester/package.json index 4f11b3a..9825a76 100644 --- a/bots/bridge-tester/package.json +++ b/bots/bridge-tester/package.json @@ -4,7 +4,7 @@ "description": "Bot for testing the Interlay and Kintsugi bridges.", "main": "build/index.js", "typings": "build/index.d.ts", - "repository": "https://github.com/interlay/bots/bridge-tester", + "repository": "https://github.com/interlay/bots/bots/bridge-tester", "author": "Interlay", "license": "Apache-2.0", "engines": { diff --git a/bots/lending-liquidator/package.json b/bots/lending-liquidator/package.json index b601445..a80e637 100644 --- a/bots/lending-liquidator/package.json +++ b/bots/lending-liquidator/package.json @@ -1,11 +1,11 @@ { "name": "lending-liquidator", - "version": "0.0.0", + "version": "1.0.0", "description": "Bot for liquidating undercollateralized borrowers on the Interlay and Kintsugi networks", "main": "build/index.js", "typings": "build/index.d.ts", - "repository": "https://github.com/interlay/bots/lending-liquidator", + "repository": "https://github.com/interlay/bots/bots/lending-liquidator", "author": "Interlay", "license": "Apache-2.0", "engines": { diff --git a/bots/lending-liquidator/src/index.ts b/bots/lending-liquidator/src/index.ts index e1931f7..89d27d6 100644 --- a/bots/lending-liquidator/src/index.ts +++ b/bots/lending-liquidator/src/index.ts @@ -7,10 +7,9 @@ export { startLiquidator }; async function main() { if (!process.env.LENDING_LIQUIDATOR_ACCOUNT || !process.env.PARACHAIN_URL) { Promise.reject( - "`PARACHAIN_URL` and `LENDING_LIQUIDATOR_ACCOUNT` environment variables not set" + "`PARACHAIN_URL` and `LENDING_LIQUIDATOR_ACCOUNT` environment variables not set" ); } - await cryptoWaitReady(); let keyring = new Keyring({ type: "sr25519" }); let account = keyring.addFromUri(`${process.env.LENDING_LIQUIDATOR_ACCOUNT}`); @@ -20,7 +19,7 @@ async function main() { undefined, account ); - await startLiquidator(interBtcApi) + await new Promise(async () => await startLiquidator(interBtcApi)); } main().catch((err) => { diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index d27a03f..424f098 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -87,17 +87,21 @@ function liquidationStrategy( return result; } - async function checkForLiquidations(interBtcApi: InterBtcApi, chainAssets: Set): Promise { + async function checkForLiquidations(interBtcApi: InterBtcApi): Promise { const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account as AddressOrPair); const liquidatorBalance: Map = new Map(); const oracleRates: Map> = new Map(); + const nativeCurrencies = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] + // This call slows down potential liquidations. + // TODO: keep a local cache of foreign assets and fetch + const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); + let chainAssets = new Set([...nativeCurrencies, ...foreignAssets]); - const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray, foreignAssets] = await Promise.all([ + const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray] = await Promise.all([ Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), interBtcApi.loans.getUndercollateralizedBorrowers(), interBtcApi.loans.getLoansMarkets(), - interBtcApi.assetRegistry.getForeignAssets() ]); console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); @@ -114,50 +118,36 @@ function liquidationStrategy( console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); // Either our liquidation will go through, or someone else's will await Promise.all([ - waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.ActivatedMarket, 10 * APPROX_BLOCK_TIME_MS), + waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.LiquidatedBorrow, 10 * APPROX_BLOCK_TIME_MS), interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) ]); } - - // Add any new foreign assets to `chainAssets` - chainAssets = new Set([...Array.from(chainAssets), ...foreignAssets]); } - - export async function startLiquidator(interBtcApi: InterBtcApi): Promise { + + export async function startLiquidator(interBtcApi: InterBtcApi): Promise<() => void> { if (interBtcApi.account == undefined) { - return Promise.reject("No account set for the lending-liquidator"); + throw new Error("No account set for the lending-liquidator"); } - console.log("Starting lending liquidator..."); - const foreignAssets = await interBtcApi.assetRegistry.getForeignAssets(); - let nativeCurrency = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] - let chainAssets = new Set([...nativeCurrency, ...foreignAssets]); - console.log("Listening to new blocks..."); - let flagPromiseResolve: () => void; + let flagPromiseResolve: (() => void) | undefined; const flagPromise = new Promise((resolve) => flagPromiseResolve = () => { resolve(); }) - // The block subscription is a Promise that never resolves - const subscriptionPromise = new Promise(() => { - interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { - console.log(`Scanning block: #${header.number}`); - checkForLiquidations(interBtcApi, chainAssets).catch((reason) => { - if (interBtcApi.api.isConnected) { - console.error(reason); - } else { - // If the provider connection is closed, terminate the bot - // Used in the test to shut the bot down - console.log("Terminating..."); - flagPromiseResolve(); - } - - }); + console.log("Starting lending liquidator..."); + console.log("Listening to new blocks..."); + const unsubscribe = await interBtcApi.api.rpc.chain.subscribeNewHeads(async (header) => { + console.log(`Scanning block: #${header.number}`); + await checkForLiquidations(interBtcApi).catch((reason) => { + console.error(reason); }); }); - await Promise.race([ - flagPromise, - subscriptionPromise - ]); - // TODO: investigate why the process doesn't gracefully terminate here even though `Promise.race` finished + // TODO: investigate why the process doesn't gracefully terminate here even though to + // block listener is unsubscribed from and the api is disconnected // Likely because of a `setTimeout` created by polkadot-js // Kill the process for now - process.exit(0); + flagPromise.then(async () => { + unsubscribe(); + await interBtcApi.disconnect(); + process.exit(0); + }); + + return flagPromiseResolve as () => void; } diff --git a/bots/lending-liquidator/test/integration/liquidate.test.ts b/bots/lending-liquidator/test/integration/liquidate.test.ts index 2a64201..aa2c269 100644 --- a/bots/lending-liquidator/test/integration/liquidate.test.ts +++ b/bots/lending-liquidator/test/integration/liquidate.test.ts @@ -144,8 +144,9 @@ describe("liquidate", () => { await userInterBtcAPI.loans.borrow(borrowAmount2.currency, borrowAmount2); // Start liquidation listener - // Do not `await` so it runs in the background - startLiquidator(sudoInterBtcAPI); + // `await`ing here is only for getting the block subscription unsubscribe function. + // Scanning blocks and liquidating does not block the return here + const terminate = await startLiquidator(sudoInterBtcAPI); const liquidationEventFoundPromise = waitForEvent(sudoInterBtcAPI.api, sudoInterBtcAPI.api.events.loans.LiquidatedBorrow, approx10Blocks); @@ -159,5 +160,6 @@ describe("liquidate", () => { liquidationOccured, `Expected the bot to have liquidated user ${userAccountId.toString()}, in collateral currency ${depositAmount.currency.ticker}` ).to.be.true; + terminate(); }); }); diff --git a/bots/lending-liquidator/test/utils.ts b/bots/lending-liquidator/test/utils.ts index 8747fff..f3e238a 100644 --- a/bots/lending-liquidator/test/utils.ts +++ b/bots/lending-liquidator/test/utils.ts @@ -10,7 +10,7 @@ export async function setExchangeRate( ): Promise { const { account: sudoAccount, api } = sudoInterBtcAPI; if (!sudoAccount) { - throw new Error("callWithExchangeRate: sudo account is not set."); + throw new Error("setExchangeRate: sudo account is not set."); } // Remove authorized oracle to make sure price won't be fed. const authorizedOracles = await api.query.oracle.authorizedOracles.entries(); From 92415171e35b9e6ba06240a160d33e071a2eb4eb Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Wed, 15 Mar 2023 18:05:45 +0000 Subject: [PATCH 14/19] chore(liquidator): add profitability todo --- bots/lending-liquidator/src/liquidate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 424f098..d0ad495 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -56,6 +56,7 @@ function liquidationStrategy( undercollateralizedBorrowers: UndercollateralizedPosition[], markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { + // TODO: check liquidation premium in each market and filter out / ignore unprofitable liquidations let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; undercollateralizedBorrowers.forEach((position) => { From 161e524bd95d383a12b1d5541d01906cfbcea6ad Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Thu, 16 Mar 2023 13:34:04 +0000 Subject: [PATCH 15/19] fix(liquidator): add profitability check --- bots/lending-liquidator/src/liquidate.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index d0ad495..e1b6fff 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -56,7 +56,6 @@ function liquidationStrategy( undercollateralizedBorrowers: UndercollateralizedPosition[], markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { - // TODO: check liquidation premium in each market and filter out / ignore unprofitable liquidations let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; undercollateralizedBorrowers.forEach((position) => { @@ -77,8 +76,12 @@ function liquidationStrategy( const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; // Can only repay a fraction of the total debt, defined by the `closeFactor` const repayableAmount = totalDebt.mul(closeFactor).min(balance.free); - const referenceRepayable = referenceValue(repayableAmount, rate).min(highestValueCollateral.referenceValue); - if (referenceRepayable.gt(maxRepayableLoan)) { + const referenceRepayable = referenceValue(repayableAmount, rate); + if ( + // The liquidation must be profitable + highestValueCollateral.referenceValue.gt(referenceRepayable) && + referenceRepayable.gt(maxRepayableLoan) + ) { maxRepayableLoan = referenceRepayable; result = [repayableAmount, highestValueCollateral.collateral.currency, position.accountId]; } @@ -104,7 +107,7 @@ function liquidationStrategy( interBtcApi.loans.getUndercollateralizedBorrowers(), interBtcApi.loans.getLoansMarkets(), ]); - + console.log(`undercollateralized borrowers: ${undercollateralizedBorrowers.length}`); // Run the liquidation strategy to find the most profitable liquidation const potentialLiquidation = liquidationStrategy( @@ -117,11 +120,7 @@ function liquidationStrategy( if (potentialLiquidation) { const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); - // Either our liquidation will go through, or someone else's will - await Promise.all([ - waitForEvent(interBtcApi.api, interBtcApi.api.events.loans.LiquidatedBorrow, 10 * APPROX_BLOCK_TIME_MS), - interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate) - ]); + await interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate); } } From 768c1a56566a715bf54fcd9215b119bab4954d71 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Thu, 16 Mar 2023 16:53:49 +0000 Subject: [PATCH 16/19] feat(liquidator): auto-redeem qToken reward, improve docs --- .../{.env.testnet => .env.example} | 2 +- bots/lending-liquidator/README.md | 22 +++++++++++++------ bots/lending-liquidator/src/liquidate.ts | 13 +++++++++-- 3 files changed, 27 insertions(+), 10 deletions(-) rename bots/lending-liquidator/{.env.testnet => .env.example} (52%) diff --git a/bots/lending-liquidator/.env.testnet b/bots/lending-liquidator/.env.example similarity index 52% rename from bots/lending-liquidator/.env.testnet rename to bots/lending-liquidator/.env.example index 87c8831..fbdfbd6 100644 --- a/bots/lending-liquidator/.env.testnet +++ b/bots/lending-liquidator/.env.example @@ -1,2 +1,2 @@ -export LENDING_LIQUIDATOR_ACCOUNT="" +export LENDING_LIQUIDATOR_ACCOUNT="YOUR ACCOUNT MNEMONIC" export PARACHAIN_URL="wss://api-dev-kintsugi.interlay.io/parachain" \ No newline at end of file diff --git a/bots/lending-liquidator/README.md b/bots/lending-liquidator/README.md index fb7a738..0e451e7 100644 --- a/bots/lending-liquidator/README.md +++ b/bots/lending-liquidator/README.md @@ -1,8 +1,12 @@ # lending-liquidator -Liquidation bot for the lending protocol on the Kintsugi and Interlay parachains. The bot only liquidates loans that can be repaid directly with its balance (swapping on the DEX is not yet supported). +Liquidation bot for the lending protocol on the Kintsugi and Interlay parachains. The bot only liquidates loans that can be repaid directly with its balance (swapping on the DEX is not yet supported). Positions are only liquidated if they are profitable at the time of liquidation. -## Kintsugi Testnet +While this bot can be used by anyone to arbitrage the lending protocol, its main purpose is educational. It is ditributed under the Apache 2.0 license: Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +## Local Testnet + +Protip: Instead of using the docker-compose file provided, you can fork mainnet using [chopsticks](https://github.com/AcalaNetwork/chopsticks) and run the bot against a local fork to do a trial of the bot in production. ```shell git clone https://github.com/interlay/bots @@ -10,18 +14,22 @@ yarn install docker-compose up # In a different terminal: -source .env.testnet +source .env.local yarn workspace lending-liquidator live ``` -## Local Testnet +## Production +### Set environment variables +Create a mnemonic for the bot account (for instance, via the polkadot-js extension) and fund it. Note that the funds have to be sent to the account's address on the Kintsugi / Interlay parachain, as opposed to the relay chain. Set this mnemonic in the `LENDING_LIQUIDATOR_ACCOUNT` environment variable; you can use the [example environment file](./.env.example) to avoid manually setting this variable each time. + +Run your own parachain node or connect to the Kintsugi (`wss://api-kusama.interlay.io/parachain`) or Interlay (`wss://api.interlay.io/parachain`) nodes run by Interlay. Set the websocket URL in the `PARACHAIN_URL` environment variable; you can use the [example environment file](./.env.example) to avoid manually setting this variable each time. + +### Install and run bot ```shell git clone https://github.com/interlay/bots yarn install -docker-compose up -# In a different terminal: -source .env.local +# Ensure the `LENDING_LIQUIDATOR_ACCOUNT` and `PARACHAIN_URL` are set in the environment yarn workspace lending-liquidator live ``` diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index e1b6fff..9f494a3 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -1,4 +1,4 @@ -import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI, decodePermill } from "@interlay/interbtc-api"; +import { ChainBalance, CollateralPosition, CurrencyExt, InterBtcApi, LoansMarket, newMonetaryAmount, UndercollateralizedPosition, addressOrPairAsAccountId, DefaultTransactionAPI, decodePermill, decodeFixedPointType } from "@interlay/interbtc-api"; import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; import { AddressOrPair } from "@polkadot/api/types"; @@ -72,6 +72,9 @@ function liquidationStrategy( if (liquidatorBalance.has(totalDebt.currency)) { const loansMarket = markets.get(totalDebt.currency) as LoansMarket; const closeFactor = decodePermill(loansMarket.closeFactor); + // `liquidationIncentive` includes the liquidation premium + // e.g. if the premium is 10%, `liquidationIncentive` is 110% + const liquidationIncentive = decodeFixedPointType(loansMarket.liquidateIncentive) const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; // Can only repay a fraction of the total debt, defined by the `closeFactor` @@ -79,7 +82,7 @@ function liquidationStrategy( const referenceRepayable = referenceValue(repayableAmount, rate); if ( // The liquidation must be profitable - highestValueCollateral.referenceValue.gt(referenceRepayable) && + highestValueCollateral.referenceValue.gte(referenceRepayable.mul(liquidationIncentive)) && referenceRepayable.gt(maxRepayableLoan) ) { maxRepayableLoan = referenceRepayable; @@ -121,6 +124,12 @@ function liquidationStrategy( const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; console.log(`Liquidating ${borrower.toString()} with ${amountToRepay.toHuman()} ${amountToRepay.currency.ticker}, collateral: ${collateralToLiquidate.ticker}`); await interBtcApi.loans.liquidateBorrowPosition(borrower, amountToRepay.currency, amountToRepay, collateralToLiquidate); + // Try withdrawing the collateral reward (received as qTokens which need to be redeemed for the underlying). + // This might fail if there is insufficient liquidity in the protocol. + // TODO: periodically try withdrawing qToken balance + interBtcApi.loans.withdrawAll(collateralToLiquidate).catch((error) => { + console.error("Error redeeming collateral reward: ", error); + }); } } From c108792cd8140fbb9f60da83a8a65f57e92c3d33 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Fri, 17 Mar 2023 14:39:56 +0000 Subject: [PATCH 17/19] fix(liquidator): rates map key, catch oracle errors, default ref currency --- bots/lending-liquidator/src/liquidate.ts | 48 ++++++++++++++++-------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index 9f494a3..c6b4b62 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -3,25 +3,27 @@ import { Currency, ExchangeRate, MonetaryAmount } from "@interlay/monetary-js"; import { AccountId } from "@polkadot/types/interfaces"; import { AddressOrPair } from "@polkadot/api/types"; -import { APPROX_BLOCK_TIME_MS } from "./consts"; -import { waitForEvent } from "./utils"; - type CollateralAndValue = { collateral: MonetaryAmount, referenceValue: MonetaryAmount } -function referenceValue(balance: MonetaryAmount, rate: ExchangeRate | undefined): MonetaryAmount { +function referenceValue( + balance: MonetaryAmount, + rate: ExchangeRate | undefined, + referenceCurrency: C +): MonetaryAmount { if (!rate) { - return new MonetaryAmount(balance.currency, 0); + return new MonetaryAmount(referenceCurrency, 0); } // Convert to the reference currency (BTC) return rate.toBase(balance) } -function findHighestValueCollateral( +function findHighestValueCollateral( positions: CollateralPosition[], - rates: Map> + rates: Map>, + referenceCurrency: C ): CollateralAndValue | undefined { // It should be impossible to have no collateral currency locked, but just in case if (positions.length == 0) { @@ -31,12 +33,17 @@ function findHighestValueCollateral( collateral: positions[0].amount, referenceValue: referenceValue( newMonetaryAmount(0, positions[0].amount.currency), - rates.get(positions[0].amount.currency) + rates.get(positions[0].amount.currency.ticker), + referenceCurrency ) }; return positions.reduce( (previous, current) => { - const liquidatableValue = referenceValue(current.amount, rates.get(current.amount.currency)); + const liquidatableValue = referenceValue( + current.amount, + rates.get(current.amount.currency.ticker), + referenceCurrency + ); if (previous.referenceValue.gt(liquidatableValue)) { return previous; } @@ -52,15 +59,20 @@ function findHighestValueCollateral( function liquidationStrategy( interBtcApi: InterBtcApi, liquidatorBalance: Map, - oracleRates: Map>, + oracleRates: Map>, undercollateralizedBorrowers: UndercollateralizedPosition[], markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { - let maxRepayableLoan = newMonetaryAmount(0, interBtcApi.getWrappedCurrency()); + const referenceCurrency = interBtcApi.getWrappedCurrency(); + let maxRepayableLoan = newMonetaryAmount(0, referenceCurrency); let result: [MonetaryAmount, CurrencyExt, AccountId] | undefined; undercollateralizedBorrowers.forEach((position) => { // Among the collateral currencies locked by this borrower, which one is worth the most? - const highestValueCollateral = findHighestValueCollateral(position.collateralPositions, oracleRates); + const highestValueCollateral = findHighestValueCollateral( + position.collateralPositions, + oracleRates, + referenceCurrency + ); if (!highestValueCollateral) { return; } @@ -76,10 +88,10 @@ function liquidationStrategy( // e.g. if the premium is 10%, `liquidationIncentive` is 110% const liquidationIncentive = decodeFixedPointType(loansMarket.liquidateIncentive) const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; - const rate = oracleRates.get(totalDebt.currency) as ExchangeRate; + const rate = oracleRates.get(totalDebt.currency.ticker) as ExchangeRate; // Can only repay a fraction of the total debt, defined by the `closeFactor` const repayableAmount = totalDebt.mul(closeFactor).min(balance.free); - const referenceRepayable = referenceValue(repayableAmount, rate); + const referenceRepayable = referenceValue(repayableAmount, rate, referenceCurrency); if ( // The liquidation must be profitable highestValueCollateral.referenceValue.gte(referenceRepayable.mul(liquidationIncentive)) && @@ -97,7 +109,7 @@ function liquidationStrategy( async function checkForLiquidations(interBtcApi: InterBtcApi): Promise { const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account as AddressOrPair); const liquidatorBalance: Map = new Map(); - const oracleRates: Map> = new Map(); + const oracleRates: Map> = new Map(); const nativeCurrencies = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] // This call slows down potential liquidations. // TODO: keep a local cache of foreign assets and fetch @@ -106,7 +118,11 @@ function liquidationStrategy( const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray] = await Promise.all([ Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), - Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset).then((rate) => oracleRates.set(asset, rate)))), + Promise.all([...chainAssets].map((asset) => + interBtcApi.oracle.getExchangeRate(asset) + .then((rate) => oracleRates.set(asset.ticker, rate)) + .catch((_) => {}) + )), interBtcApi.loans.getUndercollateralizedBorrowers(), interBtcApi.loans.getLoansMarkets(), ]); From 03d865da38ab70867c3468141a483c175d4b3044 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Fri, 17 Mar 2023 15:23:29 +0000 Subject: [PATCH 18/19] fix(liquidator): use string keys in the markets map --- bots/lending-liquidator/src/liquidate.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bots/lending-liquidator/src/liquidate.ts b/bots/lending-liquidator/src/liquidate.ts index c6b4b62..788b3f0 100644 --- a/bots/lending-liquidator/src/liquidate.ts +++ b/bots/lending-liquidator/src/liquidate.ts @@ -58,10 +58,10 @@ function findHighestValueCollateral( function liquidationStrategy( interBtcApi: InterBtcApi, - liquidatorBalance: Map, + liquidatorBalance: Map, oracleRates: Map>, undercollateralizedBorrowers: UndercollateralizedPosition[], - markets: Map + markets: Map ): [MonetaryAmount, CurrencyExt, AccountId] | undefined { const referenceCurrency = interBtcApi.getWrappedCurrency(); let maxRepayableLoan = newMonetaryAmount(0, referenceCurrency); @@ -81,13 +81,13 @@ function liquidationStrategy( position.borrowPositions.forEach((loan) => { const totalDebt = loan.amount.add(loan.accumulatedDebt); // If this loan can be repaid using the lending-liquidator's balance - if (liquidatorBalance.has(totalDebt.currency)) { - const loansMarket = markets.get(totalDebt.currency) as LoansMarket; + if (liquidatorBalance.has(totalDebt.currency.ticker)) { + const loansMarket = markets.get(totalDebt.currency.ticker) as LoansMarket; const closeFactor = decodePermill(loansMarket.closeFactor); // `liquidationIncentive` includes the liquidation premium // e.g. if the premium is 10%, `liquidationIncentive` is 110% const liquidationIncentive = decodeFixedPointType(loansMarket.liquidateIncentive) - const balance = liquidatorBalance.get(totalDebt.currency) as ChainBalance; + const balance = liquidatorBalance.get(totalDebt.currency.ticker) as ChainBalance; const rate = oracleRates.get(totalDebt.currency.ticker) as ExchangeRate; // Can only repay a fraction of the total debt, defined by the `closeFactor` const repayableAmount = totalDebt.mul(closeFactor).min(balance.free); @@ -108,7 +108,7 @@ function liquidationStrategy( async function checkForLiquidations(interBtcApi: InterBtcApi): Promise { const accountId = addressOrPairAsAccountId(interBtcApi.api, interBtcApi.account as AddressOrPair); - const liquidatorBalance: Map = new Map(); + const liquidatorBalance: Map = new Map(); const oracleRates: Map> = new Map(); const nativeCurrencies = [interBtcApi.getWrappedCurrency(), interBtcApi.getGovernanceCurrency(), interBtcApi.getRelayChainCurrency()] // This call slows down potential liquidations. @@ -117,7 +117,7 @@ function liquidationStrategy( let chainAssets = new Set([...nativeCurrencies, ...foreignAssets]); const [_balancesPromise, _oraclePromise, undercollateralizedBorrowers, marketsArray] = await Promise.all([ - Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset, balance)))), + Promise.all([...chainAssets].map((asset) => interBtcApi.tokens.balance(asset, accountId).then((balance) => liquidatorBalance.set(asset.ticker, balance)))), Promise.all([...chainAssets].map((asset) => interBtcApi.oracle.getExchangeRate(asset) .then((rate) => oracleRates.set(asset.ticker, rate)) @@ -134,7 +134,7 @@ function liquidationStrategy( liquidatorBalance, oracleRates, undercollateralizedBorrowers, - new Map(marketsArray) + new Map(marketsArray.map(([currency, market]) => [currency.ticker, market])) ) as [MonetaryAmount, CurrencyExt, AccountId]; if (potentialLiquidation) { const [amountToRepay, collateralToLiquidate, borrower] = potentialLiquidation; From 9a48e49073de429c5cb0a8b6d1b6da2a22c1aba0 Mon Sep 17 00:00:00 2001 From: Daniel Savu <23065004+daniel-savu@users.noreply.github.com> Date: Fri, 17 Mar 2023 16:06:30 +0000 Subject: [PATCH 19/19] fix(liquidator): readme --- bots/lending-liquidator/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bots/lending-liquidator/README.md b/bots/lending-liquidator/README.md index 0e451e7..0b9947e 100644 --- a/bots/lending-liquidator/README.md +++ b/bots/lending-liquidator/README.md @@ -10,6 +10,7 @@ Protip: Instead of using the docker-compose file provided, you can fork mainnet ```shell git clone https://github.com/interlay/bots +cd bots yarn install docker-compose up @@ -28,6 +29,7 @@ Run your own parachain node or connect to the Kintsugi (`wss://api-kusama.interl ### Install and run bot ```shell git clone https://github.com/interlay/bots +cd bots yarn install # Ensure the `LENDING_LIQUIDATOR_ACCOUNT` and `PARACHAIN_URL` are set in the environment