Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: enable vault withdraw all collateral #727

Merged
merged 7 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "3.8"
services:
interbtc:
image: "interlayhq/interbtc:1.25.0"
image: "interlayhq/interbtc:1.25.3"
command:
- --rpc-external
- --ws-external
Expand Down Expand Up @@ -54,7 +54,7 @@ services:
- "3002:3002"
restart: always
oracle:
image: "interlayhq/interbtc-clients:oracle-parachain-metadata-kintsugi-1.23.0-rc3"
image: "interlayhq/interbtc-clients:oracle-parachain-metadata-kintsugi-1.23.0"
command:
- oracle-parachain-metadata-kintsugi
- --keyring=bob
Expand All @@ -64,7 +64,7 @@ services:
volumes:
- ./docker/oracle-config.json:/oracle-config.json
vault_1:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0-rc3"
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0"
command:
- vault-parachain-metadata-kintsugi
- --keyfile=/keyfile.json
Expand All @@ -81,7 +81,7 @@ services:
volumes:
- ./docker/vault_1-keyfile.json:/keyfile.json
vault_2:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0-rc3"
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0"
command:
- vault-parachain-metadata-kintsugi
- --keyfile=/keyfile.json
Expand All @@ -94,7 +94,7 @@ services:
volumes:
- ./docker/vault_2-keyfile.json:/keyfile.json
vault_3:
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0-rc3"
image: "interlayhq/interbtc-clients:vault-parachain-metadata-kintsugi-1.23.0"
command:
- vault-parachain-metadata-kintsugi
- --keyfile=/keyfile.json
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/interbtc-api",
"version": "2.6.0",
"version": "2.7.0",
"description": "JavaScript library to interact with interBTC",
"main": "build/cjs/src/index.js",
"module": "build/esm/src/index.js",
Expand Down Expand Up @@ -51,7 +51,8 @@
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "jest --watch test/**/*.test.ts",
"update-metadata": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' http://localhost:9933 > src/json/parachain.json",
"update-metadata-kintnet": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' https://api-dev-kintsugi.interlay.io/parachain > src/json/parachain.json"
"update-metadata-kintnet": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' https://api-dev-kintsugi.interlay.io/parachain > src/json/parachain.json",
"update-metadata-interlay": "curl -H 'Content-Type: application/json' -d '{\"id\":\"1\", \"jsonrpc\":\"2.0\", \"method\": \"state_getMetadata\", \"params\":[]}' https://api.interlay.io/parachain > src/json/parachain.json"
},
"engines": {
"node": ">=11"
Expand Down
2 changes: 1 addition & 1 deletion src/json/parachain.json

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/parachain/nomination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ export class DefaultNominationAPI implements NominationAPI {
const parsedNonce = api.createType("Index", definedNonce);
return api.tx.nomination.withdrawCollateral(vaultId, amountAsPlanck, parsedNonce);
}

static async buildWithdrawAllCollateralExtrinsic(
api: ApiPromise,
rewardsAPI: RewardsAPI,
vaultAccountId: AccountId,
collateralCurrency: Currency,
wrappedCurrency: Currency,
nonce?: number
): Promise<SubmittableExtrinsic<"promise", ISubmittableResult>> {
const vaultId = newVaultId(api, vaultAccountId.toString(), collateralCurrency, wrappedCurrency);
const definedNonce = nonce ? nonce : await rewardsAPI.getStakingPoolNonce(collateralCurrency, vaultAccountId);
const parsedNonce = api.createType("Index", definedNonce);
return api.tx.nomination.withdrawCollateral(vaultId, null, parsedNonce);
}

async withdrawCollateral(
vaultAccountId: AccountId,
Expand Down
41 changes: 40 additions & 1 deletion src/parachain/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,22 @@ export interface VaultsAPI {
*/
withdrawCollateral(amount: MonetaryAmount<CollateralCurrencyExt>): Promise<ExtrinsicData>;

/**
* Build withdraw collateral extrinsic (transaction) without sending it.
*
* @param collateralCurrency The collateral currency for which to withdraw all
* @returns A withdraw collateral submittable extrinsic as promise.
*/
buildWithdrawAllCollateralExtrinsic(
collateralCurrency: CollateralCurrencyExt
): Promise<SubmittableExtrinsic<"promise", ISubmittableResult>>;

/**
* @param collateralCurrency The collateral currency for which to withdraw all
* @returns {Promise<ExtrinsicData>} A submittable extrinsic and an event that is emitted when extrinsic is submitted.
*/
withdrawAllCollateral(collateralCurrency: CollateralCurrencyExt): Promise<ExtrinsicData>;

/**
* Build deposit collateral extrinsic (transaction) without sending it.
*
Expand Down Expand Up @@ -462,7 +478,7 @@ export class DefaultVaultsAPI implements VaultsAPI {
this.rewardsAPI,
vaultAccountId,
amount,
this.wrappedCurrency
this.wrappedCurrency,
);
}

Expand All @@ -471,6 +487,29 @@ export class DefaultVaultsAPI implements VaultsAPI {
return { extrinsic: tx, event: this.api.events.vaultRegistry.WithdrawCollateral };
}

async buildWithdrawAllCollateralExtrinsic(
collateralCurrency: CollateralCurrencyExt
): Promise<SubmittableExtrinsic<"promise", ISubmittableResult>> {
const account = this.transactionAPI.getAccount();
if (account == undefined) {
throw new Error("Account must be connected to create a collateral withdrawal request.");
}
const vaultAccountId = addressOrPairAsAccountId(this.api, account);

return await DefaultNominationAPI.buildWithdrawAllCollateralExtrinsic(
this.api,
this.rewardsAPI,
vaultAccountId,
collateralCurrency,
this.wrappedCurrency
);
}

async withdrawAllCollateral(collateralCurrency: CollateralCurrencyExt): Promise<ExtrinsicData> {
const tx = await this.buildWithdrawAllCollateralExtrinsic(collateralCurrency);
return { extrinsic: tx, event: this.api.events.vaultRegistry.WithdrawCollateral };
}

buildDepositCollateralExtrinsic(
amount: MonetaryAmount<CollateralCurrencyExt>
): SubmittableExtrinsic<"promise", ISubmittableResult> {
Expand Down
64 changes: 61 additions & 3 deletions test/integration/parachain/staging/sequential/vaults.partial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
GovernanceCurrency,
AssetRegistryAPI,
DefaultAssetRegistryAPI,
DefaultTransactionAPI,
} from "../../../../../src/index";

import { createSubstrateAPI } from "../../../../../src/factory";
import { VAULT_1_URI, VAULT_2_URI, PARACHAIN_ENDPOINT, VAULT_3_URI, ESPLORA_BASE_PATH } from "../../../../config";
import { VAULT_1_URI, VAULT_2_URI, PARACHAIN_ENDPOINT, VAULT_3_URI, ESPLORA_BASE_PATH, SUDO_URI } from "../../../../config";
import { newAccountId, WrappedCurrency, newVaultId } from "../../../../../src";
import { getSS58Prefix, newMonetaryAmount } from "../../../../../src/utils";
import { getSS58Prefix, newCurrencyId, newMonetaryAmount } from "../../../../../src/utils";
import {
getAUSDForeignAsset,
getCorrespondingCollateralCurrenciesForTests,
Expand All @@ -27,6 +28,7 @@ import {

export const vaultsTests = () => {
describe("vaultsAPI", () => {
let sudoAccount: KeyringPair;
let vault_1: KeyringPair;
let vault_1_ids: Array<InterbtcPrimitivesVaultId>;
let vault_2: KeyringPair;
Expand Down Expand Up @@ -56,7 +58,8 @@ export const vaultsTests = () => {
// also add aUSD collateral vaults if they exist (ie. the foreign asset exists)
collateralCurrencies.push(aUSD);
}


sudoAccount = keyring.addFromUri(SUDO_URI);
vault_1 = keyring.addFromUri(VAULT_1_URI);
vault_1_ids = collateralCurrencies.map((collateralCurrency) =>
newVaultId(api, vault_1.address, collateralCurrency, wrappedCurrency)
Expand Down Expand Up @@ -142,6 +145,61 @@ export const vaultsTests = () => {
expect(collateralizationBeforeDeposit.toString()).toEqual(collateralizationAfterWithdrawal.toString());
}
});

it("should be able to withdraw all collateral", async () => {
const vaults = await interBtcAPI.vaults.list();
// find vault with issued tokens, but zero to-be-issued tokens
const vaultExt = vaults.find((vault) => vault.toBeIssuedTokens.isZero() && vault.issuedTokens.toBig().gt(0));

if (vaultExt === undefined) {
throw Error("Precondition failure: Unable to find test vault to attempt withdraw all collateral");
}

const vaultAccountId = newAccountId(api, vaultExt.id.accountId.toHuman());
const collateralCurrency = await currencyIdToMonetaryCurrency(api, vaultExt.id.currencies.collateral);

// give enough wrapped tokens to vault to be able to self redeem all
const amountIssued = vaultExt.getBackedTokens();
// .toBig(0) returns amount in atomic units
const amountIssuedAtomic = amountIssued.toBig(0).toNumber();
const issuedCurrencyId = newCurrencyId(api, amountIssued.currency);

const vaultIssuedBalance = await interBtcAPI.tokens.balance(amountIssued.currency, vaultAccountId);
if (!vaultIssuedBalance.free.gt(amountIssued)) {
// set balance and wait for event
const result = await DefaultTransactionAPI.sendLogged(
api,
sudoAccount,
api.tx.sudo.sudo(api.tx.tokens.setBalance(vaultAccountId, issuedCurrencyId , amountIssuedAtomic * 2, 0)),
api.events.tokens.BalanceSet
);
expect(result.isCompleted).toBe(true);
}

// find matching keyring
const vaultKR = (vault_1.address === vaultAccountId.toHuman())
? vault_1
: (vault_2.address === vaultAccountId.toHuman())
? vault_2
: vault_3;

// self redeem so vault has no more issued tokens
const result2 = await DefaultTransactionAPI.sendLogged(
api,
vaultKR,
api.tx.redeem.selfRedeem(vaultExt.id.currencies, amountIssuedAtomic),
api.events.redeem.ExecuteRedeem
);
expect(result2.isCompleted).toBe(true);

const vaultInterBtcApi = new DefaultInterBtcApi(api, "regtest", vaultKR, ESPLORA_BASE_PATH);

// finally, withdraw all collateral
await submitExtrinsic(vaultInterBtcApi, await vaultInterBtcApi.vaults.withdrawAllCollateral(collateralCurrency));

const collateralAfter = await vaultInterBtcApi.vaults.getCollateral(vaultAccountId, collateralCurrency);
expect(collateralAfter.toBig().toNumber()).toEqual(0);
});

it("should getLiquidationCollateralThreshold", async () => {
for (const collateralCurrency of collateralCurrencies) {
Expand Down