From a370953ade0a2a1c2b824a4510231a79e535566e Mon Sep 17 00:00:00 2001 From: jingyi2811 Date: Thu, 9 Sep 2021 20:18:15 +0800 Subject: [PATCH] Add setLoanToken rpc (#568) * Add createLoanScheme rpc * Add setDefaultLoanScheme rpc * Revert "Add setDefaultLoanScheme rpc" This reverts commit 0a291de5 * Add createVault rpc * Revert "Add createVault rpc" This reverts commit 400ed1a5 * Add setLoanToken rpc * Resolve merge conflicts * Improve code * Improve code * Improve code * Minor * Minor * Fix doc * Add afterAll function * Fix wrong doc * Minor * Change docker image * Quick push * Improve code * Improve code * Minor * Set mintable default to false * PriceFeed must have USD currency * CollateralToken's priceFeed should contain USD price * Improve code * Fix wrong text * Update docker image for DFTX * Apply code changes as per code review * Apply code changes as per code review * Change lesser to less * Fix wrong text * Apply code changes as per code review * Apply code changes as per code review * Apply code changes as per code review * To handle 2 loan tokens have the same name * Update to latest docker image as mintable default = true fix is just merged to epic/loan * Update packages/jellyfish-api-core/__tests__/category/loan/setLoanToken.test.ts committing since a name change. Co-authored-by: surangap --- .../__tests__/category/loan/loan_container.ts | 2 +- .../category/loan/setCollateralToken.test.ts | 2 +- .../category/loan/setLoanToken.test.ts | 327 ++++++++++++++++++ .../jellyfish-api-core/src/category/loan.ts | 30 ++ .../__tests__/txn/loan_container.ts | 2 +- ..._builder_loan_set_collateral_token.test.ts | 2 +- website/docs/jellyfish/api/loan.md | 23 ++ 7 files changed, 384 insertions(+), 4 deletions(-) create mode 100644 packages/jellyfish-api-core/__tests__/category/loan/setLoanToken.test.ts diff --git a/packages/jellyfish-api-core/__tests__/category/loan/loan_container.ts b/packages/jellyfish-api-core/__tests__/category/loan/loan_container.ts index d52bd52bea..5528184e80 100644 --- a/packages/jellyfish-api-core/__tests__/category/loan/loan_container.ts +++ b/packages/jellyfish-api-core/__tests__/category/loan/loan_container.ts @@ -2,7 +2,7 @@ import { MasterNodeRegTestContainer, StartOptions } from '@defichain/testcontain export class LoanMasterNodeRegTestContainer extends MasterNodeRegTestContainer { constructor () { - super(undefined, 'defi/defichain:HEAD-b638766') + super(undefined, 'defi/defichain:HEAD-85c78d8') } protected getCmd (opts: StartOptions): string[] { diff --git a/packages/jellyfish-api-core/__tests__/category/loan/setCollateralToken.test.ts b/packages/jellyfish-api-core/__tests__/category/loan/setCollateralToken.test.ts index 85b8ccb040..d126a7dfd9 100644 --- a/packages/jellyfish-api-core/__tests__/category/loan/setCollateralToken.test.ts +++ b/packages/jellyfish-api-core/__tests__/category/loan/setCollateralToken.test.ts @@ -78,7 +78,7 @@ describe('Loan setCollateralToken', () => { factor: new BigNumber(0.5), priceFeedId }) - await expect(promise).rejects.toThrow(`RpcApiError: 'Test LoanSetCollateralTokenTx execution failed:\noracle (${priceFeedId}) does not conntain USD price for this token!', code: -32600, method: setcollateraltoken`) + await expect(promise).rejects.toThrow(`RpcApiError: 'Test LoanSetCollateralTokenTx execution failed:\noracle (${priceFeedId}) does not contain USD price for this token!', code: -32600, method: setcollateraltoken`) }) it('should not setCollateralToken if oracleId does not exist', async () => { diff --git a/packages/jellyfish-api-core/__tests__/category/loan/setLoanToken.test.ts b/packages/jellyfish-api-core/__tests__/category/loan/setLoanToken.test.ts new file mode 100644 index 0000000000..2db6d62078 --- /dev/null +++ b/packages/jellyfish-api-core/__tests__/category/loan/setLoanToken.test.ts @@ -0,0 +1,327 @@ +import { LoanMasterNodeRegTestContainer } from './loan_container' +import BigNumber from 'bignumber.js' +import { Testing } from '@defichain/jellyfish-testing' +import { GenesisKeys } from '@defichain/testcontainers' + +describe('Loan', () => { + const container = new LoanMasterNodeRegTestContainer() + const testing = Testing.create(container) + + beforeAll(async () => { + await testing.container.start() + await testing.container.waitForWalletCoinbaseMaturity() + }) + + afterAll(async () => { + await testing.container.stop() + }) + + it('should setLoanToken', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token1', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token1', + priceFeedId + }) + expect(typeof loanTokenId).toStrictEqual('string') + expect(loanTokenId.length).toStrictEqual(64) + await testing.generate(1) + + const data = await testing.container.call('listloantokens', []) + expect(data).toStrictEqual({ + [loanTokenId]: { + token: { + 1: { + symbol: 'Token1', + symbolKey: 'Token1', + name: '', + decimal: 8, + limit: 0, + mintable: true, + tradeable: true, + isDAT: true, + isLPS: false, + finalized: false, + isLoanToken: true, + minted: 0, + creationTx: loanTokenId, + creationHeight: await testing.container.getBlockCount(), + destructionTx: '0000000000000000000000000000000000000000000000000000000000000000', + destructionHeight: -1, + collateralAddress: expect.any(String) + } + }, + priceFeedId, + interest: 0 + } + }) + }) + + it('should setLoanToken if symbol is more than 8 letters', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'x'.repeat(8), + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'x'.repeat(9), // 9 letters + priceFeedId + }) + await testing.generate(1) + + const data = await testing.container.call('listloantokens', []) + const index = Object.keys(data).indexOf(loanTokenId) + 1 + expect(data[loanTokenId].token[index].symbol).toStrictEqual('x'.repeat(8)) // Only remain the first 8 letters + }) + + it('should not setLoanToken if symbol is an empty string', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token2', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: '', + priceFeedId + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Test LoanSetLoanTokenTx execution failed:\ntoken symbol should be non-empty and starts with a letter\', code: -32600, method: setloantoken') + }) + + it('should not setLoanToken if token with same symbol was created before', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token3', + currency: 'USD' + }], 1]) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'Token3', + priceFeedId + }) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token3', + priceFeedId + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Test LoanSetLoanTokenTx execution failed:\ntoken \'Token3\' already exists!\', code: -32600, method: setloantoken') + }) + + it('should not setLoanToken if priceFeedId does not contain USD price', async () => { + const priceFeedId: string = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token4', + currency: 'Token4' + }], 1]) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token4', + priceFeedId + }) + await expect(promise).rejects.toThrow(`RpcApiError: 'Test LoanSetLoanTokenTx execution failed:\noracle (${priceFeedId}) does not contain USD price for this token!', code: -32600, method: setloantoken`) + }) + + it('should not setLoanToken if priceFeedId is invalid', async () => { + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token5', + priceFeedId: 'e40775f8bb396cd3d94429843453e66e68b1c7625d99b0b4c505ab004506697b' + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Test LoanSetLoanTokenTx execution failed:\noracle (e40775f8bb396cd3d94429843453e66e68b1c7625d99b0b4c505ab004506697b) does not exist or not valid oracle!\', code: -32600, method: setloantoken') + }) + + it('should setLoanToken with the given name', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token6', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token6', + name: 'Token6', + priceFeedId + }) + await testing.generate(1) + + const data = await testing.container.call('listloantokens', []) + const index = Object.keys(data).indexOf(loanTokenId) + 1 + expect(data[loanTokenId].token[index].name).toStrictEqual('Token6') + }) + + it('should setLoanToken if name is more than 128 letters', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token7', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token7', + name: 'x'.repeat(129), // 129 letters + priceFeedId + }) + await testing.generate(1) + + const data = await testing.container.call('listloantokens', []) + const index = Object.keys(data).indexOf(loanTokenId) + 1 + expect(data[loanTokenId].token[index].name).toStrictEqual('x'.repeat(128)) // Only remain the first 128 letters. + }) + + it('should setLoanToken if two loan tokens have the same name', async () => { + const priceFeedId1 = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token8', + currency: 'USD' + }], 1]) + await testing.generate(1) + + await testing.rpc.loan.setLoanToken({ + symbol: 'Token8', + name: 'TokenX', + priceFeedId: priceFeedId1 + }) + await testing.generate(1) + + const priceFeedId2 = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token9', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token9', + name: 'TokenX', + priceFeedId: priceFeedId2 + }) + await testing.generate(1) + + expect(typeof loanTokenId).toStrictEqual('string') + expect(loanTokenId.length).toStrictEqual(64) + await testing.generate(1) + }) + + it('should setLoanToken if mintable is false', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token10', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token10', + priceFeedId, + mintable: false + }) + expect(typeof loanTokenId).toStrictEqual('string') + expect(loanTokenId.length).toStrictEqual(64) + await testing.generate(1) + }) + + it('should setLoanToken if interest number is greater than 0 and has less than 9 digits in the fractional part', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token11', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token11', + priceFeedId, + interest: new BigNumber(15.12345678) // 8 digits in the fractional part + }) + expect(typeof loanTokenId).toStrictEqual('string') + expect(loanTokenId.length).toStrictEqual(64) + await testing.generate(1) + }) + + it('should not setLoanToken if interest number is greater than 0 and has more than 8 digits in the fractional part', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token12', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token12', + priceFeedId, + interest: new BigNumber(15.123456789) // 9 digits in the fractional part + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Invalid amount\', code: -3, method: setloantoken') + }) + + it('should not setLoanToken if interest number is less than 0', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token13', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token13', + priceFeedId, + interest: new BigNumber(-15.12345678) + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Amount out of range\', code: -3, method: setloantoken') + }) + + it('should not setLoanToken if interest number is greater than 1200000000', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token14', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token14', + priceFeedId, + interest: new BigNumber('1200000000').plus('0.00000001') + }) + await expect(promise).rejects.toThrow('RpcApiError: \'Amount out of range\', code: -3, method: setloantoken') + }) + + it('should setLoanToken with utxos', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token15', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const { txid, vout } = await testing.container.fundAddress(GenesisKeys[0].owner.address, 10) + const loanTokenId = await testing.rpc.loan.setLoanToken({ + symbol: 'Token15', + priceFeedId + }, [{ txid, vout }]) + expect(typeof loanTokenId).toStrictEqual('string') + expect(loanTokenId.length).toStrictEqual(64) + await testing.generate(1) + + const rawtx = await testing.container.call('getrawtransaction', [loanTokenId, true]) + expect(rawtx.vin[0].txid).toStrictEqual(txid) + expect(rawtx.vin[0].vout).toStrictEqual(vout) + + const data = await testing.container.call('listloantokens', []) + const index = Object.keys(data).indexOf(loanTokenId) + 1 + expect(data[loanTokenId].token[index].symbol).toStrictEqual('Token15') + expect(data[loanTokenId].token[index].name).toStrictEqual('') + }) + + it('should not setLoanToken with utxos not from foundation member', async () => { + const priceFeedId = await testing.container.call('appointoracle', [await testing.generateAddress(), [{ + token: 'Token16', + currency: 'USD' + }], 1]) + await testing.generate(1) + + const utxo = await testing.container.fundAddress(await testing.generateAddress(), 10) + const promise = testing.rpc.loan.setLoanToken({ + symbol: 'Token16', + priceFeedId + }, [utxo]) + await expect(promise).rejects.toThrow('RpcApiError: \'Test LoanSetLoanTokenTx execution failed:\ntx not from foundation member!\', code: -32600, method: setloantoken') + }) +}) diff --git a/packages/jellyfish-api-core/src/category/loan.ts b/packages/jellyfish-api-core/src/category/loan.ts index deafc0e895..fc5d292faf 100644 --- a/packages/jellyfish-api-core/src/category/loan.ts +++ b/packages/jellyfish-api-core/src/category/loan.ts @@ -116,6 +116,28 @@ export class Loan { async listCollateralTokens (): Promise { return await this.client.call('listcollateraltokens', [], 'bignumber') } + + /** + * Creates (and submits to local node and network) a token for a price feed set in collateral token. + * + * @param {SetLoanToken} loanToken + * @param {string} loanToken.symbol Token's symbol (unique), no longer than 8 + * @param {string} [loanToken.name] Token's name, no longer than 128 + * @param {string} loanToken.priceFeedId Txid of oracle feeding the price + * @param {boolean} [loanToken.mintable = true] Token's 'Mintable' property + * @param {BigNumber} [loanToken.interest = 0] Interest rate + * @param {UTXO[]} [utxos = []] Specific UTXOs to spend + * @param {string} utxos.txid Transaction Id + * @param {number} utxos.vout Output number + * @return {Promise} LoanTokenId, also the txn id for txn created to set loan token + */ + async setLoanToken (loanToken: SetLoanToken, utxos: UTXO[] = []): Promise { + const defaultData = { + mintable: true, + interest: 0 + } + return await this.client.call('setloantoken', [{ ...defaultData, ...loanToken }, utxos], 'number') + } } export interface CreateLoanScheme { @@ -167,6 +189,14 @@ export interface GetLoanSchemeResult { mincolratio: BigNumber } +export interface SetLoanToken { + symbol: string + name?: string + priceFeedId: string + mintable?: boolean + interest?: BigNumber +} + export interface UTXO { txid: string vout: number diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/loan_container.ts b/packages/jellyfish-transaction-builder/__tests__/txn/loan_container.ts index d52bd52bea..146136b7d5 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/loan_container.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/loan_container.ts @@ -2,7 +2,7 @@ import { MasterNodeRegTestContainer, StartOptions } from '@defichain/testcontain export class LoanMasterNodeRegTestContainer extends MasterNodeRegTestContainer { constructor () { - super(undefined, 'defi/defichain:HEAD-b638766') + super(undefined, 'defi/defichain:HEAD-3af0169') } protected getCmd (opts: StartOptions): string[] { diff --git a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_set_collateral_token.test.ts b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_set_collateral_token.test.ts index eff82e09cd..14b3fdb775 100644 --- a/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_set_collateral_token.test.ts +++ b/packages/jellyfish-transaction-builder/__tests__/txn/txn_builder_loan_set_collateral_token.test.ts @@ -123,7 +123,7 @@ describe('loan.setCollateralToken()', () => { activateAfterBlock: 0 }, script) const promise = sendTransaction(testing.container, txn) - await expect(promise).rejects.toThrow(`DeFiDRpcError: 'LoanSetCollateralTokenTx: oracle (${priceFeedId}) does not conntain USD price for this token! (code 16)', code: -26`) + await expect(promise).rejects.toThrow(`DeFiDRpcError: 'LoanSetCollateralTokenTx: oracle (${priceFeedId}) does not contain USD price for this token! (code 16)', code: -26`) }) it('should not setCollateralToken if oracleId does not exist', async () => { diff --git a/website/docs/jellyfish/api/loan.md b/website/docs/jellyfish/api/loan.md index a0cbcbb298..03b1126adc 100644 --- a/website/docs/jellyfish/api/loan.md +++ b/website/docs/jellyfish/api/loan.md @@ -166,6 +166,29 @@ interface CollateralTokenDetail { activateAfterBlock: BigNumber } +interface UTXO { + txid: string + vout: number +} +``` + +## setLoanToken + +Creates (and submits to local node and network) a token for a price feed set in collateral token. + +```ts title="client.loan.setLoanToken()" +interface loan { + setLoanToken (loanToken: SetLoanToken, utxos: UTXO[] = []): Promise +} + +interface SetLoanToken { + symbol: string + name?: string + priceFeedId: string + mintable?: boolean + interest?: BigNumber +} + interface UTXO { txid: string vout: number