diff --git a/packages/harmony-account/src/wallet.ts b/packages/harmony-account/src/wallet.ts index 6f2c738..8cb857c 100644 --- a/packages/harmony-account/src/wallet.ts +++ b/packages/harmony-account/src/wallet.ts @@ -3,13 +3,33 @@ import { isPrivateKey } from '@harmony/utils'; import { Account } from './account'; class Wallet { + /** + * @memberof Wallet + * + */ private accountMap: Map = new Map(); + /** + * @memberof Wallet + * @return {string[]} accounts addresses + */ get accounts(): string[] { return [...this.accountMap.keys()]; } + /** + * @function generateMnemonic + * @memberof Wallet + * @return {string} Mnemonics + */ generateMnemonic(): string { return bip39.generateMnemonic(); } + /** + * @function addByMnemonic + * @memberof Wallet + * @description add account using Mnemonic phrases + * @param {string} phrase - Mnemonic phrase + * @param {index} index - index to hdKey root + */ addByMnemonic(phrase: string, index: number = 0) { if (!this.isValidMnemonic(phrase)) { throw new Error(`Invalid mnemonic phrase: ${phrase}`); @@ -20,6 +40,13 @@ class Wallet { const privateKey = childKey.privateKey.toString('hex'); return this.addByPrivateKey(privateKey); } + /** + * @function addByPrivateKey + * @memberof Wallet + * @description add an account using privateKey + * @param {string} privateKey - privateKey to add + * @return {Account} return added Account + */ addByPrivateKey(privateKey: string): Account { try { const newAcc = Account.add(privateKey); @@ -33,6 +60,16 @@ class Wallet { throw error; } } + /** + * @function encryptAccount + * @memberof Wallet + * @description to encrypt an account that lives in the wallet, + * if encrypted, returns original one, if not found, throw error + * @param {string} address - address in accounts + * @param {string} password - string that used to encrypt + * @param {EncryptOptions} options - encryption options + * @return {Promise} + */ async encryptAccount( address: string, password: string, @@ -60,7 +97,15 @@ class Wallet { throw error; } } - + /** + * @function decryptAccount + * @memberof Wallet + * @description to decrypt an account that lives in the wallet,if not encrypted, return original, + * if not found, throw error + * @param {string} address - address in accounts + * @param {string} password - string that used to encrypt + * @return {Promise} + */ async decryptAccount(address: string, password: string): Promise { try { const foundAcc = this.getAccount(address); @@ -85,14 +130,34 @@ class Wallet { } } + /** + * @function getAccount + * @memberof Wallet + * @description get Account instance using address as param + * @param {string} address - address hex + * @return {Account} Account instance which lives in Wallet + */ getAccount(address: string): Account | undefined { return this.accountMap.get(address); } + /** + * @function removeAccount + * @memberof Wallet + * @description remove Account using address as param + * @param {string} address: - address hex + */ removeAccount(address: string): void { this.accountMap.delete(address); } + /** + * @function isValidMnemonic + * @memberof Wallet + * @description check if Mnemonic is valid + * @param {string} phrase - Mnemonic phrase + * @return {boolean} + */ private isValidMnemonic(phrase: string): boolean { if (phrase.trim().split(/\s+/g).length < 12) { return false; diff --git a/packages/harmony-network/package.json b/packages/harmony-network/package.json index 2fa7d54..037a48c 100644 --- a/packages/harmony-network/package.json +++ b/packages/harmony-network/package.json @@ -7,10 +7,13 @@ "browser": "dist/index.js", "module": "dist/index.js", "jsnext:main": "dist/index.js", - "typings":"lib/index.d.ts", + "typings": "lib/index.d.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "neeboo@firestack.one", - "license": "ISC" + "license": "ISC", + "dependencies": { + "cross-fetch": "^3.0.2" + } } diff --git a/packages/harmony-network/src/baseProvider.ts b/packages/harmony-network/src/baseProvider.ts index e69de29..7f5ba9c 100644 --- a/packages/harmony-network/src/baseProvider.ts +++ b/packages/harmony-network/src/baseProvider.ts @@ -0,0 +1,85 @@ +import { ReqMiddleware, ResMiddleware, MiddlewareType } from './types'; +import { RPCMethod } from './rpc'; + +class BaseProvider { + middlewares = { + request: { + use: (fn: ReqMiddleware, match: string | RPCMethod | RegExp = '*') => { + this.pushMiddleware(fn, MiddlewareType.REQ, match); + }, + }, + response: { + use: (fn: ResMiddleware, match: string | RPCMethod | RegExp = '*') => { + this.pushMiddleware(fn, MiddlewareType.RES, match); + }, + }, + }; + protected url: string; + protected reqMiddleware: ReqMiddleware = new Map().set('*', []); + protected resMiddleware: ResMiddleware = new Map().set('*', []); + + constructor( + url: string, + reqMiddleware: ReqMiddleware = new Map(), + resMiddleware: ResMiddleware = new Map(), + ) { + this.reqMiddleware = reqMiddleware; + this.resMiddleware = resMiddleware; + this.url = url; + } + + protected pushMiddleware( + fn: any, + type: MiddlewareType, + match: string | RPCMethod | RegExp, + ) { + if (type !== MiddlewareType.REQ && type !== MiddlewareType.RES) { + throw new Error('Please specify the type of middleware being added'); + } + if (type === MiddlewareType.REQ) { + const current = this.reqMiddleware.get(match) || []; + this.reqMiddleware.set(match, [...current, fn]); + } else { + const current = this.resMiddleware.get(match) || []; + this.resMiddleware.set(match, [...current, fn]); + } + } + protected getMiddleware( + method: RPCMethod, + ): [ReqMiddleware[], ResMiddleware[]] { + const requests: ReqMiddleware[] = []; + const responses: ResMiddleware[] = []; + + for (const [key, transformers] of this.reqMiddleware.entries()) { + if (typeof key === 'string' && key !== '*' && key === method) { + requests.push(...transformers); + } + + if (key instanceof RegExp && key.test(method)) { + requests.push(...transformers); + } + + if (key === '*') { + requests.push(...transformers); + } + } + + for (const [key, transformers] of this.resMiddleware.entries()) { + if (typeof key === 'string' && key !== '*' && key === method) { + responses.push(...transformers); + } + + if (key instanceof RegExp && key.test(method)) { + responses.push(...transformers); + } + + if (key === '*') { + responses.push(...transformers); + } + } + + return [requests, responses]; + } +} + +export { BaseProvider }; diff --git a/packages/harmony-network/src/defaultFetcher.ts b/packages/harmony-network/src/defaultFetcher.ts new file mode 100644 index 0000000..793fb38 --- /dev/null +++ b/packages/harmony-network/src/defaultFetcher.ts @@ -0,0 +1,34 @@ +import fetch from 'cross-fetch'; +import { RPCRequest, RPCResponseBody, RPCError, RPCResult } from './types'; + +export const fetchRPC = { + requestHandler: (request: RPCRequest, headers: any) => + fetch(request.url, { + method: + request.options && request.options.method + ? request.options.method + : 'POST', + cache: 'no-cache', + mode: 'cors', + redirect: 'follow', + referrer: 'no-referrer', + body: JSON.stringify(request.payload), + headers: { + ...headers, + ...(request.options && request.options.headers + ? request.options.headers + : {}), + }, + }), + responseHandler: ( + response: Response, + request: RPCRequest, + handler: any, + ) => + response + .json() + .then((body: RPCResponseBody) => { + return { ...body, req: request }; + }) + .then(handler), +}; diff --git a/packages/harmony-network/src/http.ts b/packages/harmony-network/src/http.ts new file mode 100644 index 0000000..bea4f21 --- /dev/null +++ b/packages/harmony-network/src/http.ts @@ -0,0 +1,161 @@ +import { BaseProvider } from './baseProvider'; +import { fetchRPC } from './defaultFetcher'; +import { + composeMiddleware, + performRPC, + DEFAULT_TIMEOUT, + DEFAULT_HEADERS, +} from './net'; + +import { RPCRequestPayload } from './types'; + +const defaultOptions = { + method: 'POST', + timeout: DEFAULT_TIMEOUT, + headers: DEFAULT_HEADERS, + user: null, + password: null, +}; + +class HttpProvider extends BaseProvider { + url: string; + fetcher: any; + options: any; + constructor(url: string, options: any, fetcher: any) { + super(url); + this.url = url || 'http://localhost:4201'; + this.fetcher = fetcher || fetchRPC; + if (options) { + this.options = { + method: options.method || defaultOptions.method, + timeout: options.timeout || defaultOptions.timeout, + user: options.user || defaultOptions.user, + password: options.password || defaultOptions.password, + headers: options.headers || defaultOptions.headers, + }; + } else { + this.options = defaultOptions; + } + } + + /** + * @function send + * @memberof HttpProvider.prototype + * @param {Object} payload - payload object + * @param {Function} callback - callback function + * @return {any} - RPC Response + */ + send(payload: RPCRequestPayload, callback: any): Promise { + return this.requestFunc({ payload, callback }); + } + + /** + * @function sendServer + * @memberof HttpProvider.prototype + * @param {String} endpoint - endpoint to server + * @param {Object} payload - payload object + * @param {Function} callback - callback function + * @return {Function} - RPC Response + */ + sendServer( + endpoint: string, + payload: RPCRequestPayload, + callback: any, + ): Promise { + return this.requestFunc({ endpoint, payload, callback }); + } + + requestFunc({ + endpoint, + payload, + callback, + }: { + endpoint?: string; + payload: RPCRequestPayload; + callback?: any; + }): Promise { + const [tReq, tRes] = this.getMiddleware(payload.method); + const reqMiddleware = composeMiddleware( + ...tReq, + (obj: object) => this.optionsHandler(obj), + (obj: object) => this.endpointHandler(obj, endpoint), + this.payloadHandler, + ); + const resMiddleware = composeMiddleware( + (data: object) => this.callbackHandler(data, callback), + ...tRes, + ); + + const req = reqMiddleware(payload); + + return performRPC(req, resMiddleware, this.fetcher); + } + + /** + * @function payloadHandler + * @memberof HttpProvider.prototype + * @param {Object} payload - payload object + * @return {Object} - to payload object + */ + payloadHandler(payload: RPCRequestPayload): object { + return { payload }; + } + + /** + * @function endpointHandler + * @memberof HttpProvider.prototype + * @param {Object} obj - payload object + * @param {String} endpoint - add the endpoint to payload object + * @return {Object} - assign a new object + */ + endpointHandler(obj: object, endpoint?: string): object { + return { + ...obj, + url: + endpoint !== null && endpoint !== undefined + ? `${this.url}${endpoint}` + : this.url, + }; + } + + /** + * @function optionsHandler + * @memberof HttpProvider.prototype + * @param {Object} obj - options object + * @return {Object} - assign a new option object + */ + optionsHandler(obj: object) { + if (this.options.user && this.options.password) { + const AUTH_TOKEN = `Basic ${Buffer.from( + `${this.options.user}:${this.options.password}`, + ).toString('base64')}`; + this.options.headers.Authorization = AUTH_TOKEN; + } + + return { ...obj, options: this.options }; + } + + /** + * @function callbackHandler + * @memberof HttpProvider.prototype + * @param {Object} data - from server + * @param {Function} cb - callback function + * @return {Object|Function} - return object or callback function + */ + callbackHandler(data: any, cb: any): any { + if (cb) { + cb(null, data); + } + return data; + } + + subscribe() { + throw new Error('HTTPProvider does not support subscriptions.'); + } + + unsubscribe() { + throw new Error('HTTPProvider does not support subscriptions.'); + } +} + +export { HttpProvider }; diff --git a/packages/harmony-network/src/index.ts b/packages/harmony-network/src/index.ts index e69de29..f5817cf 100644 --- a/packages/harmony-network/src/index.ts +++ b/packages/harmony-network/src/index.ts @@ -0,0 +1,6 @@ +export * from './baseProvider'; +export * from './defaultFetcher'; +export * from './http'; +export * from './net'; +export * from './rpc'; +export * from './types'; diff --git a/packages/harmony-network/src/net.ts b/packages/harmony-network/src/net.ts new file mode 100644 index 0000000..039c028 --- /dev/null +++ b/packages/harmony-network/src/net.ts @@ -0,0 +1,44 @@ +export const DEFAULT_TIMEOUT: number = 120000; + +export const DEFAULT_HEADERS: object = { 'Content-Type': 'application/json' }; + +function _fetch(fetchPromise: Promise, timeout: number) { + let abortFn: () => void; + + const abortPromise = new Promise((resolve, reject) => { + abortFn = () => reject(new Error(`request Timeout in ${timeout} ms`)); + }); + const abortablePromise = Promise.race([fetchPromise, abortPromise]); + + setTimeout(() => { + abortFn(); + }, timeout); + + return abortablePromise; +} + +export const performRPC = async (request: any, handler: any, fetcher: any) => { + try { + const response = await _fetch( + fetcher.requestHandler(request, DEFAULT_HEADERS), + request.options && request.options.timeout + ? request.options.timeout + : DEFAULT_TIMEOUT, + ); + return fetcher.responseHandler(response, request, handler); + } catch (err) { + throw err; + } +}; + +export function composeMiddleware(...fns: any[]): any { + if (fns.length === 0) { + return (arg: any) => arg; + } + + if (fns.length === 1) { + return fns[0]; + } + + return fns.reduce((a, b) => (arg: any) => a(b(arg))); +} diff --git a/packages/harmony-network/src/rpc.ts b/packages/harmony-network/src/rpc.ts index e69de29..c49cc58 100644 --- a/packages/harmony-network/src/rpc.ts +++ b/packages/harmony-network/src/rpc.ts @@ -0,0 +1,42 @@ +export const enum RPCMethod { + // account related + FetchBalance = 'FetchBalance', + // block info related + GetLatestBlock = 'GetLatestBlock', + GetBlock = 'GetBlock', + GetEstimtedGas = 'GetEstimatedGas', + GetLatestTransactions = 'GetLatestTransactions', + GetLatestDSBlocks = 'GetLatestDSBlocks', + // transaction related + SendTransaction = 'SendTransaction', + SendTransactionToShard = 'SendTransactionToShard', + SendTransactionToBlock = 'SendTransactionToBlock', + GetTransaction = 'GetTransaction', +} + +export const enum RPCErrorCode { + // Standard JSON-RPC 2.0 errors + // RPC_INVALID_REQUEST is internally mapped to HTTP_BAD_REQUEST (400). + // It should not be used for application-layer errors. + RPC_INVALID_REQUEST = -32600, + // RPC_METHOD_NOT_FOUND is internally mapped to HTTP_NOT_FOUND (404). + // It should not be used for application-layer errors. + RPC_METHOD_NOT_FOUND = -32601, + RPC_INVALID_PARAMS = -32602, + // RPC_INTERNAL_ERROR should only be used for genuine errors in bitcoind + // (for example datadir corruption). + RPC_INTERNAL_ERROR = -32603, + RPC_PARSE_ERROR = -32700, + + // General application defined errors + RPC_MISC_ERROR = -1, // std::exception thrown in command handling + RPC_TYPE_ERROR = -3, // Unexpected type was passed as parameter + RPC_INVALID_ADDRESS_OR_KEY = -5, // Invalid address or key + RPC_INVALID_PARAMETER = -8, // Invalid, missing or duplicate parameter + RPC_DATABASE_ERROR = -20, // Database error + RPC_DESERIALIZATION_ERROR = -22, // Error parsing or validating structure in raw format + RPC_VERIFY_ERROR = -25, // General error during transaction or block submission + RPC_VERIFY_REJECTED = -26, // Transaction or block was rejected by network rules + RPC_IN_WARMUP = -28, // Client still warming up + RPC_METHOD_DEPRECATED = -32, // RPC method is deprecated +} diff --git a/packages/harmony-network/src/types.ts b/packages/harmony-network/src/types.ts new file mode 100644 index 0000000..15a3f35 --- /dev/null +++ b/packages/harmony-network/src/types.ts @@ -0,0 +1,55 @@ +import { RPCMethod, RPCErrorCode } from './rpc'; + +export type ReqMiddleware = Map; +export type ResMiddleware = Map; + +export const enum MiddlewareType { + REQ, + RES, +} + +export interface Middleware { + request: object; + response: object; +} + +export interface RPCRequestPayload { + id: number; + jsonrpc: string; + method: RPCMethod; + params: T; +} + +export interface RPCRequestOptions { + headers: []; + method: string; +} + +export interface RPCRequest { + url: string; + payload: RPCRequestPayload; + options: RPCRequestOptions; +} + +export interface RPCResponseBase { + jsonrpc: string; + id: string; +} + +export interface RPCResponseBody extends RPCResponseBase { + result: R; + error: E; +} + +export interface RPCError { + code: RPCErrorCode; + message: string; + data: any; +} + +export interface RPCResult { + resultString: string; + resultMap: Map; + resultList: any[]; + raw: any; +}