Skip to content

Commit

Permalink
Add setLoanToken rpc (#568)
Browse files Browse the repository at this point in the history
* Add createLoanScheme rpc

* Add setDefaultLoanScheme rpc

* Revert "Add setDefaultLoanScheme rpc"

This reverts commit 0a291de

* Add createVault rpc

* Revert "Add createVault rpc"

This reverts commit 400ed1a

* 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 <[email protected]>
  • Loading branch information
jingyi2811 and surangap authored Sep 9, 2021
1 parent ee7767c commit a370953
Show file tree
Hide file tree
Showing 7 changed files with 384 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
})
})
30 changes: 30 additions & 0 deletions packages/jellyfish-api-core/src/category/loan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,28 @@ export class Loan {
async listCollateralTokens (): Promise<CollateralTokensData> {
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<string>} LoanTokenId, also the txn id for txn created to set loan token
*/
async setLoanToken (loanToken: SetLoanToken, utxos: UTXO[] = []): Promise<string> {
const defaultData = {
mintable: true,
interest: 0
}
return await this.client.call('setloantoken', [{ ...defaultData, ...loanToken }, utxos], 'number')
}
}

export interface CreateLoanScheme {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[] {
Expand Down
Loading

0 comments on commit a370953

Please sign in to comment.