Skip to content
This repository has been archived by the owner on May 13, 2022. It is now read-only.

Commit

Permalink
Fix up code gen and pull in Client
Browse files Browse the repository at this point in the history
- Switch to use promises
- Support function overloads
- Add support for events

Signed-off-by: Silas Davis <[email protected]>
  • Loading branch information
Silas Davis committed May 21, 2021
1 parent e3d11fa commit 07509f9
Show file tree
Hide file tree
Showing 40 changed files with 1,748 additions and 760 deletions.
3 changes: 2 additions & 1 deletion docs/reference/web3.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ you can simply add Burrow to the list of networks.
## Remix

[Remix](https://remix.ethereum.org/) is a web-based integrated development environment for Solidity.
To deploy and run transactions, select `Web3 Provider` as the `Environment` and enter your local RPC
To deploy and run transactions, select `Web3
Provider` as the `Environment` and enter your local RPC
address when prompted.

## Truffle
Expand Down
3 changes: 2 additions & 1 deletion js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"build": "tsc --build",
"test": "./with-burrow.sh mocha 'src/**/*.test.ts'",
"lint:fix": "eslint --fix 'src/**/*.ts'",
"test:generate": "ts-node src/solts/sol/build.ts"
"test:generate": "ts-node src/solts/sol/build.ts",
"generate:provider": "ts-node src/solts/provider.gd.ts.gr.ts && eslint --fix --quiet src/solts/provider.gd.ts"
},
"dependencies": {
"@grpc/grpc-js": "^1.3.0",
Expand Down
75 changes: 61 additions & 14 deletions js/src/burrow.ts → js/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import * as grpc from '@grpc/grpc-js';
import { Interface } from 'ethers/lib/utils';
import { TxExecution } from '../proto/exec_pb';
import { CallTx, ContractMeta } from '../proto/payload_pb';
import { ExecutionEventsClient, IExecutionEventsClient } from '../proto/rpcevents_grpc_pb';
import { BlockRange } from '../proto/rpcevents_pb';
import { IQueryClient, QueryClient } from '../proto/rpcquery_grpc_pb';
import { GetMetadataParam } from '../proto/rpcquery_pb';
import { GetMetadataParam, StatusParam } from '../proto/rpcquery_pb';
import { ITransactClient, TransactClient } from '../proto/rpctransact_grpc_pb';
import { callTx } from './contracts/call';
import { ResultStatus } from '../proto/rpc_pb';
import { ContractCodec, getContractCodec } from './codec';
import { Address } from './contracts/abi';
import { makeCallTx } from './contracts/call';
import { CallOptions, Contract, ContractInstance } from './contracts/contract';
import { toBuffer } from './convert';
import { getException } from './error';
import { EventCallback, Events, EventStream, latestStreamingBlockRange } from './events';
import { Namereg } from './namereg';
import { Provider } from './solts/provider.gd';

type TxCallback = (error: grpc.ServiceError | null, txe: TxExecution) => void;

export type Pipe = (payload: CallTx, callback: TxCallback) => void;

export class Burrow {
export type Interceptor = (result: TxExecution) => Promise<TxExecution>;

export class Client implements Provider {
interceptor: Interceptor;
readonly events: Events;
readonly namereg: Namereg;

Expand All @@ -33,14 +41,15 @@ export class Burrow {
this.executionEvents = new ExecutionEventsClient(url, credentials);
this.transact = new TransactClient(url, credentials);
this.query = new QueryClient(url, credentials);

this.callPipe = this.transact.callTxSync.bind(this.transact);
this.simPipe = this.transact.callTxSim.bind(this.transact);

// This is the execution events streaming service running on top of the raw streaming function.
this.events = new Events(this.executionEvents);
// Contracts stuff running on top of grpc
this.namereg = new Namereg(this.transact, this.query, this.account);
// NOTE: in general interceptor may be async
this.interceptor = async (data) => data;

this.callPipe = this.transact.callTxSync.bind(this.transact);
this.simPipe = this.transact.callTxSim.bind(this.transact);
}

/**
Expand Down Expand Up @@ -76,24 +85,24 @@ export class Burrow {
);
}

call(callTx: CallTx): Promise<TxExecution> {
callTxSync(callTx: CallTx): Promise<TxExecution> {
return new Promise((resolve, reject) =>
this.callPipe(callTx, (error, txe) => {
this.transact.callTxSync(callTx, (error, txe) => {
if (error) {
return reject(error);
}
const err = getException(txe);
if (err) {
return reject(err);
}
return resolve(txe);
return resolve(this.interceptor(txe));
}),
);
}

callSim(callTx: CallTx): Promise<TxExecution> {
callTxSim(callTx: CallTx): Promise<TxExecution> {
return new Promise((resolve, reject) =>
this.simPipe(callTx, (error, txe) => {
this.transact.callTxSim(callTx, (error, txe) => {
if (error) {
return reject(error);
}
Expand All @@ -106,6 +115,39 @@ export class Burrow {
);
}

status(): Promise<ResultStatus> {
return new Promise((resolve, reject) =>
this.query.status(new StatusParam(), (err, resp) => (err ? reject(err) : resolve(resp))),
);
}

async latestHeight(): Promise<number> {
const status = await this.status();
return status.getSyncinfo()?.getLatestblockheight() ?? 0;
}

// Methods below implement the generated codegen provider
// TODO: should probably generate canonical version of Provider interface somewhere outside of files

async deploy(msg: CallTx): Promise<Address> {
const txe = await this.callTxSync(msg);
const contractAddress = txe.getReceipt()?.getContractaddress_asU8();
if (!contractAddress) {
throw new Error(`deploy appears to have succeeded but contract address is missing from result: ${txe}`);
}
return Buffer.from(contractAddress).toString('hex').toUpperCase();
}

async call(msg: CallTx): Promise<Uint8Array | undefined> {
const txe = await this.callTxSync(msg);
return txe.getResult()?.getReturn_asU8();
}

async callSim(msg: CallTx): Promise<Uint8Array | undefined> {
const txe = await this.callTxSim(msg);
return txe.getResult()?.getReturn_asU8();
}

listen(
signature: string,
address: string,
Expand All @@ -115,7 +157,12 @@ export class Burrow {
return this.events.listen(range, address, signature, callback);
}

callTx(data: string | Uint8Array, address?: string, contractMeta: ContractMeta[] = []): CallTx {
return callTx(typeof data === 'string' ? toBuffer(data) : data, this.account, address, contractMeta);
payload(data: string | Uint8Array, address?: string, contractMeta: ContractMeta[] = []): CallTx {
return makeCallTx(typeof data === 'string' ? toBuffer(data) : data, this.account, address, contractMeta);
}

contractCodec(contractABI: string): ContractCodec {
const iface = new Interface(contractABI);
return getContractCodec(iface);
}
}
75 changes: 75 additions & 0 deletions js/src/codec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { EventFragment, Fragment, FunctionFragment, Interface } from 'ethers/lib/utils';
import { postDecodeResult, preEncodeResult, prefixedHexString, toBuffer } from './convert';

export type ContractCodec = {
encodeDeploy(...args: unknown[]): Buffer;
encodeFunctionData(signature: string, ...args: unknown[]): Buffer;
decodeFunctionResult(signature: string, data: Uint8Array | undefined): any;
decodeEventLog(signature: string, data: Uint8Array | undefined, topics?: Uint8Array[]): any;
};

export function getContractCodec(iface: Interface): ContractCodec {
return {
encodeDeploy(...args: unknown[]): Buffer {
const frag = iface.deploy;
try {
return toBuffer(iface.encodeDeploy(preEncodeResult(args, frag.inputs)));
} catch (err) {
throwErr(err, 'encode deploy', 'constructor', args, frag);
}
},

encodeFunctionData(signature: string, ...args: unknown[]): Buffer {
let frag: FunctionFragment | undefined;
try {
frag = iface.getFunction(formatSignature(signature));
return toBuffer(iface.encodeFunctionData(frag, preEncodeResult(args, frag.inputs)));
} catch (err) {
throwErr(err, 'encode function data', signature, args, frag);
}
},

decodeFunctionResult(signature: string, data: Uint8Array = new Uint8Array()): any {
let frag: FunctionFragment | undefined;
try {
frag = iface.getFunction(formatSignature(signature));
return postDecodeResult(iface.decodeFunctionResult(frag, data), frag.outputs);
} catch (err) {
throwErr(err, 'decode function result', signature, { data }, frag);
}
},

decodeEventLog(signature: string, data: Uint8Array = new Uint8Array(), topics?: Uint8Array[]): any {
let frag: EventFragment | undefined;
try {
frag = iface.getEvent(formatSignature(signature));
return postDecodeResult(
iface.decodeEventLog(
frag,
prefixedHexString(data),
topics?.map((topic) => prefixedHexString(topic)),
),
frag.inputs,
);
} catch (err) {
throwErr(err, 'decode event log', signature, { data, topics }, frag);
}
},
};
}

function formatSignature(signature: string): string {
return prefixedHexString(signature);
}

function throwErr(
err: unknown,
action: string,
signature: string,
args: Record<string, unknown> | unknown[],
frag?: Fragment,
): never {
const name = frag ? frag.name : `member with signature '${signature}'`;
const inputs = frag?.inputs ? ` (inputs: ${JSON.stringify(frag.inputs)})` : '';
throw new Error(`ContractCodec could not ${action} for ${name} with args ${JSON.stringify(args)}${inputs}: ${err}`);
}
35 changes: 6 additions & 29 deletions js/src/contracts/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { SolidityFunction } from 'solc';
import { Result } from '../../proto/exec_pb';
import { CallTx, ContractMeta, TxInput } from '../../proto/payload_pb';
import { Envelope } from '../../proto/txs_pb';
import { Pipe } from '../burrow';
import { abiToBurrowResult, burrowToAbiResult, toBuffer } from '../convert';
import { Pipe } from '../client';
import { postDecodeResult, preEncodeResult, toBuffer } from '../convert';
import { Address } from './abi';
import { CallOptions } from './contract';

Expand All @@ -19,7 +19,7 @@ const WasmMagic = Buffer.from('\0asm');

const coder = new AbiCoder();

export function callTx(
export function makeCallTx(
data: Uint8Array,
inputAddress: Address,
contractAddress?: Address,
Expand Down Expand Up @@ -155,32 +155,9 @@ export async function callFunction(
callee: Address,
args: unknown[],
): Promise<unknown> {
const data = toBuffer(iface.encodeFunctionData(frag, burrowToAbiResult(args, frag.inputs)));
const transactionResult = await call(pipe, middleware(callTx(data, input, callee)));
const data = toBuffer(iface.encodeFunctionData(frag, preEncodeResult(args, frag.inputs)));
const transactionResult = await call(pipe, middleware(makeCallTx(data, input, callee)));
// Unpack return arguments
const result = abiToBurrowResult(iface.decodeFunctionResult(frag, transactionResult.resultBytes), frag.outputs);
const result = postDecodeResult(iface.decodeFunctionResult(frag, transactionResult.resultBytes), frag.outputs);
return handler({ transactionResult, result });
}

// const decodeF = function (abi: Function, output: Uint8Array): DecodedResult {
// if (!output) {
// return;
// }
//
// const outputs = abi.outputs;
// const outputTypes = types(outputs);
//
// Decode raw bytes to arguments
// const raw = convert.abiToBurrow(outputTypes, coder.rawDecode(outputTypes, Buffer.from(output)));
// const result: DecodedResult = { raw: raw.slice() };
//
// result.values = outputs.reduce(function (acc, current) {
// const value = raw.shift();
// if (current.name) {
// acc[current.name] = value;
// }
// return acc;
// }, {});
//
// return result;
// };
6 changes: 3 additions & 3 deletions js/src/contracts/compile.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import solc from 'solc';
import solc from 'solc_v5';
import { CompiledContract, Contract } from './contract';

// Compile solidity source code
export function compile<T = any>(
source: string,
name: string,
errorSeverity: 'error' | 'warning' = 'error',
fatalErrorSeverity: 'error' | 'warning' = 'error',
): Contract<T> {
const desc: solc.InputDescription = { language: 'Solidity', sources: {} };
if (!desc.sources) {
Expand All @@ -16,7 +16,7 @@ export function compile<T = any>(

const json = solc.compile(JSON.stringify(desc));
const compiled: solc.OutputDescription = JSON.parse(json);
const fatalErrors = compiled.errors.filter((err) => err.severity === errorSeverity);
const fatalErrors = compiled.errors?.filter((err) => err.severity === fatalErrorSeverity) ?? [];
if (fatalErrors.length) {
throw new Error(fatalErrors.map((err) => err.formattedMessage).toString());
}
Expand Down
20 changes: 10 additions & 10 deletions js/src/contracts/contract.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { EventFragment, Fragment, FunctionFragment, Interface, LogDescription } from 'ethers/lib/utils';
import { Keccak } from 'sha3';
import { CallTx, ContractMeta } from '../../proto/payload_pb';
import { Burrow } from '../burrow';
import { burrowToAbiResult, toBuffer } from '../convert';
import { Client } from '../client';
import { preEncodeResult, toBuffer } from '../convert';
import { EventStream } from '../events';
import { ABI, Address } from './abi';
import { call, callFunction, CallResult, callTx, DecodeResult } from './call';
import { call, callFunction, CallResult, DecodeResult, makeCallTx } from './call';
import { EventCallback, listen } from './event';

export const meta: unique symbol = Symbol('meta');
Expand Down Expand Up @@ -54,7 +54,7 @@ export class Contract<T extends ContractInstance | any = any> {
this.iface = new Interface(this.code.abi);
}

at(address: Address, burrow: Burrow, options: CallOptions = defaultCallOptions): T {
at(address: Address, burrow: Client, options: CallOptions = defaultCallOptions): T {
const instance: ContractInstance = {
[meta]: {
address,
Expand All @@ -77,20 +77,20 @@ export class Contract<T extends ContractInstance | any = any> {
return [this.code, ...this.childCode].map(contractMeta).filter((m): m is ContractMeta => Boolean(m));
}

async deploy(burrow: Burrow, ...args: unknown[]): Promise<T> {
async deploy(burrow: Client, ...args: unknown[]): Promise<T> {
return this.deployWith(burrow, defaultCallOptions, ...args);
}

async deployWith(burrow: Burrow, options?: Partial<CallOptions>, ...args: unknown[]): Promise<T> {
async deployWith(burrow: Client, options?: Partial<CallOptions>, ...args: unknown[]): Promise<T> {
const opts = { ...defaultCallOptions, ...options };
if (!this.code.bytecode) {
throw new Error(`cannot deploy contract without compiled bytecode`);
}
const { middleware } = opts;
const data = Buffer.concat(
[this.code.bytecode, this.iface.encodeDeploy(burrowToAbiResult(args, this.iface.deploy.inputs))].map(toBuffer),
[this.code.bytecode, this.iface.encodeDeploy(preEncodeResult(args, this.iface.deploy.inputs))].map(toBuffer),
);
const tx = middleware(callTx(data, burrow.account, undefined, this.meta()));
const tx = middleware(makeCallTx(data, burrow.account, undefined, this.meta()));
const { contractAddress } = await call(burrow.callPipe, tx);
return this.at(contractAddress, burrow, opts);
}
Expand All @@ -116,7 +116,7 @@ export type ContractEvent = ((cb: EventCallback) => EventStream) & {
function contractFunction(
iface: Interface,
frag: FunctionFragment,
burrow: Burrow,
burrow: Client,
options: CallOptions,
contractAddress: string,
): ContractFunction {
Expand All @@ -135,7 +135,7 @@ function contractFunction(
return func;
}

function contractEvent(iface: Interface, frag: EventFragment, burrow: Burrow, contractAddress: string): ContractEvent {
function contractEvent(iface: Interface, frag: EventFragment, burrow: Client, contractAddress: string): ContractEvent {
const func = (cb: EventCallback) => listen(iface, frag, contractAddress, burrow, cb);
func.at = (address: string, cb: EventCallback) => listen(iface, frag, address, burrow, cb);
func.once = () =>
Expand Down
Loading

0 comments on commit 07509f9

Please sign in to comment.