-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #109 from casper-ecosystem/feature/casper-provider
Feature/casper provider
- Loading branch information
Showing
9 changed files
with
342 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.