diff --git a/CHANGELOG.md b/CHANGELOG.md index 937615969..5c727fef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to casper-js-sdk. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.6.0 + +### Added + +- Added `CasperProvider` + ## 2.5.2 ### Fixed diff --git a/package-lock.json b/package-lock.json index 708f5576a..acbec841c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "casper-js-sdk", - "version": "2.5.0", + "version": "2.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 795ff96f5..8452f600f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "casper-js-sdk", - "version": "2.5.2", + "version": "2.6.0", "license": "Apache 2.0", "description": "SDK to interact with the Casper blockchain", "homepage": "https://github.com/casper-ecosystem/casper-js-sdk#README.md", diff --git a/src/services/CasperServiceByJsonRPC.ts b/src/services/CasperServiceByJsonRPC.ts index 5b3a94224..438014315 100644 --- a/src/services/CasperServiceByJsonRPC.ts +++ b/src/services/CasperServiceByJsonRPC.ts @@ -1,10 +1,12 @@ import { RequestManager, HTTPTransport, Client } from '@open-rpc/client-js'; -import { jsonMember, jsonObject } from 'typedjson'; +import { TypedJSON, jsonMember, jsonObject } from 'typedjson'; import { DeployUtil, encodeBase16, CLPublicKey } from '..'; import { deployToJson } from '../lib/DeployUtil'; -import { TypedJSON } from 'typedjson'; import { StoredValue, Transfers } from '../lib/StoredValue'; import { BigNumber } from '@ethersproject/bignumber'; +import ProviderTransport, { + SafeEventEmitterProvider +} from './ProviderTransport'; interface RpcResult { api_version: string; @@ -190,10 +192,15 @@ export interface ValidatorsInfoResult extends RpcResult { } export class CasperServiceByJsonRPC { - private client: Client; + protected client: Client; - constructor(url: string) { - const transport = new HTTPTransport(url); + constructor(provider: string | SafeEventEmitterProvider) { + let transport: HTTPTransport | ProviderTransport; + if (typeof provider === 'string') { + transport = new HTTPTransport(provider); + } else { + transport = new ProviderTransport(provider); + } const requestManager = new RequestManager([transport]); this.client = new Client(requestManager); } diff --git a/src/services/ProviderTransport.ts b/src/services/ProviderTransport.ts new file mode 100644 index 000000000..87e505a91 --- /dev/null +++ b/src/services/ProviderTransport.ts @@ -0,0 +1,110 @@ +import { Transport } from '@open-rpc/client-js/build/transports/Transport'; +import { + JSONRPCRequestData, + getNotifications, + getBatchRequests, + IJSONRPCData, + IJSONRPCRequest +} from '@open-rpc/client-js/build/Request'; +import { ERR_UNKNOWN, JSONRPCError } from '@open-rpc/client-js/build/Error'; + +export type JRPCVersion = '2.0'; +export type JRPCId = number | string | void; + +export interface JRPCBase { + jsonrpc?: JRPCVersion; + id?: JRPCId; +} + +export interface JRPCRequest extends JRPCBase { + method: string; + params?: T; +} + +export interface JRPCResponse extends JRPCBase { + result?: T; + error?: any; +} + +export type SendCallBack = (err: any, providerRes: U) => void; + +export interface SafeEventEmitterProvider { + sendAsync: (req: JRPCRequest) => Promise; + send: (req: JRPCRequest, callback: SendCallBack) => void; +} + +class ProviderTransport extends Transport { + public provider: SafeEventEmitterProvider; + + constructor(provider: SafeEventEmitterProvider) { + super(); + this.provider = provider; + } + + public connect(): Promise { + return Promise.resolve(); + } + + public async sendData( + data: IJSONRPCData, + timeout: number | null = null + ): Promise { + const prom = this.transportRequestManager.addRequest(data, timeout); + const notifications = getNotifications(data); + const batch = getBatchRequests(data); + try { + const result = await this.provider.sendAsync( + (data.request as IJSONRPCRequest) as JRPCRequest + ); + const jsonrpcResponse = { + id: data.request.id, + jsonrpc: data.request.jsonrpc, + result, + error: null + }; + // requirements are that notifications are successfully sent + this.transportRequestManager.settlePendingRequest(notifications); + if (this.onlyNotifications(data)) { + return Promise.resolve(); + } + const responseErr = this.transportRequestManager.resolveResponse( + JSON.stringify(jsonrpcResponse) + ); + if (responseErr) { + // requirements are that batch requests are successfully resolved + // this ensures that individual requests within the batch request are settled + this.transportRequestManager.settlePendingRequest(batch, responseErr); + return Promise.reject(responseErr); + } + } catch (e) { + const responseErr = new JSONRPCError(e.message, ERR_UNKNOWN, e); + this.transportRequestManager.settlePendingRequest( + notifications, + responseErr + ); + this.transportRequestManager.settlePendingRequest( + getBatchRequests(data), + responseErr + ); + return Promise.reject(responseErr); + } + return prom; + } + + public close(): void { + return; + } + + private onlyNotifications = (data: JSONRPCRequestData) => { + if (data instanceof Array) { + return data.every( + datum => + datum.request.request.id === null || + datum.request.request.id === undefined + ); + } + return data.request.id === null || data.request.id === undefined; + }; +} + +export default ProviderTransport; diff --git a/test/lib/CasperClient.test.ts b/test/lib/CasperClient.test.ts index ab97dd4f5..158132ca8 100644 --- a/test/lib/CasperClient.test.ts +++ b/test/lib/CasperClient.test.ts @@ -12,9 +12,7 @@ import { decodeBase16 } from '../../src'; let casperClient: CasperClient; describe('CasperClient', () => { before(() => { - casperClient = new CasperClient( - 'http://192.168.2.166:40101/rpc' - ); + casperClient = new CasperClient('http://192.168.2.166:40101/rpc'); }); it('should generate new Ed25519 key pair, and compute public key from private key', () => { diff --git a/test/nctl/Provider.setup.ts b/test/nctl/Provider.setup.ts new file mode 100644 index 000000000..2d24033c3 --- /dev/null +++ b/test/nctl/Provider.setup.ts @@ -0,0 +1,130 @@ +import { CasperClient, DeployUtil } from '../../src'; +import { Secp256K1 } from '../../src/lib/Keys'; +import { + JRPCRequest, + SendCallBack +} from '../../src/services/ProviderTransport'; + +const keyPair = Secp256K1.new(); + +function parseResponse(fetchRes: any, body: Record): any { + // check for error code + if (fetchRes.status !== 200) { + throw new Error( + JSON.stringify({ + message: `Non-200 status code: '${fetchRes.status}'`, + data: body + }) + ); + } + // check for rpc error + if (body.error) { + throw new Error(body.error as string); + } + // return successful result + return body.result; +} + +const createFetchConfigFromReq = ({ + req, + rpcTarget +}: { + req: JRPCRequest; + rpcTarget: string; +}) => { + const parsedUrl: URL = new URL(rpcTarget); + + // prepare payload + // copy only canonical json rpc properties + const payload = { + id: req.id, + jsonrpc: req.jsonrpc, + method: req.method, + params: req.params + }; + // serialize request body + const serializedPayload: string = JSON.stringify(payload); + + // configure fetch params + const fetchParams = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: serializedPayload + }; + + return { fetchUrl: parsedUrl.href, fetchParams }; +}; +const sendRpcRequestToChain = async ( + req: JRPCRequest, + rpcTarget: string +) => { + const { fetchUrl, fetchParams } = createFetchConfigFromReq({ + req, + rpcTarget + }); + const fetchRes = await fetch(fetchUrl, fetchParams); + if (fetchRes.status >= 400) { + throw new Error( + `Request failed with status: ${ + fetchRes.status + } and body: ${JSON.stringify(fetchRes.body || {})}` + ); + } + // parse response body + const fetchBody = await fetchRes.json(); + const result = parseResponse(fetchRes, fetchBody as Record); + // set result and exit retry loop + return result; +}; + +const processDeploy = async ( + req: JRPCRequest, + client: CasperClient, + rpcTarget: string +) => { + // we can do any preprocessing or validation on deploy here, + // and then finally sign deploy and send it blockchain. + const deserializedDeploy = DeployUtil.deployFromJson(req.params as any); + if (deserializedDeploy.ok) { + const signedDeploy = client.signDeploy(deserializedDeploy.val, keyPair); + req.params = DeployUtil.deployToJson(signedDeploy); + // const jrpcResult = await sendRpcRequestToChain(req, rpcTarget); + const jrpcResult = { deploy_hash: '0x123', rpcTarget }; + return jrpcResult + } + throw new Error('Failed to parse deploy'); +}; + +export class MockProvider { + private rpcTarget: string; + private client: CasperClient; + + constructor(rpcTarget: string) { + this.rpcTarget = rpcTarget; + this.client = new CasperClient(rpcTarget); + } + + async sendAsync(req: JRPCRequest): Promise { + // we are intercepting 'account_put_deploy' (ie. signing the deploy and then submitting the signed deploy + // to blockchain) + // for rest of rpc calls we are simply sending rpc call to blockchain and returning the result. + if (req.method === 'account_put_deploy') { + return processDeploy(req, this.client, this.rpcTarget); + } else { + try { + const jrpcResult = await sendRpcRequestToChain(req, this.rpcTarget); + return jrpcResult + } catch (error) { + throw error; + } + } + } + + // currently we only use sendAsync in provider transport, so we live it unimplemented here. + send(_: JRPCRequest, __: SendCallBack): void { + return; + } +} diff --git a/test/nctl/Provider.test.ts b/test/nctl/Provider.test.ts new file mode 100644 index 000000000..a82bfae17 --- /dev/null +++ b/test/nctl/Provider.test.ts @@ -0,0 +1,67 @@ +import { assert } from 'chai'; +import { + CasperServiceByJsonRPC, +} from '../../src/services'; +import { Keys, DeployUtil, RuntimeArgs } from '../../src/index'; +import { MockProvider } from './Provider.setup'; + +const rpcTarget = 'http://127.0.0.1:11101/rpc'; +const provider = new MockProvider(rpcTarget); +const client = new CasperServiceByJsonRPC(provider); + +describe('Provider', () => { + xit('should return correct block by number', async () => { + let check = async (height: number) => { + let result = await client.getBlockInfoByHeight(height); + assert.equal(result.block?.header.height, height); + }; + let blocks_to_check = 3; + for (let i = 0; i < blocks_to_check; i++) { + await check(i); + } + }); + + xit('should return correct block by hash', async () => { + let check = async (height: number) => { + let block_by_height = await client.getBlockInfoByHeight(height); + let block_hash = block_by_height.block?.hash!; + let block = await client.getBlockInfo(block_hash); + assert.equal(block.block?.hash, block_hash); + }; + let blocks_to_check = 3; + for (let i = 0; i < blocks_to_check; i++) { + await check(i); + } + }); + + xit('should not allow to send deploy larger then 1 megabyte.', async () => { + // moduleBytes need to have length of (1 megabyte - 169 bytes) to produce + // a deploy with the size of (1 megabyte + 1 byte). + const oneMegaByte = 1048576; + const moduleBytes = Uint8Array.from(Array(oneMegaByte - 169).fill(0)); + + let deployParams = new DeployUtil.DeployParams( + Keys.Ed25519.new().publicKey, + 'test' + ); + let session = DeployUtil.ExecutableDeployItem.newModuleBytes( + moduleBytes, + RuntimeArgs.fromMap({}) + ); + let payment = DeployUtil.standardPayment(100000); + let deploy = DeployUtil.makeDeploy(deployParams, session, payment); + + assert.equal(DeployUtil.deploySizeInBytes(deploy), oneMegaByte + 1); + await client + .deploy(deploy) + .then(_ => { + assert.fail("client.deploy should't throw an error."); + }) + .catch(err => { + let expectedMessage = + `Deploy can not be send, because it's too large: ${oneMegaByte + + 1} bytes. ` + `Max size is 1 megabyte.`; + assert.equal(err.message, expectedMessage); + }); + }); +}); diff --git a/test/nctl/RPC.test.ts b/test/nctl/RPC.test.ts index 999a1901e..2a81af5d4 100644 --- a/test/nctl/RPC.test.ts +++ b/test/nctl/RPC.test.ts @@ -67,16 +67,22 @@ describe('RPC', () => { xit('DeployWatcher', () => { const client = new DeployWatcher('http://localhost:18101/events/main'); - client.subscribe([{ - deployHash: '418bd905f86cad3bc3c46340ddf5119da4c51d2da24cf07cfe7c79a7f14f50aa', - eventHandlerFn: value => console.log('SUBSCRIBED VALUE', value) - }]); + client.subscribe([ + { + deployHash: + '418bd905f86cad3bc3c46340ddf5119da4c51d2da24cf07cfe7c79a7f14f50aa', + eventHandlerFn: value => console.log('SUBSCRIBED VALUE', value) + } + ]); client.start(); setTimeout(() => { - client.subscribe([{ - deployHash: '7a28f822a89b7dd65c0d29765e28d949a343d0b2c9cbee02abc89eaba542a7e5', - eventHandlerFn: value => console.log('SUBSCRIBED VALUE 2', value) - }]); + client.subscribe([ + { + deployHash: + '7a28f822a89b7dd65c0d29765e28d949a343d0b2c9cbee02abc89eaba542a7e5', + eventHandlerFn: value => console.log('SUBSCRIBED VALUE 2', value) + } + ]); }, 3 * 10000); });