Skip to content

Commit

Permalink
Masternode RPC (createmasternode, listmasternodes, getmasternode) (#372)
Browse files Browse the repository at this point in the history
Co-authored-by: canonbrother <[email protected]>
Co-authored-by: Fuxing Loh <[email protected]>
  • Loading branch information
3 people authored Jun 23, 2021
1 parent f02ad70 commit 58fba5e
Show file tree
Hide file tree
Showing 7 changed files with 489 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { MasternodeState } from '../../../src/category/masternode'

describe('Masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await container.waitForReady()
await container.waitForWalletCoinbaseMaturity()
})

afterAll(async () => {
await container.stop()
})

it('should createMasternode ', async () => {
const masternodesLengthBefore = Object.keys(await client.masternode.listMasternodes()).length

const ownerAddress = await client.wallet.getNewAddress()
const hex = await client.masternode.createMasternode(ownerAddress)

expect(typeof hex).toStrictEqual('string')
expect(hex.length).toStrictEqual(64)

await container.generate(1)

const masternodesAfter = await client.masternode.listMasternodes()
const masternodesLengthAfter = Object.keys(masternodesAfter).length
const createdMasternode = Object.values(masternodesAfter).filter(mn => mn.ownerAuthAddress === ownerAddress)

expect(masternodesLengthAfter).toStrictEqual(masternodesLengthBefore + 1)

for (const mn of createdMasternode) {
expect(typeof mn.ownerAuthAddress).toStrictEqual('string')
expect(typeof mn.operatorAuthAddress).toStrictEqual('string')
expect(typeof mn.creationHeight).toStrictEqual('number')
expect(typeof mn.resignHeight).toStrictEqual('number')
expect(typeof mn.resignTx).toStrictEqual('string')
expect(typeof mn.banHeight).toStrictEqual('number')
expect(typeof mn.banTx).toStrictEqual('string')
expect(mn.state).toStrictEqual(MasternodeState.PRE_ENABLED)
expect(typeof mn.state).toStrictEqual('string')
expect(typeof mn.mintedBlocks).toStrictEqual('number')
expect(typeof mn.ownerIsMine).toStrictEqual('boolean')
expect(mn.ownerIsMine).toStrictEqual(true)
expect(typeof mn.localMasternode).toStrictEqual('boolean')
expect(typeof mn.operatorIsMine).toStrictEqual('boolean')
expect(mn.operatorIsMine).toStrictEqual(true)
}
})

it('should createMasternode with specified UTXOS', async () => {
const ownerAddress = await client.wallet.getNewAddress()
const { txid } = await container.fundAddress(ownerAddress, 10)

const utxos = await client.wallet.listUnspent()
const utxosBeforeLength = utxos.length

const inputs = utxos.filter((utxos) => utxos.txid === txid)
.map((utxo: { txid: string, vout: number }) => ({ txid: utxo.txid, vout: utxo.vout }))
const hex = await client.masternode.createMasternode(ownerAddress, ownerAddress, { utxos: inputs })

expect(typeof hex).toStrictEqual('string')
expect(hex.length).toStrictEqual(64)

const utxosAfterLength = (await client.wallet.listUnspent()).length
expect(utxosAfterLength).toBeLessThan((utxosBeforeLength))
})

it('should throw an error with invalid owner address', async () => {
const invalidAddress = 'invalidAddress'
await expect(client.masternode.createMasternode(invalidAddress)).rejects.toThrow('operatorAddress (invalidAddress) does not refer to a P2PKH or P2WPKH address')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { MasternodeState } from '../../../src/category/masternode'

describe('Masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await container.waitForReady()
await container.waitForWalletCoinbaseMaturity()
})

afterAll(async () => {
await container.stop()
})

it('should getMasternode', async () => {
const ownerAddress = await client.wallet.getNewAddress()
const id = await client.masternode.createMasternode(ownerAddress)

await container.generate(1)

const masternode = await client.masternode.getMasternode(id)

expect(Object.keys(masternode).length).toStrictEqual(1)
for (const masternodeKey in masternode) {
const data = masternode[masternodeKey]
expect(typeof data.operatorAuthAddress).toStrictEqual('string')
expect(typeof data.ownerAuthAddress).toStrictEqual('string')
expect(typeof data.creationHeight).toStrictEqual('number')
expect(typeof data.resignHeight).toStrictEqual('number')
expect(typeof data.resignTx).toStrictEqual('string')
expect(typeof data.banHeight).toStrictEqual('number')
expect(typeof data.banTx).toStrictEqual('string')
expect(data.state).toStrictEqual(MasternodeState.PRE_ENABLED)
expect(typeof data.state).toStrictEqual('string')
expect(typeof data.mintedBlocks).toStrictEqual('number')
expect(typeof data.ownerIsMine).toStrictEqual('boolean')
expect(typeof data.localMasternode).toStrictEqual('boolean')
expect(typeof data.operatorIsMine).toStrictEqual('boolean')
expect(data.operatorIsMine).toStrictEqual(true)
}
})

it('should fail and throw an error with invalid masternode id', async () => {
const invalidId = '8d4d987dee688e400a0cdc899386f243250d3656d802231755ab4d28178c9816'
const promise = client.masternode.getMasternode(invalidId)

await expect(promise).rejects.toThrow('Masternode not found')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../../container_adapter_client'
import { MasternodePagination, MasternodeState } from '../../../src/category/masternode'

describe('Masternode', () => {
const container = new MasterNodeRegTestContainer()
const client = new ContainerAdapterClient(container)

beforeAll(async () => {
await container.start()
await container.waitForReady()
await container.waitForWalletCoinbaseMaturity()
})

afterAll(async () => {
await container.stop()
})

it('should listMasternodes', async () => {
const masternodeList = await client.masternode.listMasternodes()

expect(Object.keys(masternodeList).length).toBeGreaterThanOrEqual(1)
for (const masternode in masternodeList) {
const currentMasternode = masternodeList[masternode]
expect(typeof currentMasternode.ownerAuthAddress).toStrictEqual('string')
expect(typeof currentMasternode.operatorAuthAddress).toStrictEqual('string')
expect(typeof currentMasternode.creationHeight).toStrictEqual('number')
expect(typeof currentMasternode.resignHeight).toStrictEqual('number')
expect(typeof currentMasternode.resignTx).toStrictEqual('string')
expect(typeof currentMasternode.banHeight).toStrictEqual('number')
expect(typeof currentMasternode.banTx).toStrictEqual('string')
expect(currentMasternode.state).toStrictEqual(MasternodeState.ENABLED)
expect(typeof currentMasternode.mintedBlocks).toStrictEqual('number')
expect(typeof currentMasternode.ownerIsMine).toStrictEqual('boolean')
expect(typeof currentMasternode.localMasternode).toStrictEqual('boolean')
expect(typeof currentMasternode.operatorIsMine).toStrictEqual('boolean')
}
})

it('should listMasternodes with verbose false', async () => {
const masternodeList = await client.masternode.listMasternodes({}, false)
for (const value of Object.values(masternodeList)) {
expect(typeof value).toStrictEqual('string')
}
})

it('should listMasternodes with limit', async () => {
const masternodeList = await client.masternode.listMasternodes({ limit: 3 })
expect(Object.keys(masternodeList).length).toStrictEqual(3)
})

it('should listMasternodes with pagination start and including_start', async () => {
const prevMasternodes = await client.masternode.listMasternodes()
const startId = Object.keys(prevMasternodes)[2]
const masterNodeLengthBefore = Object.keys(prevMasternodes).length

const pagination: MasternodePagination = {
start: startId,
including_start: true
}
const masternodeList = await client.masternode.listMasternodes(pagination)

const masternodeLengthAfter = Object.keys(masternodeList).length
expect(masternodeLengthAfter).toStrictEqual(masterNodeLengthBefore - 2)

for (const masternode in masternodeList) {
const currentMasternode = masternodeList[masternode]
expect(typeof currentMasternode.ownerAuthAddress).toStrictEqual('string')
expect(typeof currentMasternode.operatorAuthAddress).toStrictEqual('string')
expect(typeof currentMasternode.creationHeight).toStrictEqual('number')
expect(typeof currentMasternode.resignHeight).toStrictEqual('number')
expect(typeof currentMasternode.resignTx).toStrictEqual('string')
expect(typeof currentMasternode.banHeight).toStrictEqual('number')
expect(typeof currentMasternode.banTx).toStrictEqual('string')
expect(currentMasternode.state).toStrictEqual(MasternodeState.ENABLED)
expect(typeof currentMasternode.mintedBlocks).toStrictEqual('number')
expect(typeof currentMasternode.ownerIsMine).toStrictEqual('boolean')
expect(typeof currentMasternode.localMasternode).toStrictEqual('boolean')
expect(typeof currentMasternode.operatorIsMine).toStrictEqual('boolean')
}
})
})
142 changes: 142 additions & 0 deletions packages/jellyfish-api-core/src/category/masternode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { ApiClient } from '../.'

export enum MasternodeState {
PRE_ENABLED = 'PRE_ENABLED',
ENABLED = 'ENABLED',
PRE_RESIGNED = 'PRE_RESIGNED',
RESIGNED = 'RESIGNED',
PRE_BANNED = 'PRE_BANNED',
BANNED = 'BANNED',
UNKNOWN = 'UNKNOWN'
}

/**
* Masternode RPCs for DeFi Blockchain
*/
export class Masternode {
private readonly client: ApiClient

constructor (client: ApiClient) {
this.client = client
}

/**
* Creates a masternode creation transaction with given owner and operator addresses.
*
* @param {string} ownerAddress Any valid address for keeping collateral amount
* @param {string} [operatorAddress] Masternode operator auth address (P2PKH only, unique). If empty, owner address will be used.
* @param {CreateMasternodeOptions} [options]
* @param {UTXO[]} [options.utxos = []]
* @param {string} [options.uxtos.txid] The transaction id
* @param {string} [options.utxos.vout] The output number
* @return {Promise<string>}
*/
async createMasternode (
ownerAddress: string,
operatorAddress?: string,
options: CreateMasternodeOptions = { utxos: [] }
): Promise<string> {
operatorAddress = operatorAddress ?? ownerAddress
return await this.client.call('createmasternode', [ownerAddress, operatorAddress, options.utxos], 'number')
}

/**
* Returns information about multiple masternodes.
*
* @param {MasternodePagination} pagination
* @param {string} [pagination.start]
* @param {boolean} [pagination.including_start = true] Include starting position.
* @param {string} [pagination.limit = 100] Maximum number of orders to return.
* @param {boolean} [verbose = true] Flag for verbose list. Only ids are returned when false.
* @return {Promise<MasternodeResult<MasternodeInfo>>}
*/
listMasternodes (pagination?: MasternodePagination, verbose?: boolean): Promise<MasternodeResult<MasternodeInfo>>

/**
* Returns information about multiple masternodes.
*
* @param {MasternodePagination} pagination
* @param {string} [pagination.start]
* @param {boolean} [pagination.including_start = true] Include starting position.
* @param {string} [pagination.limit = 100] Maximum number of orders to return.
* @param {boolean} verbose true
* @return {Promise<MasternodeResult<MasternodeInfo>>}
*/
listMasternodes (pagination: MasternodePagination, verbose: true): Promise<MasternodeResult<MasternodeInfo>>

/**
* Returns information about multiple masternodes.
*
* @param {MasternodePagination} pagination
* @param {string} [pagination.start]
* @param {boolean} [pagination.including_start = true] Include starting position.
* @param {string} [pagination.limit = 100] Maximum number of orders to return.
* @param {boolean} verbose false.
* @return {Promise<MasternodeResult<string>>}
*/
listMasternodes (pagination: MasternodePagination, verbose: false): Promise<MasternodeResult<string>>

/**
* Returns information about multiple masternodes.
*
* @param {MasternodePagination} pagination
* @param {string} [pagination.start]
* @param {boolean} [pagination.including_start = true] Include starting position.
* @param {string} [pagination.limit = 100] Maximum number of orders to return.
* @param {boolean} [verbose = true] Flag for verbose list. Only ids are returned when false.
* @return {Promise<MasternodeResult<T>>}
*/
async listMasternodes<T> (
pagination: MasternodePagination = {
including_start: true,
limit: 100
},
verbose: boolean = true
): Promise<MasternodeResult<T>> {
return await this.client.call('listmasternodes', [pagination, verbose], 'number')
}

/**
* Returns information about a single masternode
*
* @param {string} masternodeId The masternode's id.
* @return {Promise<MasternodeResult>}
*/
async getMasternode (masternodeId: string): Promise<MasternodeResult<MasternodeInfo>> {
return await this.client.call('getmasternode', [masternodeId], 'number')
}
}

export interface UTXO {
txid: string
vout: number
}

export interface CreateMasternodeOptions {
utxos: UTXO[]
}

export interface MasternodePagination {
start?: string
including_start?: boolean
limit?: number
}

export interface MasternodeInfo {
ownerAuthAddress: string
operatorAuthAddress: string
creationHeight: number
resignHeight: number
resignTx: string
banHeight: number
banTx: string
state: MasternodeState
mintedBlocks: number
ownerIsMine: boolean
operatorIsMine: boolean
localMasternode: boolean
}

export interface MasternodeResult<T> {
[id: string]: T
}
3 changes: 3 additions & 0 deletions packages/jellyfish-api-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PoolPair } from './category/poolpair'
import { Token } from './category/token'
import { Oracle } from './category/oracle'
import { Server } from './category/server'
import { Masternode } from './category/masternode'

export * from '@defichain/jellyfish-json'

Expand All @@ -22,6 +23,7 @@ export * as token from './category/token'
export * as account from './category/account'
export * as oracle from './category/oracle'
export * as server from './category/server'
export * as masternode from './category/masternode'

/**
* A protocol agnostic DeFiChain node client, RPC calls are separated into their category.
Expand All @@ -37,6 +39,7 @@ export abstract class ApiClient {
public readonly token = new Token(this)
public readonly oracle = new Oracle(this)
public readonly server = new Server(this)
public readonly masternode = new Masternode(this)

/**
* A promise based procedure call handling
Expand Down
Loading

0 comments on commit 58fba5e

Please sign in to comment.