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

Commit

Permalink
Merge pull request #1487 from hyperledger/solts
Browse files Browse the repository at this point in the history
Pull solts into Burrow
  • Loading branch information
Casey Kuhlman committed May 24, 2021
2 parents 163912e + 8c11544 commit fe242ae
Show file tree
Hide file tree
Showing 59 changed files with 3,738 additions and 196 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
# [Hyperledger Burrow](https://github.com/hyperledger/burrow) Changelog
## [0.33.0] - 2021-05-24
### Changed
- [JS] Changed Burrow interface and renamed Burrow client object to to Client (merging in features needed for solts support)

### Fixed
- [JS] Fixed RLP encoding extra leading zeros on uint64 (thanks Matthieu Vachon!)
- [JS] Improved compatibility with legacy Solidity bytes types and padding conventions
- [Events] Fixed Burrow event stream wrongly switching to streaming mode for block ranges that are available in state (when the latest block is an empty block - so not stored in state)

### Added
- [JS] Added Solidity-to-Typescript code generation support (merging in solts) - this provides helpers (build.ts, api.ts) to compile Solidity files into corresponding .abi.ts files that include types for functions, events, the ABI, and EVM bytecode, and includes bindings into Burrow JS to deploy and interact with contracts via Typescript/Javascript with strong static types
- [JS] Improved interactions with events which can now be queried over any range and with strong types, see the listenerFor, reduceEvents, readEvents, and iterateEvents functions.


## [0.32.1] - 2021-05-15
### Changed
- [Execution] CallErrors no longer emit very long rather pointless (since there is no tooling to help interpret them currently) EVM call traces
Expand Down Expand Up @@ -753,6 +767,7 @@ This release marks the start of Eris-DB as the full permissioned blockchain node
- [Blockchain] Fix getBlocks to respect block height cap.


[0.33.0]: https://github.com/hyperledger/burrow/compare/v0.32.1...v0.33.0
[0.32.1]: https://github.com/hyperledger/burrow/compare/v0.32.0...v0.32.1
[0.32.0]: https://github.com/hyperledger/burrow/compare/v0.31.3...v0.32.0
[0.31.3]: https://github.com/hyperledger/burrow/compare/v0.31.2...v0.31.3
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ publish_js:
yarn --cwd js build
yarn --cwd js publish --access public --non-interactive --no-git-tag-version --new-version $(shell ./scripts/local_version.sh)

.PHONY: clean_js
clean_js:
find js -name '*.abi.ts' -exec rm '{}' ';' -print

.PHONY: test
test: check bin/solc bin/solang
@tests/scripts/bin_wrapper.sh go test ./... ${GO_TEST_ARGS}
Expand Down
12 changes: 10 additions & 2 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
### Changed
- [Execution] CallErrors no longer emit very long rather pointless (since there is no tooling to help interpret them currently) EVM call traces
- [JS] Return byte arrays as Buffers from decode (only return fixed-width byteNN types as hex strings)
- [JS] Changed Burrow interface and renamed Burrow client object to to Client (merging in features needed for solts support)

### Fixed
- [JS] Fixed RLP encoding extra leading zeros on uint64 (thanks Matthieu Vachon!)
- [JS] Improved compatibility with legacy Solidity bytes types and padding conventions
- [Events] Fixed Burrow event stream wrongly switching to streaming mode for block ranges that are available in state (when the latest block is an empty block - so not stored in state)

### Added
- [JS] Added Solidity-to-Typescript code generation support (merging in solts) - this provides helpers (build.ts, api.ts) to compile Solidity files into corresponding .abi.ts files that include types for functions, events, the ABI, and EVM bytecode, and includes bindings into Burrow JS to deploy and interact with contracts via Typescript/Javascript with strong static types
- [JS] Improved interactions with events which can now be queried over any range and with strong types, see the listenerFor, reduceEvents, readEvents, and iterateEvents functions.

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
20 changes: 11 additions & 9 deletions js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
"description": "TypeScript library that calls a Hyperledger Burrow server over GRPC.",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"author": {
"name": "Dennis Mckinnon"
},
"files": [
"dist",
"proto"
Expand All @@ -16,15 +13,20 @@
},
"scripts": {
"build": "tsc --build",
"test": "./with-burrow.sh mocha 'src/test/**/*.test.ts'",
"lint:fix": "eslint --fix src/**/*.ts"
"test": "./with-burrow.sh mocha 'src/**/*.test.ts'",
"lint:fix": "eslint --fix 'src/**/*.ts'",
"lint:fix:abi": "eslint --fix --quiet 'src/**/*.abi.ts'",
"test:generate": "ts-node src/solts/sol/build.ts && yarn lint:fix:abi",
"generate:interface": "ts-node src/solts/interface.gd.ts.gr.ts && eslint --fix --quiet src/solts/interface.gd.ts"
},
"dependencies": {
"@grpc/grpc-js": "^1.3.0",
"ethers": "^5.1.4",
"google-protobuf": "^3.15.8",
"solc": "^0.8.4",
"sha3": "^2.1.4"
"sha3": "^2.1.4",
"solc_v5": "npm:solc@^0.5.17",
"solc_v8": "npm:solc@^0.8.4",
"typescript": "^4.2.4"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.2",
Expand All @@ -33,11 +35,11 @@
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.25.0",
"eslint-plugin-prettier": "^3.3.1",
"prettier": "^2.2.1",
"prettier-plugin-organize-imports": "^1.1.1",
"grpc-tools": "^1.11.1",
"grpc_tools_node_protoc_ts": "^5.2.1",
"mocha": "^8.3.2",
"prettier": "^2.2.1",
"prettier-plugin-organize-imports": "^1.1.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},
Expand Down
93 changes: 68 additions & 25 deletions js/src/burrow.ts → js/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
import * as grpc from '@grpc/grpc-js';
import { Readable } from 'stream';
import { TxExecution } from '../proto/exec_pb';
import { Interface } from 'ethers/lib/utils';
import { Event, 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, latestStreamingBlockRange } from './events';
import { Bounds, EventCallback, EventStream, getBlockRange, queryFor, stream } from './events';
import { Namereg } from './namereg';
import { Provider } from './solts/interface.gd';

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

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

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

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

readonly executionEvents: IExecutionEventsClient;
Expand All @@ -34,14 +39,13 @@ export class Burrow {
this.executionEvents = new ExecutionEventsClient(url, credentials);
this.transact = new TransactClient(url, credentials);
this.query = new QueryClient(url, credentials);
// 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);

// 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);
}

/**
Expand Down Expand Up @@ -77,24 +81,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 @@ -107,16 +111,55 @@ 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,
signatures: string[],
address: string,
callback: EventCallback,
range: BlockRange = latestStreamingBlockRange,
): Readable {
return this.events.listen(range, address, signature, callback);
callback: EventCallback<Event>,
start: Bounds = 'latest',
end: Bounds = 'stream',
): EventStream {
return stream(this.executionEvents, getBlockRange(start, end), queryFor({ address, signatures }), callback);
}

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

callTx(data: string | Uint8Array, address?: string, contractMeta: ContractMeta[] = []): CallTx {
return callTx(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;
// };
Loading

0 comments on commit fe242ae

Please sign in to comment.