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

RFC: monorepo jellyfish development #29

Closed
wants to merge 1 commit into from
Closed
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
17 changes: 17 additions & 0 deletions packages/jellyfish-core/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Payload to send to DeFiChain node
*/
/* eslint @typescript-eslint/no-explicit-any: off */
export type Payload = any

export type Call<T> = (method: string, payload: Payload) => Promise<T>
export type Client = {call: Call<any>}

export class JellyfishError extends Error {
readonly status?: number

constructor(message: string, status?: number) {
super(message)
this.status = status
}
}
20 changes: 20 additions & 0 deletions packages/jellyfish-core/src/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as methods from './methods'
import {Client, Payload, JellyfishError} from './api'
export {methods, Client, Payload, JellyfishError}

/**
thedoublejay marked this conversation as resolved.
Show resolved Hide resolved
* Protocol agnostic DeFiChain node client, APIs separated into their category.
*/
export abstract class JellyfishClient implements Client {
readonly blockchain = new methods.Blockchain(this)
readonly mining = new methods.Mining(this)
readonly network = new methods.Network(this)
readonly wallet = new methods.Wallet(this)

// TODO(fuxingloh): all rpc categories to be implemented after RFC

/**
* Abstracted isomorphic promise based procedure call handling
*/
abstract call<T>(method: string, payload: Payload): Promise<T>
}
13 changes: 13 additions & 0 deletions packages/jellyfish-core/src/methods/blockchain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {Client} from '../api'

export class Blockchain {
private readonly client: Client

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

async getBestBlockHash(): Promise<any> {
return await this.client.call('getbestblockhash', [])
}
}
6 changes: 6 additions & 0 deletions packages/jellyfish-core/src/methods/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Blockchain} from './blockchain'
import {Mining} from './mining'
import {Network} from './network'
import {Wallet} from './wallet'

export {Blockchain, Mining, Network, Wallet}
23 changes: 23 additions & 0 deletions packages/jellyfish-core/src/methods/mining.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Client} from '../api'

export interface MintingInfo {
blocks: number
difficulty: number
isoperator: boolean
networkhashps: number
pooledtx: number
chain: 'main' | 'test' | 'regtest'
thedoublejay marked this conversation as resolved.
Show resolved Hide resolved
warnings: string
}

export class Mining {
private readonly client: Client

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

async getMintingInfo(): Promise<MintingInfo> {
return this.client.call('getmintinginfo', [])
}
}
18 changes: 18 additions & 0 deletions packages/jellyfish-core/src/methods/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Client} from '../api'

export class Network {
private readonly client: Client

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

/**
* Requests that a ping be sent to all other nodes, to measure ping time.
* Results provided in getpeerinfo, pingtime and pingwait fields are decimal seconds.
* Ping command is handled in queue with all other commands, so it measures processing backlog, not just network ping.
*/
async ping(): Promise<any> {
return this.client.call('ping', [])
}
}
18 changes: 18 additions & 0 deletions packages/jellyfish-core/src/methods/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Client} from '../api'

interface AddressInfo {
address: string
hex?: string
}

export class Wallet {
private readonly client: Client

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

async getAddressInfo(address: string): Promise<AddressInfo> {
return this.client.call('getaddressinfo', [address])
}
}
10 changes: 10 additions & 0 deletions packages/jellyfish-jsonrpc/__tests__/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {JellyfishJsonRpc} from '../src/jsonrpc'

describe('JSON-RPC 1.0', () => {
it('should have different ids', () => {
// TODO(fuxingloh): AOP; intercept instead of direct testing
const first = JellyfishJsonRpc.stringify('diffid', [])
const second = JellyfishJsonRpc.stringify('diffid', [])
expect(first).not.toBe(second)
})
})
48 changes: 48 additions & 0 deletions packages/jellyfish-jsonrpc/__tests__/jsonrpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {JellyfishJsonRpc} from '../src/jsonrpc'
import {RegTestDocker} from '@defichain/testcontainers'

describe('error handling', () => {
const node = new RegTestDocker()

beforeAll(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

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

it('should 401 unauthorized', async () => {
const port = await node.getRpcPort()
const rpc = new JellyfishJsonRpc(`http://foo:[email protected]:${port}`)
const call = rpc.call('getmintinginfo', [])
return expect(call).rejects.toThrow(/Unauthorized/)
})
})

describe('as expected', () => {
const node = new RegTestDocker()

beforeAll(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

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

it('should getmintinginfo', async () => {
const rpc = new JellyfishJsonRpc(await node.getRpcUrl())
const result: any = await rpc.call('getmintinginfo', [])
expect(result.blocks).toBe(0)
expect(result.chain).toBe('regtest')
})
})
50 changes: 50 additions & 0 deletions packages/jellyfish-jsonrpc/src/jsonrpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {JellyfishClient, JellyfishError, Payload} from '@defichain/jellyfish-core'
import fetch from 'cross-fetch'
import JSONBig from 'json-bigint'

/**
* A JSON-RPC client implementation for connecting to a DeFiChain node.
*/
export class JellyfishJsonRpc extends JellyfishClient {
private readonly url: string

/**
* Construct a Jellyfish client to connect to a DeFiChain node via JSON-RPC.
*
* @param url
*/
constructor(url: string) {
super()
this.url = url
}

async call<T>(method: string, payload: Payload): Promise<T> {
const body = JellyfishJsonRpc.stringify(method, payload)
const response = await fetch(this.url, {
method: 'POST',
body: body,
})

return await JellyfishJsonRpc.parse<T>(response)
}

static async parse<T>(response: Response): Promise<T> {
if (response.status !== 200) {
throw new JellyfishError(response.statusText, response.status)
}

// TODO(fuxingloh): validation?
const text = await response.text()
const data = JSONBig.parse(text)
return data.result
}

static stringify(method: string, payload: Payload): string {
return JSONBig.stringify({
jsonrpc: '1.0',
id: Math.floor(Math.random() * 10000000000000000),
method: method,
params: payload,
})
}
}
24 changes: 24 additions & 0 deletions packages/jellyfish/__tests__/jsonrpc/blockchain.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Jellyfish from '../../src/jellyfish'
import {RegTestDocker} from "@defichain/testcontainers";

describe('Blockchain', () => {
const node = new RegTestDocker()

beforeAll(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

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

it('getBestBlockHash', async () => {
const client = Jellyfish.jsonrpc(await node.getRpcUrl())
const result = await client.blockchain.getBestBlockHash()
console.log(result)
})
})
24 changes: 24 additions & 0 deletions packages/jellyfish/__tests__/jsonrpc/mining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Jellyfish from '../../src/jellyfish'
import {RegTestDocker} from "@defichain/testcontainers";

describe('Mining', () => {
const node = new RegTestDocker()

beforeAll(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

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

it('getMintingInfo', async () => {
const client = Jellyfish.jsonrpc(await node.getRpcUrl())
const result = await client.mining.getMintingInfo()
expect(result.chain).toBe('regtest')
})
})
14 changes: 14 additions & 0 deletions packages/jellyfish/src/jellyfish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {JellyfishJsonRpc} from '@defichain/jellyfish-jsonrpc'

export const Jellyfish = {
/**
* Initialize a DeFiChain client and use jsonrpc for interfacing
*
* @param url
*/
jsonrpc(url: string): JellyfishJsonRpc {
return new JellyfishJsonRpc(url)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call it new Jellyfish() if it makes sense so it's clean when it's used externally.

new Jellyfish(url, {
    useNodeWallet: false, // default to false
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely will pay more attention to this, I rewrote that part a few times.

},
}

export default Jellyfish
64 changes: 64 additions & 0 deletions packages/testcontainers/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {MainNetDocker, RegTestDocker, TestNetDocker} from '../src'

describe('reg test', () => {
const node = new RegTestDocker()

beforeEach(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

afterEach(async () => {
await node.stop()
})

it('should getmintinginfo and chain should be regtest', async () => {
const result = await node.call('getmintinginfo', [])
expect(result.result.chain).toBe('regtest')
})
})

describe('test net', () => {
const node = new TestNetDocker()

beforeEach(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

afterEach(async () => {
await node.stop()
})

it('should getmintinginfo and chain should be regtest', async () => {
const result = await node.call('getmintinginfo', [])
expect(result.result.chain).toBe('test')
})
})

describe('main net', () => {
const node = new MainNetDocker()

beforeEach(async () => {
await node.start({
user: 'foo',
password: 'bar',
})
await node.ready()
})

afterEach(async () => {
await node.stop()
})

it('should getmintinginfo and chain should be regtest', async () => {
const result = await node.call('getmintinginfo', [])
expect(result.result.chain).toBe('main')
})
})
Loading