Skip to content

Commit

Permalink
Merge pull request #109 from casper-ecosystem/feature/casper-provider
Browse files Browse the repository at this point in the history
Feature/casper provider
  • Loading branch information
hoffmannjan authored Sep 28, 2021
2 parents 8bb9da3 + d5207bf commit 144a350
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
17 changes: 12 additions & 5 deletions src/services/CasperServiceByJsonRPC.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
110 changes: 110 additions & 0 deletions src/services/ProviderTransport.ts
Original file line number Diff line number Diff line change
@@ -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<T> extends JRPCBase {
method: string;
params?: T;
}

export interface JRPCResponse<T> extends JRPCBase {
result?: T;
error?: any;
}

export type SendCallBack<U> = (err: any, providerRes: U) => void;

export interface SafeEventEmitterProvider {
sendAsync: <T, U>(req: JRPCRequest<T>) => Promise<U>;
send: <T, U>(req: JRPCRequest<T>, callback: SendCallBack<U>) => void;
}

class ProviderTransport extends Transport {
public provider: SafeEventEmitterProvider;

constructor(provider: SafeEventEmitterProvider) {
super();
this.provider = provider;
}

public connect(): Promise<any> {
return Promise.resolve();
}

public async sendData(
data: IJSONRPCData,
timeout: number | null = null
): Promise<any> {
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<any>
);
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;
4 changes: 1 addition & 3 deletions test/lib/CasperClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
130 changes: 130 additions & 0 deletions test/nctl/Provider.setup.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): 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<unknown>;
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<unknown>,
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<string, unknown>);
// set result and exit retry loop
return result;
};

const processDeploy = async (
req: JRPCRequest<unknown>,
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<unknown>): Promise<any> {
// 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<unknown>, __: SendCallBack<any>): void {
return;
}
}
67 changes: 67 additions & 0 deletions test/nctl/Provider.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Loading

0 comments on commit 144a350

Please sign in to comment.