Skip to content

Commit

Permalink
jellyfish-core protocol adapter and error handling (#41)
Browse files Browse the repository at this point in the history
* removed main/test net workflow

* testcontainers for defichain js ecosystem

* check in .idea code styles

* automatically pull docker image for testing

* cleanup code debt

* added escape-hatch, unable to find open handles

* added DeFiDRpcError for easier downstream error surfacing

* removed json-bigint from container

* testcontainers for defichain js ecosystem

* check in .idea code styles

* removed json-bigint from container

* added precision test code
updated jest and package config

* added container adapter client with mining test

* added getNetworkHashPerSecond

* added jellyfish-core error handling

* moved container_adapter_client.ts to root of test
  • Loading branch information
fuxingloh authored Mar 9, 2021
1 parent da91feb commit e2366db
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 28 deletions.
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

81 changes: 81 additions & 0 deletions packages/jellyfish-core/__tests__/category/mining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { RegTestContainer, MasterNodeRegTestContainer } from '@defichain/testcontainers'
import { ContainerAdapterClient } from '../container_adapter_client'
import waitForExpect from 'wait-for-expect'

describe('non masternode', () => {
const container = new RegTestContainer()
const client = new ContainerAdapterClient(container)

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

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

it('should getMintingInfo', async () => {
const info = await client.mining.getMintingInfo()

expect(info.blocks).toBe(0)
expect(info.difficulty).toBeDefined()
expect(info.isoperator).toBe(false)
expect(info.networkhashps).toBe(0)
expect(info.pooledtx).toBe(0)
expect(info.chain).toBe('regtest')
expect(info.warnings).toBe('')
})

it('should getNetworkHashPerSecond', async () => {
const result = await client.mining.getNetworkHashPerSecond()
expect(result).toBe(0)
})
})

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

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

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

it('should getMintingInfo', async () => {
const info = await client.mining.getMintingInfo()

await waitForExpect(async () => {
const info = await container.getMintingInfo()
expect(info.blocks).toBeGreaterThan(1)
})

expect(info.blocks).toBeGreaterThan(0)

expect(info.currentblockweight).toBeGreaterThan(0)
expect(info.currentblocktx).toBe(0)

expect(info.difficulty).toBeDefined()
expect(info.isoperator).toBe(true)

expect(info.masternodeid).toBeDefined()
expect(info.masternodeoperator).toBeDefined()
expect(info.masternodestate).toBe('ENABLED')
expect(info.generate).toBe(true)
expect(info.mintedblocks).toBe(0)

expect(info.networkhashps).toBeGreaterThan(0)
expect(info.pooledtx).toBe(0)
expect(info.chain).toBe('regtest')
expect(info.warnings).toBe('')
})

it('should getNetworkHashPerSecond', async () => {
const result = await client.mining.getNetworkHashPerSecond()
expect(result).toBeGreaterThan(0)
})
})
38 changes: 38 additions & 0 deletions packages/jellyfish-core/__tests__/container_adapter_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { JellyfishClient, JellyfishError } from '../src/core'
import { DeFiDContainer } from '@defichain/testcontainers'

/**
* Jellyfish client adapter for container
* To be used for testing jellyfish-core protocol data binding only
*/
export class ContainerAdapterClient extends JellyfishClient {
protected readonly container: DeFiDContainer

constructor (container: DeFiDContainer) {
super()
this.container = container
}

/**
* Wrap the call from client to testcontainers.
*/
async call<T> (method: string, params: any[]): Promise<T> {
const body = JSON.stringify({
jsonrpc: '1.0',
id: Math.floor(Math.random() * 100000000000000),
method: method,
params: params
})

const text = await this.container.post(body)
const response = JSON.parse(text)

const { result, error } = response

if (error !== null) {
throw new JellyfishError(error)
}

return result
}
}
50 changes: 47 additions & 3 deletions packages/jellyfish-core/__tests__/core.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
import { getName } from '../src/core'
import { MintingInfo, JellyfishError, JellyfishClient } from '../src/core'
import { ContainerAdapterClient } from './container_adapter_client'
import { RegTestContainer } from '@defichain/testcontainers'

it('should getName jellyfish-core', () => {
expect(getName()).toBe('jellyfish-core')
class TestClient extends JellyfishClient {
async call<T> (method: string, payload: any[]): Promise<T> {
throw new JellyfishError({
code: 1, message: 'error message from node'
})
}
}

it('should export client', async () => {
const client = new TestClient()
return await expect(client.call('fail', []))
.rejects.toThrowError(JellyfishError)
})

it('should export categories', async () => {
const client = new TestClient()
return await expect(async () => {
const info: MintingInfo = await client.mining.getMintingInfo()
console.log(info)
}).rejects.toThrowError(JellyfishError)
})

describe('JellyfishError handling', () => {
const container = new RegTestContainer()
const client = new ContainerAdapterClient(container)

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

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

it('invalid method should throw -32601 with message as structured', async () => {
return await expect(client.call('invalid', []))
.rejects.toThrowError(/JellyfishError from RPC: 'Method not found', code: -32601/)
})

it('importprivkey should throw -5 with message as structured', async () => {
return await expect(client.call('importprivkey', ['invalid-key']))
.rejects.toThrowError(/JellyfishError from RPC: 'Invalid private key encoding', code: -5/)
})
})
41 changes: 41 additions & 0 deletions packages/jellyfish-core/__tests__/precision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { BigNumber } from '../src/core'

/**
* Why JavaScript default number should not be used as it lose precision
*/
describe('number will lose precision', () => {
it('1200000000.00000003 should not lose precision as a number but it does', () => {
const dfi = 1200000000.00000003

expect(() => {
expect(dfi.toString()).toBe('1200000000.00000003')
}).toThrow()
})

it('12.00000003 * 1000000 should be 12000000.03 but its not', () => {
const obj = JSON.parse('{"dfi": 12.00000003}')
const dfi = obj.dfi * 1000000

expect(() => {
expect(dfi).toBe(12000000.03)
}).toThrow()
})
})

/**
* BigNumber from 'bignumber.js' implementations will not lose precision
*/
describe('BigNumber should not lose precision', () => {
it('1200000000.00000003 should not lose precision', () => {
const dfi = new BigNumber('1200000000.00000003')

expect(dfi.toString()).toBe('1200000000.00000003')
})

it('12.00000003 * 1000000 should be 12000000.03', () => {
const obj = { dfi: new BigNumber('12.00000003') }
const dfi = obj.dfi.multipliedBy(1000000)

expect(dfi.toString()).toBe('12000000.03')
})
})
5 changes: 3 additions & 2 deletions packages/jellyfish-core/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
module.exports = {
testEnvironment: 'node',
testMatch: [
'**/__tests__/**/*.ts'
'**/__tests__/**/*.test.ts'
],
transform: {
'^.+\\.ts$': 'ts-jest'
},
verbose: true,
clearMocks: true
clearMocks: true,
testTimeout: 120000
}
3 changes: 3 additions & 0 deletions packages/jellyfish-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"publish:next": "npm publish --tag next",
"publish:latest": "npm publish --tag latest"
},
"peerDependencies": {
"bignumber.js": "^9.0.1"
},
"devDependencies": {
"@defichain/testcontainers": "0.0.0",
"typescript": "^4.2.2"
Expand Down
48 changes: 48 additions & 0 deletions packages/jellyfish-core/src/category/mining.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { JellyfishClient } from '../core'

export interface MintingInfo {
blocks: number
currentblockweight?: number
currentblocktx?: number
difficulty: string
isoperator: boolean
masternodeid?: string
masternodeoperator?: string
masternodestate?: 'PRE_ENABLED' | 'ENABLED' | 'PRE_RESIGNED' | 'RESIGNED' | 'PRE_BANNED' | 'BANNED'
generate?: boolean
mintedblocks?: number
networkhashps: number
pooledtx: number
chain: 'main' | 'test' | 'regtest' | string
warnings: string
}

/**
* Minting related RPC calls for DeFiChain
*/
export class Mining {
private readonly client: JellyfishClient

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

/**
* Returns the estimated network hashes per second
*
* @param nblocks to estimate since last difficulty change.
* @param height to estimate at the time of the given height.
* @return Promise<number>
*/
async getNetworkHashPerSecond (nblocks: number = 120, height: number = -1): Promise<number> {
return await this.client.call('getnetworkhashps', [nblocks, height])
}

/**
* Get minting-related information
* @return Promise<MintingInfo>
*/
async getMintingInfo (): Promise<MintingInfo> {
return await this.client.call('getmintinginfo', [])
}
}
37 changes: 35 additions & 2 deletions packages/jellyfish-core/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
export function getName (): string {
return 'jellyfish-core'
import BigNumber from 'bignumber.js'
import { Mining } from './category/mining'

// TODO(fuxingloh): might need to restructure how it's exported as this grows, will look into this soon
export * from './category/mining'
export { BigNumber }

/**
* JellyfishClient; a protocol agnostic DeFiChain node client, RPC calls are separated into their category.
*/
export abstract class JellyfishClient {
public readonly mining = new Mining(this)

/**
* A promise based procedure call handling
*
* @param method name
* @param params payload
* @throws JellyfishError
*/
abstract call<T> (method: string, params: any[]): Promise<T>
}

/**
* JellyfishError; where jellyfish/defichain errors are encapsulated into.
*/
export class JellyfishError extends Error {
public readonly code: number
public readonly rawMessage: string

constructor (error: { code: number, message: string }) {
super(`JellyfishError from RPC: '${error.message}', code: ${error.code}`)
this.code = error.code
this.rawMessage = error.message
}
}
3 changes: 1 addition & 2 deletions packages/testcontainers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
"@types/dockerode": "^3.2.2",
"@types/node-fetch": "^2.5.8",
"dockerode": "^3.2.1",
"node-fetch": "^2.6.1",
"json-bigint": "^1.0.0"
"node-fetch": "^2.6.1"
},
"devDependencies": {
"typescript": "^4.2.2"
Expand Down
Loading

0 comments on commit e2366db

Please sign in to comment.