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
kosu.js: implement a go-kosu JSONRPC client (the NodeClient) #229
Merged
+1,389
−717
Merged
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
ba893b7
initial node RPC client implementation
hrharder 8a93ad6
missing await on promise
hrharder f040e2f
fixing validators balance conversion
hrharder 11b9cc0
adding func to convert from public key to node ID
hrharder 6f2e797
kosu.js: implement kosu node jsonrpc client
hrharder b7f6706
kosu.js: running prettier
hrharder 4048650
kosu.js: regenerating docs
hrharder 884b6c7
kosu.js: don't generate docs for private methods
hrharder 32ee5b8
Merge branch 'master' into feature/kosu.js/node-rpc
hrharder fc105a5
kosu.js: add lint:fix script
hrharder d63dab4
kosu.js: fixing linter errors
hrharder 723d4cc
Merge branch 'feature/kosu.js/node-rpc' of github.com:ParadigmFoundat…
hrharder b6bc830
kosu.js: applying prettier fixes
hrharder 1965401
use BigNumber for validator balance
hrharder a88cd29
parse number out of poster queries as well
hrharder 67be855
Merge branch 'master' into feature/kosu.js/node-rpc
hrharder e49427a
update validator and poster types to use BigNumber
hrharder 725d0ba
Merge branch 'feature/kosu.js/node-rpc' of github.com:ParadigmFoundat…
hrharder File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.
kosu.js: implement kosu node jsonrpc client
- Loading branch information
hrharder
committed
Aug 22, 2019
hrharder
Henry Harder
commit 6f2e797f87f04abe799abbb8c07d0182c1345357
Verified
This commit was signed with a verified signature.
GPG key ID: 3E4EB305434EC58E
Learn about signing commits
| @@ -1,120 +1,287 @@ | ||
| import { WebsocketProvider, WebsocketProviderOptions } from "@0x/web3-providers-fork"; | ||
| import uuid from "uuid/v4"; | ||
| import assert from "assert"; | ||
| import { createHash } from "crypto"; | ||
| import { isFunction, isString } from "lodash"; | ||
| import uuid from "uuid/v4"; | ||
|
|
||
| /** | ||
| * A simple JSONRPC/WebSocket client for the `go-kosu` JSONRPC-API. Supports the | ||
| * full Kosu JSONRPC, including subscriptions. | ||
| * | ||
| * It is built on the [web3](https://www.npmjs.com/package/web3) `WebSocketProvider` | ||
| * JSONRPC client, through a more desirable fork provided by [0x.](https://0x.org) | ||
| * As such, it can be configured with the same options supported by the underlying | ||
| * client. | ||
| * | ||
| * It must be initialized with the URL of a `go-kosu` node serving the JSONRPC | ||
| * over WebSocket. | ||
| * | ||
| * View the Kosu RPC documentation [here.](https://docs.kosu.io/go-kosu/kosu_rpc.html) | ||
| */ | ||
| export class NodeClient { | ||
| /** | ||
| * The default options specify a connection timeout of 3s, all other defaults | ||
| * are inherited from `WebsocketProviderOptions`. | ||
| */ | ||
| public static DEFAULT_OPTIONS: WebsocketProviderOptions = { timeout: 3000 }; | ||
|
|
||
| /** | ||
| * Kosu validator public key's are 32 bytes long. | ||
| */ | ||
| public static PUBLIC_KEY_LENGTH = 32; | ||
|
|
||
| /** | ||
| * Kosu validator node IDs are the first 20 bytes of the SHA-256 hash of the | ||
| * public key. | ||
| */ | ||
| public static NODE_ID_HASH_OFFSET = 20; | ||
|
|
||
| private readonly _provider: WebsocketProvider; | ||
| private readonly _heartbeatInterval: number; | ||
| private readonly _subscriptionIdMap: { [uuid: string]: string }; | ||
|
|
||
| /** | ||
| * Convert a Kosu/Tendermint public key to the corresponding node ID. | ||
| * | ||
| * The node ID is the first 20 bytes of the SHA-256 hash of the public key. | ||
| * | ||
| * @param publicKey Base64-encoded validator public key. | ||
| * @returns The node ID (tendermint "address") for that public key. | ||
| */ | ||
| public static publicKeyToNodeId(publicKey: string): string { | ||
| const hash = createHash("SHA256"); | ||
| const pub = Buffer.from(publicKey, "base64"); | ||
| return hash.update(pub).digest().slice(0, 20).toString("hex"); | ||
| assert.equal(pub.length, NodeClient.PUBLIC_KEY_LENGTH, "invalid public key"); | ||
|
|
||
| const hash = createHash("SHA256").update(pub); | ||
| const digest = hash.digest(); | ||
| const nodeId = digest.slice(0, NodeClient.NODE_ID_HASH_OFFSET); | ||
| return nodeId.toString("hex"); | ||
| } | ||
|
|
||
| private static _convertValidatorData(...rawValidators: any[]): Validator[] { | ||
| const validators = []; | ||
| for (const validator of rawValidators) { | ||
| // HACK: protobuf nests the balance as `balance: "value: N"` | ||
| const balance = parseInt(validator.balance.split(": ")[1]); | ||
|
This conversation was marked as resolved
by hrharder
hrharder
Author
Member
|
||
|
|
||
| validators.push({ ...validator, balance }); | ||
| } | ||
| return validators; | ||
| } | ||
|
|
||
| /** | ||
| * Create a new NodeClient (`node`) via a connection to a Kosu node serving | ||
| * the Kosu JSONRPC/WebSocket. | ||
| * | ||
| * @param url Full URL to the Kosu node's WebSocket JSONRPC endpoint. | ||
| * @param options Options to provide the underlying `WebSocketProvider`. | ||
| * @example | ||
| * ```typescript | ||
| * // create a node client (with a request/connection timeout of 1s) | ||
| * const node = new NodeClient("wss://localhost:14342", { timeout: 1000 }); | ||
| * ``` | ||
| */ | ||
| constructor(url: string, options?: WebsocketProviderOptions) { | ||
| this._provider = new WebsocketProvider(url, options || NodeClient.DEFAULT_OPTIONS); | ||
| this._subscriptionIdMap = {}; | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_addOrders`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#addorders) | ||
| * | ||
| * Submit poster-signed orders to the Kosu node to be subsequently proposed | ||
| * to the network. In order for them to be accepted, they must have signatures | ||
| * from valid posters who have bonded Kosu tokens. | ||
| * | ||
| * See the `posterRegistry.registerTokens()` method to bond KOSU. | ||
| * | ||
| * @param orders Orders to submit to the node as subsequent arguments. | ||
| * @returns Validation results from the Kosu node, and/or the transaction | ||
| * ID's of the accepted orders. | ||
| */ | ||
| public async addOrders(...orders: any[]): Promise<OrderValidationResult[]> { | ||
| return this._call("kosu_addOrders", ...orders); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_latestHeight`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#latestheight) | ||
| * | ||
| * Get the height of the most recently committed and finalized Kosu block. | ||
| * | ||
| * @returns The most recent Kosu block number. | ||
| */ | ||
| public async latestHeight(): Promise<number> { | ||
| return this._call("kosu_latestHeight"); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_numberPosters`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#numberposters) | ||
| * | ||
| * Get the total number registered posters from the Kosu node. | ||
| * | ||
| * @returns The total number of poster accounts the node is tracking. | ||
| */ | ||
| public async numberPosters(): Promise<number> { | ||
| return this._call("kosu_numberPosters"); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_queryPoster`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#queryposter) | ||
| * | ||
| * Get finalized (committed into current state) balance and order limit data | ||
| * about a specified poster account. | ||
| * | ||
| * @returns Balance and order limit data for the specified poster account. | ||
| */ | ||
| public async queryPoster(address: string): Promise<Poster> { | ||
| if (/^0x[a-fA-F0-9]{40}$/.test(address)) { | ||
| throw new Error("invalid Ethereum address string"); | ||
| } | ||
| assert(/^0x[a-fA-F0-9]{40}$/.test(address), "invalid Ethereum address string"); | ||
| return this._call("kosu_queryPoster", address.toLowerCase()); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_queryValidator`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#queryvalidator) | ||
| * | ||
| * Get finalized (committed into current state) information about a Kosu | ||
| * validator node, identified by their node ID (also called Tendermint | ||
| * address). | ||
| * | ||
| * See `NodeClient.publicKeyToNodeId()` to converting a validator's encoded | ||
| * public key to it's node ID. | ||
| * | ||
| * @returns Information about the requested validator (see `Validator`). | ||
| */ | ||
| public async queryValidator(nodeId: string): Promise<Validator> { | ||
| if (!/^[a-fA-F0-9]{40}$/.test(nodeId)) { | ||
| throw new Error("invalid validator node ID string"); | ||
| } | ||
| assert(/^[a-fA-F0-9]{40}$/.test(nodeId), "invalid nodeId string"); | ||
|
|
||
| // hack: dealing with protobuf decoding issues | ||
| const raw = await this._call("kosu_queryValidator", nodeId); | ||
| return NodeClient._convertValidatorData(raw)[0]; | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_remainingLimit`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#remaininglimit) | ||
| * | ||
| * Get the total number of orders that _may_ be posted this period. It is | ||
| * equal to the sum of the unutilized bandwidth allocation for each poster | ||
| * account for the current rebalance period. | ||
| * | ||
| * @returns The unutilized order bandwidth for the current period. | ||
| */ | ||
| public async remainingLimit(): Promise<number> { | ||
| return this._call("kosu_remainingLimit"); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_roundInfo`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#roundinfo) | ||
| * | ||
| * Get the current rebalance period number, starting Ethereum block, ending | ||
| * Ethereum block, and the maximum number of orders for the period. | ||
| * | ||
| * @returns Information about the current rebalance period. | ||
| */ | ||
| public async roundInfo(): Promise<RoundInfo> { | ||
| const { | ||
| number: num, | ||
| limit, | ||
| limit: lim, | ||
| starts_at: startsAt, | ||
| ends_at: endsAt, | ||
| } = await this._call("kosu_roundInfo"); | ||
| return { number: num, limit, startsAt, endsAt }; | ||
| return { number: num, limit: lim, startsAt, endsAt }; | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_totalOrders`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#totalorders) | ||
| * | ||
| * Get the total number of orders that have been processed by the network | ||
| * since genesis. | ||
| * | ||
| * @returns The total number of orders posted since network genesis. | ||
| */ | ||
| public async totalOrders(): Promise<number> { | ||
| return this._call("kosu_totalOrders"); | ||
| } | ||
|
|
||
| /** | ||
| * See [`kosu_validators`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#validators) | ||
| * | ||
| * Get finalized (committed into current state) information about the current | ||
| * full validator set. Returns the full set (not paginated). | ||
| * | ||
| * @returns Information about all active Kosu validators (see `Validator`). | ||
| */ | ||
| public async validators(): Promise<Validator[]> { | ||
| const rawValidators = await this._call("kosu_validators"); | ||
| return NodeClient._convertValidatorData(...rawValidators); | ||
| } | ||
|
|
||
| public async subscribeToOrders(cb: (orders: any[]) => void): Promise<string> { | ||
| const id = await this._subscribe("newOrders"); | ||
| const subId = this._subscriptionIdMap[id]; | ||
| this._provider.on(subId, cb as any); | ||
| return id; | ||
| /** | ||
| * Read about Kosu subscriptions [here](https://docs.kosu.io/go-kosu/kosu_rpc.html#subscriptions). | ||
| * | ||
| * See [`kosu_subscribe` for topic `newOrders`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#neworders) | ||
| * | ||
| * Subscribe to order transaction events, and be udpdated with an array of new | ||
| * orders each time they are included in a Kosu block. | ||
| * | ||
| * @param cb A callback function to handle each array of new orders. | ||
| * @returns A UUID that can be used to cancel the new subscription (see `node.unsubscribe()`). | ||
| */ | ||
| public async subscribeToOrders(cb: (order: any) => void): Promise<string> { | ||
| return this._subscribe("newOrders", cb); | ||
| } | ||
|
|
||
| public async subscribeToBlocks(cb: (orders: any[]) => void): Promise<string> { | ||
| const id = await this._subscribe("newBlocks"); | ||
| const subId = this._subscriptionIdMap[id]; | ||
| this._provider.on(subId, cb as any); | ||
| return id; | ||
| /** | ||
| * Read about Kosu subscriptions [here](https://docs.kosu.io/go-kosu/kosu_rpc.html#subscriptions). | ||
| * | ||
| * See [`kosu_subscribe` for topic `newBlocks`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#newblocks) | ||
| * | ||
| * Subscribe to new block events, and be updated with the full Tendermint block | ||
| * after each successful commit. | ||
| * | ||
| * @param cb A callback function to handle new rebalance information. | ||
| * @returns A UUID that can be used to cancel the new subscription (see `node.unsubscribe()`). | ||
| */ | ||
| public async subscribeToBlocks(cb: (block: any) => void): Promise<string> { | ||
| return this._subscribe("newBlocks", cb); | ||
| } | ||
|
|
||
| public async subscribeToRebalances(cb: (orders: any[]) => void): Promise<string> { | ||
| const id = await this._subscribe("newRebalances"); | ||
| const subId = this._subscriptionIdMap[id]; | ||
| this._provider.on(subId, cb as any); | ||
| return id; | ||
| /** | ||
| * Read about Kosu subscriptions [here](https://docs.kosu.io/go-kosu/kosu_rpc.html#subscriptions). | ||
| * | ||
| * See [`kosu_subscribe` for topic `newRebalances`.](https://docs.kosu.io/go-kosu/kosu_rpc.html#newrebalances) | ||
| * | ||
| * Subscribe to rebalance events, and be updated with each new rebalance round | ||
| * information (starting block, ending block, etc.). | ||
| * | ||
| * @param cb A callback function to handle new rebalance information. | ||
| * @returns A UUID that can be used to cancel the new subscription (see `node.unsubscribe()`). | ||
| */ | ||
| public async subscribeToRebalances(cb: (roundInfo: RoundInfo) => void): Promise<string> { | ||
| return this._subscribe("newRebalances", cb); | ||
| } | ||
|
|
||
| /** | ||
| * Cancel an active subscription. | ||
| * | ||
| * @param subscriptionId The UUID of the subscription to cancel. | ||
| */ | ||
| public async unsubscribe(subscriptionId: string): Promise<void> { | ||
| assert(isString(subscriptionId), "subscriptionId must be a string"); | ||
| const kosuSubscriptionId = this._subscriptionIdMap[subscriptionId]; | ||
| await this._call("kosu_unsubscribe", kosuSubscriptionId); | ||
| } | ||
|
|
||
| private async _call(method: string, ...params: any[]): Promise<any> { | ||
| assert(isString(method), "method must be a string"); | ||
| return this._provider.send(method, params || []); | ||
| } | ||
|
|
||
| private async _subscribe(topic: string): Promise<string> { | ||
| private async _subscribe(topic: string, cb: (...args: any) => any): Promise<string> { | ||
| assert(isString(topic), "topic must be a string"); | ||
| assert(isFunction(cb), "cb must be a function"); | ||
|
|
||
| const kosuSubscriptionId = await this._provider.subscribe("kosu_subscribe", topic, []); | ||
| const mappingId = uuid(); | ||
| this._subscriptionIdMap[mappingId] = kosuSubscriptionId; | ||
| this._provider.on(kosuSubscriptionId, cb as any); | ||
| return mappingId; | ||
| } | ||
| } | ||
ProTip!
Use n and p to navigate between commits in a pull request.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Should this be stored as a BigNumber? I am under the impression this is tokens not power. The minimum right now 5e20.