From 189b6ceba0bc78db51a3dc979da20c9cb4fc1427 Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Sun, 16 May 2021 13:52:32 +0200 Subject: [PATCH 1/4] Pull solts into Burrow In preparation for built-in Solidity codegen Signed-off-by: Silas Davis --- js/package.json | 2 +- js/src/burrow.ts | 5 +- js/src/convert.ts | 1 - js/src/events.ts | 2 +- js/src/namereg.ts | 31 ++--- js/src/solts/api.ts | 63 +++++++++ js/src/solts/lib/abi.ts | 31 +++++ js/src/solts/lib/caller.ts | 83 ++++++++++++ js/src/solts/lib/compile.test.ts | 19 +++ js/src/solts/lib/compile.ts | 94 +++++++++++++ js/src/solts/lib/contract.ts | 112 ++++++++++++++++ js/src/solts/lib/decoder.ts | 94 +++++++++++++ js/src/solts/lib/deployer.ts | 125 ++++++++++++++++++ js/src/solts/lib/encoder.ts | 94 +++++++++++++ js/src/solts/lib/provider.ts | 168 +++++++++++++++++++++++ js/src/solts/lib/replacer.ts | 85 ++++++++++++ js/src/solts/lib/solidity.test.ts | 36 +++++ js/src/solts/lib/solidity.ts | 131 ++++++++++++++++++ js/src/solts/lib/syntax.test.ts | 18 +++ js/src/solts/lib/syntax.ts | 169 ++++++++++++++++++++++++ js/src/solts/main.ts | 71 ++++++++++ js/src/test/contract-event.test.ts | 1 - js/src/test/handler-overwriting.test.ts | 4 +- 23 files changed, 1412 insertions(+), 27 deletions(-) create mode 100644 js/src/solts/api.ts create mode 100644 js/src/solts/lib/abi.ts create mode 100644 js/src/solts/lib/caller.ts create mode 100644 js/src/solts/lib/compile.test.ts create mode 100644 js/src/solts/lib/compile.ts create mode 100644 js/src/solts/lib/contract.ts create mode 100644 js/src/solts/lib/decoder.ts create mode 100644 js/src/solts/lib/deployer.ts create mode 100644 js/src/solts/lib/encoder.ts create mode 100644 js/src/solts/lib/provider.ts create mode 100644 js/src/solts/lib/replacer.ts create mode 100644 js/src/solts/lib/solidity.test.ts create mode 100644 js/src/solts/lib/solidity.ts create mode 100644 js/src/solts/lib/syntax.test.ts create mode 100644 js/src/solts/lib/syntax.ts create mode 100644 js/src/solts/main.ts diff --git a/js/package.json b/js/package.json index 4c553f806..d98975115 100644 --- a/js/package.json +++ b/js/package.json @@ -17,7 +17,7 @@ "scripts": { "build": "tsc --build", "test": "./with-burrow.sh mocha 'src/test/**/*.test.ts'", - "lint:fix": "eslint --fix src/**/*.ts" + "lint:fix": "eslint --fix 'src/**/*.ts'" }, "dependencies": { "@grpc/grpc-js": "^1.3.0", diff --git a/js/src/burrow.ts b/js/src/burrow.ts index 0d46522ee..c0a1b7555 100644 --- a/js/src/burrow.ts +++ b/js/src/burrow.ts @@ -1,5 +1,4 @@ import * as grpc from '@grpc/grpc-js'; -import { Readable } from 'stream'; import { TxExecution } from '../proto/exec_pb'; import { CallTx, ContractMeta } from '../proto/payload_pb'; import { ExecutionEventsClient, IExecutionEventsClient } from '../proto/rpcevents_grpc_pb'; @@ -11,7 +10,7 @@ import { callTx } 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 { EventCallback, Events, EventStream, latestStreamingBlockRange } from './events'; import { Namereg } from './namereg'; type TxCallback = (error: grpc.ServiceError | null, txe: TxExecution) => void; @@ -112,7 +111,7 @@ export class Burrow { address: string, callback: EventCallback, range: BlockRange = latestStreamingBlockRange, - ): Readable { + ): EventStream { return this.events.listen(range, address, signature, callback); } diff --git a/js/src/convert.ts b/js/src/convert.ts index ae7d77399..782e612f1 100644 --- a/js/src/convert.ts +++ b/js/src/convert.ts @@ -59,7 +59,6 @@ function abiToBurrow(arg: unknown, type: string): unknown { return arg; } - function burrowToAbi(arg: unknown, type: string): unknown { if (/address/.test(type)) { return recApply(prefixedHexString, arg as NestedArray); diff --git a/js/src/events.ts b/js/src/events.ts index 47a1db5da..7760d4547 100644 --- a/js/src/events.ts +++ b/js/src/events.ts @@ -64,7 +64,7 @@ function boundsToBound(bounds: Bounds): Bound { bound.setType(BoundType.STREAM); break; default: - bound.setType(BoundType.LATEST); + bound.setType(BoundType.ABSOLUTE); bound.setIndex(bounds); } diff --git a/js/src/namereg.ts b/js/src/namereg.ts index 89513e5eb..3455a87c9 100644 --- a/js/src/namereg.ts +++ b/js/src/namereg.ts @@ -1,15 +1,12 @@ -import {IQueryClient} from '../proto/rpcquery_grpc_pb'; -import {GetNameParam} from '../proto/rpcquery_pb'; -import {Entry} from '../proto/names_pb'; -import {ITransactClient} from '../proto/rpctransact_grpc_pb'; -import {TxInput, NameTx} from '../proto/payload_pb'; -import {TxExecution} from '../proto/exec_pb'; -import * as grpc from '@grpc/grpc-js'; +import { TxExecution } from '../proto/exec_pb'; +import { Entry } from '../proto/names_pb'; +import { NameTx, TxInput } from '../proto/payload_pb'; +import { IQueryClient } from '../proto/rpcquery_grpc_pb'; +import { GetNameParam } from '../proto/rpcquery_pb'; +import { ITransactClient } from '../proto/rpctransact_grpc_pb'; export class Namereg { - - constructor(private transact: ITransactClient, private query: IQueryClient, private account: string) { - } + constructor(private transact: ITransactClient, private query: IQueryClient, private account: string) {} set(name: string, data: string, lease = 50000, fee = 5000): Promise { const input = new TxInput(); @@ -25,11 +22,11 @@ export class Namereg { return new Promise((resolve, reject) => { this.transact.nameTxSync(payload, (err, txe) => { if (err) { - reject(err) + reject(err); } - resolve(txe) + resolve(txe); }); - }) + }); } get(name: string): Promise { @@ -38,10 +35,10 @@ export class Namereg { return new Promise((resolve, reject) => { this.query.getName(payload, (err, entry) => { if (err) { - reject(err) + reject(err); } - resolve(entry) - }) - }) + resolve(entry); + }); + }); } } diff --git a/js/src/solts/api.ts b/js/src/solts/api.ts new file mode 100644 index 000000000..d48e65a96 --- /dev/null +++ b/js/src/solts/api.ts @@ -0,0 +1,63 @@ +import ts from 'typescript'; +import { ABI } from './lib/abi'; +import { Caller } from './lib/caller'; +import { Contract } from './lib/contract'; +import { Decode } from './lib/decoder'; +import { Deploy } from './lib/deployer'; +import { Encode } from './lib/encoder'; +import { Provider } from './lib/provider'; +import { Replacer } from './lib/replacer'; +import { GetContractMethods } from './lib/solidity'; +import { ExportToken, ImportReadable } from './lib/syntax'; + +export { DecodeOutput, EncodeInput, ImportLocal, InputDescriptionFromFiles, TokenizeLinks } from './lib/compile'; + +export type Compiled = { + name: string; + abi: ABI.FunctionOrEvent[]; + bin: string; + links: Array; +}; + +export function NewFile(contracts: Compiled[]): ts.Node[] { + const provider = new Provider(); + + return [ + ts.addSyntheticLeadingComment( + ImportReadable(), + ts.SyntaxKind.SingleLineCommentTrivia, + 'Code generated by solts. DO NOT EDIT.', + ), + provider.createInterface(), + Caller(provider), + Replacer(), + ...contracts.map((contract) => { + const methods = GetContractMethods(contract.abi); + + let deploy: ABI.Func; + contract.abi.map((abi) => { + if (abi.type == 'constructor') { + deploy = abi; + } + }); + + return ts.createModuleDeclaration( + undefined, + [ExportToken], + ts.createIdentifier(contract.name), + ts.createModuleBlock([ + Deploy(deploy, contract.bin, contract.links, provider), + Contract(methods, provider), + Encode(methods, provider), + Decode(methods, provider), + ]), + ); + }), + ]; +} + +export function Print(...nodes: ts.Node[]) { + const target = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + return nodes.map((node) => printer.printNode(ts.EmitHint.Unspecified, node, target)).join('\n'); +} diff --git a/js/src/solts/lib/abi.ts b/js/src/solts/lib/abi.ts new file mode 100644 index 000000000..c3a82cd75 --- /dev/null +++ b/js/src/solts/lib/abi.ts @@ -0,0 +1,31 @@ +export namespace ABI { + export type Func = { + type: 'function' | 'constructor' | 'fallback'; + name: string; + inputs?: Array; + outputs?: Array; + stateMutability: 'pure' | 'view' | 'nonpayable' | 'payable'; + payable?: boolean; + constant?: boolean; + }; + + export type Event = { + type: 'event'; + name: string; + inputs: Array; + anonymous: boolean; + }; + + export type FunctionInput = { + name: string; + type: string; + components?: FunctionInput[]; + internalType?: string; + }; + + export type FunctionOutput = FunctionInput; + export type EventInput = FunctionInput & { indexed?: boolean }; + + export type FunctionIO = FunctionInput & FunctionOutput; + export type FunctionOrEvent = Func | Event; +} diff --git a/js/src/solts/lib/caller.ts b/js/src/solts/lib/caller.ts new file mode 100644 index 000000000..90fcf34d5 --- /dev/null +++ b/js/src/solts/lib/caller.ts @@ -0,0 +1,83 @@ +import ts from 'typescript'; +import { Provider } from './provider'; +import { + BooleanType, + CreateCall, + CreateCallbackDeclaration, + CreateNewPromise, + CreateParameter, + CreatePromiseBody, + DeclareConstant, + PromiseType, + StringType, + Uint8ArrayType, +} from './syntax'; + +export const CallName = ts.createIdentifier('Call'); + +export const Caller = (provider: Provider) => { + const input = provider.getType(); + const output = ts.createIdentifier('Output'); + const client = ts.createIdentifier('client'); + const payload = ts.createIdentifier('payload'); + const data = ts.createIdentifier('data'); + const isSim = ts.createIdentifier('isSim'); + const callback = ts.createIdentifier('callback'); + const err = ts.createIdentifier('err'); + const exec = ts.createIdentifier('exec'); + const addr = ts.createIdentifier('addr'); + + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + CallName, + [ts.createTypeParameterDeclaration(input), ts.createTypeParameterDeclaration(output)], + [ + CreateParameter(client, provider.getTypeNode()), + CreateParameter(addr, StringType), + CreateParameter(data, StringType), + CreateParameter(isSim, BooleanType), + CreateParameter( + callback, + ts.createFunctionTypeNode( + undefined, + [CreateParameter('exec', Uint8ArrayType)], + ts.createTypeReferenceNode(output, undefined), + ), + ), + ], + ts.createTypeReferenceNode(PromiseType, [ts.createTypeReferenceNode(output, undefined)]), + ts.createBlock( + [ + DeclareConstant(payload, provider.methods.payload.call(client, data, addr)), + ts.createIf( + isSim, + ts.createReturn( + CreateNewPromise([ + ts.createExpressionStatement( + provider.methods.callSim.call( + client, + payload, + CreateCallbackDeclaration(err, exec, [CreatePromiseBody(err, [CreateCall(callback, [exec])])]), + ), + ), + ]), + ), + ts.createReturn( + CreateNewPromise([ + ts.createExpressionStatement( + provider.methods.call.call( + client, + payload, + CreateCallbackDeclaration(err, exec, [CreatePromiseBody(err, [CreateCall(callback, [exec])])]), + ), + ), + ]), + ), + ), + ], + true, + ), + ); +}; diff --git a/js/src/solts/lib/compile.test.ts b/js/src/solts/lib/compile.test.ts new file mode 100644 index 000000000..de8e5f97c --- /dev/null +++ b/js/src/solts/lib/compile.test.ts @@ -0,0 +1,19 @@ +import assert from 'assert'; +import { TokenizeLinks } from './compile'; + +describe('compilation helpers', function () { + it('should tokenize links', () => { + const links = { + 'dir/Errors.sol': { + Errors: [], + }, + 'lib/Utils.sol': { + Utils: [], + }, + }; + + const actual = TokenizeLinks(links); + assert.equal(actual[0], 'dir/Errors.sol:Errors'); + assert.equal(actual[1], 'lib/Utils.sol:Utils'); + }); +}); diff --git a/js/src/solts/lib/compile.ts b/js/src/solts/lib/compile.ts new file mode 100644 index 000000000..284b546a1 --- /dev/null +++ b/js/src/solts/lib/compile.ts @@ -0,0 +1,94 @@ +import * as fs from 'fs'; +import { ABI } from './abi'; + +export namespace Solidity { + type Bytecode = { + linkReferences: any; + object: string; + opcodes: string; + sourceMap: string; + }; + + type Contract = { + assembly: any; + evm: { + bytecode: Bytecode; + }; + functionHashes: any; + gasEstimates: any; + abi: ABI.FunctionOrEvent[]; + opcodes: string; + runtimeBytecode: string; + srcmap: string; + srcmapRuntime: string; + }; + + type Source = { + AST: any; + }; + + export type InputDescription = { + language: string; + sources?: Record; + settings?: { + outputSelection: Record>>; + }; + }; + + type Error = { + sourceLocation?: { + file: string; + start: number; + end: number; + }; + type: string; + component: string; + severity: 'error' | 'warning'; + message: string; + formattedMessage?: string; + }; + + export type OutputDescription = { + contracts: Record>; + errors: Array; + sourceList: Array; + sources: Record; + }; +} + +function NewInputDescription(): Solidity.InputDescription { + return { + language: 'Solidity', + sources: {}, + settings: { outputSelection: {} }, + }; +} + +export const EncodeInput = (obj: Solidity.InputDescription): string => JSON.stringify(obj); +export const DecodeOutput = (str: string): Solidity.OutputDescription => JSON.parse(str); + +export function InputDescriptionFromFiles(...names: string[]) { + const desc = NewInputDescription(); + names.map((name) => { + desc.sources[name] = { content: fs.readFileSync(name).toString() }; + desc.settings.outputSelection[name] = {}; + desc.settings.outputSelection[name]['*'] = ['*']; + }); + return desc; +} + +export function ImportLocal(path: string) { + return { + contents: fs.readFileSync(path).toString(), + }; +} + +export function TokenizeLinks(links: Record>) { + const libraries: Array = []; + for (const file in links) { + for (const library in links[file]) { + libraries.push(file + ':' + library); + } + } + return libraries; +} diff --git a/js/src/solts/lib/contract.ts b/js/src/solts/lib/contract.ts new file mode 100644 index 000000000..e3126163c --- /dev/null +++ b/js/src/solts/lib/contract.ts @@ -0,0 +1,112 @@ +import ts from 'typescript'; +import { CallName } from './caller'; +import { DecodeName } from './decoder'; +import { EncodeName } from './encoder'; +import { ErrParameter, EventParameter, Provider } from './provider'; +import { CollapseInputs, CombineTypes, ContractMethodsList, OutputToType, Signature } from './solidity'; +import { + AccessThis, + AsRefNode, + CreateCall, + CreateCallbackExpression, + CreateParameter, + DeclareConstant, + ExportToken, + Method, + PrivateToken, + PublicToken, + ReadableType, + StringType, + Uint8ArrayType, +} from './syntax'; + +const exec = ts.createIdentifier('exec'); +const data = ts.createIdentifier('data'); +const client = ts.createIdentifier('client'); +const address = ts.createIdentifier('address'); + +export const ContractName = ts.createIdentifier('Contract'); + +function SolidityFunction(name: string, signatures: Signature[]) { + const args = Array.from(CollapseInputs(signatures).keys()).map((key) => ts.createIdentifier(key)); + const encode = DeclareConstant( + data, + CreateCall(ts.createPropertyAccess(CreateCall(EncodeName, [AccessThis(client)]), name), args), + ); + + const call = ts.createCall( + CallName, + [ts.createTypeReferenceNode('Tx', undefined), OutputToType(signatures[0])], + [ + AccessThis(client), + AccessThis(address), + data, + ts.createLiteral(signatures[0].constant), + ts.createArrowFunction( + undefined, + undefined, + [CreateParameter(exec, Uint8ArrayType)], + undefined, + undefined, + ts.createBlock( + [ + ts.createReturn( + CreateCall(ts.createPropertyAccess(CreateCall(DecodeName, [AccessThis(client), exec]), name), []), + ), + ], + true, + ), + ), + ], + ); + + const params = Array.from(CollapseInputs(signatures), ([key, value]) => CreateParameter(key, CombineTypes(value))); + return new Method(name).parameters(params).declaration([encode, ts.createReturn(call)], true); +} + +function SolidityEvent(name: string, provider: Provider) { + const callback = ts.createIdentifier('callback'); + return new Method(name) + .parameter(callback, CreateCallbackExpression([ErrParameter, EventParameter])) + .returns(AsRefNode(ReadableType)) + .declaration([ + ts.createReturn( + provider.methods.listen.call(AccessThis(client), ts.createLiteral(name), AccessThis(address), callback), + ), + ]); +} + +function createMethodFromABI(name: string, type: 'function' | 'event', signatures: Signature[], provider: Provider) { + if (type === 'function') { + return SolidityFunction(name, signatures); + } else if (type === 'event') { + return SolidityEvent(name, provider); + } +} + +export const Contract = (abi: ContractMethodsList, provider: Provider) => { + return ts.createClassDeclaration( + undefined, + [ExportToken], + ContractName, + [provider.getTypeArgumentDecl()], + undefined, + [ + ts.createProperty(undefined, [PrivateToken], client, undefined, provider.getTypeNode(), undefined), + ts.createProperty(undefined, [PublicToken], address, undefined, StringType, undefined), + ts.createConstructor( + undefined, + undefined, + [CreateParameter(client, provider.getTypeNode()), CreateParameter(address, StringType)], + ts.createBlock( + [ + ts.createStatement(ts.createAssignment(AccessThis(client), client)), + ts.createStatement(ts.createAssignment(AccessThis(address), address)), + ], + true, + ), + ), + ...abi.map((abi) => createMethodFromABI(abi.name, abi.type, abi.signatures, provider)), + ], + ); +}; diff --git a/js/src/solts/lib/decoder.ts b/js/src/solts/lib/decoder.ts new file mode 100644 index 000000000..3ce7da91c --- /dev/null +++ b/js/src/solts/lib/decoder.ts @@ -0,0 +1,94 @@ +import ts from 'typescript'; +import { Provider } from './provider'; +import { ContractMethodsList, OutputToType, Signature } from './solidity'; +import { CreateParameter, DeclareConstant, Uint8ArrayType } from './syntax'; + +export const DecodeName = ts.createIdentifier('Decode'); + +function output(decodeFn: ts.CallExpression, sig: Signature): ts.Block { + let named = []; + if (sig.outputs && sig.outputs[0] !== undefined) { + named = sig.outputs.filter((out) => out.name !== ''); + } + + if (sig.outputs.length !== 0 && sig.outputs.length === named.length) { + const setter = ts.createVariableStatement( + [], + ts.createVariableDeclarationList( + [ + ts.createVariableDeclaration( + ts.createArrayBindingPattern( + sig.outputs.map((out) => ts.createBindingElement(undefined, undefined, out.name)), + ), + undefined, + decodeFn, + ), + ], + ts.NodeFlags.Const, + ), + ); + + return ts.createBlock( + [ + setter, + ts.createReturn( + ts.createObjectLiteral( + sig.outputs.map((out) => ts.createPropertyAssignment(out.name, ts.createIdentifier(out.name))), + ), + ), + ], + true, + ); + } else { + return ts.createBlock([ts.createReturn(sig.outputs.length > 0 ? decodeFn : undefined)]); + } +} + +function decoder(sig: Signature, client: ts.Identifier, provider: Provider, data: ts.Identifier) { + let args = []; + if (sig.outputs && sig.outputs[0] !== undefined) { + args = sig.outputs.map((arg) => ts.createLiteral(arg.type)); + } + const types = ts.createArrayLiteral(args); + const decodeFn = provider.methods.decode.call(client, data, types); + return decodeFn; +} + +export const Decode = (methods: ContractMethodsList, provider: Provider) => { + const client = ts.createIdentifier('client'); + const data = ts.createIdentifier('data'); + + return DeclareConstant( + DecodeName, + ts.createArrowFunction( + undefined, + [provider.getTypeArgumentDecl()], + [CreateParameter(client, provider.getTypeNode()), CreateParameter(data, Uint8ArrayType)], + undefined, + undefined, + ts.createBlock([ + ts.createReturn( + ts.createObjectLiteral( + methods + .filter((method) => method.type === 'function') + .map((method) => { + return ts.createPropertyAssignment( + method.name, + ts.createArrowFunction( + undefined, + undefined, + [], + OutputToType(method.signatures[0]), + undefined, + output(decoder(method.signatures[0], client, provider, data), method.signatures[0]), + ), + ); + }, true), + true, + ), + ), + ]), + ), + true, + ); +}; diff --git a/js/src/solts/lib/deployer.ts b/js/src/solts/lib/deployer.ts new file mode 100644 index 000000000..fb65511d1 --- /dev/null +++ b/js/src/solts/lib/deployer.ts @@ -0,0 +1,125 @@ +import ts from 'typescript'; +import { ABI } from './abi'; +import { Provider } from './provider'; +import { ReplacerName } from './replacer'; +import { GetRealType, Hash, TokenizeString } from './solidity'; +import { + BufferFrom, + CreateCall, + CreateCallbackDeclaration, + CreateNewPromise, + CreateParameter, + DeclareConstant, + DeclareLet, + ExportToken, + PromiseType, + RejectOrResolve, + StringType, +} from './syntax'; + +const data = ts.createIdentifier('data'); +const payload = ts.createIdentifier('payload'); +const bytecode = ts.createIdentifier('bytecode'); + +const err = ts.createIdentifier('err'); +const addr = ts.createIdentifier('addr'); + +const client = ts.createIdentifier('client'); +const address = ts.createIdentifier('address'); + +export const DeployName = ts.createIdentifier('Deploy'); + +export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Provider) => { + if (bin === '') { + return undefined; + } + + const parameters = abi ? abi.inputs.map((input) => CreateParameter(input.name, GetRealType(input.type))) : []; + // const output = ts.createExpressionWithTypeArguments([ts.createTypeReferenceNode(ContractName, [ts.createTypeReferenceNode('Tx', undefined)])], PromiseType); + const output = ts.createExpressionWithTypeArguments([StringType], PromiseType); + + const statements: ts.Statement[] = []; + statements.push(DeclareLet(bytecode, ts.createLiteral(bin))); + statements.push( + ...links.map((link) => { + return ts.createExpressionStatement( + ts.createAssignment( + bytecode, + CreateCall(ReplacerName, [ + bytecode, + ts.createStringLiteral('$' + Hash(link).toLowerCase().slice(0, 34) + '$'), + ts.createIdentifier(TokenizeString(link)), + ]), + ), + ); + }), + ); + + if (abi) { + const inputs = ts.createArrayLiteral(abi.inputs.map((arg) => ts.createLiteral(arg.type))); + const args = abi.inputs.map((arg) => ts.createIdentifier(arg.name)); + statements.push( + DeclareConstant( + data, + ts.createBinary( + bytecode, + ts.SyntaxKind.PlusToken, + provider.methods.encode.call(client, ts.createLiteral(''), inputs, ...args), + ), + ), + ); + } else { + statements.push(DeclareConstant(data, bytecode)); + } + statements.push(DeclareConstant(payload, provider.methods.payload.call(client, data, undefined))); + + const deployFn = provider.methods.deploy.call( + client, + payload, + CreateCallbackDeclaration( + err, + addr, + [ + RejectOrResolve( + err, + [ + DeclareConstant( + address, + CreateCall( + ts.createPropertyAccess( + CreateCall(ts.createPropertyAccess(BufferFrom(addr), ts.createIdentifier('toString')), [ + ts.createLiteral('hex'), + ]), + ts.createIdentifier('toUpperCase'), + ), + undefined, + ), + ), + ], + // [ts.createNew(ContractName, [], [client, address])]) + [address], + ), + ], + undefined, + true, + ), + ); + + statements.push(ts.createReturn(CreateNewPromise([ts.createStatement(deployFn)]))); + + const type = 'Tx'; + return ts.createFunctionDeclaration( + undefined, + [ExportToken], + undefined, + DeployName, + [ts.createTypeParameterDeclaration(type)], + [ + CreateParameter(client, ts.createTypeReferenceNode('Provider', [ts.createTypeReferenceNode(type, [])])), + ...links.map((link) => CreateParameter(ts.createIdentifier(TokenizeString(link)), StringType)), + ...parameters, + ], + output, + ts.createBlock(statements, true), + ); +}; diff --git a/js/src/solts/lib/encoder.ts b/js/src/solts/lib/encoder.ts new file mode 100644 index 000000000..05982646e --- /dev/null +++ b/js/src/solts/lib/encoder.ts @@ -0,0 +1,94 @@ +import ts from 'typescript'; +import { Provider } from './provider'; +import { CollapseInputs, CombineTypes, ContractMethodsList, Signature } from './solidity'; +import { CreateParameter, DeclareConstant } from './syntax'; + +export const EncodeName = ts.createIdentifier('Encode'); + +function join(...exp: ts.Expression[]) { + if (exp.length === 0) { + return undefined; + } + return exp.reduce((all, next) => { + return ts.createLogicalAnd(all, next); + }); +} + +function output(signatures: Array, client: ts.Identifier, provider: Provider): ts.Block { + if (signatures.length === 0) { + return ts.createBlock([ts.createReturn()]); + } else if (signatures.length === 1) { + return ts.createBlock([ts.createReturn(encoder(signatures[0], client, provider))]); + } + + return ts.createBlock( + [ + ...signatures + .filter((sig) => sig.inputs.length > 0) + .map((sig) => { + return ts.createIf( + join( + ...sig.inputs.map((input) => { + return ts.createStrictEquality( + ts.createTypeOf(ts.createIdentifier(input.name)), + ts.createLiteral('string'), + ); + }), + ), + ts.createReturn(encoder(sig, client, provider)), + ); + }), + ], + true, + ); +} + +function encoder(sig: Signature, client: ts.Identifier, provider: Provider) { + const inputs = ts.createArrayLiteral(sig.inputs.map((arg) => ts.createLiteral(arg.type))); + const args = sig.inputs.map((arg) => ts.createIdentifier(arg.name)); + const encodeFn = provider.methods.encode.call(client, ts.createLiteral(sig.hash), inputs, ...args); + return encodeFn; +} + +export const Encode = (methods: ContractMethodsList, provider: Provider) => { + const client = ts.createIdentifier('client'); + + return DeclareConstant( + EncodeName, + ts.createArrowFunction( + undefined, + [provider.getTypeArgumentDecl()], + [CreateParameter(client, provider.getTypeNode())], + undefined, + undefined, + ts.createBlock([ + ts.createReturn( + ts.createObjectLiteral( + methods + .filter((method) => method.type === 'function') + .map((method) => { + if (method.type !== 'function') { + return; + } + return ts.createPropertyAssignment( + method.name, + ts.createArrowFunction( + undefined, + undefined, + Array.from(CollapseInputs(method.signatures), ([key, value]) => + CreateParameter(key, CombineTypes(value)), + ), + undefined, + undefined, + output(method.signatures, client, provider), + ), + ); + }, true), + true, + ), + ), + ]), + ), + true, + ); +}; diff --git a/js/src/solts/lib/provider.ts b/js/src/solts/lib/provider.ts new file mode 100644 index 000000000..6c1e20d13 --- /dev/null +++ b/js/src/solts/lib/provider.ts @@ -0,0 +1,168 @@ +import ts from 'typescript'; +import { + AnyType, + AsArray, + AsRefNode, + CreateCall, + CreateCallbackExpression, + CreateParameter, + ErrorType, + Method, + ReadableType, + StringType, + Uint8ArrayType, + VoidType, +} from './syntax'; + +export const ErrParameter = CreateParameter(ts.createIdentifier('err'), ErrorType); +export const ExecParameter = CreateParameter(ts.createIdentifier('exec'), Uint8ArrayType); +export const AddrParameter = CreateParameter(ts.createIdentifier('addr'), Uint8ArrayType); +export const EventParameter = CreateParameter(ts.createIdentifier('event'), AnyType); + +const type = ts.createIdentifier('Tx'); +const typeArgument = ts.createTypeReferenceNode(type, undefined); + +class Deploy extends Method { + params = [ + CreateParameter('msg', typeArgument), + CreateParameter('callback', CreateCallbackExpression([ErrParameter, AddrParameter])), + ]; + ret = VoidType; + + constructor() { + super('deploy'); + } + call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + } +} + +class Call extends Method { + params = [ + CreateParameter('msg', typeArgument), + CreateParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), + ]; + ret = VoidType; + + constructor() { + super('call'); + } + call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + } +} + +class CallSim extends Method { + params = [ + CreateParameter('msg', typeArgument), + CreateParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), + ]; + ret = VoidType; + + constructor() { + super('callSim'); + } + call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + } +} + +class Listen extends Method { + params = [ + CreateParameter('signature', StringType), + CreateParameter('address', StringType), + CreateParameter('callback', CreateCallbackExpression([ErrParameter, EventParameter])), + ]; + ret = AsRefNode(ReadableType); + + constructor() { + super('listen'); + } + call(exp: ts.Expression, sig: ts.StringLiteral, addr: ts.Expression, callback: ts.Identifier) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [sig, addr, callback]); + } +} + +class Payload extends Method { + params = [CreateParameter('data', StringType), CreateParameter('address', StringType, undefined, true)]; + ret = typeArgument; + + constructor() { + super('payload'); + } + call(exp: ts.Expression, data: ts.Identifier, addr: ts.Expression) { + return addr + ? CreateCall(ts.createPropertyAccess(exp, this.id), [data, addr]) + : CreateCall(ts.createPropertyAccess(exp, this.id), [data]); + } +} + +class Encode extends Method { + params = [ + CreateParameter('name', StringType), + CreateParameter('inputs', AsArray(StringType)), + CreateParameter('args', AsArray(AnyType), undefined, false, true), + ]; + ret = StringType; + + constructor() { + super('encode'); + } + call(exp: ts.Expression, name: ts.StringLiteral, inputs: ts.ArrayLiteralExpression, ...args: ts.Identifier[]) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [name, inputs, ...args]); + } +} + +class Decode extends Method { + params = [CreateParameter('data', Uint8ArrayType), CreateParameter('outputs', AsArray(StringType))]; + ret = AnyType; + + constructor() { + super('decode'); + } + call(exp: ts.Expression, data: ts.Identifier, outputs: ts.ArrayLiteralExpression) { + return CreateCall(ts.createPropertyAccess(exp, this.id), [data, outputs]); + } +} + +export class Provider { + private name = ts.createIdentifier('Provider'); + + methods = { + deploy: new Deploy(), + call: new Call(), + callSim: new CallSim(), + listen: new Listen(), + payload: new Payload(), + encode: new Encode(), + decode: new Decode(), + }; + + createInterface() { + return ts.createInterfaceDeclaration(undefined, undefined, this.name, [this.getTypeArgumentDecl()], undefined, [ + this.methods.deploy.signature(), + this.methods.call.signature(), + this.methods.callSim.signature(), + this.methods.listen.signature(), + this.methods.payload.signature(), + this.methods.encode.signature(), + this.methods.decode.signature(), + ]); + } + + getType() { + return type; + } + + getTypeNode() { + return ts.createTypeReferenceNode(this.name, [typeArgument]); + } + + getTypeArgument() { + return typeArgument; + } + + getTypeArgumentDecl() { + return ts.createTypeParameterDeclaration(type); + } +} diff --git a/js/src/solts/lib/replacer.ts b/js/src/solts/lib/replacer.ts new file mode 100644 index 000000000..1c048f8f6 --- /dev/null +++ b/js/src/solts/lib/replacer.ts @@ -0,0 +1,85 @@ +import ts from 'typescript'; +import { CreateCall, CreateParameter, DeclareConstant, StringType } from './syntax'; + +export const ReplacerName = ts.createIdentifier('Replace'); + +export const Replacer = () => { + const bytecode = ts.createIdentifier('bytecode'); + const name = ts.createIdentifier('name'); + const address = ts.createIdentifier('address'); + + const truncated = ts.createIdentifier('truncated'); + const label = ts.createIdentifier('label'); + + return ts.createFunctionDeclaration( + undefined, + undefined, + undefined, + ReplacerName, + undefined, + [CreateParameter(bytecode, StringType), CreateParameter(name, StringType), CreateParameter(address, StringType)], + StringType, + ts.createBlock( + [ + ts.createExpressionStatement( + ts.createAssignment( + address, + adds( + address, + arrayJoin( + ts.createAdd( + ts.createSubtract(ts.createNumericLiteral('40'), ts.createPropertyAccess(address, 'length')), + ts.createNumericLiteral('1'), + ), + '0', + ), + ), + ), + ), + DeclareConstant( + truncated, + CreateCall(ts.createPropertyAccess(name, 'slice'), [ + ts.createNumericLiteral('0'), + ts.createNumericLiteral('36'), + ]), + ), + DeclareConstant( + label, + adds( + ts.createAdd(ts.createStringLiteral('__'), truncated), + arrayJoin( + ts.createSubtract(ts.createNumericLiteral('37'), ts.createPropertyAccess(truncated, 'length')), + '_', + ), + ts.createStringLiteral('__'), + ), + ), + ts.createWhile( + ts.createBinary( + CreateCall(ts.createPropertyAccess(bytecode, 'indexOf'), [label]), + ts.SyntaxKind.GreaterThanEqualsToken, + ts.createNumericLiteral('0'), + ), + ts.createExpressionStatement( + ts.createAssignment(bytecode, CreateCall(ts.createPropertyAccess(bytecode, 'replace'), [label, address])), + ), + ), + ts.createReturn(bytecode), + ], + true, + ), + ); +}; + +function adds(...exp: ts.Expression[]) { + return exp.reduce((all, next) => { + return ts.createAdd(all, next); + }); +} + +function arrayJoin(length: ts.Expression, literal: string) { + return CreateCall( + ts.createPropertyAccess(CreateCall(ts.createIdentifier('Array'), [length]), ts.createIdentifier('join')), + [ts.createStringLiteral(literal)], + ); +} diff --git a/js/src/solts/lib/solidity.test.ts b/js/src/solts/lib/solidity.test.ts new file mode 100644 index 000000000..bbc52b010 --- /dev/null +++ b/js/src/solts/lib/solidity.test.ts @@ -0,0 +1,36 @@ +import assert from 'assert'; +import { ABI } from './abi'; +import { GetSize, Hash, NameFromABI, TokenizeString } from './solidity'; + +describe('abi helpers', function () { + it('should compute a valid method id', async function () { + assert.equal(Hash('baz(uint32,bool)').slice(0, 8), 'CDCD77C0'); + }); + + it('should return the full function name with args', async function () { + const abi: ABI.Func = { + type: 'function', + name: 'baz', + stateMutability: 'pure', + inputs: [ + { + name: '1', + type: 'uint32', + }, + { + name: '2', + type: 'bool', + }, + ], + }; + assert.equal(NameFromABI(abi), 'baz(uint32,bool)'); + }); + + it('should strip array size', () => { + assert.equal(GetSize('uint[3]'), 3); + }); + + it('should tokenize string', () => { + assert.equal(TokenizeString('sol/Storage.sol:Storage'), 'sol_Storage_sol_Storage'); + }); +}); diff --git a/js/src/solts/lib/solidity.ts b/js/src/solts/lib/solidity.ts new file mode 100644 index 000000000..23b56bf2f --- /dev/null +++ b/js/src/solts/lib/solidity.ts @@ -0,0 +1,131 @@ +import { Keccak } from 'sha3'; +import ts from 'typescript'; +import { ABI } from './abi'; +import { AsArray, AsRefNode, AsTuple, BooleanType, BufferType, NumberType, StringType, VoidType } from './syntax'; + +export function Hash(str: string) { + const hash = new Keccak(256).update(str); + return hash.digest('hex').toUpperCase(); +} + +export function NameFromABI(abi: ABI.Func | ABI.Event): string { + if (abi.name.indexOf('(') !== -1) { + return abi.name; + } + const typeName = (abi.inputs as (ABI.EventInput | ABI.FunctionIO)[]).map((i) => i.type).join(); + return abi.name + '(' + typeName + ')'; +} + +export function GetSize(type: string) { + return parseInt(type.replace(/.*\[|\].*/gi, ''), 10); +} + +export function GetRealType(type: string): ts.TypeNode { + if (/\[\]/i.test(type)) { + return AsArray(GetRealType(type.replace(/\[\]/, ''))); + } + if (/\[.*\]/i.test(type)) { + return AsTuple(GetRealType(type.replace(/\[.*\]/, '')), GetSize(type)); + } else if (/int/i.test(type)) { + return NumberType; + } else if (/bool/i.test(type)) { + return BooleanType; + } else if (/bytes/i.test(type)) { + return AsRefNode(BufferType); + } else { + return StringType; + } // address, bytes +} + +export function OutputToType(sig: Signature) { + if (sig.outputs.length === 0) { + return VoidType; + } + const named = sig.outputs.filter((out) => out !== undefined && out.name !== ''); + if (sig.outputs.length === named.length) { + return ts.createTypeLiteralNode( + sig.outputs.map((out) => + ts.createPropertySignature(undefined, out.name, undefined, GetRealType(out.type), undefined), + ), + ); + } else { + return ts.createTupleTypeNode(sig.outputs.map((out) => GetRealType(out.type))); + } +} + +export function CollapseInputs(signatures: Array) { + return signatures.reduce((args, next) => { + next.inputs.map((item) => { + if (!item) { + return; + } + const prev = args.get(item.name); + args.set(item.name, prev ? [...prev, item.type] : [item.type]); + }); + return args; + }, new Map>()); +} + +export function CombineTypes(types: Array) { + return types.length === 1 ? GetRealType(types[0]) : ts.createUnionTypeNode(types.map((type) => GetRealType(type))); +} + +export type InputOutput = { + name: string; + type: string; +}; +export type MethodType = 'function' | 'event'; +export type Signature = { + hash: string; + constant: boolean; + inputs: Array; + outputs?: Array; +}; +export type Method = { + type: MethodType; + signatures?: Array; +}; +export type ContractMethods = Map; +export type ContractMethodsList = Array<{ name: string } & Method>; + +export function GetContractMethods(abi: ABI.FunctionOrEvent[]) { + // solidity allows duplicate function names + return Array.from( + abi.reduce((signatures, abi) => { + if (abi.name === '') { + return signatures; + } + if (abi.type === 'function') { + const body = signatures.get(abi.name) || { + type: 'function', + signatures: new Array(), + }; + + body.signatures.push({ + hash: Hash(NameFromABI(abi)).slice(0, 8), + inputs: abi.inputs + .filter((abi) => abi.name !== '') + .map((abi) => { + return { name: abi.name, type: abi.type }; + }), + outputs: abi.outputs.map((abi) => { + return { name: abi.name, type: abi.type }; + }), + constant: abi.constant || false, + }); + + signatures.set(abi.name, body); + } else if (abi.type === 'event') { + signatures.set(abi.name, { type: 'event' }); + } + return signatures; + }, new Map()), + ([name, method]) => { + return { name: name, type: method.type, signatures: method.signatures }; + }, + ); +} + +export function TokenizeString(input: string) { + return input.replace(/\W+/g, '_'); +} diff --git a/js/src/solts/lib/syntax.test.ts b/js/src/solts/lib/syntax.test.ts new file mode 100644 index 000000000..f6dc74d26 --- /dev/null +++ b/js/src/solts/lib/syntax.test.ts @@ -0,0 +1,18 @@ +import assert from 'assert'; +import ts from 'typescript'; +import { CreateCallbackExpression, CreateParameter } from './syntax'; + +function print(node: ts.Node) { + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + return printer.printNode(ts.EmitHint.Unspecified, node, undefined); +} + +describe('syntax helpers', function () { + it('should create callback expression', async function () { + const ErrAndResult = [ + CreateParameter(ts.createIdentifier('err'), undefined), + CreateParameter(ts.createIdentifier('result'), undefined), + ]; + assert.equal(print(CreateCallbackExpression(ErrAndResult)), '(err, result) => void'); + }); +}); diff --git a/js/src/solts/lib/syntax.ts b/js/src/solts/lib/syntax.ts new file mode 100644 index 000000000..dc6c60ad3 --- /dev/null +++ b/js/src/solts/lib/syntax.ts @@ -0,0 +1,169 @@ +import ts from 'typescript'; + +export const Uint8ArrayType = ts.createTypeReferenceNode('Uint8Array', undefined); +export const ErrorType = ts.createTypeReferenceNode('Error', undefined); +export const VoidType = ts.createTypeReferenceNode('void', undefined); +export const StringType = ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); +export const NumberType = ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); +export const BooleanType = ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); +export const AnyType = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); +export const PromiseType = ts.createIdentifier('Promise'); +export const ReadableType = ts.createIdentifier('Readable'); +export const BufferType = ts.createIdentifier('Buffer'); +export const TupleType = (elements: ts.TypeNode[]) => ts.createTupleTypeNode(elements); + +export const PrivateToken = ts.createToken(ts.SyntaxKind.PrivateKeyword); +export const PublicToken = ts.createToken(ts.SyntaxKind.PublicKeyword); +export const ExportToken = ts.createToken(ts.SyntaxKind.ExportKeyword); +export const EllipsisToken = ts.createToken(ts.SyntaxKind.DotDotDotToken); +export const QuestionToken = ts.createToken(ts.SyntaxKind.QuestionToken); + +export const CreateCall = (fn: ts.Expression, args: ts.Expression[]) => ts.createCall(fn, undefined, args); +export const AccessThis = (name: ts.Identifier) => ts.createPropertyAccess(ts.createThis(), name); +export const BufferFrom = (...args: ts.Expression[]) => + CreateCall(ts.createPropertyAccess(BufferType, ts.createIdentifier('from')), args); +export const AsArray = (type: ts.TypeNode) => ts.createArrayTypeNode(type); +export const AsTuple = (type: ts.TypeNode, size: number) => ts.createTupleTypeNode(Array(size).fill(type)); +export const AsRefNode = (id: ts.Identifier) => ts.createTypeReferenceNode(id, undefined); + +export function CreateParameter( + name: string | ts.Identifier, + typeNode: ts.TypeNode | undefined, + initializer?: ts.Expression, + isOptional?: boolean, + isVariadic?: boolean, +): ts.ParameterDeclaration { + return ts.createParameter( + undefined, + undefined, + isVariadic ? EllipsisToken : undefined, + typeof name === 'string' ? ts.createIdentifier(name) : name, + isOptional ? QuestionToken : undefined, + typeNode, + initializer, + ); +} + +export function DeclareConstant(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { + return ts.createVariableStatement( + extern ? [ExportToken] : [], + ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Const), + ); +} + +export function DeclareLet(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { + return ts.createVariableStatement( + extern ? [ExportToken] : [], + ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Let), + ); +} + +const resolveFn = ts.createIdentifier('resolve'); +const rejectFn = ts.createIdentifier('reject'); + +export function CreatePromiseBody(error: ts.Identifier, statements: ts.Expression[]) { + return ts.createExpressionStatement( + ts.createConditional( + error, + CreateCall(rejectFn, [error]), + CreateCall(resolveFn, statements ? statements : undefined), + ), + ); +} + +export function RejectOrResolve(error: ts.Identifier, statements: ts.Statement[], success: ts.Expression[]) { + return ts.createIf( + error, + ts.createExpressionStatement(CreateCall(rejectFn, [error])), + ts.createBlock([...statements, ts.createExpressionStatement(CreateCall(resolveFn, success))]), + ); +} + +export function CreateNewPromise( + body: ts.Statement[], + returnType?: ts.TypeNode, + multiLine?: boolean, +): ts.NewExpression { + return ts.createNew(PromiseType, undefined, [ + CreateCallbackDeclaration(resolveFn, rejectFn, body, returnType, multiLine || false), + ]); +} + +export function CreateCallbackDeclaration( + first: ts.Identifier, + second: ts.Identifier, + body: ts.Statement[], + returnType?: ts.TypeNode, + multiLine?: boolean, +) { + return ts.createArrowFunction( + undefined, + undefined, + [CreateParameter(first, undefined), CreateParameter(second, undefined)], + returnType, + undefined, + ts.createBlock(body, multiLine), + ); +} + +export function CreateCallbackExpression(params: ts.ParameterDeclaration[]) { + return ts.createFunctionTypeNode(undefined, params, VoidType); +} + +function ImportFrom(thing: ts.Identifier, pkg: string) { + return ts.createImportDeclaration( + undefined, + undefined, + ts.createImportClause(undefined, ts.createNamedImports([ts.createImportSpecifier(undefined, thing)])), + ts.createLiteral(pkg), + ); +} + +export function ImportReadable() { + return ImportFrom(ReadableType, 'stream'); +} + +export class Method { + id: ts.Identifier; + type: ts.TypeReferenceNode; + params: ts.ParameterDeclaration[]; + ret: ts.TypeNode; + + constructor(name: string) { + this.id = ts.createIdentifier(name); + this.params = []; + } + + parameter(name: string | ts.Identifier, type: ts.TypeNode, optional?: boolean, isVariadic?: boolean) { + this.params.push(CreateParameter(name, type, undefined, optional, isVariadic)); + return this; + } + + parameters(args: ts.ParameterDeclaration[]) { + this.params.push(...args); + return this; + } + + returns(type: ts.TypeNode) { + this.ret = type; + return this; + } + + signature() { + return ts.createMethodSignature(undefined, this.params, this.ret, this.id, undefined); + } + + declaration(statements: ts.Statement[], multiLine?: boolean) { + return ts.createMethod( + undefined, + undefined, + undefined, + this.id, + undefined, + undefined, + this.params, + this.ret, + ts.createBlock(statements, multiLine), + ); + } +} diff --git a/js/src/solts/main.ts b/js/src/solts/main.ts new file mode 100644 index 000000000..9639bad0a --- /dev/null +++ b/js/src/solts/main.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env node + +import program from 'commander'; +import fs from 'fs'; +import { Readable } from 'stream'; +import { Compiled, NewFile, Print } from './api'; + +const STDIN = '-'; + +async function ReadAll(file: Readable) { + const chunks: Buffer[] = []; + for await (const chunk of file) { + chunks.push(chunk); + } + return Buffer.concat(chunks).toString(); +} + +type Combined = { + contracts: Record< + string, + { + abi?: string; + bin?: string; + } + >; + version: string; +}; + +async function main() { + program + .name('ts-sol') + .arguments(' [destination]') + .description('Generate typescript classes for an ABI. If no destination specified, print result to STDOUT.') + .action(async function (src, dst) { + let source: string; + if (src === STDIN) { + source = await ReadAll(Readable.from(process.stdin)); + } else { + source = await ReadAll(Readable.from(fs.createReadStream(src))); + } + const input: Combined = JSON.parse(source); + + const compiled: Compiled[] = []; + for (const k in input.contracts) { + if (!input.contracts[k].abi) { + throw new Error(`ABI not given for: ${k}`); + } + if (!input.contracts[k].bin) { + throw new Error(`Bin not given for: ${k}`); + } + compiled.push({ + name: k, + abi: JSON.parse(input.contracts[k].abi), + bin: input.contracts[k].bin, + links: [], + }); + } + + const target = NewFile(compiled); + dst ? fs.writeFileSync(dst, Print(...target)) : console.log(Print(...target)); + }); + + try { + await program.parseAsync(process.argv); + } catch (err) { + console.log(err); + process.exit(1); + } +} + +main(); diff --git a/js/src/test/contract-event.test.ts b/js/src/test/contract-event.test.ts index b658bcbb3..4e2320acd 100644 --- a/js/src/test/contract-event.test.ts +++ b/js/src/test/contract-event.test.ts @@ -1,6 +1,5 @@ import { compile } from '../contracts/compile'; import { ContractEvent, getAddress } from '../contracts/contract'; -import { Contract } from '../index'; import { burrow } from './test'; describe('Nested contract event emission', function () { diff --git a/js/src/test/handler-overwriting.test.ts b/js/src/test/handler-overwriting.test.ts index 1a22fd5db..1453c3b7c 100644 --- a/js/src/test/handler-overwriting.test.ts +++ b/js/src/test/handler-overwriting.test.ts @@ -1,9 +1,8 @@ import * as assert from 'assert'; -import { Result } from 'ethers/lib/utils'; import { CallResult } from '../contracts/call'; import { compile } from '../contracts/compile'; import { getMetadata } from '../contracts/contract'; -import { withoutArrayElements } from "../convert"; +import { withoutArrayElements } from '../convert'; import { burrow } from './test'; describe('Testing Per-contract handler overwriting', function () { @@ -54,4 +53,3 @@ describe('Testing Per-contract handler overwriting', function () { assert.deepStrictEqual(returnObject, expected); }); }); - From e3d11fa3009104ad24e95085319447251026f678 Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Sun, 16 May 2021 16:53:36 +0200 Subject: [PATCH 2/4] Expose 'build' code generation function Fix code non-compliance with 'strict' typescript mode and various refactorings Signed-off-by: Silas Davis --- Makefile | 4 + js/package.json | 14 ++-- js/src/index.ts | 1 + js/src/solts/api.ts | 43 +++++----- js/src/solts/build.ts | 66 +++++++++++++++ js/src/solts/lib/caller.ts | 26 +++--- js/src/solts/lib/compile.test.ts | 4 +- js/src/solts/lib/compile.ts | 21 +++-- js/src/solts/lib/contract.ts | 47 ++++++----- js/src/solts/lib/decoder.ts | 25 +++--- js/src/solts/lib/deployer.ts | 57 +++++++------ js/src/solts/lib/encoder.ts | 84 ++++++++----------- js/src/solts/lib/provider.ts | 58 ++++++------- js/src/solts/lib/replacer.ts | 18 ++-- js/src/solts/lib/solidity.test.ts | 10 +-- js/src/solts/lib/solidity.ts | 121 ++++++++++++++++----------- js/src/solts/lib/syntax.test.ts | 14 ++-- js/src/solts/lib/syntax.ts | 39 +++++---- js/src/solts/main.ts | 71 ---------------- js/src/solts/sol/Addition.sol | 7 ++ js/src/solts/sol/Contains.sol | 17 ++++ js/src/solts/sol/Creator.sol | 19 +++++ js/src/solts/sol/Event.sol | 11 +++ js/src/solts/sol/MultipleReturns.sol | 7 ++ js/src/solts/sol/Storage.sol | 18 ++++ js/src/solts/sol/Unnamed.sol | 7 ++ js/src/solts/sol/build.ts | 5 ++ js/types/solc/index.d.ts | 10 ++- js/types/solc_v5/index.d.ts | 92 ++++++++++++++++++++ js/types/solc_v8/index.d.ts | 92 ++++++++++++++++++++ js/yarn.lock | 18 +++- 31 files changed, 672 insertions(+), 354 deletions(-) create mode 100644 js/src/solts/build.ts delete mode 100644 js/src/solts/main.ts create mode 100644 js/src/solts/sol/Addition.sol create mode 100644 js/src/solts/sol/Contains.sol create mode 100644 js/src/solts/sol/Creator.sol create mode 100644 js/src/solts/sol/Event.sol create mode 100644 js/src/solts/sol/MultipleReturns.sol create mode 100644 js/src/solts/sol/Storage.sol create mode 100644 js/src/solts/sol/Unnamed.sol create mode 100644 js/src/solts/sol/build.ts create mode 100644 js/types/solc_v5/index.d.ts create mode 100644 js/types/solc_v8/index.d.ts diff --git a/Makefile b/Makefile index 4975b6f0a..ad8b68863 100644 --- a/Makefile +++ b/Makefile @@ -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} diff --git a/js/package.json b/js/package.json index d98975115..aafa72bf3 100644 --- a/js/package.json +++ b/js/package.json @@ -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" @@ -16,15 +13,18 @@ }, "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'", + "test:generate": "ts-node src/solts/sol/build.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" + "solc_v8@npm:solc": "^0.8.4", + "solc_v5@npm:solc": "^0.5.17", + "sha3": "^2.1.4", + "typescript": "^4.2.4" }, "devDependencies": { "@types/google-protobuf": "^3.15.2", diff --git a/js/src/index.ts b/js/src/index.ts index f4aee56f8..ff4312d62 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -2,3 +2,4 @@ export { TxExecution } from '../proto/exec_pb'; export { CallTx, TxInput } from '../proto/payload_pb'; export { Burrow } from './burrow'; export { Contract } from './contracts/contract'; +export { build } from './solts/build'; diff --git a/js/src/solts/api.ts b/js/src/solts/api.ts index d48e65a96..e06271474 100644 --- a/js/src/solts/api.ts +++ b/js/src/solts/api.ts @@ -1,16 +1,17 @@ import ts from 'typescript'; import { ABI } from './lib/abi'; import { Caller } from './lib/caller'; -import { Contract } from './lib/contract'; -import { Decode } from './lib/decoder'; -import { Deploy } from './lib/deployer'; -import { Encode } from './lib/encoder'; +import { generateContractClass } from './lib/contract'; +import { generateDecodeObject } from './lib/decoder'; +import { generateDeployFunction } from './lib/deployer'; +import { generateEncodeObject } from './lib/encoder'; import { Provider } from './lib/provider'; import { Replacer } from './lib/replacer'; -import { GetContractMethods } from './lib/solidity'; +import { getContractMethods } from './lib/solidity'; import { ExportToken, ImportReadable } from './lib/syntax'; +import Func = ABI.Func; -export { DecodeOutput, EncodeInput, ImportLocal, InputDescriptionFromFiles, TokenizeLinks } from './lib/compile'; +export { decodeOutput, encodeInput, importLocal, inputDescriptionFromFiles, tokenizeLinks } from './lib/compile'; export type Compiled = { name: string; @@ -19,7 +20,7 @@ export type Compiled = { links: Array; }; -export function NewFile(contracts: Compiled[]): ts.Node[] { +export function newFile(contracts: Compiled[]): ts.Node[] { const provider = new Provider(); return [ @@ -32,31 +33,31 @@ export function NewFile(contracts: Compiled[]): ts.Node[] { Caller(provider), Replacer(), ...contracts.map((contract) => { - const methods = GetContractMethods(contract.abi); + const methods = getContractMethods(contract.abi); - let deploy: ABI.Func; - contract.abi.map((abi) => { - if (abi.type == 'constructor') { - deploy = abi; - } - }); + const deploy = contract.abi.find((abi): abi is Func => abi.type === 'constructor'); + // No deploy function for interfaces + const deployFunction = contract.bin + ? [generateDeployFunction(deploy, contract.bin, contract.links, provider)] + : []; + const statements = [ + ...deployFunction, + generateContractClass(methods, provider), + generateEncodeObject(methods, provider), + generateDecodeObject(methods, provider), + ]; return ts.createModuleDeclaration( undefined, [ExportToken], ts.createIdentifier(contract.name), - ts.createModuleBlock([ - Deploy(deploy, contract.bin, contract.links, provider), - Contract(methods, provider), - Encode(methods, provider), - Decode(methods, provider), - ]), + ts.createModuleBlock(statements), ); }), ]; } -export function Print(...nodes: ts.Node[]) { +export function printNodes(...nodes: ts.Node[]): string { const target = ts.createSourceFile('', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS); const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); return nodes.map((node) => printer.printNode(ts.EmitHint.Unspecified, node, target)).join('\n'); diff --git a/js/src/solts/build.ts b/js/src/solts/build.ts new file mode 100644 index 000000000..5d557ea48 --- /dev/null +++ b/js/src/solts/build.ts @@ -0,0 +1,66 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as solcv5 from 'solc_v5'; +import * as solcv8 from 'solc_v8'; +import { Compiled, newFile, printNodes, tokenizeLinks } from './api'; +import { decodeOutput, encodeInput, importLocal, inputDescriptionFromFiles } from './lib/compile'; + +const solcCompilers = { + v5: solcv5, + v8: solcv8, +} as const; + +/** + * This is our Solidity -> Typescript code generation function, it: + * - Compiles Solidity source + * - Generates typescript code wrapping the Solidity contracts and functions that calls Burrow + * - Generates typescript code to deploy the contracts + * - Outputs the ABI files into bin to be later included in the distribution (for Vent and other ABI-consuming services) + */ +export function build( + srcPathOrFiles: string | string[], + binPath = 'bin', + solcVersion: keyof typeof solcCompilers = 'v5', +): void { + fs.mkdirSync(binPath, { recursive: true }); + const solidityFiles = getSourceFilesList(srcPathOrFiles); + const inputDescription = inputDescriptionFromFiles(solidityFiles); + const input = encodeInput(inputDescription); + const solc = solcCompilers[solcVersion]; + const solcOutput = solc.compile(input, { import: importLocal }); + const output = decodeOutput(solcOutput); + if (output.errors && output.errors.length > 0) { + throw new Error(output.errors.map((err) => err.formattedMessage).join('\n')); + } + + for (const filename of Object.keys(output.contracts)) { + const compiled: Compiled[] = []; + const solidity = output.contracts[filename]; + for (const contract of Object.keys(solidity)) { + const comp = output.contracts[filename][contract]; + compiled.push({ + name: contract, + abi: comp.abi, + bin: comp.evm.bytecode.object, + links: tokenizeLinks(comp.evm.bytecode.linkReferences), + }); + } + const target = filename.replace(/\.[^/.]+$/, '.abi.ts'); + // Write the ABIs emitted for each file to the name of that file without extension. We flatten into a single + // directory because that's what burrow deploy has always done. + + for (const [name, contract] of Object.entries(solidity)) { + fs.writeFileSync(path.join('bin', name + '.bin'), JSON.stringify(contract)); + } + fs.writeFileSync(target, printNodes(...newFile(compiled))); + } +} +function getSourceFilesList(srcPathOrFiles: string | string[]): string[] { + if (typeof srcPathOrFiles === 'string') { + return fs + .readdirSync(srcPathOrFiles, { withFileTypes: true }) + .filter((f) => path.extname(f.name) === '.sol') + .map((f) => path.join(srcPathOrFiles, f.name)); + } + return srcPathOrFiles; +} diff --git a/js/src/solts/lib/caller.ts b/js/src/solts/lib/caller.ts index 90fcf34d5..2c1637dd1 100644 --- a/js/src/solts/lib/caller.ts +++ b/js/src/solts/lib/caller.ts @@ -2,12 +2,12 @@ import ts from 'typescript'; import { Provider } from './provider'; import { BooleanType, - CreateCall, + createCall, CreateCallbackDeclaration, CreateNewPromise, - CreateParameter, - CreatePromiseBody, - DeclareConstant, + createParameter, + createPromiseBody, + declareConstant, PromiseType, StringType, Uint8ArrayType, @@ -34,15 +34,15 @@ export const Caller = (provider: Provider) => { CallName, [ts.createTypeParameterDeclaration(input), ts.createTypeParameterDeclaration(output)], [ - CreateParameter(client, provider.getTypeNode()), - CreateParameter(addr, StringType), - CreateParameter(data, StringType), - CreateParameter(isSim, BooleanType), - CreateParameter( + createParameter(client, provider.getTypeNode()), + createParameter(addr, StringType), + createParameter(data, StringType), + createParameter(isSim, BooleanType), + createParameter( callback, ts.createFunctionTypeNode( undefined, - [CreateParameter('exec', Uint8ArrayType)], + [createParameter('exec', Uint8ArrayType)], ts.createTypeReferenceNode(output, undefined), ), ), @@ -50,7 +50,7 @@ export const Caller = (provider: Provider) => { ts.createTypeReferenceNode(PromiseType, [ts.createTypeReferenceNode(output, undefined)]), ts.createBlock( [ - DeclareConstant(payload, provider.methods.payload.call(client, data, addr)), + declareConstant(payload, provider.methods.payload.call(client, data, addr)), ts.createIf( isSim, ts.createReturn( @@ -59,7 +59,7 @@ export const Caller = (provider: Provider) => { provider.methods.callSim.call( client, payload, - CreateCallbackDeclaration(err, exec, [CreatePromiseBody(err, [CreateCall(callback, [exec])])]), + CreateCallbackDeclaration(err, exec, [createPromiseBody(err, [createCall(callback, [exec])])]), ), ), ]), @@ -70,7 +70,7 @@ export const Caller = (provider: Provider) => { provider.methods.call.call( client, payload, - CreateCallbackDeclaration(err, exec, [CreatePromiseBody(err, [CreateCall(callback, [exec])])]), + CreateCallbackDeclaration(err, exec, [createPromiseBody(err, [createCall(callback, [exec])])]), ), ), ]), diff --git a/js/src/solts/lib/compile.test.ts b/js/src/solts/lib/compile.test.ts index de8e5f97c..ef9d6ac28 100644 --- a/js/src/solts/lib/compile.test.ts +++ b/js/src/solts/lib/compile.test.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import { TokenizeLinks } from './compile'; +import { tokenizeLinks } from './compile'; describe('compilation helpers', function () { it('should tokenize links', () => { @@ -12,7 +12,7 @@ describe('compilation helpers', function () { }, }; - const actual = TokenizeLinks(links); + const actual = tokenizeLinks(links); assert.equal(actual[0], 'dir/Errors.sol:Errors'); assert.equal(actual[1], 'lib/Utils.sol:Utils'); }); diff --git a/js/src/solts/lib/compile.ts b/js/src/solts/lib/compile.ts index 284b546a1..4b10eaa61 100644 --- a/js/src/solts/lib/compile.ts +++ b/js/src/solts/lib/compile.ts @@ -1,5 +1,7 @@ import * as fs from 'fs'; +import { ResolvedImport } from 'solc'; import { ABI } from './abi'; +import InputDescription = Solidity.InputDescription; export namespace Solidity { type Bytecode = { @@ -29,8 +31,8 @@ export namespace Solidity { export type InputDescription = { language: string; - sources?: Record; - settings?: { + sources: Record; + settings: { outputSelection: Record>>; }; }; @@ -64,10 +66,15 @@ function NewInputDescription(): Solidity.InputDescription { }; } -export const EncodeInput = (obj: Solidity.InputDescription): string => JSON.stringify(obj); -export const DecodeOutput = (str: string): Solidity.OutputDescription => JSON.parse(str); +export function encodeInput(obj: Solidity.InputDescription): string { + return JSON.stringify(obj); +} + +export function decodeOutput(str: string): Solidity.OutputDescription { + return JSON.parse(str); +} -export function InputDescriptionFromFiles(...names: string[]) { +export function inputDescriptionFromFiles(names: string[]): InputDescription { const desc = NewInputDescription(); names.map((name) => { desc.sources[name] = { content: fs.readFileSync(name).toString() }; @@ -77,13 +84,13 @@ export function InputDescriptionFromFiles(...names: string[]) { return desc; } -export function ImportLocal(path: string) { +export function importLocal(path: string): ResolvedImport { return { contents: fs.readFileSync(path).toString(), }; } -export function TokenizeLinks(links: Record>) { +export function tokenizeLinks(links: Record>): string[] { const libraries: Array = []; for (const file in links) { for (const library in links[file]) { diff --git a/js/src/solts/lib/contract.ts b/js/src/solts/lib/contract.ts index e3126163c..8b42a9fd3 100644 --- a/js/src/solts/lib/contract.ts +++ b/js/src/solts/lib/contract.ts @@ -1,16 +1,16 @@ -import ts from 'typescript'; +import ts, { ClassDeclaration, MethodDeclaration } from 'typescript'; import { CallName } from './caller'; import { DecodeName } from './decoder'; import { EncodeName } from './encoder'; import { ErrParameter, EventParameter, Provider } from './provider'; -import { CollapseInputs, CombineTypes, ContractMethodsList, OutputToType, Signature } from './solidity'; +import { collapseInputs, combineTypes, ContractMethodsList, outputToType, Signature } from './solidity'; import { AccessThis, AsRefNode, - CreateCall, + createCall, CreateCallbackExpression, - CreateParameter, - DeclareConstant, + createParameter, + declareConstant, ExportToken, Method, PrivateToken, @@ -27,16 +27,16 @@ const address = ts.createIdentifier('address'); export const ContractName = ts.createIdentifier('Contract'); -function SolidityFunction(name: string, signatures: Signature[]) { - const args = Array.from(CollapseInputs(signatures).keys()).map((key) => ts.createIdentifier(key)); - const encode = DeclareConstant( +function solidityFunction(name: string, signatures: Signature[]): MethodDeclaration { + const args = Array.from(collapseInputs(signatures).keys()).map((key) => ts.createIdentifier(key)); + const encode = declareConstant( data, - CreateCall(ts.createPropertyAccess(CreateCall(EncodeName, [AccessThis(client)]), name), args), + createCall(ts.createPropertyAccess(createCall(EncodeName, [AccessThis(client)]), name), args), ); const call = ts.createCall( CallName, - [ts.createTypeReferenceNode('Tx', undefined), OutputToType(signatures[0])], + [ts.createTypeReferenceNode('Tx', undefined), outputToType(signatures[0])], [ AccessThis(client), AccessThis(address), @@ -45,13 +45,13 @@ function SolidityFunction(name: string, signatures: Signature[]) { ts.createArrowFunction( undefined, undefined, - [CreateParameter(exec, Uint8ArrayType)], + [createParameter(exec, Uint8ArrayType)], undefined, undefined, ts.createBlock( [ ts.createReturn( - CreateCall(ts.createPropertyAccess(CreateCall(DecodeName, [AccessThis(client), exec]), name), []), + createCall(ts.createPropertyAccess(createCall(DecodeName, [AccessThis(client), exec]), name), []), ), ], true, @@ -60,11 +60,11 @@ function SolidityFunction(name: string, signatures: Signature[]) { ], ); - const params = Array.from(CollapseInputs(signatures), ([key, value]) => CreateParameter(key, CombineTypes(value))); + const params = Array.from(collapseInputs(signatures), ([key, value]) => createParameter(key, combineTypes(value))); return new Method(name).parameters(params).declaration([encode, ts.createReturn(call)], true); } -function SolidityEvent(name: string, provider: Provider) { +function solidityEvent(name: string, provider: Provider): MethodDeclaration { const callback = ts.createIdentifier('callback'); return new Method(name) .parameter(callback, CreateCallbackExpression([ErrParameter, EventParameter])) @@ -76,15 +76,22 @@ function SolidityEvent(name: string, provider: Provider) { ]); } -function createMethodFromABI(name: string, type: 'function' | 'event', signatures: Signature[], provider: Provider) { +function createMethodFromABI( + name: string, + type: 'function' | 'event', + signatures: Signature[], + provider: Provider, +): MethodDeclaration { if (type === 'function') { - return SolidityFunction(name, signatures); + return solidityFunction(name, signatures); } else if (type === 'event') { - return SolidityEvent(name, provider); + return solidityEvent(name, provider); } + // FIXME: Not sure why this is not inferred since if is exhaustive + return undefined as never; } -export const Contract = (abi: ContractMethodsList, provider: Provider) => { +export function generateContractClass(abi: ContractMethodsList, provider: Provider): ClassDeclaration { return ts.createClassDeclaration( undefined, [ExportToken], @@ -97,7 +104,7 @@ export const Contract = (abi: ContractMethodsList, provider: Provider) => { ts.createConstructor( undefined, undefined, - [CreateParameter(client, provider.getTypeNode()), CreateParameter(address, StringType)], + [createParameter(client, provider.getTypeNode()), createParameter(address, StringType)], ts.createBlock( [ ts.createStatement(ts.createAssignment(AccessThis(client), client)), @@ -109,4 +116,4 @@ export const Contract = (abi: ContractMethodsList, provider: Provider) => { ...abi.map((abi) => createMethodFromABI(abi.name, abi.type, abi.signatures, provider)), ], ); -}; +} diff --git a/js/src/solts/lib/decoder.ts b/js/src/solts/lib/decoder.ts index 3ce7da91c..690972ad2 100644 --- a/js/src/solts/lib/decoder.ts +++ b/js/src/solts/lib/decoder.ts @@ -1,7 +1,7 @@ -import ts from 'typescript'; +import ts, { Expression, VariableStatement } from 'typescript'; import { Provider } from './provider'; -import { ContractMethodsList, OutputToType, Signature } from './solidity'; -import { CreateParameter, DeclareConstant, Uint8ArrayType } from './syntax'; +import { ContractMethodsList, outputToType, Signature } from './solidity'; +import { createParameter, declareConstant, Uint8ArrayType } from './syntax'; export const DecodeName = ts.createIdentifier('Decode'); @@ -11,7 +11,7 @@ function output(decodeFn: ts.CallExpression, sig: Signature): ts.Block { named = sig.outputs.filter((out) => out.name !== ''); } - if (sig.outputs.length !== 0 && sig.outputs.length === named.length) { + if (sig.outputs?.length && sig.outputs.length === named.length) { const setter = ts.createVariableStatement( [], ts.createVariableDeclarationList( @@ -40,30 +40,29 @@ function output(decodeFn: ts.CallExpression, sig: Signature): ts.Block { true, ); } else { - return ts.createBlock([ts.createReturn(sig.outputs.length > 0 ? decodeFn : undefined)]); + return ts.createBlock([ts.createReturn(sig.outputs?.length ? decodeFn : undefined)]); } } function decoder(sig: Signature, client: ts.Identifier, provider: Provider, data: ts.Identifier) { - let args = []; + let args: Expression[] = []; if (sig.outputs && sig.outputs[0] !== undefined) { args = sig.outputs.map((arg) => ts.createLiteral(arg.type)); } const types = ts.createArrayLiteral(args); - const decodeFn = provider.methods.decode.call(client, data, types); - return decodeFn; + return provider.methods.decode.call(client, data, types); } -export const Decode = (methods: ContractMethodsList, provider: Provider) => { +export function generateDecodeObject(methods: ContractMethodsList, provider: Provider): VariableStatement { const client = ts.createIdentifier('client'); const data = ts.createIdentifier('data'); - return DeclareConstant( + return declareConstant( DecodeName, ts.createArrowFunction( undefined, [provider.getTypeArgumentDecl()], - [CreateParameter(client, provider.getTypeNode()), CreateParameter(data, Uint8ArrayType)], + [createParameter(client, provider.getTypeNode()), createParameter(data, Uint8ArrayType)], undefined, undefined, ts.createBlock([ @@ -78,7 +77,7 @@ export const Decode = (methods: ContractMethodsList, provider: Provider) => { undefined, undefined, [], - OutputToType(method.signatures[0]), + outputToType(method.signatures[0]), undefined, output(decoder(method.signatures[0], client, provider, data), method.signatures[0]), ), @@ -91,4 +90,4 @@ export const Decode = (methods: ContractMethodsList, provider: Provider) => { ), true, ); -}; +} diff --git a/js/src/solts/lib/deployer.ts b/js/src/solts/lib/deployer.ts index fb65511d1..fe8eefb8f 100644 --- a/js/src/solts/lib/deployer.ts +++ b/js/src/solts/lib/deployer.ts @@ -1,19 +1,19 @@ -import ts from 'typescript'; +import ts, { FunctionDeclaration } from 'typescript'; import { ABI } from './abi'; import { Provider } from './provider'; import { ReplacerName } from './replacer'; -import { GetRealType, Hash, TokenizeString } from './solidity'; +import { getRealType, sha3, tokenizeString } from './solidity'; import { BufferFrom, - CreateCall, + createCall, CreateCallbackDeclaration, CreateNewPromise, - CreateParameter, - DeclareConstant, - DeclareLet, + createParameter, + declareConstant, + declareLet, ExportToken, PromiseType, - RejectOrResolve, + rejectOrResolve, StringType, } from './syntax'; @@ -29,26 +29,31 @@ const address = ts.createIdentifier('address'); export const DeployName = ts.createIdentifier('Deploy'); -export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Provider) => { +export function generateDeployFunction( + abi: ABI.Func | undefined, + bin: string, + links: string[], + provider: Provider, +): FunctionDeclaration { if (bin === '') { - return undefined; + throw new Error(`Cannot deploy without binary`); } - const parameters = abi ? abi.inputs.map((input) => CreateParameter(input.name, GetRealType(input.type))) : []; + const parameters = abi ? abi.inputs?.map((input) => createParameter(input.name, getRealType(input.type))) ?? [] : []; // const output = ts.createExpressionWithTypeArguments([ts.createTypeReferenceNode(ContractName, [ts.createTypeReferenceNode('Tx', undefined)])], PromiseType); const output = ts.createExpressionWithTypeArguments([StringType], PromiseType); const statements: ts.Statement[] = []; - statements.push(DeclareLet(bytecode, ts.createLiteral(bin))); + statements.push(declareLet(bytecode, ts.createLiteral(bin))); statements.push( ...links.map((link) => { return ts.createExpressionStatement( ts.createAssignment( bytecode, - CreateCall(ReplacerName, [ + createCall(ReplacerName, [ bytecode, - ts.createStringLiteral('$' + Hash(link).toLowerCase().slice(0, 34) + '$'), - ts.createIdentifier(TokenizeString(link)), + ts.createStringLiteral('$' + sha3(link).toLowerCase().slice(0, 34) + '$'), + ts.createIdentifier(tokenizeString(link)), ]), ), ); @@ -56,10 +61,10 @@ export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Pr ); if (abi) { - const inputs = ts.createArrayLiteral(abi.inputs.map((arg) => ts.createLiteral(arg.type))); - const args = abi.inputs.map((arg) => ts.createIdentifier(arg.name)); + const inputs = ts.createArrayLiteral(abi.inputs?.map((arg) => ts.createLiteral(arg.type))); + const args = abi.inputs?.map((arg) => ts.createIdentifier(arg.name)) ?? []; statements.push( - DeclareConstant( + declareConstant( data, ts.createBinary( bytecode, @@ -69,9 +74,9 @@ export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Pr ), ); } else { - statements.push(DeclareConstant(data, bytecode)); + statements.push(declareConstant(data, bytecode)); } - statements.push(DeclareConstant(payload, provider.methods.payload.call(client, data, undefined))); + statements.push(declareConstant(payload, provider.methods.payload.call(client, data, undefined))); const deployFn = provider.methods.deploy.call( client, @@ -80,14 +85,14 @@ export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Pr err, addr, [ - RejectOrResolve( + rejectOrResolve( err, [ - DeclareConstant( + declareConstant( address, - CreateCall( + createCall( ts.createPropertyAccess( - CreateCall(ts.createPropertyAccess(BufferFrom(addr), ts.createIdentifier('toString')), [ + createCall(ts.createPropertyAccess(BufferFrom(addr), ts.createIdentifier('toString')), [ ts.createLiteral('hex'), ]), ts.createIdentifier('toUpperCase'), @@ -115,11 +120,11 @@ export const Deploy = (abi: ABI.Func, bin: string, links: string[], provider: Pr DeployName, [ts.createTypeParameterDeclaration(type)], [ - CreateParameter(client, ts.createTypeReferenceNode('Provider', [ts.createTypeReferenceNode(type, [])])), - ...links.map((link) => CreateParameter(ts.createIdentifier(TokenizeString(link)), StringType)), + createParameter(client, ts.createTypeReferenceNode('Provider', [ts.createTypeReferenceNode(type, [])])), + ...links.map((link) => createParameter(ts.createIdentifier(tokenizeString(link)), StringType)), ...parameters, ], output, ts.createBlock(statements, true), ); -}; +} diff --git a/js/src/solts/lib/encoder.ts b/js/src/solts/lib/encoder.ts index 05982646e..ea772407b 100644 --- a/js/src/solts/lib/encoder.ts +++ b/js/src/solts/lib/encoder.ts @@ -1,19 +1,10 @@ -import ts from 'typescript'; +import ts, { FunctionDeclaration, VariableStatement } from 'typescript'; import { Provider } from './provider'; -import { CollapseInputs, CombineTypes, ContractMethodsList, Signature } from './solidity'; -import { CreateParameter, DeclareConstant } from './syntax'; +import { collapseInputs, combineTypes, ContractMethodsList, Signature } from './solidity'; +import { createParameter, declareConstant } from './syntax'; export const EncodeName = ts.createIdentifier('Encode'); -function join(...exp: ts.Expression[]) { - if (exp.length === 0) { - return undefined; - } - return exp.reduce((all, next) => { - return ts.createLogicalAnd(all, next); - }); -} - function output(signatures: Array, client: ts.Identifier, provider: Provider): ts.Block { if (signatures.length === 0) { return ts.createBlock([ts.createReturn()]); @@ -22,23 +13,18 @@ function output(signatures: Array, client: ts.Identifier, provider: P } return ts.createBlock( - [ - ...signatures - .filter((sig) => sig.inputs.length > 0) - .map((sig) => { - return ts.createIf( - join( - ...sig.inputs.map((input) => { - return ts.createStrictEquality( - ts.createTypeOf(ts.createIdentifier(input.name)), - ts.createLiteral('string'), - ); - }), - ), - ts.createReturn(encoder(sig, client, provider)), - ); - }), - ], + signatures + .filter((sig) => sig.inputs.length > 0) + .map((sig) => { + return ts.createIf( + sig.inputs + .map((input) => + ts.createStrictEquality(ts.createTypeOf(ts.createIdentifier(input.name)), ts.createLiteral('string')), + ) + .reduce((all, next) => ts.createLogicalAnd(all, next)), + ts.createReturn(encoder(sig, client, provider)), + ); + }), true, ); } @@ -46,19 +32,18 @@ function output(signatures: Array, client: ts.Identifier, provider: P function encoder(sig: Signature, client: ts.Identifier, provider: Provider) { const inputs = ts.createArrayLiteral(sig.inputs.map((arg) => ts.createLiteral(arg.type))); const args = sig.inputs.map((arg) => ts.createIdentifier(arg.name)); - const encodeFn = provider.methods.encode.call(client, ts.createLiteral(sig.hash), inputs, ...args); - return encodeFn; + return provider.methods.encode.call(client, ts.createLiteral(sig.hash), inputs, ...args); } -export const Encode = (methods: ContractMethodsList, provider: Provider) => { +export function generateEncodeObject(methods: ContractMethodsList, provider: Provider): VariableStatement { const client = ts.createIdentifier('client'); - return DeclareConstant( + return declareConstant( EncodeName, ts.createArrowFunction( undefined, [provider.getTypeArgumentDecl()], - [CreateParameter(client, provider.getTypeNode())], + [createParameter(client, provider.getTypeNode())], undefined, undefined, ts.createBlock([ @@ -66,24 +51,23 @@ export const Encode = (methods: ContractMethodsList, provider: Provider) => { ts.createObjectLiteral( methods .filter((method) => method.type === 'function') - .map((method) => { - if (method.type !== 'function') { - return; - } - return ts.createPropertyAssignment( - method.name, - ts.createArrowFunction( - undefined, - undefined, - Array.from(CollapseInputs(method.signatures), ([key, value]) => - CreateParameter(key, CombineTypes(value)), + .map( + (method) => + ts.createPropertyAssignment( + method.name, + ts.createArrowFunction( + undefined, + undefined, + Array.from(collapseInputs(method.signatures), ([key, value]) => + createParameter(key, combineTypes(value)), + ), + undefined, + undefined, + output(method.signatures, client, provider), ), - undefined, - undefined, - output(method.signatures, client, provider), ), - ); - }, true), + true, + ), true, ), ), diff --git a/js/src/solts/lib/provider.ts b/js/src/solts/lib/provider.ts index 6c1e20d13..c84247fbf 100644 --- a/js/src/solts/lib/provider.ts +++ b/js/src/solts/lib/provider.ts @@ -3,9 +3,9 @@ import { AnyType, AsArray, AsRefNode, - CreateCall, + createCall, CreateCallbackExpression, - CreateParameter, + createParameter, ErrorType, Method, ReadableType, @@ -14,18 +14,18 @@ import { VoidType, } from './syntax'; -export const ErrParameter = CreateParameter(ts.createIdentifier('err'), ErrorType); -export const ExecParameter = CreateParameter(ts.createIdentifier('exec'), Uint8ArrayType); -export const AddrParameter = CreateParameter(ts.createIdentifier('addr'), Uint8ArrayType); -export const EventParameter = CreateParameter(ts.createIdentifier('event'), AnyType); +export const ErrParameter = createParameter(ts.createIdentifier('err'), ErrorType); +export const ExecParameter = createParameter(ts.createIdentifier('exec'), Uint8ArrayType); +export const AddrParameter = createParameter(ts.createIdentifier('addr'), Uint8ArrayType); +export const EventParameter = createParameter(ts.createIdentifier('event'), AnyType); const type = ts.createIdentifier('Tx'); const typeArgument = ts.createTypeReferenceNode(type, undefined); class Deploy extends Method { params = [ - CreateParameter('msg', typeArgument), - CreateParameter('callback', CreateCallbackExpression([ErrParameter, AddrParameter])), + createParameter('msg', typeArgument), + createParameter('callback', CreateCallbackExpression([ErrParameter, AddrParameter])), ]; ret = VoidType; @@ -33,14 +33,14 @@ class Deploy extends Method { super('deploy'); } call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); } } class Call extends Method { params = [ - CreateParameter('msg', typeArgument), - CreateParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), + createParameter('msg', typeArgument), + createParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), ]; ret = VoidType; @@ -48,14 +48,14 @@ class Call extends Method { super('call'); } call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); } } class CallSim extends Method { params = [ - CreateParameter('msg', typeArgument), - CreateParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), + createParameter('msg', typeArgument), + createParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), ]; ret = VoidType; @@ -63,15 +63,15 @@ class CallSim extends Method { super('callSim'); } call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); } } class Listen extends Method { params = [ - CreateParameter('signature', StringType), - CreateParameter('address', StringType), - CreateParameter('callback', CreateCallbackExpression([ErrParameter, EventParameter])), + createParameter('signature', StringType), + createParameter('address', StringType), + createParameter('callback', CreateCallbackExpression([ErrParameter, EventParameter])), ]; ret = AsRefNode(ReadableType); @@ -79,29 +79,29 @@ class Listen extends Method { super('listen'); } call(exp: ts.Expression, sig: ts.StringLiteral, addr: ts.Expression, callback: ts.Identifier) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [sig, addr, callback]); + return createCall(ts.createPropertyAccess(exp, this.id), [sig, addr, callback]); } } class Payload extends Method { - params = [CreateParameter('data', StringType), CreateParameter('address', StringType, undefined, true)]; + params = [createParameter('data', StringType), createParameter('address', StringType, undefined, true)]; ret = typeArgument; constructor() { super('payload'); } - call(exp: ts.Expression, data: ts.Identifier, addr: ts.Expression) { + call(exp: ts.Expression, data: ts.Identifier, addr?: ts.Expression) { return addr - ? CreateCall(ts.createPropertyAccess(exp, this.id), [data, addr]) - : CreateCall(ts.createPropertyAccess(exp, this.id), [data]); + ? createCall(ts.createPropertyAccess(exp, this.id), [data, addr]) + : createCall(ts.createPropertyAccess(exp, this.id), [data]); } } class Encode extends Method { params = [ - CreateParameter('name', StringType), - CreateParameter('inputs', AsArray(StringType)), - CreateParameter('args', AsArray(AnyType), undefined, false, true), + createParameter('name', StringType), + createParameter('inputs', AsArray(StringType)), + createParameter('args', AsArray(AnyType), undefined, false, true), ]; ret = StringType; @@ -109,19 +109,19 @@ class Encode extends Method { super('encode'); } call(exp: ts.Expression, name: ts.StringLiteral, inputs: ts.ArrayLiteralExpression, ...args: ts.Identifier[]) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [name, inputs, ...args]); + return createCall(ts.createPropertyAccess(exp, this.id), [name, inputs, ...args]); } } class Decode extends Method { - params = [CreateParameter('data', Uint8ArrayType), CreateParameter('outputs', AsArray(StringType))]; + params = [createParameter('data', Uint8ArrayType), createParameter('outputs', AsArray(StringType))]; ret = AnyType; constructor() { super('decode'); } call(exp: ts.Expression, data: ts.Identifier, outputs: ts.ArrayLiteralExpression) { - return CreateCall(ts.createPropertyAccess(exp, this.id), [data, outputs]); + return createCall(ts.createPropertyAccess(exp, this.id), [data, outputs]); } } diff --git a/js/src/solts/lib/replacer.ts b/js/src/solts/lib/replacer.ts index 1c048f8f6..b230b3202 100644 --- a/js/src/solts/lib/replacer.ts +++ b/js/src/solts/lib/replacer.ts @@ -1,5 +1,5 @@ import ts from 'typescript'; -import { CreateCall, CreateParameter, DeclareConstant, StringType } from './syntax'; +import { createCall, createParameter, declareConstant, StringType } from './syntax'; export const ReplacerName = ts.createIdentifier('Replace'); @@ -17,7 +17,7 @@ export const Replacer = () => { undefined, ReplacerName, undefined, - [CreateParameter(bytecode, StringType), CreateParameter(name, StringType), CreateParameter(address, StringType)], + [createParameter(bytecode, StringType), createParameter(name, StringType), createParameter(address, StringType)], StringType, ts.createBlock( [ @@ -36,14 +36,14 @@ export const Replacer = () => { ), ), ), - DeclareConstant( + declareConstant( truncated, - CreateCall(ts.createPropertyAccess(name, 'slice'), [ + createCall(ts.createPropertyAccess(name, 'slice'), [ ts.createNumericLiteral('0'), ts.createNumericLiteral('36'), ]), ), - DeclareConstant( + declareConstant( label, adds( ts.createAdd(ts.createStringLiteral('__'), truncated), @@ -56,12 +56,12 @@ export const Replacer = () => { ), ts.createWhile( ts.createBinary( - CreateCall(ts.createPropertyAccess(bytecode, 'indexOf'), [label]), + createCall(ts.createPropertyAccess(bytecode, 'indexOf'), [label]), ts.SyntaxKind.GreaterThanEqualsToken, ts.createNumericLiteral('0'), ), ts.createExpressionStatement( - ts.createAssignment(bytecode, CreateCall(ts.createPropertyAccess(bytecode, 'replace'), [label, address])), + ts.createAssignment(bytecode, createCall(ts.createPropertyAccess(bytecode, 'replace'), [label, address])), ), ), ts.createReturn(bytecode), @@ -78,8 +78,8 @@ function adds(...exp: ts.Expression[]) { } function arrayJoin(length: ts.Expression, literal: string) { - return CreateCall( - ts.createPropertyAccess(CreateCall(ts.createIdentifier('Array'), [length]), ts.createIdentifier('join')), + return createCall( + ts.createPropertyAccess(createCall(ts.createIdentifier('Array'), [length]), ts.createIdentifier('join')), [ts.createStringLiteral(literal)], ); } diff --git a/js/src/solts/lib/solidity.test.ts b/js/src/solts/lib/solidity.test.ts index bbc52b010..2348b5b47 100644 --- a/js/src/solts/lib/solidity.test.ts +++ b/js/src/solts/lib/solidity.test.ts @@ -1,10 +1,10 @@ import assert from 'assert'; import { ABI } from './abi'; -import { GetSize, Hash, NameFromABI, TokenizeString } from './solidity'; +import { getSize, sha3, nameFromABI, tokenizeString } from './solidity'; describe('abi helpers', function () { it('should compute a valid method id', async function () { - assert.equal(Hash('baz(uint32,bool)').slice(0, 8), 'CDCD77C0'); + assert.equal(sha3('baz(uint32,bool)').slice(0, 8), 'CDCD77C0'); }); it('should return the full function name with args', async function () { @@ -23,14 +23,14 @@ describe('abi helpers', function () { }, ], }; - assert.equal(NameFromABI(abi), 'baz(uint32,bool)'); + assert.equal(nameFromABI(abi), 'baz(uint32,bool)'); }); it('should strip array size', () => { - assert.equal(GetSize('uint[3]'), 3); + assert.equal(getSize('uint[3]'), 3); }); it('should tokenize string', () => { - assert.equal(TokenizeString('sol/Storage.sol:Storage'), 'sol_Storage_sol_Storage'); + assert.equal(tokenizeString('sol/Storage.sol:Storage'), 'sol_Storage_sol_Storage'); }); }); diff --git a/js/src/solts/lib/solidity.ts b/js/src/solts/lib/solidity.ts index 23b56bf2f..d7d4fc70f 100644 --- a/js/src/solts/lib/solidity.ts +++ b/js/src/solts/lib/solidity.ts @@ -1,14 +1,15 @@ import { Keccak } from 'sha3'; -import ts from 'typescript'; +import ts, { TypeNode } from 'typescript'; import { ABI } from './abi'; import { AsArray, AsRefNode, AsTuple, BooleanType, BufferType, NumberType, StringType, VoidType } from './syntax'; +import FunctionOrEvent = ABI.FunctionOrEvent; -export function Hash(str: string) { +export function sha3(str: string): string { const hash = new Keccak(256).update(str); return hash.digest('hex').toUpperCase(); } -export function NameFromABI(abi: ABI.Func | ABI.Event): string { +export function nameFromABI(abi: ABI.Func | ABI.Event): string { if (abi.name.indexOf('(') !== -1) { return abi.name; } @@ -16,16 +17,16 @@ export function NameFromABI(abi: ABI.Func | ABI.Event): string { return abi.name + '(' + typeName + ')'; } -export function GetSize(type: string) { +export function getSize(type: string): number { return parseInt(type.replace(/.*\[|\].*/gi, ''), 10); } -export function GetRealType(type: string): ts.TypeNode { +export function getRealType(type: string): ts.TypeNode { if (/\[\]/i.test(type)) { - return AsArray(GetRealType(type.replace(/\[\]/, ''))); + return AsArray(getRealType(type.replace(/\[\]/, ''))); } if (/\[.*\]/i.test(type)) { - return AsTuple(GetRealType(type.replace(/\[.*\]/, '')), GetSize(type)); + return AsTuple(getRealType(type.replace(/\[.*\]/, '')), getSize(type)); } else if (/int/i.test(type)) { return NumberType; } else if (/bool/i.test(type)) { @@ -37,23 +38,23 @@ export function GetRealType(type: string): ts.TypeNode { } // address, bytes } -export function OutputToType(sig: Signature) { - if (sig.outputs.length === 0) { +export function outputToType(sig: Signature): TypeNode { + if (!sig.outputs?.length) { return VoidType; } const named = sig.outputs.filter((out) => out !== undefined && out.name !== ''); if (sig.outputs.length === named.length) { return ts.createTypeLiteralNode( sig.outputs.map((out) => - ts.createPropertySignature(undefined, out.name, undefined, GetRealType(out.type), undefined), + ts.createPropertySignature(undefined, out.name, undefined, getRealType(out.type), undefined), ), ); } else { - return ts.createTupleTypeNode(sig.outputs.map((out) => GetRealType(out.type))); + return ts.createTupleTypeNode(sig.outputs.map((out) => getRealType(out.type))); } } -export function CollapseInputs(signatures: Array) { +export function collapseInputs(signatures: Array): Map { return signatures.reduce((args, next) => { next.inputs.map((item) => { if (!item) { @@ -66,8 +67,8 @@ export function CollapseInputs(signatures: Array) { }, new Map>()); } -export function CombineTypes(types: Array) { - return types.length === 1 ? GetRealType(types[0]) : ts.createUnionTypeNode(types.map((type) => GetRealType(type))); +export function combineTypes(types: Array): TypeNode { + return types.length === 1 ? getRealType(types[0]) : ts.createUnionTypeNode(types.map((type) => getRealType(type))); } export type InputOutput = { @@ -82,50 +83,72 @@ export type Signature = { outputs?: Array; }; export type Method = { + name: string; type: MethodType; - signatures?: Array; + signatures: Array; }; export type ContractMethods = Map; export type ContractMethodsList = Array<{ name: string } & Method>; -export function GetContractMethods(abi: ABI.FunctionOrEvent[]) { +export function getContractMethods(abi: ABI.FunctionOrEvent[]): Method[] { // solidity allows duplicate function names - return Array.from( - abi.reduce((signatures, abi) => { - if (abi.name === '') { - return signatures; - } - if (abi.type === 'function') { - const body = signatures.get(abi.name) || { - type: 'function', - signatures: new Array(), - }; + const contractMethods = abi.reduce((signatures, abi) => { + if (abi.name === '') { + return signatures; + } + if (abi.type === 'function') { + const method = signatures.get(abi.name) || { + name: abi.name, + type: 'function', + signatures: [], + }; - body.signatures.push({ - hash: Hash(NameFromABI(abi)).slice(0, 8), - inputs: abi.inputs - .filter((abi) => abi.name !== '') - .map((abi) => { - return { name: abi.name, type: abi.type }; - }), - outputs: abi.outputs.map((abi) => { - return { name: abi.name, type: abi.type }; - }), - constant: abi.constant || false, - }); + const signature = { + hash: getSigHash(abi), + constant: abi.constant || false, + inputs: getInputs(abi), + outputs: abi.outputs?.map((abi) => { + return { name: abi.name, type: abi.type }; + }), + }; - signatures.set(abi.name, body); - } else if (abi.type === 'event') { - signatures.set(abi.name, { type: 'event' }); - } - return signatures; - }, new Map()), - ([name, method]) => { - return { name: name, type: method.type, signatures: method.signatures }; - }, - ); + method.signatures.push(signature); + + signatures.set(method.name, method); + } else if (abi.type === 'event') { + signatures.set(abi.name, { + name: abi.name, + type: 'event', + signatures: [ + { + hash: getSigHash(abi), + constant: false, + inputs: getInputs(abi), + }, + ], + }); + } + return signatures; + }, new Map()); + return Array.from(contractMethods, ([name, method]) => { + return { name: name, type: method.type, signatures: method.signatures }; + }); } -export function TokenizeString(input: string) { +export function tokenizeString(input: string): string { return input.replace(/\W+/g, '_'); } + +function getSigHash(abi: FunctionOrEvent): string { + return sha3(nameFromABI(abi)).slice(0, 8); +} + +function getInputs(abi: FunctionOrEvent): InputOutput[] { + return ( + abi.inputs + ?.filter((abi) => abi.name !== '') + .map((abi) => { + return { name: abi.name, type: abi.type }; + }) ?? [] + ); +} diff --git a/js/src/solts/lib/syntax.test.ts b/js/src/solts/lib/syntax.test.ts index f6dc74d26..11fc38de6 100644 --- a/js/src/solts/lib/syntax.test.ts +++ b/js/src/solts/lib/syntax.test.ts @@ -1,18 +1,14 @@ import assert from 'assert'; import ts from 'typescript'; -import { CreateCallbackExpression, CreateParameter } from './syntax'; - -function print(node: ts.Node) { - const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); - return printer.printNode(ts.EmitHint.Unspecified, node, undefined); -} +import { printNodes } from '../api'; +import { CreateCallbackExpression, createParameter } from './syntax'; describe('syntax helpers', function () { it('should create callback expression', async function () { const ErrAndResult = [ - CreateParameter(ts.createIdentifier('err'), undefined), - CreateParameter(ts.createIdentifier('result'), undefined), + createParameter(ts.createIdentifier('err'), undefined), + createParameter(ts.createIdentifier('result'), undefined), ]; - assert.equal(print(CreateCallbackExpression(ErrAndResult)), '(err, result) => void'); + assert.equal(printNodes(CreateCallbackExpression(ErrAndResult)), '(err, result) => void'); }); }); diff --git a/js/src/solts/lib/syntax.ts b/js/src/solts/lib/syntax.ts index dc6c60ad3..f6550cb6f 100644 --- a/js/src/solts/lib/syntax.ts +++ b/js/src/solts/lib/syntax.ts @@ -1,4 +1,4 @@ -import ts from 'typescript'; +import ts, { MethodDeclaration, VariableStatement } from 'typescript'; export const Uint8ArrayType = ts.createTypeReferenceNode('Uint8Array', undefined); export const ErrorType = ts.createTypeReferenceNode('Error', undefined); @@ -18,15 +18,15 @@ export const ExportToken = ts.createToken(ts.SyntaxKind.ExportKeyword); export const EllipsisToken = ts.createToken(ts.SyntaxKind.DotDotDotToken); export const QuestionToken = ts.createToken(ts.SyntaxKind.QuestionToken); -export const CreateCall = (fn: ts.Expression, args: ts.Expression[]) => ts.createCall(fn, undefined, args); +export const createCall = (fn: ts.Expression, args?: ts.Expression[]) => ts.createCall(fn, undefined, args); export const AccessThis = (name: ts.Identifier) => ts.createPropertyAccess(ts.createThis(), name); export const BufferFrom = (...args: ts.Expression[]) => - CreateCall(ts.createPropertyAccess(BufferType, ts.createIdentifier('from')), args); + createCall(ts.createPropertyAccess(BufferType, ts.createIdentifier('from')), args); export const AsArray = (type: ts.TypeNode) => ts.createArrayTypeNode(type); export const AsTuple = (type: ts.TypeNode, size: number) => ts.createTupleTypeNode(Array(size).fill(type)); export const AsRefNode = (id: ts.Identifier) => ts.createTypeReferenceNode(id, undefined); -export function CreateParameter( +export function createParameter( name: string | ts.Identifier, typeNode: ts.TypeNode | undefined, initializer?: ts.Expression, @@ -44,14 +44,14 @@ export function CreateParameter( ); } -export function DeclareConstant(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { +export function declareConstant(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean): VariableStatement { return ts.createVariableStatement( extern ? [ExportToken] : [], ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Const), ); } -export function DeclareLet(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { +export function declareLet(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { return ts.createVariableStatement( extern ? [ExportToken] : [], ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Let), @@ -61,21 +61,21 @@ export function DeclareLet(name: ts.Identifier, initializer?: ts.Expression, ext const resolveFn = ts.createIdentifier('resolve'); const rejectFn = ts.createIdentifier('reject'); -export function CreatePromiseBody(error: ts.Identifier, statements: ts.Expression[]) { +export function createPromiseBody(error: ts.Identifier, statements: ts.Expression[]) { return ts.createExpressionStatement( ts.createConditional( error, - CreateCall(rejectFn, [error]), - CreateCall(resolveFn, statements ? statements : undefined), + createCall(rejectFn, [error]), + createCall(resolveFn, statements ? statements : undefined), ), ); } -export function RejectOrResolve(error: ts.Identifier, statements: ts.Statement[], success: ts.Expression[]) { +export function rejectOrResolve(error: ts.Identifier, statements: ts.Statement[], success: ts.Expression[]) { return ts.createIf( error, - ts.createExpressionStatement(CreateCall(rejectFn, [error])), - ts.createBlock([...statements, ts.createExpressionStatement(CreateCall(resolveFn, success))]), + ts.createExpressionStatement(createCall(rejectFn, [error])), + ts.createBlock([...statements, ts.createExpressionStatement(createCall(resolveFn, success))]), ); } @@ -99,7 +99,7 @@ export function CreateCallbackDeclaration( return ts.createArrowFunction( undefined, undefined, - [CreateParameter(first, undefined), CreateParameter(second, undefined)], + [createParameter(first, undefined), createParameter(second, undefined)], returnType, undefined, ts.createBlock(body, multiLine), @@ -124,18 +124,17 @@ export function ImportReadable() { } export class Method { - id: ts.Identifier; - type: ts.TypeReferenceNode; - params: ts.ParameterDeclaration[]; - ret: ts.TypeNode; + readonly id: ts.Identifier; + type?: ts.TypeReferenceNode; + params: ts.ParameterDeclaration[] = []; + ret?: ts.TypeNode; constructor(name: string) { this.id = ts.createIdentifier(name); - this.params = []; } parameter(name: string | ts.Identifier, type: ts.TypeNode, optional?: boolean, isVariadic?: boolean) { - this.params.push(CreateParameter(name, type, undefined, optional, isVariadic)); + this.params.push(createParameter(name, type, undefined, optional, isVariadic)); return this; } @@ -153,7 +152,7 @@ export class Method { return ts.createMethodSignature(undefined, this.params, this.ret, this.id, undefined); } - declaration(statements: ts.Statement[], multiLine?: boolean) { + declaration(statements: ts.Statement[], multiLine?: boolean): MethodDeclaration { return ts.createMethod( undefined, undefined, diff --git a/js/src/solts/main.ts b/js/src/solts/main.ts deleted file mode 100644 index 9639bad0a..000000000 --- a/js/src/solts/main.ts +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env node - -import program from 'commander'; -import fs from 'fs'; -import { Readable } from 'stream'; -import { Compiled, NewFile, Print } from './api'; - -const STDIN = '-'; - -async function ReadAll(file: Readable) { - const chunks: Buffer[] = []; - for await (const chunk of file) { - chunks.push(chunk); - } - return Buffer.concat(chunks).toString(); -} - -type Combined = { - contracts: Record< - string, - { - abi?: string; - bin?: string; - } - >; - version: string; -}; - -async function main() { - program - .name('ts-sol') - .arguments(' [destination]') - .description('Generate typescript classes for an ABI. If no destination specified, print result to STDOUT.') - .action(async function (src, dst) { - let source: string; - if (src === STDIN) { - source = await ReadAll(Readable.from(process.stdin)); - } else { - source = await ReadAll(Readable.from(fs.createReadStream(src))); - } - const input: Combined = JSON.parse(source); - - const compiled: Compiled[] = []; - for (const k in input.contracts) { - if (!input.contracts[k].abi) { - throw new Error(`ABI not given for: ${k}`); - } - if (!input.contracts[k].bin) { - throw new Error(`Bin not given for: ${k}`); - } - compiled.push({ - name: k, - abi: JSON.parse(input.contracts[k].abi), - bin: input.contracts[k].bin, - links: [], - }); - } - - const target = NewFile(compiled); - dst ? fs.writeFileSync(dst, Print(...target)) : console.log(Print(...target)); - }); - - try { - await program.parseAsync(process.argv); - } catch (err) { - console.log(err); - process.exit(1); - } -} - -main(); diff --git a/js/src/solts/sol/Addition.sol b/js/src/solts/sol/Addition.sol new file mode 100644 index 000000000..1e50c0f17 --- /dev/null +++ b/js/src/solts/sol/Addition.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.0.0; + +contract Addition { + function add(int a, int b) public pure returns (int sum) { + sum = a + b; + } +} diff --git a/js/src/solts/sol/Contains.sol b/js/src/solts/sol/Contains.sol new file mode 100644 index 000000000..3927d8289 --- /dev/null +++ b/js/src/solts/sol/Contains.sol @@ -0,0 +1,17 @@ +pragma solidity >=0.0.0; + +contract Contains { + function contains(address[] memory _list, address _value) public pure returns (bool) { + for (uint i = 0; i < _list.length; i++) { + if (_list[i] == _value) return true; + } + return false; + } + + function contains(uint[] memory _list, uint _value) public pure returns (bool) { + for (uint i = 0; i < _list.length; i++) { + if (_list[i] == _value) return true; + } + return false; + } +} \ No newline at end of file diff --git a/js/src/solts/sol/Creator.sol b/js/src/solts/sol/Creator.sol new file mode 100644 index 000000000..f2316d50d --- /dev/null +++ b/js/src/solts/sol/Creator.sol @@ -0,0 +1,19 @@ +pragma solidity >=0.0.0; + +contract Proxy { + string name = ''; + + constructor(string memory _name) public { + name = _name; + } + + function get() external view returns (string memory) { + return name; + } +} + +contract Creator { + function create(string calldata _name) external returns (address proxy) { + return address(new Proxy(_name)); + } +} diff --git a/js/src/solts/sol/Event.sol b/js/src/solts/sol/Event.sol new file mode 100644 index 000000000..764c672fa --- /dev/null +++ b/js/src/solts/sol/Event.sol @@ -0,0 +1,11 @@ +pragma solidity >=0.0.0; + +contract Contract { + event Event ( + address from + ); + + function announce() public { + emit Event(msg.sender); + } +} \ No newline at end of file diff --git a/js/src/solts/sol/MultipleReturns.sol b/js/src/solts/sol/MultipleReturns.sol new file mode 100644 index 000000000..7a144811e --- /dev/null +++ b/js/src/solts/sol/MultipleReturns.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.0.0; + +contract Multiple { + function get() public pure returns (int, int, int) { + return (1, 2, 3); + } +} \ No newline at end of file diff --git a/js/src/solts/sol/Storage.sol b/js/src/solts/sol/Storage.sol new file mode 100644 index 000000000..8d2d4a75a --- /dev/null +++ b/js/src/solts/sol/Storage.sol @@ -0,0 +1,18 @@ +pragma solidity >=0.0.0; + +contract Storage { + int data; + + constructor(int x) public { + data = x; + } + + function set(int x) public { + data = x; + } + + function get() public view returns (int ret) { + return data; + } +} + diff --git a/js/src/solts/sol/Unnamed.sol b/js/src/solts/sol/Unnamed.sol new file mode 100644 index 000000000..abd112a4d --- /dev/null +++ b/js/src/solts/sol/Unnamed.sol @@ -0,0 +1,7 @@ +pragma solidity >=0.0.0; + +contract Unnamed { + function set(int a, int) public pure returns (int sum) { + sum = a + a; + } +} diff --git a/js/src/solts/sol/build.ts b/js/src/solts/sol/build.ts new file mode 100644 index 000000000..264742f5b --- /dev/null +++ b/js/src/solts/sol/build.ts @@ -0,0 +1,5 @@ +import * as path from 'path'; +import { build } from '../build'; + +// Build these before any tests tha may rely on the generated output +build(path.join(__dirname, '..', '..', '..', 'src', 'solts', 'sol')); diff --git a/js/types/solc/index.d.ts b/js/types/solc/index.d.ts index 5f82f2c5f..3ed6a82bd 100644 --- a/js/types/solc/index.d.ts +++ b/js/types/solc/index.d.ts @@ -80,5 +80,13 @@ declare module 'solc' { sources: Record; }; - export function compile(input: string): string; + export type ResolvedImport = { + contents: string; + }; + + export type CompilerOptions = { + import: (path: string) => ResolvedImport; + }; + + export function compile(input: string, opts?: CompilerOptions): string; } diff --git a/js/types/solc_v5/index.d.ts b/js/types/solc_v5/index.d.ts new file mode 100644 index 000000000..6e7f30905 --- /dev/null +++ b/js/types/solc_v5/index.d.ts @@ -0,0 +1,92 @@ +declare module 'solc_v5' { + export type SolidityFunction = { + type: 'function' | 'constructor' | 'fallback'; + name: string; + inputs: Array; + outputs?: Array; + stateMutability?: 'pure' | 'view' | 'nonpayable' | 'payable'; + payable?: boolean; + constant?: boolean; + }; + + export type Event = { + type: 'event'; + name: string; + inputs: Array; + anonymous: boolean; + }; + + export type FunctionInput = { + name: string; + type: string; + components?: FunctionInput[]; + internalType?: string; + }; + + export type FunctionOutput = FunctionInput; + export type EventInput = FunctionInput & { indexed?: boolean }; + + type Bytecode = { + linkReferences: any; + object: string; + opcodes: string; + sourceMap: string; + }; + + type Contract = { + assembly: any; + evm: { + bytecode: Bytecode; + deployedBytecode: Bytecode; + }; + functionHashes: any; + gasEstimates: any; + abi: (SolidityFunction | Event)[]; + opcodes: string; + runtimeBytecode: string; + srcmap: string; + srcmapRuntime: string; + }; + + type Source = { + AST: any; + }; + + export type InputDescription = { + language: string; + sources?: Record; + settings?: { + outputSelection: Record>>; + }; + }; + + type Error = { + sourceLocation?: { + file: string; + start: number; + end: number; + }; + type: string; + component: string; + severity: 'error' | 'warning'; + message: string; + formattedMessage?: string; + }; + + export type OutputDescription = { + contracts: Record>; + errors: Array; + sourceList: Array; + sources: Record; + }; + + export type ResolvedImport = { + contents: string; + }; + + export type CompilerOptions = { + import: (path: string) => ResolvedImport; + }; + + export function compile(input: string, opts?: CompilerOptions): string; +} diff --git a/js/types/solc_v8/index.d.ts b/js/types/solc_v8/index.d.ts new file mode 100644 index 000000000..0a6c274f6 --- /dev/null +++ b/js/types/solc_v8/index.d.ts @@ -0,0 +1,92 @@ +declare module 'solc_v8' { + export type SolidityFunction = { + type: 'function' | 'constructor' | 'fallback'; + name: string; + inputs: Array; + outputs?: Array; + stateMutability?: 'pure' | 'view' | 'nonpayable' | 'payable'; + payable?: boolean; + constant?: boolean; + }; + + export type Event = { + type: 'event'; + name: string; + inputs: Array; + anonymous: boolean; + }; + + export type FunctionInput = { + name: string; + type: string; + components?: FunctionInput[]; + internalType?: string; + }; + + export type FunctionOutput = FunctionInput; + export type EventInput = FunctionInput & { indexed?: boolean }; + + type Bytecode = { + linkReferences: any; + object: string; + opcodes: string; + sourceMap: string; + }; + + type Contract = { + assembly: any; + evm: { + bytecode: Bytecode; + deployedBytecode: Bytecode; + }; + functionHashes: any; + gasEstimates: any; + abi: (SolidityFunction | Event)[]; + opcodes: string; + runtimeBytecode: string; + srcmap: string; + srcmapRuntime: string; + }; + + type Source = { + AST: any; + }; + + export type InputDescription = { + language: string; + sources?: Record; + settings?: { + outputSelection: Record>>; + }; + }; + + type Error = { + sourceLocation?: { + file: string; + start: number; + end: number; + }; + type: string; + component: string; + severity: 'error' | 'warning'; + message: string; + formattedMessage?: string; + }; + + export type OutputDescription = { + contracts: Record>; + errors: Array; + sourceList: Array; + sources: Record; + }; + + export type ResolvedImport = { + contents: string; + }; + + export type CompilerOptions = { + import: (path: string) => ResolvedImport; + }; + + export function compile(input: string, opts?: CompilerOptions): string; +} diff --git a/js/yarn.lock b/js/yarn.lock index ac4497bed..d8f4b6658 100644 --- a/js/yarn.lock +++ b/js/yarn.lock @@ -2076,9 +2076,23 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -solc@^0.8.4: +"solc_v5@npm:solc@^0.5.17": + version "0.5.17" + resolved "https://registry.npmjs.org/solc/-/solc-0.5.17.tgz#8a76c50e98d49ca7610cca2fdc78ff3016540c67" + integrity sha512-qpX+PGaU0Q3c6lh2vDzMoIbhv6bIrecI4bYsx+xUs01xsGFnY6Nr0L8y/QMyutTnrHN6Lb/Yl672ZVRqxka96w== + dependencies: + command-exists "^1.2.8" + commander "3.0.2" + fs-extra "^0.30.0" + js-sha3 "0.8.0" + memorystream "^0.3.1" + require-from-string "^2.0.0" + semver "^5.5.0" + tmp "0.0.33" + +"solc_v8@npm:solc@^0.8.4": version "0.8.4" - resolved "https://registry.yarnpkg.com/solc/-/solc-0.8.4.tgz#c7e606e5fc07f3fe37414bc3dd868404399ea8bb" + resolved "https://registry.npmjs.org/solc/-/solc-0.8.4.tgz#c7e606e5fc07f3fe37414bc3dd868404399ea8bb" integrity sha512-krEdbucX9yY362l79gXTK2UHhsZ02aQjQOYTzcgTd/waApueo3yWGzjX0CDJ1ByOuW46WuKAyzfbRWdFNr6OYQ== dependencies: command-exists "^1.2.8" From 07509f991bcb02e2cd31d680de42cab2b68df5c9 Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Mon, 17 May 2021 15:06:13 +0200 Subject: [PATCH 3/4] Fix up code gen and pull in Client - Switch to use promises - Support function overloads - Add support for events Signed-off-by: Silas Davis --- docs/reference/web3.md | 3 +- js/package.json | 3 +- js/src/{burrow.ts => client.ts} | 75 +++++++-- js/src/codec.ts | 75 +++++++++ js/src/contracts/call.ts | 35 +--- js/src/contracts/compile.ts | 6 +- js/src/contracts/contract.ts | 20 +-- js/src/contracts/event.ts | 12 +- js/src/convert.ts | 93 ++++++---- js/src/events.ts | 58 +++++-- js/src/index.ts | 10 +- js/src/solts/api.ts | 38 +++-- js/src/solts/build.ts | 18 +- js/src/solts/lib/caller.ts | 90 +++++----- js/src/solts/lib/contract.ts | 187 +++++++++++++-------- js/src/solts/lib/decoder.ts | 158 +++++++++-------- js/src/solts/lib/deployer.ts | 136 +++++---------- js/src/solts/lib/encoder.ts | 111 ++++++------ js/src/solts/lib/events.ts | 27 +++ js/src/solts/lib/linker.ts | 97 +++++++++++ js/src/solts/lib/provider.ts | 198 ++++++++++++---------- js/src/solts/lib/replacer.ts | 85 ---------- js/src/solts/lib/solidity.ts | 60 +++---- js/src/solts/lib/syntax.test.ts | 19 ++- js/src/solts/lib/syntax.ts | 214 +++++++++++++++++------- js/src/solts/provider.gd.ts | 15 ++ js/src/solts/provider.gd.ts.gr.ts | 20 +++ js/src/solts/sol/Addition.abi.ts | 64 +++++++ js/src/solts/sol/Contains.abi.ts | 73 ++++++++ js/src/solts/sol/Creator.abi.ts | 101 +++++++++++ js/src/solts/sol/Event.abi.ts | 137 +++++++++++++++ js/src/solts/sol/Event.sol | 23 ++- js/src/solts/sol/MultipleReturns.abi.ts | 67 ++++++++ js/src/solts/sol/Storage.abi.ts | 72 ++++++++ js/src/solts/sol/Unnamed.abi.ts | 64 +++++++ js/src/solts/sol/build.ts | 2 +- js/src/test/solts.test.ts | 28 ++++ js/src/test/test.ts | 4 +- js/types/solc_v5/index.d.ts | 2 +- tests/chain/burrow.toml | 8 +- 40 files changed, 1748 insertions(+), 760 deletions(-) rename js/src/{burrow.ts => client.ts} (60%) create mode 100644 js/src/codec.ts create mode 100644 js/src/solts/lib/events.ts create mode 100644 js/src/solts/lib/linker.ts delete mode 100644 js/src/solts/lib/replacer.ts create mode 100644 js/src/solts/provider.gd.ts create mode 100644 js/src/solts/provider.gd.ts.gr.ts create mode 100644 js/src/solts/sol/Addition.abi.ts create mode 100644 js/src/solts/sol/Contains.abi.ts create mode 100644 js/src/solts/sol/Creator.abi.ts create mode 100644 js/src/solts/sol/Event.abi.ts create mode 100644 js/src/solts/sol/MultipleReturns.abi.ts create mode 100644 js/src/solts/sol/Storage.abi.ts create mode 100644 js/src/solts/sol/Unnamed.abi.ts create mode 100644 js/src/test/solts.test.ts diff --git a/docs/reference/web3.md b/docs/reference/web3.md index 848cddeb4..485c116c0 100644 --- a/docs/reference/web3.md +++ b/docs/reference/web3.md @@ -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 diff --git a/js/package.json b/js/package.json index aafa72bf3..55b187b1f 100644 --- a/js/package.json +++ b/js/package.json @@ -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", diff --git a/js/src/burrow.ts b/js/src/client.ts similarity index 60% rename from js/src/burrow.ts rename to js/src/client.ts index c0a1b7555..796acf172 100644 --- a/js/src/burrow.ts +++ b/js/src/client.ts @@ -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; + +export class Client implements Provider { + interceptor: Interceptor; readonly events: Events; readonly namereg: Namereg; @@ -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); } /** @@ -76,9 +85,9 @@ export class Burrow { ); } - call(callTx: CallTx): Promise { + callTxSync(callTx: CallTx): Promise { return new Promise((resolve, reject) => - this.callPipe(callTx, (error, txe) => { + this.transact.callTxSync(callTx, (error, txe) => { if (error) { return reject(error); } @@ -86,14 +95,14 @@ export class Burrow { if (err) { return reject(err); } - return resolve(txe); + return resolve(this.interceptor(txe)); }), ); } - callSim(callTx: CallTx): Promise { + callTxSim(callTx: CallTx): Promise { return new Promise((resolve, reject) => - this.simPipe(callTx, (error, txe) => { + this.transact.callTxSim(callTx, (error, txe) => { if (error) { return reject(error); } @@ -106,6 +115,39 @@ export class Burrow { ); } + status(): Promise { + return new Promise((resolve, reject) => + this.query.status(new StatusParam(), (err, resp) => (err ? reject(err) : resolve(resp))), + ); + } + + async latestHeight(): Promise { + 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
{ + 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 { + const txe = await this.callTxSync(msg); + return txe.getResult()?.getReturn_asU8(); + } + + async callSim(msg: CallTx): Promise { + const txe = await this.callTxSim(msg); + return txe.getResult()?.getReturn_asU8(); + } + listen( signature: string, address: string, @@ -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); } } diff --git a/js/src/codec.ts b/js/src/codec.ts new file mode 100644 index 000000000..bd3ef8726 --- /dev/null +++ b/js/src/codec.ts @@ -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 | 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}`); +} diff --git a/js/src/contracts/call.ts b/js/src/contracts/call.ts index 542ce2242..a13c87cc8 100644 --- a/js/src/contracts/call.ts +++ b/js/src/contracts/call.ts @@ -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'; @@ -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, @@ -155,32 +155,9 @@ export async function callFunction( callee: Address, args: unknown[], ): Promise { - 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; -// }; diff --git a/js/src/contracts/compile.ts b/js/src/contracts/compile.ts index a8a4a2c33..8bd825a72 100644 --- a/js/src/contracts/compile.ts +++ b/js/src/contracts/compile.ts @@ -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( source: string, name: string, - errorSeverity: 'error' | 'warning' = 'error', + fatalErrorSeverity: 'error' | 'warning' = 'error', ): Contract { const desc: solc.InputDescription = { language: 'Solidity', sources: {} }; if (!desc.sources) { @@ -16,7 +16,7 @@ export function compile( 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()); } diff --git a/js/src/contracts/contract.ts b/js/src/contracts/contract.ts index b7a13b481..ad1c6e7db 100644 --- a/js/src/contracts/contract.ts +++ b/js/src/contracts/contract.ts @@ -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'); @@ -54,7 +54,7 @@ export class Contract { 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, @@ -77,20 +77,20 @@ export class Contract { return [this.code, ...this.childCode].map(contractMeta).filter((m): m is ContractMeta => Boolean(m)); } - async deploy(burrow: Burrow, ...args: unknown[]): Promise { + async deploy(burrow: Client, ...args: unknown[]): Promise { return this.deployWith(burrow, defaultCallOptions, ...args); } - async deployWith(burrow: Burrow, options?: Partial, ...args: unknown[]): Promise { + async deployWith(burrow: Client, options?: Partial, ...args: unknown[]): Promise { 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); } @@ -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 { @@ -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 = () => diff --git a/js/src/contracts/event.ts b/js/src/contracts/event.ts index 4a741f7f1..cb5aaeef6 100644 --- a/js/src/contracts/event.ts +++ b/js/src/contracts/event.ts @@ -1,16 +1,16 @@ import { EventFragment, FormatTypes, Interface, LogDescription } from 'ethers/lib/utils'; import { Keccak } from 'sha3'; -import { Burrow } from '../burrow'; -import { abiToBurrowResult, prefixedHexString } from '../convert'; -import { EventStream, latestStreamingBlockRange } from '../events'; +import { Client } from '../client'; +import { postDecodeResult, prefixedHexString } from '../convert'; +import { EndOfStream, EventStream, latestStreamingBlockRange } from '../events'; -export type EventCallback = (err?: Error, result?: LogDescription) => void; +export type EventCallback = (err?: Error | EndOfStream, result?: LogDescription) => void; export function listen( iface: Interface, frag: EventFragment, address: string, - burrow: Burrow, + burrow: Client, callback: EventCallback, range = latestStreamingBlockRange, ): EventStream { @@ -30,7 +30,7 @@ export function listen( }); return callback(undefined, { ...result, - args: abiToBurrowResult(result.args, frag.inputs), + args: postDecodeResult(result.args, frag.inputs), }); } catch (err) { return callback(err); diff --git a/js/src/convert.ts b/js/src/convert.ts index 782e612f1..4073e1027 100644 --- a/js/src/convert.ts +++ b/js/src/convert.ts @@ -2,7 +2,7 @@ // functional dependency on those types minimal // Same as ethers hybrid array/record type used for dynamic returns -type Result = readonly T[] & { readonly [key: string]: T }; +export type Result = readonly T[] & { readonly [key: string]: T }; // Bash-in-place version of Result type Presult = T[] & { [key: string]: T }; @@ -18,53 +18,66 @@ type BigNumber = { toNumber(): number; }; -// Converts values from those returned by ABI decoder to those expected by Burrow's GRPC bindings -export function abiToBurrowResult(args: Result, outputs: ParamType[] | undefined): Result { - const out: Presult = []; - if (!outputs) { - return Object.freeze(out); - } - checkParamTypesAndArgs('abiToBurrow', outputs, args); - for (let i = 0; i < outputs.length; i++) { - pushValue(out, abiToBurrow(args[i], outputs[i].type), outputs[i]); - } - return Object.freeze(out); -} +const bytesNN = /bytes([0-9]+)/; +const zeroAddress = '0x0000000000000000000000000000000000000000'; // Converts values from those returned by Burrow's GRPC bindings to those expected by ABI encoder -export function burrowToAbiResult(args: Result, inputs: ParamType[]): Result { +export function preEncodeResult(args: Result, inputs: ParamType[]): Result { const out: Presult = []; checkParamTypesAndArgs('burrowToAbi', inputs, args); for (let i = 0; i < inputs.length; i++) { - pushValue(out, burrowToAbi(args[i], inputs[i].type), inputs[i]); + pushValue(out, preEncode(args[i], inputs[i].type), inputs[i]); } return Object.freeze(out); } -export function withoutArrayElements(result: Result): Record { - return Object.fromEntries(Object.entries(result).filter(([k]) => isNaN(Number(k)))); -} - -function abiToBurrow(arg: unknown, type: string): unknown { +function preEncode(arg: unknown, type: string): unknown { if (/address/.test(type)) { - return recApply(unprefixedHexString, arg as NestedArray); - } else if (/bytes[0-9]+/.test(type)) { + return recApply( + (input) => (input === '0x0' || !input ? zeroAddress : prefixedHexString(input)), + arg as NestedArray, + ); + } + const match = bytesNN.exec(type); + if (match) { // Handle bytes32 differently - for legacy reasons they are used as identifiers and represented as hex strings - return recApply(unprefixedHexString, arg as NestedArray); - } else if (/bytes/.test(type)) { + return recApply((input) => { + return padBytes(input, Number(match[1])); + }, arg as NestedArray); + } + if (/bytes/.test(type)) { return recApply(toBuffer, arg as NestedArray); - } else if (/int/.test(type)) { - return recApply(numberToBurrow, arg as NestedArray); } return arg; } -function burrowToAbi(arg: unknown, type: string): unknown { +// Converts values from those returned by ABI decoder to those expected by Burrow's GRPC bindings +export function postDecodeResult(args: Result, outputs: ParamType[] | undefined): Result { + const out: Presult = []; + if (!outputs) { + return Object.freeze(out); + } + checkParamTypesAndArgs('abiToBurrow', outputs, args); + for (let i = 0; i < outputs.length; i++) { + pushValue(out, postDecode(args[i], outputs[i].type), outputs[i]); + } + return Object.freeze(out); +} + +function postDecode(arg: unknown, type: string): unknown { if (/address/.test(type)) { - return recApply(prefixedHexString, arg as NestedArray); - } else if (/bytes/.test(type)) { + return recApply(unprefixedHexString, arg as NestedArray); + } + if (bytesNN.test(type)) { + // Handle bytes32 differently - for legacy reasons they are used as identifiers and represented as hex strings + return recApply(unprefixedHexString, arg as NestedArray); + } + if (/bytes/.test(type)) { return recApply(toBuffer, arg as NestedArray); } + if (/int/.test(type)) { + return recApply(numberToBurrow, arg as NestedArray); + } return arg; } @@ -78,6 +91,10 @@ function numberToBurrow(arg: BigNumber): BigNumber | number { } } +export function withoutArrayElements(result: Result): Record { + return Object.fromEntries(Object.entries(result).filter(([k]) => isNaN(Number(k)))); +} + export function unprefixedHexString(arg: string | Uint8Array): string { if (arg instanceof Uint8Array) { return Buffer.from(arg).toString('hex').toUpperCase(); @@ -89,7 +106,10 @@ export function unprefixedHexString(arg: string | Uint8Array): string { } // Adds a 0x prefix to a hex string if it doesn't have one already or returns a prefixed hex string from bytes -export function prefixedHexString(arg: string | Uint8Array): string { +export function prefixedHexString(arg?: string | Uint8Array): string { + if (!arg) { + return ''; + } if (typeof arg === 'string' && !/^0x/i.test(arg)) { return '0x' + arg.toLowerCase(); } @@ -136,3 +156,16 @@ function pushValue(out: Presult, value: unknown, type: ParamType): void { out[type.name] = value; } } + +export function padBytes(buf: Uint8Array | string, n: number): Buffer { + if (typeof buf === 'string') { + // Parse hex (possible 0x prefixed) into bytes! + buf = toBuffer(buf); + } + if (buf.length > n) { + throw new Error(`cannot pad buffer ${buf} of length ${buf.length} to ${n} because it is longer than ${n}`); + } + const padded = Buffer.alloc(n); + Buffer.from(buf).copy(padded); + return padded; +} diff --git a/js/src/events.ts b/js/src/events.ts index 7760d4547..dcd57b169 100644 --- a/js/src/events.ts +++ b/js/src/events.ts @@ -6,7 +6,10 @@ import BoundType = Bound.BoundType; export type EventStream = grpc.ClientReadableStream; -export type EventCallback = (err?: Error, log?: LogEvent) => void; +// Surprisingly, this seems to be as good as it gets at the time of writing (https://github.com/Microsoft/TypeScript/pull/17819) +// that is, defining various types of union here does not help on the consumer side to infer exactly one of err or log +// will be defined +export type EventCallback = (err?: Error | EndOfStream, event?: T) => void; export function getBlockRange(start: Bounds = 'latest', end: Bounds = 'stream'): BlockRange { const range = new BlockRange(); @@ -17,6 +20,9 @@ export function getBlockRange(start: Bounds = 'latest', end: Bounds = 'stream'): export const latestStreamingBlockRange = Object.freeze(getBlockRange('latest', 'stream')); +export const EndOfStream: unique symbol = Symbol('EndOfStream'); +export type EndOfStream = typeof EndOfStream; + export class Events { constructor(private burrow: IExecutionEventsClient) {} @@ -28,26 +34,31 @@ export class Events { const stream = this.burrow.events(arg); stream.on('data', (data: EventsResponse) => { data.getEventsList().map((event) => { - return callback(undefined, event.getLog()); + const log = event.getLog(); + if (!log) { + return callback(new Error(`received non-log event: ${log}`)); + } + return callback(undefined, log); }); }); + stream.on('end', () => callback(EndOfStream)); stream.on('error', (err: grpc.ServiceError) => err.code === grpc.status.CANCELLED || callback(err)); return stream; } listen(range: BlockRange, address: string, signature: string, callback: EventCallback): EventStream { - const filter = - "EventType = 'LogEvent' AND Address = '" + - address.toUpperCase() + - "'" + - " AND Log0 = '" + - signature.toUpperCase() + - "'"; + const filter = "EventType = 'LogEvent' AND Address = '" + address.toUpperCase() + "'"; + // + + // " AND Log0 = '" + + // signature.toUpperCase() + + // "'"; return this.stream(range, filter, callback); } } -export type Bounds = 'first' | 'latest' | 'stream' | number; +export type Bounds = number | 'first' | 'latest' | 'stream'; + +export type FiniteBounds = number | 'first' | 'latest'; function boundsToBound(bounds: Bounds): Bound { const bound = new Bound(); @@ -70,3 +81,30 @@ function boundsToBound(bounds: Bounds): Bound { return bound; } + +export function readEvents( + listen: (callback: EventCallback, range?: BlockRange) => EventStream, + start: FiniteBounds = 'first', + end: FiniteBounds = 'latest', + limit?: number, +): Promise { + return new Promise((resolve, reject) => { + const events: T[] = []; + const stream = listen((err, event) => { + if (err) { + if (err === EndOfStream) { + return resolve(events); + } + return reject(err); + } + if (!event) { + return reject(new Error(`received empty event`)); + } + events.push(event); + if (limit && events.length === limit) { + stream.cancel(); + return resolve(events); + } + }, getBlockRange(start, end)); + }); +} diff --git a/js/src/index.ts b/js/src/index.ts index ff4312d62..5a9c54c7a 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,5 +1,11 @@ -export { TxExecution } from '../proto/exec_pb'; +export { LogEvent, TxExecution } from '../proto/exec_pb'; export { CallTx, TxInput } from '../proto/payload_pb'; -export { Burrow } from './burrow'; +export { BlockRange } from '../proto/rpcevents_pb'; +export { Client } from './client'; +export { ContractCodec } from './codec'; +export { Address } from './contracts/abi'; +export { makeCallTx } from './contracts/call'; export { Contract } from './contracts/contract'; +export { Result } from './convert'; +export { EndOfStream, EventStream } from './events'; export { build } from './solts/build'; diff --git a/js/src/solts/api.ts b/js/src/solts/api.ts index e06271474..f0d888ef2 100644 --- a/js/src/solts/api.ts +++ b/js/src/solts/api.ts @@ -1,14 +1,14 @@ -import ts from 'typescript'; +import ts, { factory } from 'typescript'; import { ABI } from './lib/abi'; -import { Caller } from './lib/caller'; +import { createCallerFunction } from './lib/caller'; import { generateContractClass } from './lib/contract'; import { generateDecodeObject } from './lib/decoder'; import { generateDeployFunction } from './lib/deployer'; import { generateEncodeObject } from './lib/encoder'; +import { createLinkerFunction } from './lib/linker'; import { Provider } from './lib/provider'; -import { Replacer } from './lib/replacer'; import { getContractMethods } from './lib/solidity'; -import { ExportToken, ImportReadable } from './lib/syntax'; +import { declareConstant, ExportToken, importBurrow, importReadable } from './lib/syntax'; import Func = ABI.Func; export { decodeOutput, encodeInput, importLocal, inputDescriptionFromFiles, tokenizeLinks } from './lib/compile'; @@ -20,18 +20,24 @@ export type Compiled = { links: Array; }; -export function newFile(contracts: Compiled[]): ts.Node[] { +const abiName = factory.createIdentifier('abi'); +const bytecodeName = factory.createIdentifier('bytecode'); + +// Note: this is a very useful tool for discovering the correct Typescript factory API calls to produce a particular +//piece of syntax: https://ts-ast-viewer.com +export function newFile(contracts: Compiled[], burrowImportPath: string): ts.Node[] { const provider = new Provider(); return [ ts.addSyntheticLeadingComment( - ImportReadable(), + importReadable(), ts.SyntaxKind.SingleLineCommentTrivia, 'Code generated by solts. DO NOT EDIT.', ), + importBurrow(burrowImportPath), provider.createInterface(), - Caller(provider), - Replacer(), + createCallerFunction(provider), + createLinkerFunction(), ...contracts.map((contract) => { const methods = getContractMethods(contract.abi); @@ -39,19 +45,23 @@ export function newFile(contracts: Compiled[]): ts.Node[] { // No deploy function for interfaces const deployFunction = contract.bin - ? [generateDeployFunction(deploy, contract.bin, contract.links, provider)] + ? [ + declareConstant(bytecodeName, factory.createStringLiteral(contract.bin, true), true), + generateDeployFunction(deploy, bytecodeName, contract.links, provider, abiName), + ] : []; const statements = [ + declareConstant(abiName, factory.createStringLiteral(JSON.stringify(contract.abi), true), true), ...deployFunction, generateContractClass(methods, provider), - generateEncodeObject(methods, provider), - generateDecodeObject(methods, provider), + generateEncodeObject(methods, provider, abiName), + generateDecodeObject(methods, provider, abiName), ]; - return ts.createModuleDeclaration( + return factory.createModuleDeclaration( undefined, [ExportToken], - ts.createIdentifier(contract.name), - ts.createModuleBlock(statements), + factory.createIdentifier(contract.name), + factory.createModuleBlock(statements), ); }), ]; diff --git a/js/src/solts/build.ts b/js/src/solts/build.ts index 5d557ea48..c7a94e247 100644 --- a/js/src/solts/build.ts +++ b/js/src/solts/build.ts @@ -10,6 +10,14 @@ const solcCompilers = { v8: solcv8, } as const; +export const defaultBuildOptions = { + solcVersion: 'v5' as keyof typeof solcCompilers, + burrowImportPath: '@hyperledger/burrow' as string, + binPath: 'bin' as string, +} as const; + +export type BuildOptions = typeof defaultBuildOptions; + /** * This is our Solidity -> Typescript code generation function, it: * - Compiles Solidity source @@ -17,11 +25,8 @@ const solcCompilers = { * - Generates typescript code to deploy the contracts * - Outputs the ABI files into bin to be later included in the distribution (for Vent and other ABI-consuming services) */ -export function build( - srcPathOrFiles: string | string[], - binPath = 'bin', - solcVersion: keyof typeof solcCompilers = 'v5', -): void { +export function build(srcPathOrFiles: string | string[], opts?: Partial): void { + const { solcVersion, binPath, burrowImportPath } = { ...defaultBuildOptions, ...opts }; fs.mkdirSync(binPath, { recursive: true }); const solidityFiles = getSourceFilesList(srcPathOrFiles); const inputDescription = inputDescriptionFromFiles(solidityFiles); @@ -52,9 +57,10 @@ export function build( for (const [name, contract] of Object.entries(solidity)) { fs.writeFileSync(path.join('bin', name + '.bin'), JSON.stringify(contract)); } - fs.writeFileSync(target, printNodes(...newFile(compiled))); + fs.writeFileSync(target, printNodes(...newFile(compiled, burrowImportPath))); } } + function getSourceFilesList(srcPathOrFiles: string | string[]): string[] { if (typeof srcPathOrFiles === 'string') { return fs diff --git a/js/src/solts/lib/caller.ts b/js/src/solts/lib/caller.ts index 2c1637dd1..5ff26bbaf 100644 --- a/js/src/solts/lib/caller.ts +++ b/js/src/solts/lib/caller.ts @@ -1,83 +1,71 @@ -import ts from 'typescript'; +import * as ts from 'typescript'; +import { factory } from 'typescript'; import { Provider } from './provider'; import { + AsyncToken, BooleanType, + ColonToken, createCall, - CreateCallbackDeclaration, - CreateNewPromise, createParameter, - createPromiseBody, declareConstant, + MaybeUint8ArrayType, PromiseType, + QuestionToken, StringType, Uint8ArrayType, } from './syntax'; -export const CallName = ts.createIdentifier('Call'); +export const callName = factory.createIdentifier('call'); -export const Caller = (provider: Provider) => { - const input = provider.getType(); - const output = ts.createIdentifier('Output'); - const client = ts.createIdentifier('client'); - const payload = ts.createIdentifier('payload'); - const data = ts.createIdentifier('data'); - const isSim = ts.createIdentifier('isSim'); - const callback = ts.createIdentifier('callback'); - const err = ts.createIdentifier('err'); - const exec = ts.createIdentifier('exec'); - const addr = ts.createIdentifier('addr'); +export function createCallerFunction(provider: Provider): ts.FunctionDeclaration { + const output = factory.createIdentifier('Output'); + const client = factory.createIdentifier('client'); + const payload = factory.createIdentifier('payload'); + const txe = factory.createIdentifier('txe'); + const data = factory.createIdentifier('data'); + const isSim = factory.createIdentifier('isSim'); + const callback = factory.createIdentifier('callback'); + const addr = factory.createIdentifier('addr'); - return ts.createFunctionDeclaration( + return factory.createFunctionDeclaration( undefined, + [AsyncToken], undefined, - undefined, - CallName, - [ts.createTypeParameterDeclaration(input), ts.createTypeParameterDeclaration(output)], + callName, + [factory.createTypeParameterDeclaration(output)], [ - createParameter(client, provider.getTypeNode()), + createParameter(client, provider.type()), createParameter(addr, StringType), - createParameter(data, StringType), + createParameter(data, Uint8ArrayType), createParameter(isSim, BooleanType), createParameter( callback, - ts.createFunctionTypeNode( + factory.createFunctionTypeNode( undefined, - [createParameter('exec', Uint8ArrayType)], - ts.createTypeReferenceNode(output, undefined), + [createParameter('exec', MaybeUint8ArrayType)], + factory.createTypeReferenceNode(output, undefined), ), ), ], - ts.createTypeReferenceNode(PromiseType, [ts.createTypeReferenceNode(output, undefined)]), - ts.createBlock( + factory.createTypeReferenceNode(PromiseType, [factory.createTypeReferenceNode(output, undefined)]), + factory.createBlock( [ declareConstant(payload, provider.methods.payload.call(client, data, addr)), - ts.createIf( - isSim, - ts.createReturn( - CreateNewPromise([ - ts.createExpressionStatement( - provider.methods.callSim.call( - client, - payload, - CreateCallbackDeclaration(err, exec, [createPromiseBody(err, [createCall(callback, [exec])])]), - ), - ), - ]), - ), - ts.createReturn( - CreateNewPromise([ - ts.createExpressionStatement( - provider.methods.call.call( - client, - payload, - CreateCallbackDeclaration(err, exec, [createPromiseBody(err, [createCall(callback, [exec])])]), - ), - ), - ]), + declareConstant( + txe, + factory.createAwaitExpression( + factory.createConditionalExpression( + isSim, + QuestionToken, + provider.methods.callSim.call(client, payload), + ColonToken, + provider.methods.call.call(client, payload), + ), ), ), + factory.createReturnStatement(createCall(callback, [txe])), ], true, ), ); -}; +} diff --git a/js/src/solts/lib/contract.ts b/js/src/solts/lib/contract.ts index 8b42a9fd3..2fa1991dc 100644 --- a/js/src/solts/lib/contract.ts +++ b/js/src/solts/lib/contract.ts @@ -1,119 +1,158 @@ -import ts, { ClassDeclaration, MethodDeclaration } from 'typescript'; -import { CallName } from './caller'; -import { DecodeName } from './decoder'; -import { EncodeName } from './encoder'; -import { ErrParameter, EventParameter, Provider } from './provider'; -import { collapseInputs, combineTypes, ContractMethodsList, outputToType, Signature } from './solidity'; +import ts, { ClassDeclaration, factory, MethodDeclaration } from 'typescript'; +import { callName } from './caller'; +import { decodeName } from './decoder'; +import { encodeName } from './encoder'; +import { callGetDataFromLog, callGetTopicsFromLog, eventSigHash } from './events'; +import { errName, EventErrParameter, LogEventParameter, logName, Provider } from './provider'; +import { ContractMethodsList, getRealType, inputOuputsToType, Signature } from './solidity'; import { - AccessThis, - AsRefNode, + accessThis, + asRefNode, + BlockRangeType, createCall, - CreateCallbackExpression, + createCallbackExpression, createParameter, + createPromiseOf, declareConstant, + EventStream, ExportToken, + MaybeUint8ArrayType, Method, PrivateToken, + prop, PublicToken, - ReadableType, StringType, - Uint8ArrayType, + Undefined, } from './syntax'; -const exec = ts.createIdentifier('exec'); -const data = ts.createIdentifier('data'); -const client = ts.createIdentifier('client'); -const address = ts.createIdentifier('address'); +const dataName = factory.createIdentifier('data'); +const clientName = factory.createIdentifier('client'); +const addressName = factory.createIdentifier('address'); +const eventName = factory.createIdentifier('eventName'); -export const ContractName = ts.createIdentifier('Contract'); +export const ContractName = factory.createIdentifier('Contract'); -function solidityFunction(name: string, signatures: Signature[]): MethodDeclaration { - const args = Array.from(collapseInputs(signatures).keys()).map((key) => ts.createIdentifier(key)); - const encode = declareConstant( - data, - createCall(ts.createPropertyAccess(createCall(EncodeName, [AccessThis(client)]), name), args), - ); +function solidityFunction(name: string, signatures: Signature[], index: number): ts.MethodDeclaration { + const signature = signatures[index]; + const args = signature.inputs.map((input) => factory.createIdentifier(input.name)); + const encodeFunctionOrOverloadsArray = prop(createCall(encodeName, [accessThis(clientName)]), name); + + // Special case for overloads + const hasOverloads = signatures.length > 1; + + const encoderFunction = hasOverloads + ? factory.createElementAccessExpression(encodeFunctionOrOverloadsArray, index) + : encodeFunctionOrOverloadsArray; + + const decoderFunctionOrOverloadsArray = prop(createCall(decodeName, [accessThis(clientName), dataName]), name); - const call = ts.createCall( - CallName, - [ts.createTypeReferenceNode('Tx', undefined), outputToType(signatures[0])], + const decoderFunction = hasOverloads + ? factory.createElementAccessExpression(decoderFunctionOrOverloadsArray, index) + : decoderFunctionOrOverloadsArray; + + const encode = declareConstant(dataName, createCall(encoderFunction, args)); + + const returnType = inputOuputsToType(signature.outputs); + + const call = factory.createCallExpression( + callName, + [returnType], [ - AccessThis(client), - AccessThis(address), - data, - ts.createLiteral(signatures[0].constant), - ts.createArrowFunction( + accessThis(clientName), + accessThis(addressName), + dataName, + signature.constant ? factory.createTrue() : factory.createFalse(), + factory.createArrowFunction( undefined, undefined, - [createParameter(exec, Uint8ArrayType)], + [createParameter(dataName, MaybeUint8ArrayType)], undefined, undefined, - ts.createBlock( - [ - ts.createReturn( - createCall(ts.createPropertyAccess(createCall(DecodeName, [AccessThis(client), exec]), name), []), - ), - ], - true, - ), + factory.createBlock([factory.createReturnStatement(createCall(decoderFunction, []))], true), ), ], ); - const params = Array.from(collapseInputs(signatures), ([key, value]) => createParameter(key, combineTypes(value))); - return new Method(name).parameters(params).declaration([encode, ts.createReturn(call)], true); + const params = signature.inputs.map((input) => createParameter(input.name, getRealType(input.type))); + // Suffix overloads + return new Method(index > 0 ? `${name}_${index}` : name) + .parameters(params) + .returns(createPromiseOf(returnType)) + .declaration([encode, factory.createReturnStatement(call)], true); } -function solidityEvent(name: string, provider: Provider): MethodDeclaration { - const callback = ts.createIdentifier('callback'); +function solidityEvent(name: string, signature: Signature, provider: Provider): ts.MethodDeclaration { + const callback = factory.createIdentifier('callback'); + const range = factory.createIdentifier('range'); + // Receivers of LogEventParameter + const data = callGetDataFromLog(logName); + const topics = callGetTopicsFromLog(logName); + const decoderFunction = prop(createCall(decodeName, [accessThis(clientName), data, topics]), name); return new Method(name) - .parameter(callback, CreateCallbackExpression([ErrParameter, EventParameter])) - .returns(AsRefNode(ReadableType)) + .parameter( + callback, + createCallbackExpression([ + EventErrParameter, + createParameter(eventName, inputOuputsToType(signature.inputs), undefined, true), + ]), + ) + .parameter(range, BlockRangeType, true) + .returns(asRefNode(EventStream)) .declaration([ - ts.createReturn( - provider.methods.listen.call(AccessThis(client), ts.createLiteral(name), AccessThis(address), callback), + factory.createReturnStatement( + provider.methods.listen.call( + accessThis(clientName), + factory.createStringLiteral(eventSigHash(name, signature.inputs)), + accessThis(addressName), + + factory.createArrowFunction( + undefined, + undefined, + [EventErrParameter, LogEventParameter], + undefined, + undefined, + factory.createBlock([ + factory.createIfStatement(errName, factory.createReturnStatement(createCall(callback, [errName]))), + factory.createReturnStatement(createCall(callback, [Undefined, createCall(decoderFunction)])), + ]), + ), + range, + ), ), ]); } -function createMethodFromABI( +function createMethodsFromABI( name: string, type: 'function' | 'event', signatures: Signature[], provider: Provider, -): MethodDeclaration { +): MethodDeclaration[] { if (type === 'function') { - return solidityFunction(name, signatures); + return signatures.map((signature, index) => solidityFunction(name, signatures, index)); } else if (type === 'event') { - return solidityEvent(name, provider); + return [solidityEvent(name, signatures[0], provider)]; } // FIXME: Not sure why this is not inferred since if is exhaustive return undefined as never; } export function generateContractClass(abi: ContractMethodsList, provider: Provider): ClassDeclaration { - return ts.createClassDeclaration( - undefined, - [ExportToken], - ContractName, - [provider.getTypeArgumentDecl()], - undefined, - [ - ts.createProperty(undefined, [PrivateToken], client, undefined, provider.getTypeNode(), undefined), - ts.createProperty(undefined, [PublicToken], address, undefined, StringType, undefined), - ts.createConstructor( - undefined, - undefined, - [createParameter(client, provider.getTypeNode()), createParameter(address, StringType)], - ts.createBlock( - [ - ts.createStatement(ts.createAssignment(AccessThis(client), client)), - ts.createStatement(ts.createAssignment(AccessThis(address), address)), - ], - true, - ), + return factory.createClassDeclaration(undefined, [ExportToken], ContractName, undefined, undefined, [ + factory.createPropertyDeclaration(undefined, [PrivateToken], clientName, undefined, provider.type(), undefined), + factory.createPropertyDeclaration(undefined, [PublicToken], addressName, undefined, StringType, undefined), + factory.createConstructorDeclaration( + undefined, + undefined, + [createParameter(clientName, provider.type()), createParameter(addressName, StringType)], + factory.createBlock( + [ + factory.createExpressionStatement(factory.createAssignment(accessThis(clientName), clientName)), + factory.createExpressionStatement(factory.createAssignment(accessThis(addressName), addressName)), + ], + true, ), - ...abi.map((abi) => createMethodFromABI(abi.name, abi.type, abi.signatures, provider)), - ], - ); + ), + ...abi.flatMap((abi) => createMethodsFromABI(abi.name, abi.type, abi.signatures, provider)), + ]); } diff --git a/js/src/solts/lib/decoder.ts b/js/src/solts/lib/decoder.ts index 690972ad2..7dfcb0598 100644 --- a/js/src/solts/lib/decoder.ts +++ b/js/src/solts/lib/decoder.ts @@ -1,26 +1,98 @@ -import ts, { Expression, VariableStatement } from 'typescript'; -import { Provider } from './provider'; -import { ContractMethodsList, outputToType, Signature } from './solidity'; -import { createParameter, declareConstant, Uint8ArrayType } from './syntax'; +import ts, { factory, VariableStatement } from 'typescript'; +import { callDecodeEventLog, callDecodeFunctionResult, Provider } from './provider'; +import { ContractMethodsList, inputOuputsToType, InputOutput, Method, Signature } from './solidity'; +import { asConst, createParameter, declareConstant, MaybeUint8ArrayType, Uint8ArrayType } from './syntax'; -export const DecodeName = ts.createIdentifier('Decode'); +export const decodeName = factory.createIdentifier('decode'); +const clientName = factory.createIdentifier('client'); +const dataName = factory.createIdentifier('data'); +const topicsName = factory.createIdentifier('topics'); -function output(decodeFn: ts.CallExpression, sig: Signature): ts.Block { +export function generateDecodeObject( + methods: ContractMethodsList, + provider: Provider, + abiName: ts.Identifier, +): VariableStatement { + return generateDecoderObject(methods, provider, abiName, (method) => { + const decodeFunction = (signature: Signature) => { + const isFunction = method.type === 'function'; + const inputsOrOutputs = isFunction ? signature.outputs : signature.inputs; + return factory.createArrowFunction( + undefined, + undefined, + [], + inputOuputsToType(inputsOrOutputs), + undefined, + body( + isFunction + ? callDecodeFunctionResult(signature.hash, dataName) + : callDecodeEventLog(signature.hash, dataName, topicsName), + inputsOrOutputs, + ), + ); + }; + if (method.signatures.length === 1) { + return decodeFunction(method.signatures[0]); + } + return asConst(factory.createArrayLiteralExpression(method.signatures.map(decodeFunction))); + }); +} + +function generateDecoderObject( + methods: ContractMethodsList, + provider: Provider, + abiName: ts.Identifier, + functionMaker: (m: Method) => ts.Expression, +): VariableStatement { + return declareConstant( + decodeName, + factory.createArrowFunction( + undefined, + undefined, + [ + createParameter(clientName, provider.type()), + createParameter(dataName, MaybeUint8ArrayType), + createParameter( + topicsName, + factory.createArrayTypeNode(Uint8ArrayType), + factory.createArrayLiteralExpression(), + ), + ], + undefined, + undefined, + factory.createBlock([ + provider.declareContractCodec(clientName, abiName), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + methods.map((method) => { + return factory.createPropertyAssignment(method.name, functionMaker(method)); + }, true), + true, + ), + ), + ]), + ), + true, + ); +} + +function body(decodeFn: ts.CallExpression, ios?: InputOutput[]): ts.Block { let named = []; - if (sig.outputs && sig.outputs[0] !== undefined) { - named = sig.outputs.filter((out) => out.name !== ''); + if (ios && ios[0] !== undefined) { + named = ios.filter((out) => out.name !== ''); } - if (sig.outputs?.length && sig.outputs.length === named.length) { - const setter = ts.createVariableStatement( + if (ios?.length && ios.length === named.length) { + const setter = factory.createVariableStatement( [], - ts.createVariableDeclarationList( + factory.createVariableDeclarationList( [ - ts.createVariableDeclaration( - ts.createArrayBindingPattern( - sig.outputs.map((out) => ts.createBindingElement(undefined, undefined, out.name)), + factory.createVariableDeclaration( + factory.createArrayBindingPattern( + ios.map((out) => factory.createBindingElement(undefined, undefined, out.name)), ), undefined, + undefined, decodeFn, ), ], @@ -28,66 +100,18 @@ function output(decodeFn: ts.CallExpression, sig: Signature): ts.Block { ), ); - return ts.createBlock( + return factory.createBlock( [ setter, - ts.createReturn( - ts.createObjectLiteral( - sig.outputs.map((out) => ts.createPropertyAssignment(out.name, ts.createIdentifier(out.name))), + factory.createReturnStatement( + factory.createObjectLiteralExpression( + ios.map(({ name }) => factory.createPropertyAssignment(name, factory.createIdentifier(name))), ), ), ], true, ); } else { - return ts.createBlock([ts.createReturn(sig.outputs?.length ? decodeFn : undefined)]); - } -} - -function decoder(sig: Signature, client: ts.Identifier, provider: Provider, data: ts.Identifier) { - let args: Expression[] = []; - if (sig.outputs && sig.outputs[0] !== undefined) { - args = sig.outputs.map((arg) => ts.createLiteral(arg.type)); + return factory.createBlock([factory.createReturnStatement(ios?.length ? decodeFn : undefined)]); } - const types = ts.createArrayLiteral(args); - return provider.methods.decode.call(client, data, types); -} - -export function generateDecodeObject(methods: ContractMethodsList, provider: Provider): VariableStatement { - const client = ts.createIdentifier('client'); - const data = ts.createIdentifier('data'); - - return declareConstant( - DecodeName, - ts.createArrowFunction( - undefined, - [provider.getTypeArgumentDecl()], - [createParameter(client, provider.getTypeNode()), createParameter(data, Uint8ArrayType)], - undefined, - undefined, - ts.createBlock([ - ts.createReturn( - ts.createObjectLiteral( - methods - .filter((method) => method.type === 'function') - .map((method) => { - return ts.createPropertyAssignment( - method.name, - ts.createArrowFunction( - undefined, - undefined, - [], - outputToType(method.signatures[0]), - undefined, - output(decoder(method.signatures[0], client, provider, data), method.signatures[0]), - ), - ); - }, true), - true, - ), - ), - ]), - ), - true, - ); } diff --git a/js/src/solts/lib/deployer.ts b/js/src/solts/lib/deployer.ts index fe8eefb8f..3de4f967a 100644 --- a/js/src/solts/lib/deployer.ts +++ b/js/src/solts/lib/deployer.ts @@ -1,130 +1,86 @@ -import ts, { FunctionDeclaration } from 'typescript'; +import ts, { factory, FunctionDeclaration } from 'typescript'; import { ABI } from './abi'; -import { Provider } from './provider'; -import { ReplacerName } from './replacer'; +import { linkerName } from './linker'; +import { callEncodeDeploy, Provider } from './provider'; import { getRealType, sha3, tokenizeString } from './solidity'; import { - BufferFrom, + BufferType, + createAssignmentStatement, createCall, - CreateCallbackDeclaration, - CreateNewPromise, createParameter, declareConstant, declareLet, ExportToken, PromiseType, - rejectOrResolve, + prop, StringType, } from './syntax'; -const data = ts.createIdentifier('data'); -const payload = ts.createIdentifier('payload'); -const bytecode = ts.createIdentifier('bytecode'); +// Variable names +const payloadName = factory.createIdentifier('payload'); +const linkedBytecodeName = factory.createIdentifier('linkedBytecode'); +const dataName = factory.createIdentifier('data'); +const clientName = factory.createIdentifier('client'); -const err = ts.createIdentifier('err'); -const addr = ts.createIdentifier('addr'); - -const client = ts.createIdentifier('client'); -const address = ts.createIdentifier('address'); - -export const DeployName = ts.createIdentifier('Deploy'); +export const DeployName = factory.createIdentifier('deploy'); export function generateDeployFunction( abi: ABI.Func | undefined, - bin: string, + bytecodeName: ts.Identifier, links: string[], provider: Provider, + abiName: ts.Identifier, ): FunctionDeclaration { - if (bin === '') { - throw new Error(`Cannot deploy without binary`); - } - const parameters = abi ? abi.inputs?.map((input) => createParameter(input.name, getRealType(input.type))) ?? [] : []; - // const output = ts.createExpressionWithTypeArguments([ts.createTypeReferenceNode(ContractName, [ts.createTypeReferenceNode('Tx', undefined)])], PromiseType); - const output = ts.createExpressionWithTypeArguments([StringType], PromiseType); + const output = factory.createExpressionWithTypeArguments(PromiseType, [StringType]); const statements: ts.Statement[] = []; - statements.push(declareLet(bytecode, ts.createLiteral(bin))); + statements.push(provider.declareContractCodec(clientName, abiName)); + statements.push(declareLet(linkedBytecodeName, bytecodeName)); statements.push( - ...links.map((link) => { - return ts.createExpressionStatement( - ts.createAssignment( - bytecode, - createCall(ReplacerName, [ - bytecode, - ts.createStringLiteral('$' + sha3(link).toLowerCase().slice(0, 34) + '$'), - ts.createIdentifier(tokenizeString(link)), - ]), - ), - ); - }), + ...links.map((link) => + createAssignmentStatement( + linkedBytecodeName, + createCall(linkerName, [ + linkedBytecodeName, + factory.createStringLiteral('$' + sha3(link).toLowerCase().slice(0, 34) + '$'), + factory.createIdentifier(tokenizeString(link)), + ]), + ), + ), ); - if (abi) { - const inputs = ts.createArrayLiteral(abi.inputs?.map((arg) => ts.createLiteral(arg.type))); - const args = abi.inputs?.map((arg) => ts.createIdentifier(arg.name)) ?? []; - statements.push( - declareConstant( - data, - ts.createBinary( - bytecode, - ts.SyntaxKind.PlusToken, - provider.methods.encode.call(client, ts.createLiteral(''), inputs, ...args), - ), - ), - ); - } else { - statements.push(declareConstant(data, bytecode)); - } - statements.push(declareConstant(payload, provider.methods.payload.call(client, data, undefined))); + const args = abi?.inputs?.map((arg) => factory.createIdentifier(arg.name)) ?? []; - const deployFn = provider.methods.deploy.call( - client, - payload, - CreateCallbackDeclaration( - err, - addr, - [ - rejectOrResolve( - err, - [ - declareConstant( - address, - createCall( - ts.createPropertyAccess( - createCall(ts.createPropertyAccess(BufferFrom(addr), ts.createIdentifier('toString')), [ - ts.createLiteral('hex'), - ]), - ts.createIdentifier('toUpperCase'), - ), - undefined, - ), - ), - ], - // [ts.createNew(ContractName, [], [client, address])]) - [address], - ), - ], - undefined, - true, + statements.push( + declareConstant( + dataName, + createCall(prop(BufferType, 'concat'), [ + factory.createArrayLiteralExpression([ + createCall(prop(BufferType, 'from'), [linkedBytecodeName, factory.createStringLiteral('hex')]), + callEncodeDeploy(args), + ]), + ]), ), ); + statements.push(declareConstant(payloadName, provider.methods.payload.call(clientName, dataName, undefined))); - statements.push(ts.createReturn(CreateNewPromise([ts.createStatement(deployFn)]))); + const deployFn = provider.methods.deploy.call(clientName, payloadName); - const type = 'Tx'; - return ts.createFunctionDeclaration( + statements.push(factory.createReturnStatement(deployFn)); + + return factory.createFunctionDeclaration( undefined, [ExportToken], undefined, DeployName, - [ts.createTypeParameterDeclaration(type)], + undefined, [ - createParameter(client, ts.createTypeReferenceNode('Provider', [ts.createTypeReferenceNode(type, [])])), - ...links.map((link) => createParameter(ts.createIdentifier(tokenizeString(link)), StringType)), + createParameter(clientName, provider.type()), + ...links.map((link) => createParameter(factory.createIdentifier(tokenizeString(link)), StringType)), ...parameters, ], output, - ts.createBlock(statements, true), + factory.createBlock(statements, true), ); } diff --git a/js/src/solts/lib/encoder.ts b/js/src/solts/lib/encoder.ts index ea772407b..def4bd0d7 100644 --- a/js/src/solts/lib/encoder.ts +++ b/js/src/solts/lib/encoder.ts @@ -1,73 +1,62 @@ -import ts, { FunctionDeclaration, VariableStatement } from 'typescript'; -import { Provider } from './provider'; -import { collapseInputs, combineTypes, ContractMethodsList, Signature } from './solidity'; -import { createParameter, declareConstant } from './syntax'; +import ts, { factory, VariableStatement } from 'typescript'; +import { callEncodeFunctionData, Provider } from './provider'; +import { ContractMethodsList, getRealType, Method, Signature } from './solidity'; +import { asConst, createParameter, declareConstant } from './syntax'; -export const EncodeName = ts.createIdentifier('Encode'); +export const encodeName = factory.createIdentifier('encode'); +const client = factory.createIdentifier('client'); -function output(signatures: Array, client: ts.Identifier, provider: Provider): ts.Block { - if (signatures.length === 0) { - return ts.createBlock([ts.createReturn()]); - } else if (signatures.length === 1) { - return ts.createBlock([ts.createReturn(encoder(signatures[0], client, provider))]); - } - - return ts.createBlock( - signatures - .filter((sig) => sig.inputs.length > 0) - .map((sig) => { - return ts.createIf( - sig.inputs - .map((input) => - ts.createStrictEquality(ts.createTypeOf(ts.createIdentifier(input.name)), ts.createLiteral('string')), - ) - .reduce((all, next) => ts.createLogicalAnd(all, next)), - ts.createReturn(encoder(sig, client, provider)), - ); - }), - true, - ); -} - -function encoder(sig: Signature, client: ts.Identifier, provider: Provider) { - const inputs = ts.createArrayLiteral(sig.inputs.map((arg) => ts.createLiteral(arg.type))); - const args = sig.inputs.map((arg) => ts.createIdentifier(arg.name)); - return provider.methods.encode.call(client, ts.createLiteral(sig.hash), inputs, ...args); +export function generateEncodeObject( + methods: ContractMethodsList, + provider: Provider, + abiName: ts.Identifier, +): VariableStatement { + return generateEncoderObject(encodeName, methods, provider, abiName, (method) => { + const encodeFunction = (signature: Signature) => + factory.createArrowFunction( + undefined, + undefined, + signature.inputs.map((i) => createParameter(i.name, getRealType(i.type))), + undefined, + undefined, + factory.createBlock([ + factory.createReturnStatement( + callEncodeFunctionData( + signature.hash, + signature.inputs.map((arg) => factory.createIdentifier(arg.name)), + ), + ), + ]), + ); + if (method.signatures.length === 1) { + return encodeFunction(method.signatures[0]); + } + return asConst(factory.createArrayLiteralExpression(method.signatures.map(encodeFunction))); + }); } -export function generateEncodeObject(methods: ContractMethodsList, provider: Provider): VariableStatement { - const client = ts.createIdentifier('client'); - +function generateEncoderObject( + name: ts.Identifier, + methods: ContractMethodsList, + provider: Provider, + abiName: ts.Identifier, + functionMaker: (m: Method) => ts.Expression, +): VariableStatement { return declareConstant( - EncodeName, - ts.createArrowFunction( + name, + factory.createArrowFunction( undefined, - [provider.getTypeArgumentDecl()], - [createParameter(client, provider.getTypeNode())], undefined, + [createParameter(client, provider.type())], undefined, - ts.createBlock([ - ts.createReturn( - ts.createObjectLiteral( + undefined, + factory.createBlock([ + provider.declareContractCodec(client, abiName), + factory.createReturnStatement( + factory.createObjectLiteralExpression( methods .filter((method) => method.type === 'function') - .map( - (method) => - ts.createPropertyAssignment( - method.name, - ts.createArrowFunction( - undefined, - undefined, - Array.from(collapseInputs(method.signatures), ([key, value]) => - createParameter(key, combineTypes(value)), - ), - undefined, - undefined, - output(method.signatures, client, provider), - ), - ), - true, - ), + .map((method) => factory.createPropertyAssignment(method.name, functionMaker(method)), true), true, ), ), @@ -75,4 +64,4 @@ export function generateEncodeObject(methods: ContractMethodsList, provider: Pro ), true, ); -}; +} diff --git a/js/src/solts/lib/events.ts b/js/src/solts/lib/events.ts new file mode 100644 index 000000000..ba3249ec3 --- /dev/null +++ b/js/src/solts/lib/events.ts @@ -0,0 +1,27 @@ +import * as ts from 'typescript'; +import { factory } from 'typescript'; +import { getRealType, InputOutput, sha3, Signature } from './solidity'; +import { createCall, createParameter, prop } from './syntax'; + +const getDataName = factory.createIdentifier('getData_asU8'); +const getTopicsName = factory.createIdentifier('getTopicsList_asU8'); + +export function generateEventArgs(name: string, signature: Signature): ts.ParameterDeclaration[] { + return signature.inputs.map(({ name, type }) => createParameter(name, getRealType(type))); +} + +export function eventSignature(name: string, inputs: InputOutput[]): string { + return `${name}(${inputs.map(({ type }) => type).join(',')})`; +} + +export function eventSigHash(name: string, inputs: InputOutput[]): string { + return sha3(eventSignature(name, inputs)); +} + +export function callGetDataFromLog(log: ts.Expression): ts.CallExpression { + return createCall(prop(log, getDataName, true)); +} + +export function callGetTopicsFromLog(log: ts.Expression): ts.CallExpression { + return createCall(prop(log, getTopicsName, true)); +} diff --git a/js/src/solts/lib/linker.ts b/js/src/solts/lib/linker.ts new file mode 100644 index 000000000..2c09c8669 --- /dev/null +++ b/js/src/solts/lib/linker.ts @@ -0,0 +1,97 @@ +import ts, { factory } from 'typescript'; +import { createCall, createParameter, declareConstant, StringType } from './syntax'; + +export const linkerName = factory.createIdentifier('linker'); + +export function createLinkerFunction(): ts.FunctionDeclaration { + const bytecode = factory.createIdentifier('bytecode'); + const name = factory.createIdentifier('name'); + const address = factory.createIdentifier('address'); + + const truncated = factory.createIdentifier('truncated'); + const label = factory.createIdentifier('label'); + + return factory.createFunctionDeclaration( + undefined, + undefined, + undefined, + linkerName, + undefined, + [createParameter(bytecode, StringType), createParameter(name, StringType), createParameter(address, StringType)], + StringType, + factory.createBlock( + [ + factory.createExpressionStatement( + factory.createAssignment( + address, + adds( + address, + arrayJoin( + factory.createAdd( + factory.createSubtract( + factory.createNumericLiteral('40'), + factory.createPropertyAccessExpression(address, 'length'), + ), + factory.createNumericLiteral('1'), + ), + '0', + ), + ), + ), + ), + declareConstant( + truncated, + createCall(factory.createPropertyAccessExpression(name, 'slice'), [ + factory.createNumericLiteral('0'), + factory.createNumericLiteral('36'), + ]), + ), + declareConstant( + label, + adds( + factory.createAdd(factory.createStringLiteral('__'), truncated), + arrayJoin( + factory.createSubtract( + factory.createNumericLiteral('37'), + factory.createPropertyAccessExpression(truncated, 'length'), + ), + '_', + ), + factory.createStringLiteral('__'), + ), + ), + factory.createWhileStatement( + factory.createBinaryExpression( + createCall(factory.createPropertyAccessExpression(bytecode, 'indexOf'), [label]), + ts.SyntaxKind.GreaterThanEqualsToken, + factory.createNumericLiteral('0'), + ), + factory.createExpressionStatement( + factory.createAssignment( + bytecode, + createCall(factory.createPropertyAccessExpression(bytecode, 'replace'), [label, address]), + ), + ), + ), + factory.createReturnStatement(bytecode), + ], + true, + ), + ); +} + +function adds(...exp: ts.Expression[]) { + return exp.reduce((all, next) => { + return factory.createAdd(all, next); + }); +} + +function arrayJoin(length: ts.Expression, literal: string) { + return createCall( + factory.createPropertyAccessExpression( + createCall(factory.createIdentifier('Array'), [length]), + factory.createIdentifier('join'), + ), + [factory.createStringLiteral(literal)], + ); +} diff --git a/js/src/solts/lib/provider.ts b/js/src/solts/lib/provider.ts index c84247fbf..e9b21a914 100644 --- a/js/src/solts/lib/provider.ts +++ b/js/src/solts/lib/provider.ts @@ -1,69 +1,74 @@ -import ts from 'typescript'; +import ts, { factory } from 'typescript'; import { - AnyType, - AsArray, - AsRefNode, + AddressType, + asRefNode, + BlockRangeType, + CallTxType, + ContractCodecType, createCall, - CreateCallbackExpression, + createCallbackExpression, createParameter, + createPromiseOf, + declareConstant, + EndOfStreamType, ErrorType, + EventStream, + ExportToken, + LogEventType, + MaybeUint8ArrayType, Method, - ReadableType, StringType, Uint8ArrayType, - VoidType, } from './syntax'; -export const ErrParameter = createParameter(ts.createIdentifier('err'), ErrorType); -export const ExecParameter = createParameter(ts.createIdentifier('exec'), Uint8ArrayType); -export const AddrParameter = createParameter(ts.createIdentifier('addr'), Uint8ArrayType); -export const EventParameter = createParameter(ts.createIdentifier('event'), AnyType); +export const errName = factory.createIdentifier('err'); +export const contractCodecName = factory.createIdentifier('codec'); +export const logName = factory.createIdentifier('log'); -const type = ts.createIdentifier('Tx'); -const typeArgument = ts.createTypeReferenceNode(type, undefined); +export const EventErrParameter = createParameter( + errName, + factory.createUnionTypeNode([ErrorType, EndOfStreamType]), + undefined, + true, +); +export const LogEventParameter = createParameter(logName, LogEventType, undefined, true); class Deploy extends Method { - params = [ - createParameter('msg', typeArgument), - createParameter('callback', CreateCallbackExpression([ErrParameter, AddrParameter])), - ]; - ret = VoidType; + params = [createParameter('msg', CallTxType)]; + ret = createPromiseOf(AddressType); constructor() { super('deploy'); } - call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + + call(exp: ts.Expression, tx: ts.Identifier): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(exp, this.id), [tx]); } } class Call extends Method { - params = [ - createParameter('msg', typeArgument), - createParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), - ]; - ret = VoidType; + params = [createParameter('msg', CallTxType)]; + ret = createPromiseOf(MaybeUint8ArrayType); constructor() { super('call'); } - call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + + call(exp: ts.Expression, tx: ts.Identifier) { + return createCall(factory.createPropertyAccessExpression(exp, this.id), [tx]); } } class CallSim extends Method { - params = [ - createParameter('msg', typeArgument), - createParameter('callback', CreateCallbackExpression([ErrParameter, ExecParameter])), - ]; - ret = VoidType; + params = [createParameter('msg', CallTxType)]; + ret = createPromiseOf(MaybeUint8ArrayType); constructor() { super('callSim'); } - call(exp: ts.Expression, tx: ts.Identifier, callback: ts.ArrowFunction) { - return createCall(ts.createPropertyAccess(exp, this.id), [tx, callback]); + + call(exp: ts.Expression, tx: ts.Identifier) { + return createCall(factory.createPropertyAccessExpression(exp, this.id), [tx]); } } @@ -71,62 +76,53 @@ class Listen extends Method { params = [ createParameter('signature', StringType), createParameter('address', StringType), - createParameter('callback', CreateCallbackExpression([ErrParameter, EventParameter])), + createParameter('callback', createCallbackExpression([EventErrParameter, LogEventParameter])), + createParameter('range', BlockRangeType, undefined, true), ]; - ret = AsRefNode(ReadableType); + ret = asRefNode(EventStream); constructor() { super('listen'); } - call(exp: ts.Expression, sig: ts.StringLiteral, addr: ts.Expression, callback: ts.Identifier) { - return createCall(ts.createPropertyAccess(exp, this.id), [sig, addr, callback]); + + call(exp: ts.Expression, sig: ts.StringLiteral, addr: ts.Expression, callback: ts.Expression, range: ts.Expression) { + return createCall(factory.createPropertyAccessExpression(exp, this.id), [sig, addr, callback, range]); } } class Payload extends Method { - params = [createParameter('data', StringType), createParameter('address', StringType, undefined, true)]; - ret = typeArgument; + params = [ + createParameter('data', factory.createUnionTypeNode([StringType, Uint8ArrayType])), + createParameter('address', StringType, undefined, true), + ]; + ret = CallTxType; constructor() { super('payload'); } - call(exp: ts.Expression, data: ts.Identifier, addr?: ts.Expression) { + + call(provider: ts.Expression, data: ts.Expression, addr?: ts.Expression) { return addr - ? createCall(ts.createPropertyAccess(exp, this.id), [data, addr]) - : createCall(ts.createPropertyAccess(exp, this.id), [data]); + ? createCall(factory.createPropertyAccessExpression(provider, this.id), [data, addr]) + : createCall(factory.createPropertyAccessExpression(provider, this.id), [data]); } } -class Encode extends Method { - params = [ - createParameter('name', StringType), - createParameter('inputs', AsArray(StringType)), - createParameter('args', AsArray(AnyType), undefined, false, true), - ]; - ret = StringType; +class ContractCodec extends Method { + params = [createParameter('contractABI', StringType)]; + ret = ContractCodecType; constructor() { - super('encode'); + super('contractCodec'); } - call(exp: ts.Expression, name: ts.StringLiteral, inputs: ts.ArrayLiteralExpression, ...args: ts.Identifier[]) { - return createCall(ts.createPropertyAccess(exp, this.id), [name, inputs, ...args]); - } -} -class Decode extends Method { - params = [createParameter('data', Uint8ArrayType), createParameter('outputs', AsArray(StringType))]; - ret = AnyType; - - constructor() { - super('decode'); - } - call(exp: ts.Expression, data: ts.Identifier, outputs: ts.ArrayLiteralExpression) { - return createCall(ts.createPropertyAccess(exp, this.id), [data, outputs]); + call(provider: ts.Expression, contractABI: ts.Expression) { + return createCall(factory.createPropertyAccessExpression(provider, this.id), [contractABI]); } } export class Provider { - private name = ts.createIdentifier('Provider'); + private name = factory.createIdentifier('Provider'); methods = { deploy: new Deploy(), @@ -134,35 +130,63 @@ export class Provider { callSim: new CallSim(), listen: new Listen(), payload: new Payload(), - encode: new Encode(), - decode: new Decode(), + contractCodec: new ContractCodec(), }; - createInterface() { - return ts.createInterfaceDeclaration(undefined, undefined, this.name, [this.getTypeArgumentDecl()], undefined, [ - this.methods.deploy.signature(), - this.methods.call.signature(), - this.methods.callSim.signature(), - this.methods.listen.signature(), - this.methods.payload.signature(), - this.methods.encode.signature(), - this.methods.decode.signature(), - ]); + createInterface(extern?: boolean): ts.InterfaceDeclaration { + return factory.createInterfaceDeclaration( + undefined, + extern ? [ExportToken] : undefined, + this.name, + undefined, + undefined, + [ + this.methods.deploy.signature(), + this.methods.call.signature(), + this.methods.callSim.signature(), + this.methods.listen.signature(), + this.methods.payload.signature(), + this.methods.contractCodec.signature(), + ], + ); } - getType() { - return type; + declareContractCodec(client: ts.Identifier, abiName: ts.Identifier): ts.VariableStatement { + return declareConstant(contractCodecName, this.methods.contractCodec.call(client, abiName)); } - getTypeNode() { - return ts.createTypeReferenceNode(this.name, [typeArgument]); + type(): ts.TypeReferenceNode { + return factory.createTypeReferenceNode(this.name); } +} - getTypeArgument() { - return typeArgument; - } +const encodeDeploy = factory.createIdentifier('encodeDeploy'); +const encodeFunctionData = factory.createIdentifier('encodeFunctionData'); +const decodeFunctionResult = factory.createIdentifier('decodeFunctionResult '); +const decodeEventLog = factory.createIdentifier('decodeEventLog '); - getTypeArgumentDecl() { - return ts.createTypeParameterDeclaration(type); - } +export function callEncodeDeploy(args: ts.Expression[]): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(contractCodecName, encodeDeploy), [...args]); +} + +export function callEncodeFunctionData(signature: string, args: ts.Expression[]): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(contractCodecName, encodeFunctionData), [ + factory.createStringLiteral(signature), + ...args, + ]); +} + +export function callDecodeFunctionResult(signature: string, data: ts.Expression): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(contractCodecName, decodeFunctionResult), [ + factory.createStringLiteral(signature), + data, + ]); +} + +export function callDecodeEventLog(signature: string, data: ts.Expression, topics: ts.Expression): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(contractCodecName, decodeEventLog), [ + factory.createStringLiteral(signature), + data, + topics, + ]); } diff --git a/js/src/solts/lib/replacer.ts b/js/src/solts/lib/replacer.ts deleted file mode 100644 index b230b3202..000000000 --- a/js/src/solts/lib/replacer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import ts from 'typescript'; -import { createCall, createParameter, declareConstant, StringType } from './syntax'; - -export const ReplacerName = ts.createIdentifier('Replace'); - -export const Replacer = () => { - const bytecode = ts.createIdentifier('bytecode'); - const name = ts.createIdentifier('name'); - const address = ts.createIdentifier('address'); - - const truncated = ts.createIdentifier('truncated'); - const label = ts.createIdentifier('label'); - - return ts.createFunctionDeclaration( - undefined, - undefined, - undefined, - ReplacerName, - undefined, - [createParameter(bytecode, StringType), createParameter(name, StringType), createParameter(address, StringType)], - StringType, - ts.createBlock( - [ - ts.createExpressionStatement( - ts.createAssignment( - address, - adds( - address, - arrayJoin( - ts.createAdd( - ts.createSubtract(ts.createNumericLiteral('40'), ts.createPropertyAccess(address, 'length')), - ts.createNumericLiteral('1'), - ), - '0', - ), - ), - ), - ), - declareConstant( - truncated, - createCall(ts.createPropertyAccess(name, 'slice'), [ - ts.createNumericLiteral('0'), - ts.createNumericLiteral('36'), - ]), - ), - declareConstant( - label, - adds( - ts.createAdd(ts.createStringLiteral('__'), truncated), - arrayJoin( - ts.createSubtract(ts.createNumericLiteral('37'), ts.createPropertyAccess(truncated, 'length')), - '_', - ), - ts.createStringLiteral('__'), - ), - ), - ts.createWhile( - ts.createBinary( - createCall(ts.createPropertyAccess(bytecode, 'indexOf'), [label]), - ts.SyntaxKind.GreaterThanEqualsToken, - ts.createNumericLiteral('0'), - ), - ts.createExpressionStatement( - ts.createAssignment(bytecode, createCall(ts.createPropertyAccess(bytecode, 'replace'), [label, address])), - ), - ), - ts.createReturn(bytecode), - ], - true, - ), - ); -}; - -function adds(...exp: ts.Expression[]) { - return exp.reduce((all, next) => { - return ts.createAdd(all, next); - }); -} - -function arrayJoin(length: ts.Expression, literal: string) { - return createCall( - ts.createPropertyAccess(createCall(ts.createIdentifier('Array'), [length]), ts.createIdentifier('join')), - [ts.createStringLiteral(literal)], - ); -} diff --git a/js/src/solts/lib/solidity.ts b/js/src/solts/lib/solidity.ts index d7d4fc70f..c0bac89c7 100644 --- a/js/src/solts/lib/solidity.ts +++ b/js/src/solts/lib/solidity.ts @@ -1,8 +1,7 @@ import { Keccak } from 'sha3'; -import ts, { TypeNode } from 'typescript'; +import ts, { factory, TypeNode } from 'typescript'; import { ABI } from './abi'; -import { AsArray, AsRefNode, AsTuple, BooleanType, BufferType, NumberType, StringType, VoidType } from './syntax'; -import FunctionOrEvent = ABI.FunctionOrEvent; +import { asArray, asRefNode, asTuple, BooleanType, BufferType, NumberType, StringType, VoidType } from './syntax'; export function sha3(str: string): string { const hash = new Keccak(256).update(str); @@ -13,7 +12,7 @@ export function nameFromABI(abi: ABI.Func | ABI.Event): string { if (abi.name.indexOf('(') !== -1) { return abi.name; } - const typeName = (abi.inputs as (ABI.EventInput | ABI.FunctionIO)[]).map((i) => i.type).join(); + const typeName = (abi.inputs as (ABI.EventInput | ABI.FunctionIO)[]).map((i) => i.type).join(','); return abi.name + '(' + typeName + ')'; } @@ -23,54 +22,35 @@ export function getSize(type: string): number { export function getRealType(type: string): ts.TypeNode { if (/\[\]/i.test(type)) { - return AsArray(getRealType(type.replace(/\[\]/, ''))); + return asArray(getRealType(type.replace(/\[\]/, ''))); } if (/\[.*\]/i.test(type)) { - return AsTuple(getRealType(type.replace(/\[.*\]/, '')), getSize(type)); + return asTuple(getRealType(type.replace(/\[.*\]/, '')), getSize(type)); } else if (/int/i.test(type)) { return NumberType; } else if (/bool/i.test(type)) { return BooleanType; } else if (/bytes/i.test(type)) { - return AsRefNode(BufferType); + return asRefNode(BufferType); } else { return StringType; } // address, bytes } -export function outputToType(sig: Signature): TypeNode { - if (!sig.outputs?.length) { +export function inputOuputsToType(ios?: InputOutput[]): TypeNode { + if (!ios?.length) { return VoidType; } - const named = sig.outputs.filter((out) => out !== undefined && out.name !== ''); - if (sig.outputs.length === named.length) { - return ts.createTypeLiteralNode( - sig.outputs.map((out) => - ts.createPropertySignature(undefined, out.name, undefined, getRealType(out.type), undefined), - ), + const named = ios.filter((out) => out !== undefined && out.name !== ''); + if (ios.length === named.length) { + return factory.createTypeLiteralNode( + ios.map(({ name, type }) => factory.createPropertySignature(undefined, name, undefined, getRealType(type))), ); } else { - return ts.createTupleTypeNode(sig.outputs.map((out) => getRealType(out.type))); + return factory.createTupleTypeNode(ios.map(({ type }) => getRealType(type))); } } -export function collapseInputs(signatures: Array): Map { - return signatures.reduce((args, next) => { - next.inputs.map((item) => { - if (!item) { - return; - } - const prev = args.get(item.name); - args.set(item.name, prev ? [...prev, item.type] : [item.type]); - }); - return args; - }, new Map>()); -} - -export function combineTypes(types: Array): TypeNode { - return types.length === 1 ? getRealType(types[0]) : ts.createUnionTypeNode(types.map((type) => getRealType(type))); -} - export type InputOutput = { name: string; type: string; @@ -104,16 +84,14 @@ export function getContractMethods(abi: ABI.FunctionOrEvent[]): Method[] { }; const signature = { - hash: getSigHash(abi), + hash: getFunctionSelector(abi), constant: abi.constant || false, inputs: getInputs(abi), outputs: abi.outputs?.map((abi) => { return { name: abi.name, type: abi.type }; }), }; - method.signatures.push(signature); - signatures.set(method.name, method); } else if (abi.type === 'event') { signatures.set(abi.name, { @@ -121,7 +99,7 @@ export function getContractMethods(abi: ABI.FunctionOrEvent[]): Method[] { type: 'event', signatures: [ { - hash: getSigHash(abi), + hash: getEventSignature(abi), constant: false, inputs: getInputs(abi), }, @@ -139,11 +117,15 @@ export function tokenizeString(input: string): string { return input.replace(/\W+/g, '_'); } -function getSigHash(abi: FunctionOrEvent): string { +function getFunctionSelector(abi: ABI.Func): string { return sha3(nameFromABI(abi)).slice(0, 8); } -function getInputs(abi: FunctionOrEvent): InputOutput[] { +function getEventSignature(abi: ABI.Event): string { + return sha3(nameFromABI(abi)); +} + +function getInputs(abi: ABI.FunctionOrEvent): InputOutput[] { return ( abi.inputs ?.filter((abi) => abi.name !== '') diff --git a/js/src/solts/lib/syntax.test.ts b/js/src/solts/lib/syntax.test.ts index 11fc38de6..6eaf13e42 100644 --- a/js/src/solts/lib/syntax.test.ts +++ b/js/src/solts/lib/syntax.test.ts @@ -1,14 +1,23 @@ import assert from 'assert'; -import ts from 'typescript'; +import { factory, Node } from 'typescript'; import { printNodes } from '../api'; -import { CreateCallbackExpression, createParameter } from './syntax'; +import { createCallbackExpression, createParameter, createPromiseOf, PromiseType, StringType } from './syntax'; describe('syntax helpers', function () { it('should create callback expression', async function () { const ErrAndResult = [ - createParameter(ts.createIdentifier('err'), undefined), - createParameter(ts.createIdentifier('result'), undefined), + createParameter(factory.createIdentifier('err'), undefined), + createParameter(factory.createIdentifier('result'), undefined), ]; - assert.equal(printNodes(CreateCallbackExpression(ErrAndResult)), '(err, result) => void'); + assertGenerates(createCallbackExpression(ErrAndResult), '(err, result) => void'); + }); + + it('should create promise type', () => { + assertGenerates(factory.createExpressionWithTypeArguments(PromiseType, [StringType]), 'Promise'); + assertGenerates(createPromiseOf(StringType), 'Promise'); }); }); + +function assertGenerates(node: Node, expected: string) { + assert.strictEqual(printNodes(node), expected); +} diff --git a/js/src/solts/lib/syntax.ts b/js/src/solts/lib/syntax.ts index f6550cb6f..53dd31d0a 100644 --- a/js/src/solts/lib/syntax.ts +++ b/js/src/solts/lib/syntax.ts @@ -1,81 +1,151 @@ -import ts, { MethodDeclaration, VariableStatement } from 'typescript'; - -export const Uint8ArrayType = ts.createTypeReferenceNode('Uint8Array', undefined); -export const ErrorType = ts.createTypeReferenceNode('Error', undefined); -export const VoidType = ts.createTypeReferenceNode('void', undefined); -export const StringType = ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); -export const NumberType = ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); -export const BooleanType = ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); -export const AnyType = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); -export const PromiseType = ts.createIdentifier('Promise'); -export const ReadableType = ts.createIdentifier('Readable'); -export const BufferType = ts.createIdentifier('Buffer'); -export const TupleType = (elements: ts.TypeNode[]) => ts.createTupleTypeNode(elements); - -export const PrivateToken = ts.createToken(ts.SyntaxKind.PrivateKeyword); -export const PublicToken = ts.createToken(ts.SyntaxKind.PublicKeyword); -export const ExportToken = ts.createToken(ts.SyntaxKind.ExportKeyword); -export const EllipsisToken = ts.createToken(ts.SyntaxKind.DotDotDotToken); -export const QuestionToken = ts.createToken(ts.SyntaxKind.QuestionToken); - -export const createCall = (fn: ts.Expression, args?: ts.Expression[]) => ts.createCall(fn, undefined, args); -export const AccessThis = (name: ts.Identifier) => ts.createPropertyAccess(ts.createThis(), name); -export const BufferFrom = (...args: ts.Expression[]) => - createCall(ts.createPropertyAccess(BufferType, ts.createIdentifier('from')), args); -export const AsArray = (type: ts.TypeNode) => ts.createArrayTypeNode(type); -export const AsTuple = (type: ts.TypeNode, size: number) => ts.createTupleTypeNode(Array(size).fill(type)); -export const AsRefNode = (id: ts.Identifier) => ts.createTypeReferenceNode(id, undefined); +import ts, { factory, MethodDeclaration } from 'typescript'; + +export const ErrorType = factory.createTypeReferenceNode('Error'); +export const VoidType = factory.createTypeReferenceNode('void', undefined); +export const StringType = factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); +export const NumberType = factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); +export const BooleanType = factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); +export const AnyType = factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); +export const UnknownType = factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword); +export const UndefinedType = factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword); +export const Uint8ArrayType = factory.createTypeReferenceNode('Uint8Array'); +export const MaybeUint8ArrayType = factory.createUnionTypeNode([Uint8ArrayType, UndefinedType]); + +export const PromiseType = factory.createIdentifier('Promise'); +export const ReadableType = factory.createIdentifier('Readable'); +export const BufferType = factory.createIdentifier('Buffer'); +export const CallTx = factory.createIdentifier('CallTx'); +export const BlockRange = factory.createIdentifier('BlockRange'); +export const Address = factory.createIdentifier('Address'); +export const EventStream = factory.createIdentifier('EventStream'); +export const LogEvent = factory.createIdentifier('LogEvent'); +export const ContractCodec = factory.createIdentifier('ContractCodec'); +export const Result = factory.createIdentifier('Result'); +export const EndOfStream = factory.createIdentifier('EndOfStream'); + +export const CallTxType = factory.createTypeReferenceNode(CallTx); +export const BlockRangeType = factory.createTypeReferenceNode(BlockRange); +export const AddressType = factory.createTypeReferenceNode(Address); +export const LogEventType = factory.createTypeReferenceNode(LogEvent); +export const ContractCodecType = factory.createTypeReferenceNode(ContractCodec); +export const EndOfStreamType = factory.createTypeReferenceNode(EndOfStream); + +export const PrivateToken = factory.createToken(ts.SyntaxKind.PrivateKeyword); +export const PublicToken = factory.createToken(ts.SyntaxKind.PublicKeyword); +export const ExportToken = factory.createToken(ts.SyntaxKind.ExportKeyword); +export const EllipsisToken = factory.createToken(ts.SyntaxKind.DotDotDotToken); +export const QuestionToken = factory.createToken(ts.SyntaxKind.QuestionToken); +export const QuestionDotToken = factory.createToken(ts.SyntaxKind.QuestionDotToken); +export const ColonToken = factory.createToken(ts.SyntaxKind.ColonToken); +export const AsyncToken = factory.createToken(ts.SyntaxKind.AsyncKeyword); +export const Undefined = factory.createIdentifier('undefined'); + +export function createCall(fn: ts.Expression, args?: ts.Expression[]): ts.CallExpression { + return factory.createCallExpression(fn, undefined, args); +} + +export function accessThis(name: ts.Identifier): ts.PropertyAccessExpression { + return factory.createPropertyAccessExpression(factory.createThis(), name); +} + +export function bufferFrom(...args: ts.Expression[]): ts.CallExpression { + return createCall(factory.createPropertyAccessExpression(BufferType, factory.createIdentifier('from')), args); +} + +export function asArray(type: ts.TypeNode): ts.ArrayTypeNode { + return factory.createArrayTypeNode(type); +} + +export function asTuple(type: ts.TypeNode, size: number): ts.TupleTypeNode { + return factory.createTupleTypeNode(Array(size).fill(type)); +} + +export function asRefNode(id: ts.Identifier): ts.TypeReferenceNode { + return factory.createTypeReferenceNode(id, undefined); +} + +export function asConst(exp: ts.Expression): ts.AsExpression { + return factory.createAsExpression(exp, factory.createTypeReferenceNode('const')); +} + +export function prop( + obj: ts.Expression, + name: string | ts.Identifier, + optionChain?: boolean, +): ts.PropertyAccessExpression { + return factory.createPropertyAccessChain(obj, optionChain ? QuestionDotToken : undefined, name); +} export function createParameter( name: string | ts.Identifier, - typeNode: ts.TypeNode | undefined, + typeNode?: ts.TypeNode, initializer?: ts.Expression, isOptional?: boolean, isVariadic?: boolean, ): ts.ParameterDeclaration { - return ts.createParameter( + return factory.createParameterDeclaration( undefined, undefined, isVariadic ? EllipsisToken : undefined, - typeof name === 'string' ? ts.createIdentifier(name) : name, + typeof name === 'string' ? factory.createIdentifier(name) : name, isOptional ? QuestionToken : undefined, typeNode, initializer, ); } -export function declareConstant(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean): VariableStatement { - return ts.createVariableStatement( +export function declareConstant( + name: ts.Identifier | string, + initializer?: ts.Expression, + extern?: boolean, +): ts.VariableStatement { + return factory.createVariableStatement( extern ? [ExportToken] : [], - ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Const), + factory.createVariableDeclarationList( + [factory.createVariableDeclaration(name, undefined, undefined, initializer)], + ts.NodeFlags.Const, + ), ); } -export function declareLet(name: ts.Identifier, initializer?: ts.Expression, extern?: boolean) { - return ts.createVariableStatement( +export function declareLet(name: ts.Identifier, initializer: ts.Expression, extern?: boolean): ts.VariableStatement { + return factory.createVariableStatement( extern ? [ExportToken] : [], - ts.createVariableDeclarationList([ts.createVariableDeclaration(name, undefined, initializer)], ts.NodeFlags.Let), + factory.createVariableDeclarationList( + [factory.createVariableDeclaration(name, undefined, undefined, initializer)], + ts.NodeFlags.Let, + ), ); } -const resolveFn = ts.createIdentifier('resolve'); -const rejectFn = ts.createIdentifier('reject'); +const resolveFn = factory.createIdentifier('resolve'); +const rejectFn = factory.createIdentifier('reject'); + +export function createPromiseOf(...nodes: ts.TypeNode[]): ts.ExpressionWithTypeArguments { + return factory.createExpressionWithTypeArguments(PromiseType, nodes); +} + +export function createAssignmentStatement(left: ts.Expression, right: ts.Expression): ts.ExpressionStatement { + return factory.createExpressionStatement(factory.createAssignment(left, right)); +} -export function createPromiseBody(error: ts.Identifier, statements: ts.Expression[]) { - return ts.createExpressionStatement( - ts.createConditional( +export function createPromiseBody(error: ts.Identifier, statements: ts.Expression[]): ts.ExpressionStatement { + return factory.createExpressionStatement( + factory.createConditionalExpression( error, + QuestionToken, createCall(rejectFn, [error]), + ColonToken, createCall(resolveFn, statements ? statements : undefined), ), ); } export function rejectOrResolve(error: ts.Identifier, statements: ts.Statement[], success: ts.Expression[]) { - return ts.createIf( + return factory.createIfStatement( error, - ts.createExpressionStatement(createCall(rejectFn, [error])), - ts.createBlock([...statements, ts.createExpressionStatement(createCall(resolveFn, success))]), + factory.createExpressionStatement(createCall(rejectFn, [error])), + factory.createBlock([...statements, factory.createExpressionStatement(createCall(resolveFn, success))]), ); } @@ -84,7 +154,7 @@ export function CreateNewPromise( returnType?: ts.TypeNode, multiLine?: boolean, ): ts.NewExpression { - return ts.createNew(PromiseType, undefined, [ + return factory.createNewExpression(PromiseType, undefined, [ CreateCallbackDeclaration(resolveFn, rejectFn, body, returnType, multiLine || false), ]); } @@ -96,31 +166,49 @@ export function CreateCallbackDeclaration( returnType?: ts.TypeNode, multiLine?: boolean, ) { - return ts.createArrowFunction( + return factory.createArrowFunction( undefined, undefined, [createParameter(first, undefined), createParameter(second, undefined)], returnType, undefined, - ts.createBlock(body, multiLine), + factory.createBlock(body, multiLine), ); } -export function CreateCallbackExpression(params: ts.ParameterDeclaration[]) { - return ts.createFunctionTypeNode(undefined, params, VoidType); +export function createCallbackExpression(params: ts.ParameterDeclaration[]): ts.FunctionTypeNode { + return factory.createFunctionTypeNode(undefined, params, VoidType); } -function ImportFrom(thing: ts.Identifier, pkg: string) { - return ts.createImportDeclaration( +function importFrom(pkg: string, ...things: ts.Identifier[]) { + return factory.createImportDeclaration( undefined, undefined, - ts.createImportClause(undefined, ts.createNamedImports([ts.createImportSpecifier(undefined, thing)])), - ts.createLiteral(pkg), + factory.createImportClause( + false, + undefined, + factory.createNamedImports(things.map((t) => factory.createImportSpecifier(undefined, t))), + ), + factory.createStringLiteral(pkg), ); } -export function ImportReadable() { - return ImportFrom(ReadableType, 'stream'); +export function importReadable(): ts.ImportDeclaration { + return importFrom('stream', ReadableType); +} + +export function importBurrow(burrowImportPath: string): ts.ImportDeclaration { + return importFrom( + burrowImportPath, + Address, + BlockRange, + CallTx, + ContractCodec, + EndOfStream, + EventStream, + LogEvent, + Result, + ); } export class Method { @@ -130,30 +218,30 @@ export class Method { ret?: ts.TypeNode; constructor(name: string) { - this.id = ts.createIdentifier(name); + this.id = factory.createIdentifier(name); } - parameter(name: string | ts.Identifier, type: ts.TypeNode, optional?: boolean, isVariadic?: boolean) { + parameter(name: string | ts.Identifier, type: ts.TypeNode, optional?: boolean, isVariadic?: boolean): Method { this.params.push(createParameter(name, type, undefined, optional, isVariadic)); return this; } - parameters(args: ts.ParameterDeclaration[]) { + parameters(args: ts.ParameterDeclaration[]): Method { this.params.push(...args); return this; } - returns(type: ts.TypeNode) { + returns(type: ts.TypeNode): Method { this.ret = type; return this; } - signature() { - return ts.createMethodSignature(undefined, this.params, this.ret, this.id, undefined); + signature(): ts.MethodSignature { + return factory.createMethodSignature(undefined, this.id, undefined, undefined, this.params, this.ret); } declaration(statements: ts.Statement[], multiLine?: boolean): MethodDeclaration { - return ts.createMethod( + return factory.createMethodDeclaration( undefined, undefined, undefined, @@ -162,7 +250,7 @@ export class Method { undefined, this.params, this.ret, - ts.createBlock(statements, multiLine), + factory.createBlock(statements, multiLine), ); } } diff --git a/js/src/solts/provider.gd.ts b/js/src/solts/provider.gd.ts new file mode 100644 index 000000000..c001e38a5 --- /dev/null +++ b/js/src/solts/provider.gd.ts @@ -0,0 +1,15 @@ +//Generated by solts - run yarn generate:provider to regenerate if solts Provider code is changed +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent } from '../index'; +export interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signature: string, + address: string, + callback: (err?: Error | EndOfStream, log?: LogEvent) => void, + range?: BlockRange, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} diff --git a/js/src/solts/provider.gd.ts.gr.ts b/js/src/solts/provider.gd.ts.gr.ts new file mode 100644 index 000000000..3479a7d8c --- /dev/null +++ b/js/src/solts/provider.gd.ts.gr.ts @@ -0,0 +1,20 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import ts from 'typescript'; +import { printNodes } from './api'; +import { Provider } from './lib/provider'; +import { importBurrow } from './lib/syntax'; + +const provider = new Provider(); +const providerInterface = printNodes( + ts.addSyntheticLeadingComment( + importBurrow('../index'), + ts.SyntaxKind.SingleLineCommentTrivia, + 'Generated by solts - run yarn generate:provider to regenerate if solts Provider code is changed', + ), + provider.createInterface(true), +); + +const providerFile = path.join(__dirname, 'provider.gd.ts'); + +fs.writeFileSync(providerFile, providerInterface); diff --git a/js/src/solts/sol/Addition.abi.ts b/js/src/solts/sol/Addition.abi.ts new file mode 100644 index 000000000..fed529aeb --- /dev/null +++ b/js/src/solts/sol/Addition.abi.ts @@ -0,0 +1,64 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Addition { + export const abi = '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"b","type":"int256"}],"name":"add","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a5f3c23b14602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600081830190509291505056fea265627a7a72315820bf9f1b3176bb0e5383e0dbeabc3258cbe895f39124127f38452c4ce85df9672964736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + add(a: number, b: number): Promise<{ + sum: number; + }> { + const data = encode(this.client).add(a, b); + return call<{ + sum: number; + }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).add(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + add: (a: number, b: number) => { return codec.encodeFunctionData("A5F3C23B", a, b); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + add: (): { + sum: number; + } => { + const [sum] = codec.decodeFunctionResult ("A5F3C23B", data); + return { sum: sum }; + } + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/Contains.abi.ts b/js/src/solts/sol/Contains.abi.ts new file mode 100644 index 000000000..e601ead70 --- /dev/null +++ b/js/src/solts/sol/Contains.abi.ts @@ -0,0 +1,73 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Contains { + export const abi = '[{"constant":true,"inputs":[{"internalType":"address[]","name":"_list","type":"address[]"},{"internalType":"address","name":"_value","type":"address"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256[]","name":"_list","type":"uint256[]"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = '608060405234801561001057600080fd5b50610304806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633da80d661461003b578063b32b8e2c1461012b575b600080fd5b6101116004803603604081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460208302840111640100000000831117156100a257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f820116905080830192505050505050509192919290803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610205565b604051808215151515815260200191505060405180910390f35b6101eb6004803603604081101561014157600080fd5b810190808035906020019064010000000081111561015e57600080fd5b82018360208201111561017057600080fd5b8035906020019184602083028401116401000000008311171561019257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f82011690508083019250505050505050919291929080359060200190929190505050610280565b604051808215151515815260200191505060405180910390f35b600080600090505b8351811015610274578273ffffffffffffffffffffffffffffffffffffffff1684828151811061023957fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff16141561026757600191505061027a565b808060010191505061020d565b50600090505b92915050565b600080600090505b83518110156102c3578284828151811061029e57fe5b602002602001015114156102b65760019150506102c9565b8080600101915050610288565b50600090505b9291505056fea265627a7a7231582032d6f06ddf667cc291458c6ed27a81fe8ef6fa213f3cf25c0fef50500bc05f2264736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + contains(_list: string[], _value: string): Promise<[ + boolean + ]> { + const data = encode(this.client).contains[0](_list, _value); + return call<[ + boolean + ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).contains[0](); + }); + } + contains_1(_list: number[], _value: number): Promise<[ + boolean + ]> { + const data = encode(this.client).contains[1](_list, _value); + return call<[ + boolean + ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).contains[1](); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + contains: [(_list: string[], _value: string) => { return codec.encodeFunctionData("3DA80D66", _list, _value); }, (_list: number[], _value: number) => { return codec.encodeFunctionData("B32B8E2C", _list, _value); }] as const + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + contains: [(): [ + boolean + ] => { return codec.decodeFunctionResult ("3DA80D66", data); }, (): [ + boolean + ] => { return codec.decodeFunctionResult ("B32B8E2C", data); }] as const + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/Creator.abi.ts b/js/src/solts/sol/Creator.abi.ts new file mode 100644 index 000000000..aa527f93d --- /dev/null +++ b/js/src/solts/sol/Creator.abi.ts @@ -0,0 +1,101 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Creator { + export const abi = '[{"constant":false,"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"create","outputs":[{"internalType":"address","name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = '608060405234801561001057600080fd5b50610504806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063b6a46b3b14610030575b600080fd5b6100a76004803603602081101561004657600080fd5b810190808035906020019064010000000081111561006357600080fd5b82018360208201111561007557600080fd5b8035906020019184600183028401116401000000008311171561009757600080fd5b90919293919293905050506100e9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600082826040516100f990610153565b80806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509350505050604051809103906000f08015801561014a573d6000803e3d6000fd5b50905092915050565b61036f806101618339019056fe60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032a265627a7a72315820cf64c1da8de972eb1f9f2b5e6799c7af3385d3ee9b08b51fb8d7b060fa7eb4e864736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + create(_name: string): Promise<{ + proxy: string; + }> { + const data = encode(this.client).create(_name); + return call<{ + proxy: string; + }>(this.client, this.address, data, false, (data: Uint8Array | undefined) => { + return decode(this.client, data).create(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + create: (_name: string) => { return codec.encodeFunctionData("B6A46B3B", _name); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + create: (): { + proxy: string; + } => { + const [proxy] = codec.decodeFunctionResult ("B6A46B3B", data); + return { proxy: proxy }; + } + }; }; +} +export module Proxy { + export const abi = '[{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]'; + export const bytecode = '60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032'; + export function deploy(client: Provider, _name: string): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy(_name)]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + get(): Promise<[ + string + ]> { + const data = encode(this.client).get(); + return call<[ + string + ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).get(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + get: () => { return codec.encodeFunctionData("6D4CE63C"); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + get: (): [ + string + ] => { return codec.decodeFunctionResult ("6D4CE63C", data); } + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/Event.abi.ts b/js/src/solts/sol/Event.abi.ts new file mode 100644 index 000000000..e3668293b --- /dev/null +++ b/js/src/solts/sol/Event.abi.ts @@ -0,0 +1,137 @@ +//Code generated by solts. DO NOT EDIT. +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent } from '../../index'; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signature: string, + address: string, + callback: (err?: Error | EndOfStream, log?: LogEvent) => void, + range?: BlockRange, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (exec: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; +} +export namespace Contract { + export const abi = + '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"eventId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"intervalId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"eventAddress","type":"address"},{"indexed":false,"internalType":"string","name":"namespace","type":"string"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"},{"indexed":false,"internalType":"string","name":"metadata","type":"string"}],"name":"Init","type":"event"},{"constant":false,"inputs":[],"name":"announce","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = + '6080604052348015600f57600080fd5b506102128061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80638f970df014610030575b600080fd5b61003861003a565b005b7f696e74657276616c3200000000000000000000000000000000000000000000007f6576656e743100000000000000000000000000000000000000000000000000007f5f20df97ee573ab8b43581cf3ff905f3507ad2329b7efe6f92e802b4fad031c17359c99d4ebf520619ee7f806f11d90a9cac02ce06336004604051808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001806020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200184815260200180602001848103845260068152602001807f64696e696e670000000000000000000000000000000000000000000000000000815250602001848103835260098152602001807f627265616b666173740000000000000000000000000000000000000000000000815250602001848103825260178152602001807f6261636f6e2c6265616e732c656767732c746f6d61746f000000000000000000815250602001965050505050505060405180910390a356fea265627a7a723158202bd41fa5fa358aebac4419f707c61d5fc012d8362230234c07ba3481be69ae1464736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + Init( + callback: ( + err?: Error | EndOfStream, + eventName?: { + eventId: Buffer; + intervalId: Buffer; + eventAddress: string; + namespace: string; + name: string; + controller: string; + threshold: number; + metadata: string; + }, + ) => void, + range?: BlockRange, + ): EventStream { + return this.client.listen( + '5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', + this.address, + (err?: Error | EndOfStream, log?: LogEvent) => { + if (err) { + return callback(err); + } + return callback(undefined, decode(this.client, log?.getData_asU8(), log?.getTopicsList_asU8()).Init()); + }, + range, + ); + } + announce(): Promise { + const data = encode(this.client).announce(); + return call(this.client, this.address, data, false, (data: Uint8Array | undefined) => { + return decode(this.client, data).announce(); + }); + } + } + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + announce: () => { + return codec.encodeFunctionData('8F970DF0'); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + Init: (): { + eventId: Buffer; + intervalId: Buffer; + eventAddress: string; + namespace: string; + name: string; + controller: string; + threshold: number; + metadata: string; + } => { + const [ + eventId, + intervalId, + eventAddress, + namespace, + name, + controller, + threshold, + metadata, + ] = codec.decodeEventLog('5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', data, topics); + return { + eventId: eventId, + intervalId: intervalId, + eventAddress: eventAddress, + namespace: namespace, + name: name, + controller: controller, + threshold: threshold, + metadata: metadata, + }; + }, + announce: (): void => { + return; + }, + }; + }; +} diff --git a/js/src/solts/sol/Event.sol b/js/src/solts/sol/Event.sol index 764c672fa..9f7063022 100644 --- a/js/src/solts/sol/Event.sol +++ b/js/src/solts/sol/Event.sol @@ -1,11 +1,26 @@ pragma solidity >=0.0.0; contract Contract { - event Event ( - address from + event Init( + bytes32 indexed eventId, + bytes32 indexed intervalId, + address eventAddress, + string namespace, + string name, + address controller, + uint threshold, + string metadata ); function announce() public { - emit Event(msg.sender); + emit Init(bytes32("event1"), + bytes32("interval2"), + 0x59C99d4EbF520619ee7F806f11d90a9cac02CE06, + "dining", + "breakfast", + msg.sender, + 4, + "bacon,beans,eggs,tomato" + ); } -} \ No newline at end of file +} diff --git a/js/src/solts/sol/MultipleReturns.abi.ts b/js/src/solts/sol/MultipleReturns.abi.ts new file mode 100644 index 000000000..bc2dbf87e --- /dev/null +++ b/js/src/solts/sol/MultipleReturns.abi.ts @@ -0,0 +1,67 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Multiple { + export const abi = '[{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = '6080604052348015600f57600080fd5b5060ab8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80636d4ce63c14602d575b600080fd5b60336057565b60405180848152602001838152602001828152602001935050505060405180910390f35b600080600060016002600382925081915080905092509250925090919256fea265627a7a72315820db13aa50f1e1ad94cdc8fb2b158b105d0d0812d53d9851ed8a9bfa5447e3c17864736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + get(): Promise<[ + number, + number, + number + ]> { + const data = encode(this.client).get(); + return call<[ + number, + number, + number + ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).get(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + get: () => { return codec.encodeFunctionData("6D4CE63C"); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + get: (): [ + number, + number, + number + ] => { return codec.decodeFunctionResult ("6D4CE63C", data); } + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/Storage.abi.ts b/js/src/solts/sol/Storage.abi.ts new file mode 100644 index 000000000..51a88241e --- /dev/null +++ b/js/src/solts/sol/Storage.abi.ts @@ -0,0 +1,72 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Storage { + export const abi = '[{"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"ret","type":"int256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = '608060405234801561001057600080fd5b506040516101203803806101208339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505060c68061005a6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80636d4ce63c146037578063e5c19b2d146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea265627a7a72315820f111616fdeed05afb6d4e43891f3c42451310bf48d12f72b2864c23005c11fb664736f6c63430005110032'; + export function deploy(client: Provider, x: number): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy(x)]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + get(): Promise<{ + ret: number; + }> { + const data = encode(this.client).get(); + return call<{ + ret: number; + }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).get(); + }); + } + set(x: number): Promise { + const data = encode(this.client).set(x); + return call(this.client, this.address, data, false, (data: Uint8Array | undefined) => { + return decode(this.client, data).set(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + get: () => { return codec.encodeFunctionData("6D4CE63C"); }, + set: (x: number) => { return codec.encodeFunctionData("E5C19B2D", x); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + get: (): { + ret: number; + } => { + const [ret] = codec.decodeFunctionResult ("6D4CE63C", data); + return { ret: ret }; + }, + set: (): void => { return; } + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/Unnamed.abi.ts b/js/src/solts/sol/Unnamed.abi.ts new file mode 100644 index 000000000..84e8aae16 --- /dev/null +++ b/js/src/solts/sol/Unnamed.abi.ts @@ -0,0 +1,64 @@ +//Code generated by solts. DO NOT EDIT. +import { Readable } from "stream"; +import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { + const payload = client.payload(data, addr); + const txe = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(txe); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join("0"); + const truncated = name.slice(0, 36); + const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; + while (bytecode.indexOf(label) >= 0) + bytecode = bytecode.replace(label, address); + return bytecode; +} +export module Unnamed { + export const abi = '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"name":"set","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806304c402f414602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600082830190509291505056fea265627a7a72315820c692aa5593a39c498f58c4a0221aa0b6ca567436b02c62f90b9214af7b7c390064736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + let linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export class Contract { + private client: Provider; + public address: string; + constructor(client: Provider, address: string) { + this.client = client; + this.address = address; + } + set(a: number): Promise<{ + sum: number; + }> { + const data = encode(this.client).set(a); + return call<{ + sum: number; + }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { + return decode(this.client, data).set(); + }); + } + } + export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { + set: (a: number) => { return codec.encodeFunctionData("04C402F4", a); } + }; }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { + set: (): { + sum: number; + } => { + const [sum] = codec.decodeFunctionResult ("04C402F4", data); + return { sum: sum }; + } + }; }; +} \ No newline at end of file diff --git a/js/src/solts/sol/build.ts b/js/src/solts/sol/build.ts index 264742f5b..5e823488a 100644 --- a/js/src/solts/sol/build.ts +++ b/js/src/solts/sol/build.ts @@ -2,4 +2,4 @@ import * as path from 'path'; import { build } from '../build'; // Build these before any tests tha may rely on the generated output -build(path.join(__dirname, '..', '..', '..', 'src', 'solts', 'sol')); +build(path.join(__dirname, '..', '..', '..', 'src', 'solts', 'sol'), { burrowImportPath: '../../index' }); diff --git a/js/src/test/solts.test.ts b/js/src/test/solts.test.ts new file mode 100644 index 000000000..763d0cbf5 --- /dev/null +++ b/js/src/test/solts.test.ts @@ -0,0 +1,28 @@ +import * as assert from 'assert'; +import { readEvents } from '../events'; +import { Addition } from '../solts/sol/Addition.abi'; +import { Contract } from '../solts/sol/Event.abi'; +import { burrow } from './test'; + +describe('solts', () => { + it('can deploy and call from codegen', async () => { + const address = await Addition.deploy(burrow); + const add = new Addition.Contract(burrow, address); + const { sum } = await add.add(2342, 23432); + assert.strictEqual(sum, 25774); + }); + + it('can receive events', async () => { + const address = await Contract.deploy(burrow); + const eventer = new Contract.Contract(burrow, address); + const height = await burrow.latestHeight(); + await eventer.announce(); + await eventer.announce(); + await eventer.announce(); + const events = await readEvents(eventer.Init.bind(eventer)); + assert.strictEqual(events.length, 3); + const event = events[0]; + assert.strictEqual(event.controller, 'C9F239591C593CB8EE192B0009C6A0F2C9F8D768'); + assert.strictEqual(event.metadata, 'bacon,beans,eggs,tomato'); + }); +}); diff --git a/js/src/test/test.ts b/js/src/test/test.ts index 6f7222a8c..fdf01066b 100644 --- a/js/src/test/test.ts +++ b/js/src/test/test.ts @@ -1,5 +1,5 @@ -import { Burrow } from '../index'; +import { Client } from '../index'; const url = process.env.BURROW_URL || 'localhost:20123'; const addr = process.env.SIGNING_ADDRESS || 'C9F239591C593CB8EE192B0009C6A0F2C9F8D768'; -export const burrow = new Burrow(url, addr); +export const burrow = new Client(url, addr); diff --git a/js/types/solc_v5/index.d.ts b/js/types/solc_v5/index.d.ts index 6e7f30905..06ba4eba9 100644 --- a/js/types/solc_v5/index.d.ts +++ b/js/types/solc_v5/index.d.ts @@ -75,7 +75,7 @@ declare module 'solc_v5' { export type OutputDescription = { contracts: Record>; - errors: Array; + errors?: Array; sourceList: Array; sources: Record; }; diff --git a/tests/chain/burrow.toml b/tests/chain/burrow.toml index 0d3415065..e6cb28ad7 100644 --- a/tests/chain/burrow.toml +++ b/tests/chain/burrow.toml @@ -53,10 +53,10 @@ BurrowDir = ".burrow" ListenPort = "26660" [Logging] - Trace = false + Trace = true NonBlocking = false [Logging.RootSink] -# [Logging.RootSink.Output] -# OutputType = "stderr" -# Format = "json" + [Logging.RootSink.Output] + OutputType = "stderr" + Format = "json" From 8c1154417722d319d3f90d48e1d6ed0f6ef51e85 Mon Sep 17 00:00:00 2001 From: Silas Davis Date: Tue, 18 May 2021 18:20:24 +0200 Subject: [PATCH 4/4] Add support for strongly typed events and better streaming support Signed-off-by: Silas Davis --- CHANGELOG.md | 15 ++ NOTES.md | 12 +- js/package.json | 13 +- js/src/client.ts | 19 +- js/src/contracts/event.ts | 54 ++-- js/src/events.ts | 228 ++++++++++++---- js/src/index.ts | 14 +- js/src/solts/api.ts | 24 +- js/src/solts/interface.gd.ts | 28 ++ ...ider.gd.ts.gr.ts => interface.gd.ts.gr.ts} | 5 +- js/src/solts/lib/caller.ts | 26 +- js/src/solts/lib/contract.ts | 171 +++++++----- js/src/solts/lib/decoder.ts | 4 +- js/src/solts/lib/deployer.ts | 61 ++++- js/src/solts/lib/events.ts | 215 ++++++++++++++- js/src/solts/lib/provider.ts | 36 +-- js/src/solts/lib/syntax.test.ts | 4 +- js/src/solts/lib/syntax.ts | 77 ++++-- js/src/solts/provider.gd.ts | 15 -- js/src/solts/sol/Addition.abi.ts | 143 ++++++---- js/src/solts/sol/Contains.abi.ts | 161 +++++++----- js/src/solts/sol/Creator.abi.ts | 224 +++++++++------- js/src/solts/sol/Event.abi.ts | 137 ---------- js/src/solts/sol/Eventer.abi.ts | 246 ++++++++++++++++++ js/src/solts/sol/{Event.sol => Eventer.sol} | 9 +- js/src/solts/sol/MultipleReturns.abi.ts | 139 +++++----- js/src/solts/sol/Storage.abi.ts | 159 ++++++----- js/src/solts/sol/Unnamed.abi.ts | 142 ++++++---- js/src/test/solts.test.ts | 35 ++- project/history.go | 15 +- 30 files changed, 1641 insertions(+), 790 deletions(-) create mode 100644 js/src/solts/interface.gd.ts rename js/src/solts/{provider.gd.ts.gr.ts => interface.gd.ts.gr.ts} (76%) delete mode 100644 js/src/solts/provider.gd.ts delete mode 100644 js/src/solts/sol/Event.abi.ts create mode 100644 js/src/solts/sol/Eventer.abi.ts rename js/src/solts/sol/{Event.sol => Eventer.sol} (74%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c973999d..83af8e69f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/NOTES.md b/NOTES.md index 1596c5f82..6985f3c7b 100644 --- a/NOTES.md +++ b/NOTES.md @@ -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. diff --git a/js/package.json b/js/package.json index 55b187b1f..134fd6288 100644 --- a/js/package.json +++ b/js/package.json @@ -15,16 +15,17 @@ "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", - "generate:provider": "ts-node src/solts/provider.gd.ts.gr.ts && eslint --fix --quiet src/solts/provider.gd.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_v8@npm:solc": "^0.8.4", - "solc_v5@npm:solc": "^0.5.17", "sha3": "^2.1.4", + "solc_v5": "npm:solc@^0.5.17", + "solc_v8": "npm:solc@^0.8.4", "typescript": "^4.2.4" }, "devDependencies": { @@ -34,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" }, diff --git a/js/src/client.ts b/js/src/client.ts index 796acf172..b02bfd11d 100644 --- a/js/src/client.ts +++ b/js/src/client.ts @@ -1,9 +1,8 @@ import * as grpc from '@grpc/grpc-js'; import { Interface } from 'ethers/lib/utils'; -import { TxExecution } from '../proto/exec_pb'; +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, StatusParam } from '../proto/rpcquery_pb'; import { ITransactClient, TransactClient } from '../proto/rpctransact_grpc_pb'; @@ -14,9 +13,9 @@ 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 { Bounds, EventCallback, EventStream, getBlockRange, queryFor, stream } from './events'; import { Namereg } from './namereg'; -import { Provider } from './solts/provider.gd'; +import { Provider } from './solts/interface.gd'; type TxCallback = (error: grpc.ServiceError | null, txe: TxExecution) => void; @@ -26,7 +25,6 @@ export type Interceptor = (result: TxExecution) => Promise; export class Client implements Provider { interceptor: Interceptor; - readonly events: Events; readonly namereg: Namereg; readonly executionEvents: IExecutionEventsClient; @@ -41,8 +39,6 @@ export class Client implements Provider { this.executionEvents = new ExecutionEventsClient(url, credentials); this.transact = new TransactClient(url, credentials); this.query = new QueryClient(url, credentials); - // 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 @@ -149,12 +145,13 @@ export class Client implements Provider { } listen( - signature: string, + signatures: string[], address: string, - callback: EventCallback, - range: BlockRange = latestStreamingBlockRange, + callback: EventCallback, + start: Bounds = 'latest', + end: Bounds = 'stream', ): EventStream { - return this.events.listen(range, address, signature, callback); + return stream(this.executionEvents, getBlockRange(start, end), queryFor({ address, signatures }), callback); } payload(data: string | Uint8Array, address?: string, contractMeta: ContractMeta[] = []): CallTx { diff --git a/js/src/contracts/event.ts b/js/src/contracts/event.ts index cb5aaeef6..879b12915 100644 --- a/js/src/contracts/event.ts +++ b/js/src/contracts/event.ts @@ -2,9 +2,9 @@ import { EventFragment, FormatTypes, Interface, LogDescription } from 'ethers/li import { Keccak } from 'sha3'; import { Client } from '../client'; import { postDecodeResult, prefixedHexString } from '../convert'; -import { EndOfStream, EventStream, latestStreamingBlockRange } from '../events'; +import { Bounds, EventStream, isEndOfStream } from '../events'; -export type EventCallback = (err?: Error | EndOfStream, result?: LogDescription) => void; +export type EventCallback = (err?: Error, result?: LogDescription) => void; export function listen( iface: Interface, @@ -12,30 +12,38 @@ export function listen( address: string, burrow: Client, callback: EventCallback, - range = latestStreamingBlockRange, + start: Bounds = 'latest', + end: Bounds = 'stream', ): EventStream { const signature = sha3(frag.format(FormatTypes.sighash)); - return burrow.events.listen(range, address, signature, (err, log) => { - if (err) { - return callback(err); - } - if (!log) { - return callback(new Error(`no LogEvent or Error provided`)); - } - try { - const result = iface.parseLog({ - topics: log.getTopicsList_asU8().map((topic) => prefixedHexString(topic)), - data: prefixedHexString(log.getData_asU8()), - }); - return callback(undefined, { - ...result, - args: postDecodeResult(result.args, frag.inputs), - }); - } catch (err) { - return callback(err); - } - }); + return burrow.listen( + [signature], + address, + (err, event) => { + if (err) { + return isEndOfStream(err) ? undefined : callback(err); + } + const log = event?.getLog(); + if (!log) { + return callback(new Error(`no LogEvent or Error provided`)); + } + try { + const result = iface.parseLog({ + topics: log.getTopicsList_asU8().map((topic) => prefixedHexString(topic)), + data: prefixedHexString(log.getData_asU8()), + }); + return callback(undefined, { + ...result, + args: postDecodeResult(result.args, frag.inputs), + }); + } catch (err) { + return callback(err); + } + }, + start, + end, + ); } function sha3(str: string) { diff --git a/js/src/events.ts b/js/src/events.ts index dcd57b169..dbf1580f1 100644 --- a/js/src/events.ts +++ b/js/src/events.ts @@ -1,7 +1,9 @@ import * as grpc from '@grpc/grpc-js'; -import { LogEvent } from '../proto/exec_pb'; +import { Event } from '../proto/exec_pb'; import { IExecutionEventsClient } from '../proto/rpcevents_grpc_pb'; import { BlockRange, BlocksRequest, Bound, EventsResponse } from '../proto/rpcevents_pb'; +import { Address } from './contracts/abi'; +import { Provider } from './solts/interface.gd'; import BoundType = Bound.BoundType; export type EventStream = grpc.ClientReadableStream; @@ -9,7 +11,35 @@ export type EventStream = grpc.ClientReadableStream; // Surprisingly, this seems to be as good as it gets at the time of writing (https://github.com/Microsoft/TypeScript/pull/17819) // that is, defining various types of union here does not help on the consumer side to infer exactly one of err or log // will be defined -export type EventCallback = (err?: Error | EndOfStream, event?: T) => void; +export type EventCallback = (err?: Error, event?: T) => Signal | void; + +export type Bounds = number | 'first' | 'latest' | 'stream'; + +export type FiniteBounds = number | 'first' | 'latest'; + +type EventRegistry = Record; + +// Emitted for consumers when stream ends +const endOfStream: unique symbol = Symbol('EndOfStream'); +// Emitted by consumers to signal the stream should end +export const cancelStream: unique symbol = Symbol('CancelStream'); + +export type Signal = typeof cancelStream; + +// Note: typescript breaks instanceof for Error (https://github.com/microsoft/TypeScript/issues/13965) +class EndOfStreamError extends Error { + public readonly endOfStream = endOfStream; + + constructor() { + super('End of stream, no more data will be sent - use isEndOfStream(err) to check for this signal'); + } +} + +const endOfStreamError = Object.freeze(new EndOfStreamError()); + +export function isEndOfStream(err: Error): boolean { + return (err as EndOfStreamError).endOfStream === endOfStream; +} export function getBlockRange(start: Bounds = 'latest', end: Bounds = 'stream'): BlockRange { const range = new BlockRange(); @@ -18,47 +48,62 @@ export function getBlockRange(start: Bounds = 'latest', end: Bounds = 'stream'): return range; } -export const latestStreamingBlockRange = Object.freeze(getBlockRange('latest', 'stream')); +export function stream( + client: IExecutionEventsClient, + range: BlockRange, + query: string, + callback: EventCallback, +): EventStream { + const arg = new BlocksRequest(); + arg.setBlockrange(range); + arg.setQuery(query); -export const EndOfStream: unique symbol = Symbol('EndOfStream'); -export type EndOfStream = typeof EndOfStream; + const stream = client.events(arg); + stream.on('data', (data: EventsResponse) => { + const cancel = data + .getEventsList() + .map((event) => callback(undefined, event)) + .find((s) => s === cancelStream); + if (cancel) { + stream.cancel(); + } + }); + stream.on('end', () => callback(endOfStreamError)); + stream.on('error', (err: grpc.ServiceError) => err.code === grpc.status.CANCELLED || callback(err)); + return stream; +} -export class Events { - constructor(private burrow: IExecutionEventsClient) {} +export type QueryOptions = { + signatures: string[]; + address?: string; +}; - stream(range: BlockRange, query: string, callback: EventCallback): EventStream { - const arg = new BlocksRequest(); - arg.setBlockrange(range); - arg.setQuery(query); +export function queryFor({ signatures, address }: QueryOptions): string { + return and( + equals('EventType', 'LogEvent'), + equals('Address', address), + or(...signatures.map((s) => equals('Log0', s))), + ); +} - const stream = this.burrow.events(arg); - stream.on('data', (data: EventsResponse) => { - data.getEventsList().map((event) => { - const log = event.getLog(); - if (!log) { - return callback(new Error(`received non-log event: ${log}`)); - } - return callback(undefined, log); - }); - }); - stream.on('end', () => callback(EndOfStream)); - stream.on('error', (err: grpc.ServiceError) => err.code === grpc.status.CANCELLED || callback(err)); - return stream; - } +function and(...predicates: string[]): string { + return predicates.filter((p) => p).join(' AND '); +} - listen(range: BlockRange, address: string, signature: string, callback: EventCallback): EventStream { - const filter = "EventType = 'LogEvent' AND Address = '" + address.toUpperCase() + "'"; - // + - // " AND Log0 = '" + - // signature.toUpperCase() + - // "'"; - return this.stream(range, filter, callback); +function or(...predicates: string[]): string { + const query = predicates.filter((p) => p).join(' OR '); + if (!query) { + return ''; } + return '(' + query + ')'; } -export type Bounds = number | 'first' | 'latest' | 'stream'; - -export type FiniteBounds = number | 'first' | 'latest'; +function equals(key: string, value?: string): string { + if (!value) { + return ''; + } + return key + " = '" + value + "'"; +} function boundsToBound(bounds: Bounds): Bound { const bound = new Bound(); @@ -83,28 +128,109 @@ function boundsToBound(bounds: Bounds): Bound { } export function readEvents( - listen: (callback: EventCallback, range?: BlockRange) => EventStream, + listener: (callback: EventCallback, start?: Bounds, end?: Bounds) => EventStream, start: FiniteBounds = 'first', end: FiniteBounds = 'latest', limit?: number, ): Promise { - return new Promise((resolve, reject) => { - const events: T[] = []; - const stream = listen((err, event) => { - if (err) { - if (err === EndOfStream) { - return resolve(events); + return reduceEvents( + listener, + (events, event) => { + if (limit && events.length === limit) { + return cancelStream; + } + events.push(event); + return events; + }, + [] as T[], + start, + end, + ); +} + +export function iterateEvents( + listener: (callback: EventCallback, start?: Bounds, end?: Bounds) => EventStream, + reducer: (event: T) => Signal | void, + start: FiniteBounds = 'first', + end: FiniteBounds = 'latest', +): Promise { + return reduceEvents(listener, (acc, event) => reducer(event), undefined as void, start, end); +} + +export function reduceEvents( + listener: (callback: EventCallback, start?: Bounds, end?: Bounds) => EventStream, + reducer: (accumulator: U, event: T) => U | Signal, + initialValue: U, + start: FiniteBounds = 'first', + end: FiniteBounds = 'latest', +): Promise { + let accumulator = initialValue; + return new Promise((resolve, reject) => + listener( + (err, event) => { + if (err) { + if (isEndOfStream(err)) { + return resolve(accumulator); + } + return reject(err); + } + if (!event) { + reject(new Error(`received empty event`)); + return cancelStream; + } + const reduced = reducer(accumulator, event); + if (reduced === cancelStream) { + resolve(accumulator); + return cancelStream; } - return reject(err); + accumulator = reduced; + }, + start, + end, + ), + ); +} + +export const listenerFor = ( + client: Provider, + address: Address, + eventRegistry: EventRegistry, + decode: (client: Provider, data?: Uint8Array, topics?: Uint8Array[]) => Record unknown>, + eventNames: T[], +) => ( + callback: EventCallback<{ name: T; payload: unknown; event: Event }>, + start?: Bounds, + end?: Bounds, +): EventStream => { + const signatureToName = eventNames.reduce((acc, n) => acc.set(eventRegistry[n].signature, n), new Map()); + + return client.listen( + Array.from(signatureToName.keys()), + address, + (err, event) => { + if (err) { + return callback(err); } if (!event) { - return reject(new Error(`received empty event`)); + return callback(new Error(`Empty event received`)); } - events.push(event); - if (limit && events.length === limit) { - stream.cancel(); - return resolve(events); + const topics = event?.getLog()?.getTopicsList_asU8(); + const log0 = topics?.[0]; + if (!log0) { + return callback(new Error(`Event has no Log0: ${event?.toString()}`)); } - }, getBlockRange(start, end)); - }); -} + const signature = Buffer.from(log0).toString('hex').toUpperCase(); + const name = signatureToName.get(signature); + if (!name) { + return callback( + new Error(`Could not find event with signature ${signature} in registry: ${JSON.stringify(eventRegistry)}`), + ); + } + const data = event?.getLog()?.getData_asU8(); + const payload = decode(client, data, topics)[name](); + return callback(undefined, { name, payload, event }); + }, + start, + end, + ); +}; diff --git a/js/src/index.ts b/js/src/index.ts index 5a9c54c7a..084dbd829 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,4 +1,4 @@ -export { LogEvent, TxExecution } from '../proto/exec_pb'; +export { Event, LogEvent, TxExecution } from '../proto/exec_pb'; export { CallTx, TxInput } from '../proto/payload_pb'; export { BlockRange } from '../proto/rpcevents_pb'; export { Client } from './client'; @@ -7,5 +7,15 @@ export { Address } from './contracts/abi'; export { makeCallTx } from './contracts/call'; export { Contract } from './contracts/contract'; export { Result } from './convert'; -export { EndOfStream, EventStream } from './events'; +export { + cancelStream, + EventStream, + isEndOfStream, + iterateEvents, + listenerFor, + readEvents, + reduceEvents, + Signal, +} from './events'; export { build } from './solts/build'; +export { Caller, defaultCall, Provider } from './solts/interface.gd'; diff --git a/js/src/solts/api.ts b/js/src/solts/api.ts index f0d888ef2..2d9be2bc2 100644 --- a/js/src/solts/api.ts +++ b/js/src/solts/api.ts @@ -1,10 +1,11 @@ import ts, { factory } from 'typescript'; import { ABI } from './lib/abi'; -import { createCallerFunction } from './lib/caller'; -import { generateContractClass } from './lib/contract'; +import { callerTypes, createCallerFunction } from './lib/caller'; +import { declareContractType, generateContractObject } from './lib/contract'; import { generateDecodeObject } from './lib/decoder'; -import { generateDeployFunction } from './lib/deployer'; +import { generateDeployContractFunction, generateDeployFunction } from './lib/deployer'; import { generateEncodeObject } from './lib/encoder'; +import { declareEvents, eventTypes } from './lib/events'; import { createLinkerFunction } from './lib/linker'; import { Provider } from './lib/provider'; import { getContractMethods } from './lib/solidity'; @@ -20,6 +21,7 @@ export type Compiled = { links: Array; }; +const contractNameName = factory.createIdentifier('contactName'); const abiName = factory.createIdentifier('abi'); const bytecodeName = factory.createIdentifier('bytecode'); @@ -36,6 +38,7 @@ export function newFile(contracts: Compiled[], burrowImportPath: string): ts.Nod ), importBurrow(burrowImportPath), provider.createInterface(), + ...callerTypes, createCallerFunction(provider), createLinkerFunction(), ...contracts.map((contract) => { @@ -44,16 +47,25 @@ export function newFile(contracts: Compiled[], burrowImportPath: string): ts.Nod const deploy = contract.abi.find((abi): abi is Func => abi.type === 'constructor'); // No deploy function for interfaces - const deployFunction = contract.bin + const deployMembers = contract.bin ? [ declareConstant(bytecodeName, factory.createStringLiteral(contract.bin, true), true), generateDeployFunction(deploy, bytecodeName, contract.links, provider, abiName), + generateDeployContractFunction(deploy, bytecodeName, contract.links, provider), ] : []; + + const events = methods.filter((a) => a.type === 'event'); + + const eventMembers = events.length ? [...eventTypes(), declareEvents(events)] : []; + const statements = [ + declareConstant(contractNameName, factory.createStringLiteral(contract.name), true), declareConstant(abiName, factory.createStringLiteral(JSON.stringify(contract.abi), true), true), - ...deployFunction, - generateContractClass(methods, provider), + ...deployMembers, + ...eventMembers, + declareContractType(), + generateContractObject(contractNameName, methods, provider), generateEncodeObject(methods, provider, abiName), generateDecodeObject(methods, provider, abiName), ]; diff --git a/js/src/solts/interface.gd.ts b/js/src/solts/interface.gd.ts new file mode 100644 index 000000000..fd251f8c3 --- /dev/null +++ b/js/src/solts/interface.gd.ts @@ -0,0 +1,28 @@ +//Generated by solts - run yarn generate:provider to regenerate if solts Provider code is changed +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../index'; +export type Caller = typeof defaultCall; +export interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); +} diff --git a/js/src/solts/provider.gd.ts.gr.ts b/js/src/solts/interface.gd.ts.gr.ts similarity index 76% rename from js/src/solts/provider.gd.ts.gr.ts rename to js/src/solts/interface.gd.ts.gr.ts index 3479a7d8c..c7d25e0c7 100644 --- a/js/src/solts/provider.gd.ts.gr.ts +++ b/js/src/solts/interface.gd.ts.gr.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import ts from 'typescript'; import { printNodes } from './api'; +import { callerTypes, createCallerFunction } from './lib/caller'; import { Provider } from './lib/provider'; import { importBurrow } from './lib/syntax'; @@ -12,9 +13,11 @@ const providerInterface = printNodes( ts.SyntaxKind.SingleLineCommentTrivia, 'Generated by solts - run yarn generate:provider to regenerate if solts Provider code is changed', ), + ...callerTypes, provider.createInterface(true), + createCallerFunction(provider), ); -const providerFile = path.join(__dirname, 'provider.gd.ts'); +const providerFile = path.join(__dirname, 'interface.gd.ts'); fs.writeFileSync(providerFile, providerInterface); diff --git a/js/src/solts/lib/caller.ts b/js/src/solts/lib/caller.ts index 5ff26bbaf..8af02f12e 100644 --- a/js/src/solts/lib/caller.ts +++ b/js/src/solts/lib/caller.ts @@ -8,6 +8,7 @@ import { createCall, createParameter, declareConstant, + ExportToken, MaybeUint8ArrayType, PromiseType, QuestionToken, @@ -15,13 +16,14 @@ import { Uint8ArrayType, } from './syntax'; -export const callName = factory.createIdentifier('call'); +export const defaultCallName = factory.createIdentifier('defaultCall'); +export const callerTypeName = factory.createIdentifier('Caller'); export function createCallerFunction(provider: Provider): ts.FunctionDeclaration { const output = factory.createIdentifier('Output'); const client = factory.createIdentifier('client'); const payload = factory.createIdentifier('payload'); - const txe = factory.createIdentifier('txe'); + const returnData = factory.createIdentifier('returnData'); const data = factory.createIdentifier('data'); const isSim = factory.createIdentifier('isSim'); const callback = factory.createIdentifier('callback'); @@ -29,9 +31,9 @@ export function createCallerFunction(provider: Provider): ts.FunctionDeclaration return factory.createFunctionDeclaration( undefined, - [AsyncToken], + [ExportToken, AsyncToken], undefined, - callName, + defaultCallName, [factory.createTypeParameterDeclaration(output)], [ createParameter(client, provider.type()), @@ -42,7 +44,7 @@ export function createCallerFunction(provider: Provider): ts.FunctionDeclaration callback, factory.createFunctionTypeNode( undefined, - [createParameter('exec', MaybeUint8ArrayType)], + [createParameter(returnData, MaybeUint8ArrayType)], factory.createTypeReferenceNode(output, undefined), ), ), @@ -52,7 +54,7 @@ export function createCallerFunction(provider: Provider): ts.FunctionDeclaration [ declareConstant(payload, provider.methods.payload.call(client, data, addr)), declareConstant( - txe, + returnData, factory.createAwaitExpression( factory.createConditionalExpression( isSim, @@ -63,9 +65,19 @@ export function createCallerFunction(provider: Provider): ts.FunctionDeclaration ), ), ), - factory.createReturnStatement(createCall(callback, [txe])), + factory.createReturnStatement(createCall(callback, [returnData])), ], true, ), ); } + +export const callerTypes: ts.TypeAliasDeclaration[] = [ + factory.createTypeAliasDeclaration( + undefined, + [ExportToken], + callerTypeName, + undefined, + factory.createTypeQueryNode(defaultCallName), + ), +]; diff --git a/js/src/solts/lib/contract.ts b/js/src/solts/lib/contract.ts index 2fa1991dc..46b9b257d 100644 --- a/js/src/solts/lib/contract.ts +++ b/js/src/solts/lib/contract.ts @@ -1,41 +1,113 @@ -import ts, { ClassDeclaration, factory, MethodDeclaration } from 'typescript'; -import { callName } from './caller'; +import ts, { factory, ObjectLiteralElementLike } from 'typescript'; +import { defaultCallName } from './caller'; import { decodeName } from './decoder'; import { encodeName } from './encoder'; -import { callGetDataFromLog, callGetTopicsFromLog, eventSigHash } from './events'; -import { errName, EventErrParameter, LogEventParameter, logName, Provider } from './provider'; +import { + BoundsType, + CallbackReturnType, + callGetDataFromEvent, + callGetTopicsFromEvent, + createListener, + createListenerForFunction, + eventSigHash, +} from './events'; +import { errName, EventErrParameter, eventName, EventParameter, Provider } from './provider'; import { ContractMethodsList, getRealType, inputOuputsToType, Signature } from './solidity'; import { - accessThis, + asConst, asRefNode, - BlockRangeType, + constObject, createCall, - createCallbackExpression, + createCallbackType, createParameter, createPromiseOf, declareConstant, + EqualsGreaterThanToken, EventStream, ExportToken, MaybeUint8ArrayType, Method, - PrivateToken, prop, - PublicToken, + ReturnType, StringType, Undefined, } from './syntax'; +export const contractFunctionName = factory.createIdentifier('contract'); +export const contractTypeName = factory.createIdentifier('Contract'); +export const functionsGroupName = factory.createIdentifier('functions'); +export const listenersGroupName = factory.createIdentifier('listeners'); const dataName = factory.createIdentifier('data'); const clientName = factory.createIdentifier('client'); const addressName = factory.createIdentifier('address'); -const eventName = factory.createIdentifier('eventName'); +const listenerForName = factory.createIdentifier('listenerFor'); +const listenerName = factory.createIdentifier('listener'); + +export function declareContractType(): ts.TypeAliasDeclaration { + return factory.createTypeAliasDeclaration( + undefined, + [ExportToken], + contractTypeName, + undefined, + factory.createTypeReferenceNode(ReturnType, [factory.createTypeQueryNode(contractFunctionName)]), + ); +} -export const ContractName = factory.createIdentifier('Contract'); +export function generateContractObject( + contractNameName: ts.Identifier, + abi: ContractMethodsList, + provider: Provider, +): ts.VariableStatement { + const functions = abi.filter((a) => a.type === 'function'); + const events = abi.filter((a) => a.type === 'event'); + + const functionObjectProperties = functions.length + ? [ + createGroup( + functionsGroupName, + functions.flatMap((a) => + a.signatures.map((signature, index) => solidityFunction(a.name, a.signatures, index)), + ), + ), + ] + : []; + + const eventObjectProperties = events.length + ? [ + createGroup( + listenersGroupName, + events.map((a) => solidityEvent(a.name, a.signatures[0], provider)), + ), + factory.createPropertyAssignment(listenerForName, createListenerForFunction(clientName, addressName)), + factory.createPropertyAssignment(listenerName, createListener(clientName, addressName)), + ] + : []; + + return declareConstant( + contractFunctionName, + factory.createArrowFunction( + undefined, + undefined, + [createParameter(clientName, provider.type()), createParameter(addressName, StringType)], + undefined, + EqualsGreaterThanToken, + asConst( + factory.createObjectLiteralExpression([ + factory.createShorthandPropertyAssignment(addressName), + ...functionObjectProperties, + ...eventObjectProperties, + ]), + ), + ), + true, + ); +} function solidityFunction(name: string, signatures: Signature[], index: number): ts.MethodDeclaration { const signature = signatures[index]; const args = signature.inputs.map((input) => factory.createIdentifier(input.name)); - const encodeFunctionOrOverloadsArray = prop(createCall(encodeName, [accessThis(clientName)]), name); + const encodeFunctionOrOverloadsArray = prop(createCall(encodeName, [clientName]), name); + const callName = factory.createIdentifier('call'); // Special case for overloads const hasOverloads = signatures.length > 1; @@ -44,7 +116,7 @@ function solidityFunction(name: string, signatures: Signature[], index: number): ? factory.createElementAccessExpression(encodeFunctionOrOverloadsArray, index) : encodeFunctionOrOverloadsArray; - const decoderFunctionOrOverloadsArray = prop(createCall(decodeName, [accessThis(clientName), dataName]), name); + const decoderFunctionOrOverloadsArray = prop(createCall(decodeName, [clientName, dataName]), name); const decoderFunction = hasOverloads ? factory.createElementAccessExpression(decoderFunctionOrOverloadsArray, index) @@ -58,8 +130,8 @@ function solidityFunction(name: string, signatures: Signature[], index: number): callName, [returnType], [ - accessThis(clientName), - accessThis(addressName), + clientName, + addressName, dataName, signature.constant ? factory.createTrue() : factory.createFalse(), factory.createArrowFunction( @@ -73,42 +145,47 @@ function solidityFunction(name: string, signatures: Signature[], index: number): ], ); + const callParameter = createParameter(callName, undefined, defaultCallName); + const params = signature.inputs.map((input) => createParameter(input.name, getRealType(input.type))); // Suffix overloads return new Method(index > 0 ? `${name}_${index}` : name) .parameters(params) + .parameters(callParameter) .returns(createPromiseOf(returnType)) .declaration([encode, factory.createReturnStatement(call)], true); } function solidityEvent(name: string, signature: Signature, provider: Provider): ts.MethodDeclaration { const callback = factory.createIdentifier('callback'); - const range = factory.createIdentifier('range'); + const start = factory.createIdentifier('start'); + const end = factory.createIdentifier('end'); // Receivers of LogEventParameter - const data = callGetDataFromLog(logName); - const topics = callGetTopicsFromLog(logName); - const decoderFunction = prop(createCall(decodeName, [accessThis(clientName), data, topics]), name); + const data = callGetDataFromEvent(eventName); + const topics = callGetTopicsFromEvent(eventName); + const decoderFunction = prop(createCall(decodeName, [clientName, data, topics]), name); return new Method(name) .parameter( callback, - createCallbackExpression([ - EventErrParameter, - createParameter(eventName, inputOuputsToType(signature.inputs), undefined, true), - ]), + createCallbackType( + [EventErrParameter, createParameter(eventName, inputOuputsToType(signature.inputs), undefined, true)], + CallbackReturnType, + ), ) - .parameter(range, BlockRangeType, true) + .parameter(start, BoundsType, true) + .parameter(end, BoundsType, true) .returns(asRefNode(EventStream)) .declaration([ factory.createReturnStatement( provider.methods.listen.call( - accessThis(clientName), - factory.createStringLiteral(eventSigHash(name, signature.inputs)), - accessThis(addressName), + clientName, + factory.createArrayLiteralExpression([factory.createStringLiteral(eventSigHash(name, signature.inputs))]), + addressName, factory.createArrowFunction( undefined, undefined, - [EventErrParameter, LogEventParameter], + [EventErrParameter, EventParameter], undefined, undefined, factory.createBlock([ @@ -116,43 +193,13 @@ function solidityEvent(name: string, signature: Signature, provider: Provider): factory.createReturnStatement(createCall(callback, [Undefined, createCall(decoderFunction)])), ]), ), - range, + start, + end, ), ), ]); } -function createMethodsFromABI( - name: string, - type: 'function' | 'event', - signatures: Signature[], - provider: Provider, -): MethodDeclaration[] { - if (type === 'function') { - return signatures.map((signature, index) => solidityFunction(name, signatures, index)); - } else if (type === 'event') { - return [solidityEvent(name, signatures[0], provider)]; - } - // FIXME: Not sure why this is not inferred since if is exhaustive - return undefined as never; -} - -export function generateContractClass(abi: ContractMethodsList, provider: Provider): ClassDeclaration { - return factory.createClassDeclaration(undefined, [ExportToken], ContractName, undefined, undefined, [ - factory.createPropertyDeclaration(undefined, [PrivateToken], clientName, undefined, provider.type(), undefined), - factory.createPropertyDeclaration(undefined, [PublicToken], addressName, undefined, StringType, undefined), - factory.createConstructorDeclaration( - undefined, - undefined, - [createParameter(clientName, provider.type()), createParameter(addressName, StringType)], - factory.createBlock( - [ - factory.createExpressionStatement(factory.createAssignment(accessThis(clientName), clientName)), - factory.createExpressionStatement(factory.createAssignment(accessThis(addressName), addressName)), - ], - true, - ), - ), - ...abi.flatMap((abi) => createMethodsFromABI(abi.name, abi.type, abi.signatures, provider)), - ]); +function createGroup(name: ts.Identifier, elements: ObjectLiteralElementLike[]): ts.PropertyAssignment { + return factory.createPropertyAssignment(name, constObject(elements)); } diff --git a/js/src/solts/lib/decoder.ts b/js/src/solts/lib/decoder.ts index 7dfcb0598..26c09a185 100644 --- a/js/src/solts/lib/decoder.ts +++ b/js/src/solts/lib/decoder.ts @@ -12,7 +12,7 @@ export function generateDecodeObject( methods: ContractMethodsList, provider: Provider, abiName: ts.Identifier, -): VariableStatement { +): ts.VariableStatement { return generateDecoderObject(methods, provider, abiName, (method) => { const decodeFunction = (signature: Signature) => { const isFunction = method.type === 'function'; @@ -43,7 +43,7 @@ function generateDecoderObject( provider: Provider, abiName: ts.Identifier, functionMaker: (m: Method) => ts.Expression, -): VariableStatement { +): ts.VariableStatement { return declareConstant( decodeName, factory.createArrowFunction( diff --git a/js/src/solts/lib/deployer.ts b/js/src/solts/lib/deployer.ts index 3de4f967a..883d2b338 100644 --- a/js/src/solts/lib/deployer.ts +++ b/js/src/solts/lib/deployer.ts @@ -1,13 +1,16 @@ -import ts, { factory, FunctionDeclaration } from 'typescript'; +import ts, { factory, FunctionDeclaration, SyntaxKind } from 'typescript'; import { ABI } from './abi'; +import { contractFunctionName, contractTypeName } from './contract'; import { linkerName } from './linker'; import { callEncodeDeploy, Provider } from './provider'; import { getRealType, sha3, tokenizeString } from './solidity'; import { + AsyncToken, BufferType, createAssignmentStatement, createCall, createParameter, + createPromiseOf, declareConstant, declareLet, ExportToken, @@ -16,14 +19,15 @@ import { StringType, } from './syntax'; +export const deployName = factory.createIdentifier('deploy'); +export const deployContractName = factory.createIdentifier('deployContract'); + // Variable names const payloadName = factory.createIdentifier('payload'); const linkedBytecodeName = factory.createIdentifier('linkedBytecode'); const dataName = factory.createIdentifier('data'); const clientName = factory.createIdentifier('client'); -export const DeployName = factory.createIdentifier('deploy'); - export function generateDeployFunction( abi: ABI.Func | undefined, bytecodeName: ts.Identifier, @@ -31,7 +35,6 @@ export function generateDeployFunction( provider: Provider, abiName: ts.Identifier, ): FunctionDeclaration { - const parameters = abi ? abi.inputs?.map((input) => createParameter(input.name, getRealType(input.type))) ?? [] : []; const output = factory.createExpressionWithTypeArguments(PromiseType, [StringType]); const statements: ts.Statement[] = []; @@ -73,14 +76,52 @@ export function generateDeployFunction( undefined, [ExportToken], undefined, - DeployName, + deployName, undefined, - [ - createParameter(clientName, provider.type()), - ...links.map((link) => createParameter(factory.createIdentifier(tokenizeString(link)), StringType)), - ...parameters, - ], + deployParameters(abi, bytecodeName, links, provider), output, factory.createBlock(statements, true), ); } + +export function generateDeployContractFunction( + abi: ABI.Func | undefined, + bytecodeName: ts.Identifier, + links: string[], + provider: Provider, +) { + const parameters = deployParameters(abi, bytecodeName, links, provider); + const addressName = factory.createIdentifier('address'); + const callDeploy = factory.createAwaitExpression( + createCall(deployName, [ + ...parameters.map((p) => p.name).filter((n): n is ts.Identifier => n.kind === SyntaxKind.Identifier), + ]), + ); + return factory.createFunctionDeclaration( + undefined, + [ExportToken, AsyncToken], + undefined, + deployContractName, + undefined, + parameters, + createPromiseOf(factory.createTypeReferenceNode(contractTypeName)), + factory.createBlock([ + declareConstant(addressName, callDeploy), + factory.createReturnStatement(createCall(contractFunctionName, [clientName, addressName])), + ]), + ); +} + +function deployParameters( + abi: ABI.Func | undefined, + bytecodeName: ts.Identifier, + links: string[], + provider: Provider, +): ts.ParameterDeclaration[] { + const parameters = abi ? abi.inputs?.map((input) => createParameter(input.name, getRealType(input.type))) ?? [] : []; + return [ + createParameter(clientName, provider.type()), + ...links.map((link) => createParameter(factory.createIdentifier(tokenizeString(link)), StringType)), + ...parameters, + ]; +} diff --git a/js/src/solts/lib/events.ts b/js/src/solts/lib/events.ts index ba3249ec3..963a8ef4a 100644 --- a/js/src/solts/lib/events.ts +++ b/js/src/solts/lib/events.ts @@ -1,14 +1,47 @@ import * as ts from 'typescript'; import { factory } from 'typescript'; -import { getRealType, InputOutput, sha3, Signature } from './solidity'; -import { createCall, createParameter, prop } from './syntax'; +import { decodeName } from './decoder'; +import { EventErrParameter, eventName } from './provider'; +import { ContractMethodsList, getRealType, InputOutput, sha3 } from './solidity'; +import { + arrowFuncT, + constObject, + createCall, + createParameter, + declareConstant, + EqualsGreaterThanToken, + EventStreamType, + EventType, + ExportToken, + listenerForName, + NumberType, + prop, + ReturnType, + SignalType, + TType, + UnknownType, + VoidType, +} from './syntax'; +export const eventsName = factory.createIdentifier('events'); +export const eventNameTypeName = factory.createIdentifier('EventName'); +export const BoundsType = factory.createUnionTypeNode([ + ...['first', 'latest', 'stream'].map((s) => factory.createLiteralTypeNode(factory.createStringLiteral(s))), + NumberType, +]); +export const CallbackReturnType = factory.createUnionTypeNode([SignalType, VoidType]); + +const typedListenerName = factory.createIdentifier('TypedListener'); + +const getLogName = factory.createIdentifier('getLog'); const getDataName = factory.createIdentifier('getData_asU8'); const getTopicsName = factory.createIdentifier('getTopicsList_asU8'); - -export function generateEventArgs(name: string, signature: Signature): ts.ParameterDeclaration[] { - return signature.inputs.map(({ name, type }) => createParameter(name, getRealType(type))); -} +const taggedPayloadName = factory.createIdentifier('TaggedPayload'); +const solidityEventName = factory.createIdentifier('SolidityEvent'); +const eventRegistryName = factory.createIdentifier('EventRegistry'); +const signatureName = factory.createIdentifier('signature'); +const taggedName = factory.createIdentifier('tagged'); +const payloadName = factory.createIdentifier('payload'); export function eventSignature(name: string, inputs: InputOutput[]): string { return `${name}(${inputs.map(({ type }) => type).join(',')})`; @@ -18,10 +51,172 @@ export function eventSigHash(name: string, inputs: InputOutput[]): string { return sha3(eventSignature(name, inputs)); } -export function callGetDataFromLog(log: ts.Expression): ts.CallExpression { - return createCall(prop(log, getDataName, true)); +function callGetLogFromEvent(event: ts.Expression): ts.CallExpression { + return createCall(prop(event, getLogName, true)); +} + +export function callGetDataFromEvent(event: ts.Expression): ts.CallExpression { + return createCall(prop(callGetLogFromEvent(event), getDataName, true)); +} + +export function callGetTopicsFromEvent(event: ts.Expression): ts.CallExpression { + return createCall(prop(callGetLogFromEvent(event), getTopicsName, true)); +} + +export function createListenerForFunction(clientName: ts.Identifier, addressName: ts.Identifier): ts.ArrowFunction { + const eventNamesName = factory.createIdentifier('eventNames'); + const typedListener = factory.createTypeReferenceNode(typedListenerName, [TType]); + return arrowFuncT( + [createParameter(eventNamesName, factory.createArrayTypeNode(TType))], + factory.createTypeReferenceNode(eventNameTypeName), + typedListener, + factory.createAsExpression( + // The intermediate as unknown expression is not needed if tsconfig is in 'strict mode' but otherwise tsc complains + // that the types do not sufficiently overlap + factory.createAsExpression( + createCall(listenerForName, [clientName, addressName, eventsName, decodeName, eventNamesName]), + UnknownType, + ), + typedListener, + ), + ); +} + +export function createListener(clientName: ts.Identifier, addressName: ts.Identifier): ts.AsExpression { + const eventNamesType = factory.createTypeReferenceNode(eventNameTypeName); + const typedListener = factory.createTypeReferenceNode(typedListenerName, [eventNamesType]); + return factory.createAsExpression( + createCall(listenerForName, [ + clientName, + addressName, + eventsName, + decodeName, + factory.createAsExpression( + createCall(prop('Object', 'keys'), [eventsName]), + factory.createArrayTypeNode(eventNamesType), + ), + ]), + typedListener, + ); +} + +export function declareEvents(events: ContractMethodsList): ts.VariableStatement { + return declareConstant( + eventsName, + constObject( + events.map((a) => { + const signature = a.signatures[0]; + return factory.createPropertyAssignment( + a.name, + constObject([ + factory.createPropertyAssignment(signatureName, factory.createStringLiteral(signature.hash)), + factory.createPropertyAssignment( + taggedName, + factory.createArrowFunction( + undefined, + undefined, + signature.inputs.map((i) => createParameter(i.name, getRealType(i.type))), + undefined, + EqualsGreaterThanToken, + constObject([ + factory.createPropertyAssignment('name', factory.createStringLiteral(a.name)), + factory.createPropertyAssignment( + payloadName, + constObject( + signature.inputs.map(({ name }) => + factory.createPropertyAssignment(name, factory.createIdentifier(name)), + ), + ), + ), + ]), + ), + ), + ]), + ); + }), + ), + ); } -export function callGetTopicsFromLog(log: ts.Expression): ts.CallExpression { - return createCall(prop(log, getTopicsName, true)); +export function eventTypes(): ts.TypeAliasDeclaration[] { + const tExtendsEventName = factory.createTypeParameterDeclaration( + factory.createIdentifier('T'), + factory.createTypeReferenceNode(eventNameTypeName, undefined), + undefined, + ); + const tType = factory.createTypeReferenceNode(factory.createIdentifier('T'), undefined); + return [ + factory.createTypeAliasDeclaration( + undefined, + undefined, + eventRegistryName, + undefined, + factory.createTypeQueryNode(eventsName), + ), + factory.createTypeAliasDeclaration( + undefined, + [factory.createModifier(ts.SyntaxKind.ExportKeyword)], + eventNameTypeName, + undefined, + factory.createTypeOperatorNode( + ts.SyntaxKind.KeyOfKeyword, + factory.createTypeReferenceNode(eventRegistryName, undefined), + ), + ), + factory.createTypeAliasDeclaration( + undefined, + [ExportToken], + taggedPayloadName, + [tExtendsEventName], + factory.createIntersectionTypeNode([ + factory.createTypeReferenceNode(ReturnType, [ + factory.createIndexedAccessTypeNode( + factory.createIndexedAccessTypeNode(factory.createTypeReferenceNode(eventRegistryName, undefined), tType), + factory.createLiteralTypeNode(factory.createStringLiteral(taggedName.text)), + ), + ]), + factory.createTypeLiteralNode([factory.createPropertySignature(undefined, eventName, undefined, EventType)]), + ]), + ), + factory.createTypeAliasDeclaration( + undefined, + [ExportToken], + solidityEventName, + [tExtendsEventName], + factory.createIndexedAccessTypeNode( + factory.createTypeReferenceNode(taggedPayloadName, [tType]), + factory.createLiteralTypeNode(factory.createStringLiteral(payloadName.text)), + ), + ), + factory.createTypeAliasDeclaration( + undefined, + [ExportToken], + typedListenerName, + [factory.createTypeParameterDeclaration('T', factory.createTypeReferenceNode(eventNameTypeName))], + factory.createFunctionTypeNode( + [], + [ + createParameter( + 'callback', + factory.createFunctionTypeNode( + [], + [ + EventErrParameter, + createParameter( + eventName, + factory.createTypeReferenceNode(taggedPayloadName, [TType]), + undefined, + true, + ), + ], + VoidType, + ), + ), + createParameter('start', BoundsType, undefined, true), + createParameter('end', BoundsType, undefined, true), + ], + EventStreamType, + ), + ), + ]; } diff --git a/js/src/solts/lib/provider.ts b/js/src/solts/lib/provider.ts index e9b21a914..455dbf648 100644 --- a/js/src/solts/lib/provider.ts +++ b/js/src/solts/lib/provider.ts @@ -1,20 +1,19 @@ import ts, { factory } from 'typescript'; +import { BoundsType, CallbackReturnType } from './events'; import { AddressType, asRefNode, - BlockRangeType, CallTxType, ContractCodecType, createCall, - createCallbackExpression, + createCallbackType, createParameter, createPromiseOf, declareConstant, - EndOfStreamType, ErrorType, EventStream, + EventType, ExportToken, - LogEventType, MaybeUint8ArrayType, Method, StringType, @@ -23,15 +22,10 @@ import { export const errName = factory.createIdentifier('err'); export const contractCodecName = factory.createIdentifier('codec'); -export const logName = factory.createIdentifier('log'); +export const eventName = factory.createIdentifier('event'); -export const EventErrParameter = createParameter( - errName, - factory.createUnionTypeNode([ErrorType, EndOfStreamType]), - undefined, - true, -); -export const LogEventParameter = createParameter(logName, LogEventType, undefined, true); +export const EventErrParameter = createParameter(errName, ErrorType, undefined, true); +export const EventParameter = createParameter(eventName, EventType, undefined, true); class Deploy extends Method { params = [createParameter('msg', CallTxType)]; @@ -74,10 +68,11 @@ class CallSim extends Method { class Listen extends Method { params = [ - createParameter('signature', StringType), + createParameter('signatures', factory.createArrayTypeNode(StringType)), createParameter('address', StringType), - createParameter('callback', createCallbackExpression([EventErrParameter, LogEventParameter])), - createParameter('range', BlockRangeType, undefined, true), + createParameter('callback', createCallbackType([EventErrParameter, EventParameter], CallbackReturnType)), + createParameter('start', BoundsType, undefined, true), + createParameter('end', BoundsType, undefined, true), ]; ret = asRefNode(EventStream); @@ -85,8 +80,15 @@ class Listen extends Method { super('listen'); } - call(exp: ts.Expression, sig: ts.StringLiteral, addr: ts.Expression, callback: ts.Expression, range: ts.Expression) { - return createCall(factory.createPropertyAccessExpression(exp, this.id), [sig, addr, callback, range]); + call( + exp: ts.Expression, + sig: ts.Expression, + addr: ts.Expression, + callback: ts.Expression, + start: ts.Expression, + end: ts.Expression, + ) { + return createCall(factory.createPropertyAccessExpression(exp, this.id), [sig, addr, callback, start, end]); } } diff --git a/js/src/solts/lib/syntax.test.ts b/js/src/solts/lib/syntax.test.ts index 6eaf13e42..513f75b51 100644 --- a/js/src/solts/lib/syntax.test.ts +++ b/js/src/solts/lib/syntax.test.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import { factory, Node } from 'typescript'; import { printNodes } from '../api'; -import { createCallbackExpression, createParameter, createPromiseOf, PromiseType, StringType } from './syntax'; +import { createCallbackType, createParameter, createPromiseOf, PromiseType, StringType } from './syntax'; describe('syntax helpers', function () { it('should create callback expression', async function () { @@ -9,7 +9,7 @@ describe('syntax helpers', function () { createParameter(factory.createIdentifier('err'), undefined), createParameter(factory.createIdentifier('result'), undefined), ]; - assertGenerates(createCallbackExpression(ErrAndResult), '(err, result) => void'); + assertGenerates(createCallbackType(ErrAndResult), '(err, result) => void'); }); it('should create promise type', () => { diff --git a/js/src/solts/lib/syntax.ts b/js/src/solts/lib/syntax.ts index 53dd31d0a..ccd02eb76 100644 --- a/js/src/solts/lib/syntax.ts +++ b/js/src/solts/lib/syntax.ts @@ -1,4 +1,4 @@ -import ts, { factory, MethodDeclaration } from 'typescript'; +import ts, { ConciseBody, factory, MethodDeclaration, TypeNode } from 'typescript'; export const ErrorType = factory.createTypeReferenceNode('Error'); export const VoidType = factory.createTypeReferenceNode('void', undefined); @@ -15,20 +15,22 @@ export const PromiseType = factory.createIdentifier('Promise'); export const ReadableType = factory.createIdentifier('Readable'); export const BufferType = factory.createIdentifier('Buffer'); export const CallTx = factory.createIdentifier('CallTx'); -export const BlockRange = factory.createIdentifier('BlockRange'); export const Address = factory.createIdentifier('Address'); export const EventStream = factory.createIdentifier('EventStream'); -export const LogEvent = factory.createIdentifier('LogEvent'); +export const Event = factory.createIdentifier('Event'); export const ContractCodec = factory.createIdentifier('ContractCodec'); export const Result = factory.createIdentifier('Result'); -export const EndOfStream = factory.createIdentifier('EndOfStream'); +export const Signal = factory.createIdentifier('Signal'); +export const ReturnType = factory.createIdentifier('ReturnType'); +export const listenerForName = factory.createIdentifier('listenerFor'); +export const TType = factory.createTypeReferenceNode('T'); export const CallTxType = factory.createTypeReferenceNode(CallTx); -export const BlockRangeType = factory.createTypeReferenceNode(BlockRange); export const AddressType = factory.createTypeReferenceNode(Address); -export const LogEventType = factory.createTypeReferenceNode(LogEvent); +export const EventType = factory.createTypeReferenceNode(Event); export const ContractCodecType = factory.createTypeReferenceNode(ContractCodec); -export const EndOfStreamType = factory.createTypeReferenceNode(EndOfStream); +export const EventStreamType = factory.createTypeReferenceNode(EventStream); +export const SignalType = factory.createTypeReferenceNode(Signal); export const PrivateToken = factory.createToken(ts.SyntaxKind.PrivateKeyword); export const PublicToken = factory.createToken(ts.SyntaxKind.PublicKeyword); @@ -38,10 +40,12 @@ export const QuestionToken = factory.createToken(ts.SyntaxKind.QuestionToken); export const QuestionDotToken = factory.createToken(ts.SyntaxKind.QuestionDotToken); export const ColonToken = factory.createToken(ts.SyntaxKind.ColonToken); export const AsyncToken = factory.createToken(ts.SyntaxKind.AsyncKeyword); +export const ReadonlyToken = factory.createToken(ts.SyntaxKind.ReadonlyKeyword); +export const EqualsGreaterThanToken = factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken); export const Undefined = factory.createIdentifier('undefined'); -export function createCall(fn: ts.Expression, args?: ts.Expression[]): ts.CallExpression { - return factory.createCallExpression(fn, undefined, args); +export function createCall(fn: ts.Expression | string, args?: ts.Expression[]): ts.CallExpression { + return factory.createCallExpression(asExp(fn), undefined, args); } export function accessThis(name: ts.Identifier): ts.PropertyAccessExpression { @@ -68,12 +72,40 @@ export function asConst(exp: ts.Expression): ts.AsExpression { return factory.createAsExpression(exp, factory.createTypeReferenceNode('const')); } +export function constObject(elements: ts.ObjectLiteralElementLike[]): ts.AsExpression { + return asConst(factory.createObjectLiteralExpression(elements)); +} + +export function arrowFunc(params: ts.ParameterDeclaration[], body: ConciseBody): ts.ArrowFunction { + return factory.createArrowFunction(undefined, undefined, params, undefined, EqualsGreaterThanToken, body); +} + +export function arrowFuncT( + params: ts.ParameterDeclaration[], + constraint: TypeNode | undefined, + type: TypeNode | undefined, + body: ConciseBody, +): ts.ArrowFunction { + return factory.createArrowFunction( + undefined, + [factory.createTypeParameterDeclaration('T', constraint)], + params, + type, + EqualsGreaterThanToken, + body, + ); +} + export function prop( - obj: ts.Expression, + obj: ts.Expression | string, name: string | ts.Identifier, optionChain?: boolean, ): ts.PropertyAccessExpression { - return factory.createPropertyAccessChain(obj, optionChain ? QuestionDotToken : undefined, name); + return factory.createPropertyAccessChain(asExp(obj), optionChain ? QuestionDotToken : undefined, name); +} + +function asExp(exp: ts.Expression | string): ts.Expression { + return typeof exp === 'string' ? factory.createIdentifier(exp) : exp; } export function createParameter( @@ -165,7 +197,7 @@ export function CreateCallbackDeclaration( body: ts.Statement[], returnType?: ts.TypeNode, multiLine?: boolean, -) { +): ts.ArrowFunction { return factory.createArrowFunction( undefined, undefined, @@ -176,8 +208,11 @@ export function CreateCallbackDeclaration( ); } -export function createCallbackExpression(params: ts.ParameterDeclaration[]): ts.FunctionTypeNode { - return factory.createFunctionTypeNode(undefined, params, VoidType); +export function createCallbackType( + params: ts.ParameterDeclaration[], + type: ts.TypeNode = VoidType, +): ts.FunctionTypeNode { + return factory.createFunctionTypeNode(undefined, params, type); } function importFrom(pkg: string, ...things: ts.Identifier[]) { @@ -201,12 +236,12 @@ export function importBurrow(burrowImportPath: string): ts.ImportDeclaration { return importFrom( burrowImportPath, Address, - BlockRange, CallTx, ContractCodec, - EndOfStream, + Signal, + Event, EventStream, - LogEvent, + listenerForName, Result, ); } @@ -226,8 +261,12 @@ export class Method { return this; } - parameters(args: ts.ParameterDeclaration[]): Method { - this.params.push(...args); + parameters(arg: ts.ParameterDeclaration | ts.ParameterDeclaration[]): Method { + if (Array.isArray(arg)) { + this.params.push(...arg); + } else { + this.params.push(arg); + } return this; } diff --git a/js/src/solts/provider.gd.ts b/js/src/solts/provider.gd.ts deleted file mode 100644 index c001e38a5..000000000 --- a/js/src/solts/provider.gd.ts +++ /dev/null @@ -1,15 +0,0 @@ -//Generated by solts - run yarn generate:provider to regenerate if solts Provider code is changed -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent } from '../index'; -export interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen( - signature: string, - address: string, - callback: (err?: Error | EndOfStream, log?: LogEvent) => void, - range?: BlockRange, - ): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; -} diff --git a/js/src/solts/sol/Addition.abi.ts b/js/src/solts/sol/Addition.abi.ts index fed529aeb..d9ceeb1ae 100644 --- a/js/src/solts/sol/Addition.abi.ts +++ b/js/src/solts/sol/Addition.abi.ts @@ -1,64 +1,95 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; } -export module Addition { - export const abi = '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"b","type":"int256"}],"name":"add","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; - export const bytecode = '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a5f3c23b14602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600081830190509291505056fea265627a7a72315820bf9f1b3176bb0e5383e0dbeabc3258cbe895f39124127f38452c4ce85df9672964736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - add(a: number, b: number): Promise<{ - sum: number; +export namespace Addition { + export const contactName = 'Addition'; + export const abi = + '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"b","type":"int256"}],"name":"add","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = + '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063a5f3c23b14602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600081830190509291505056fea265627a7a72315820bf9f1b3176bb0e5383e0dbeabc3258cbe895f39124127f38452c4ce85df9672964736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + add( + a: number, + b: number, + call = defaultCall, + ): Promise<{ + sum: number; }> { - const data = encode(this.client).add(a, b); - return call<{ - sum: number; - }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).add(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - add: (a: number, b: number) => { return codec.encodeFunctionData("A5F3C23B", a, b); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - add: (): { + const data = encode(client).add(a, b); + return call<{ sum: number; - } => { - const [sum] = codec.decodeFunctionResult ("A5F3C23B", data); - return { sum: sum }; - } - }; }; -} \ No newline at end of file + }>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).add(); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + add: (a: number, b: number) => { + return codec.encodeFunctionData('A5F3C23B', a, b); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + add: (): { + sum: number; + } => { + const [sum] = codec.decodeFunctionResult('A5F3C23B', data); + return { sum: sum }; + }, + }; + }; +} diff --git a/js/src/solts/sol/Contains.abi.ts b/js/src/solts/sol/Contains.abi.ts index e601ead70..e4532da42 100644 --- a/js/src/solts/sol/Contains.abi.ts +++ b/js/src/solts/sol/Contains.abi.ts @@ -1,73 +1,100 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; +} +export namespace Contains { + export const contactName = 'Contains'; + export const abi = + '[{"constant":true,"inputs":[{"internalType":"address[]","name":"_list","type":"address[]"},{"internalType":"address","name":"_value","type":"address"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256[]","name":"_list","type":"uint256[]"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = + '608060405234801561001057600080fd5b50610304806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633da80d661461003b578063b32b8e2c1461012b575b600080fd5b6101116004803603604081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460208302840111640100000000831117156100a257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f820116905080830192505050505050509192919290803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610205565b604051808215151515815260200191505060405180910390f35b6101eb6004803603604081101561014157600080fd5b810190808035906020019064010000000081111561015e57600080fd5b82018360208201111561017057600080fd5b8035906020019184602083028401116401000000008311171561019257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f82011690508083019250505050505050919291929080359060200190929190505050610280565b604051808215151515815260200191505060405180910390f35b600080600090505b8351811015610274578273ffffffffffffffffffffffffffffffffffffffff1684828151811061023957fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff16141561026757600191505061027a565b808060010191505061020d565b50600090505b92915050565b600080600090505b83518110156102c3578284828151811061029e57fe5b602002602001015114156102b65760019150506102c9565b8080600101915050610288565b50600090505b9291505056fea265627a7a7231582032d6f06ddf667cc291458c6ed27a81fe8ef6fa213f3cf25c0fef50500bc05f2264736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + contains(_list: string[], _value: string, call = defaultCall): Promise<[boolean]> { + const data = encode(client).contains[0](_list, _value); + return call<[boolean]>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).contains[0](); + }); + }, + contains_1(_list: number[], _value: number, call = defaultCall): Promise<[boolean]> { + const data = encode(client).contains[1](_list, _value); + return call<[boolean]>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).contains[1](); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + contains: [ + (_list: string[], _value: string) => { + return codec.encodeFunctionData('3DA80D66', _list, _value); + }, + (_list: number[], _value: number) => { + return codec.encodeFunctionData('B32B8E2C', _list, _value); + }, + ] as const, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + contains: [ + (): [boolean] => { + return codec.decodeFunctionResult('3DA80D66', data); + }, + (): [boolean] => { + return codec.decodeFunctionResult('B32B8E2C', data); + }, + ] as const, + }; + }; } -export module Contains { - export const abi = '[{"constant":true,"inputs":[{"internalType":"address[]","name":"_list","type":"address[]"},{"internalType":"address","name":"_value","type":"address"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":true,"inputs":[{"internalType":"uint256[]","name":"_list","type":"uint256[]"},{"internalType":"uint256","name":"_value","type":"uint256"}],"name":"contains","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"pure","type":"function"}]'; - export const bytecode = '608060405234801561001057600080fd5b50610304806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633da80d661461003b578063b32b8e2c1461012b575b600080fd5b6101116004803603604081101561005157600080fd5b810190808035906020019064010000000081111561006e57600080fd5b82018360208201111561008057600080fd5b803590602001918460208302840111640100000000831117156100a257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f820116905080830192505050505050509192919290803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610205565b604051808215151515815260200191505060405180910390f35b6101eb6004803603604081101561014157600080fd5b810190808035906020019064010000000081111561015e57600080fd5b82018360208201111561017057600080fd5b8035906020019184602083028401116401000000008311171561019257600080fd5b919080806020026020016040519081016040528093929190818152602001838360200280828437600081840152601f19601f82011690508083019250505050505050919291929080359060200190929190505050610280565b604051808215151515815260200191505060405180910390f35b600080600090505b8351811015610274578273ffffffffffffffffffffffffffffffffffffffff1684828151811061023957fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff16141561026757600191505061027a565b808060010191505061020d565b50600090505b92915050565b600080600090505b83518110156102c3578284828151811061029e57fe5b602002602001015114156102b65760019150506102c9565b8080600101915050610288565b50600090505b9291505056fea265627a7a7231582032d6f06ddf667cc291458c6ed27a81fe8ef6fa213f3cf25c0fef50500bc05f2264736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - contains(_list: string[], _value: string): Promise<[ - boolean - ]> { - const data = encode(this.client).contains[0](_list, _value); - return call<[ - boolean - ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).contains[0](); - }); - } - contains_1(_list: number[], _value: number): Promise<[ - boolean - ]> { - const data = encode(this.client).contains[1](_list, _value); - return call<[ - boolean - ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).contains[1](); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - contains: [(_list: string[], _value: string) => { return codec.encodeFunctionData("3DA80D66", _list, _value); }, (_list: number[], _value: number) => { return codec.encodeFunctionData("B32B8E2C", _list, _value); }] as const - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - contains: [(): [ - boolean - ] => { return codec.decodeFunctionResult ("3DA80D66", data); }, (): [ - boolean - ] => { return codec.decodeFunctionResult ("B32B8E2C", data); }] as const - }; }; -} \ No newline at end of file diff --git a/js/src/solts/sol/Creator.abi.ts b/js/src/solts/sol/Creator.abi.ts index aa527f93d..2bcdb0ed6 100644 --- a/js/src/solts/sol/Creator.abi.ts +++ b/js/src/solts/sol/Creator.abi.ts @@ -1,101 +1,141 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; } -export module Creator { - export const abi = '[{"constant":false,"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"create","outputs":[{"internalType":"address","name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; - export const bytecode = '608060405234801561001057600080fd5b50610504806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063b6a46b3b14610030575b600080fd5b6100a76004803603602081101561004657600080fd5b810190808035906020019064010000000081111561006357600080fd5b82018360208201111561007557600080fd5b8035906020019184600183028401116401000000008311171561009757600080fd5b90919293919293905050506100e9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600082826040516100f990610153565b80806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509350505050604051809103906000f08015801561014a573d6000803e3d6000fd5b50905092915050565b61036f806101618339019056fe60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032a265627a7a72315820cf64c1da8de972eb1f9f2b5e6799c7af3385d3ee9b08b51fb8d7b060fa7eb4e864736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - create(_name: string): Promise<{ - proxy: string; +export namespace Creator { + export const contactName = 'Creator'; + export const abi = + '[{"constant":false,"inputs":[{"internalType":"string","name":"_name","type":"string"}],"name":"create","outputs":[{"internalType":"address","name":"proxy","type":"address"}],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = + '608060405234801561001057600080fd5b50610504806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c8063b6a46b3b14610030575b600080fd5b6100a76004803603602081101561004657600080fd5b810190808035906020019064010000000081111561006357600080fd5b82018360208201111561007557600080fd5b8035906020019184600183028401116401000000008311171561009757600080fd5b90919293919293905050506100e9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b600082826040516100f990610153565b80806020018281038252848482818152602001925080828437600081840152601f19601f8201169050808301925050509350505050604051809103906000f08015801561014a573d6000803e3d6000fd5b50905092915050565b61036f806101618339019056fe60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032a265627a7a72315820cf64c1da8de972eb1f9f2b5e6799c7af3385d3ee9b08b51fb8d7b060fa7eb4e864736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + create( + _name: string, + call = defaultCall, + ): Promise<{ + proxy: string; }> { - const data = encode(this.client).create(_name); - return call<{ - proxy: string; - }>(this.client, this.address, data, false, (data: Uint8Array | undefined) => { - return decode(this.client, data).create(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - create: (_name: string) => { return codec.encodeFunctionData("B6A46B3B", _name); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - create: (): { + const data = encode(client).create(_name); + return call<{ proxy: string; - } => { - const [proxy] = codec.decodeFunctionResult ("B6A46B3B", data); - return { proxy: proxy }; - } - }; }; + }>(client, address, data, false, (data: Uint8Array | undefined) => { + return decode(client, data).create(); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + create: (_name: string) => { + return codec.encodeFunctionData('B6A46B3B', _name); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + create: (): { + proxy: string; + } => { + const [proxy] = codec.decodeFunctionResult('B6A46B3B', data); + return { proxy: proxy }; + }, + }; + }; +} +export namespace Proxy { + export const contactName = 'Proxy'; + export const abi = + '[{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]'; + export const bytecode = + '60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032'; + export function deploy(client: Provider, _name: string): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy(_name)]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider, _name: string): Promise { + const address = await deploy(client, _name); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + get(call = defaultCall): Promise<[string]> { + const data = encode(client).get(); + return call<[string]>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).get(); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + get: () => { + return codec.encodeFunctionData('6D4CE63C'); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + get: (): [string] => { + return codec.decodeFunctionResult('6D4CE63C', data); + }, + }; + }; } -export module Proxy { - export const abi = '[{"inputs":[{"internalType":"string","name":"_name","type":"string"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"string","name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"}]'; - export const bytecode = '60806040526040518060200160405280600081525060009080519060200190610029929190610131565b5034801561003657600080fd5b5060405161036f38038061036f8339818101604052602081101561005957600080fd5b810190808051604051939291908464010000000082111561007957600080fd5b8382019150602082018581111561008f57600080fd5b82518660018202830111640100000000821117156100ac57600080fd5b8083526020830192505050908051906020019080838360005b838110156100e05780820151818401526020810190506100c5565b50505050905090810190601f16801561010d5780820380516001836020036101000a031916815260200191505b50604052505050806000908051906020019061012a929190610131565b50506101d6565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061017257805160ff19168380011785556101a0565b828001600101855582156101a0579182015b8281111561019f578251825591602001919060010190610184565b5b5090506101ad91906101b1565b5090565b6101d391905b808211156101cf5760008160009055506001016101b7565b5090565b90565b61018a806101e56000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80636d4ce63c14610030575b600080fd5b6100386100b3565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561007857808201518184015260208101905061005d565b50505050905090810190601f1680156100a55780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b606060008054600181600116156101000203166002900480601f01602080910402602001604051908101604052809291908181526020018280546001816001161561010002031660029004801561014b5780601f106101205761010080835404028352916020019161014b565b820191906000526020600020905b81548152906001019060200180831161012e57829003601f168201915b505050505090509056fea265627a7a723158206774e6bd107164a63f42ab8950431a6d5e09eed4da0c066049dc9db7278bbeb364736f6c63430005110032'; - export function deploy(client: Provider, _name: string): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy(_name)]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - get(): Promise<[ - string - ]> { - const data = encode(this.client).get(); - return call<[ - string - ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).get(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - get: () => { return codec.encodeFunctionData("6D4CE63C"); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - get: (): [ - string - ] => { return codec.decodeFunctionResult ("6D4CE63C", data); } - }; }; -} \ No newline at end of file diff --git a/js/src/solts/sol/Event.abi.ts b/js/src/solts/sol/Event.abi.ts deleted file mode 100644 index e3668293b..000000000 --- a/js/src/solts/sol/Event.abi.ts +++ /dev/null @@ -1,137 +0,0 @@ -//Code generated by solts. DO NOT EDIT. -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent } from '../../index'; -interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen( - signature: string, - address: string, - callback: (err?: Error | EndOfStream, log?: LogEvent) => void, - range?: BlockRange, - ): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; -} -async function call( - client: Provider, - addr: string, - data: Uint8Array, - isSim: boolean, - callback: (exec: Uint8Array | undefined) => Output, -): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); -} -function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join('0'); - const truncated = name.slice(0, 36); - const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; - while (bytecode.indexOf(label) >= 0) { - bytecode = bytecode.replace(label, address); - } - return bytecode; -} -export namespace Contract { - export const abi = - '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"eventId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"intervalId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"eventAddress","type":"address"},{"indexed":false,"internalType":"string","name":"namespace","type":"string"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"},{"indexed":false,"internalType":"string","name":"metadata","type":"string"}],"name":"Init","type":"event"},{"constant":false,"inputs":[],"name":"announce","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; - export const bytecode = - '6080604052348015600f57600080fd5b506102128061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80638f970df014610030575b600080fd5b61003861003a565b005b7f696e74657276616c3200000000000000000000000000000000000000000000007f6576656e743100000000000000000000000000000000000000000000000000007f5f20df97ee573ab8b43581cf3ff905f3507ad2329b7efe6f92e802b4fad031c17359c99d4ebf520619ee7f806f11d90a9cac02ce06336004604051808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001806020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200184815260200180602001848103845260068152602001807f64696e696e670000000000000000000000000000000000000000000000000000815250602001848103835260098152602001807f627265616b666173740000000000000000000000000000000000000000000000815250602001848103825260178152602001807f6261636f6e2c6265616e732c656767732c746f6d61746f000000000000000000815250602001965050505050505060405180910390a356fea265627a7a723158202bd41fa5fa358aebac4419f707c61d5fc012d8362230234c07ba3481be69ae1464736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - const linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - Init( - callback: ( - err?: Error | EndOfStream, - eventName?: { - eventId: Buffer; - intervalId: Buffer; - eventAddress: string; - namespace: string; - name: string; - controller: string; - threshold: number; - metadata: string; - }, - ) => void, - range?: BlockRange, - ): EventStream { - return this.client.listen( - '5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', - this.address, - (err?: Error | EndOfStream, log?: LogEvent) => { - if (err) { - return callback(err); - } - return callback(undefined, decode(this.client, log?.getData_asU8(), log?.getTopicsList_asU8()).Init()); - }, - range, - ); - } - announce(): Promise { - const data = encode(this.client).announce(); - return call(this.client, this.address, data, false, (data: Uint8Array | undefined) => { - return decode(this.client, data).announce(); - }); - } - } - export const encode = (client: Provider) => { - const codec = client.contractCodec(abi); - return { - announce: () => { - return codec.encodeFunctionData('8F970DF0'); - }, - }; - }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { - const codec = client.contractCodec(abi); - return { - Init: (): { - eventId: Buffer; - intervalId: Buffer; - eventAddress: string; - namespace: string; - name: string; - controller: string; - threshold: number; - metadata: string; - } => { - const [ - eventId, - intervalId, - eventAddress, - namespace, - name, - controller, - threshold, - metadata, - ] = codec.decodeEventLog('5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', data, topics); - return { - eventId: eventId, - intervalId: intervalId, - eventAddress: eventAddress, - namespace: namespace, - name: name, - controller: controller, - threshold: threshold, - metadata: metadata, - }; - }, - announce: (): void => { - return; - }, - }; - }; -} diff --git a/js/src/solts/sol/Eventer.abi.ts b/js/src/solts/sol/Eventer.abi.ts new file mode 100644 index 000000000..33edc85d1 --- /dev/null +++ b/js/src/solts/sol/Eventer.abi.ts @@ -0,0 +1,246 @@ +//Code generated by solts. DO NOT EDIT. +import { Address, CallTx, ContractCodec, Event, EventStream, listenerFor, Signal } from '../../index'; +interface Provider { + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; +} +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); +} +function linker(bytecode: string, name: string, address: string): string { + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; +} +export namespace Eventer { + export const contactName = 'Eventer'; + export const abi = + '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"eventId","type":"bytes32"},{"indexed":true,"internalType":"bytes32","name":"intervalId","type":"bytes32"},{"indexed":false,"internalType":"address","name":"eventAddress","type":"address"},{"indexed":false,"internalType":"string","name":"namespace","type":"string"},{"indexed":false,"internalType":"string","name":"name","type":"string"},{"indexed":false,"internalType":"address","name":"controller","type":"address"},{"indexed":false,"internalType":"uint256","name":"threshold","type":"uint256"},{"indexed":false,"internalType":"string","name":"metadata","type":"string"}],"name":"Init","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"int256","name":"timestamp","type":"int256"},{"indexed":false,"internalType":"string","name":"place","type":"string"},{"indexed":false,"internalType":"string","name":"postalAddress","type":"string"}],"name":"MonoRampage","type":"event"},{"constant":false,"inputs":[],"name":"announce","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = + '6080604052348015600f57600080fd5b506102b58061001f6000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80638f970df014610030575b600080fd5b61003861003a565b005b607b7f6a35688e78094e86ac7dd4593423fa89415105dc68a0766b27106861ef4102146040518080602001806020018381038352600d8152602001807f53616e74612045756c61726961000000000000000000000000000000000000008152506020018381038252600a8152602001807f53616e74204a75616d65000000000000000000000000000000000000000000008152506020019250505060405180910390a27f696e74657276616c3200000000000000000000000000000000000000000000007f6576656e743100000000000000000000000000000000000000000000000000007f5f20df97ee573ab8b43581cf3ff905f3507ad2329b7efe6f92e802b4fad031c17359c99d4ebf520619ee7f806f11d90a9cac02ce06336004604051808473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200180602001806020018573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200184815260200180602001848103845260068152602001807f64696e696e670000000000000000000000000000000000000000000000000000815250602001848103835260098152602001807f627265616b666173740000000000000000000000000000000000000000000000815250602001848103825260178152602001807f6261636f6e2c6265616e732c656767732c746f6d61746f000000000000000000815250602001965050505050505060405180910390a356fea265627a7a72315820a6cd65593c9a8f5ed08f75519d9dd5664bcdf3a4bb67c8f507e1a69ed348f04f64736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + type EventRegistry = typeof events; + export type EventName = keyof EventRegistry; + export type TaggedPayload = ReturnType & { + event: Event; + }; + export type SolidityEvent = TaggedPayload['payload']; + export type TypedListener = ( + callback: (err?: Error, event?: TaggedPayload) => void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ) => EventStream; + const events = { + Init: { + signature: '5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', + tagged: ( + eventId: Buffer, + intervalId: Buffer, + eventAddress: string, + namespace: string, + name: string, + controller: string, + threshold: number, + metadata: string, + ) => + ({ + name: 'Init', + payload: { + eventId: eventId, + intervalId: intervalId, + eventAddress: eventAddress, + namespace: namespace, + name: name, + controller: controller, + threshold: threshold, + metadata: metadata, + } as const, + } as const), + } as const, + MonoRampage: { + signature: '6A35688E78094E86AC7DD4593423FA89415105DC68A0766B27106861EF410214', + tagged: (timestamp: number, place: string, postalAddress: string) => + ({ + name: 'MonoRampage', + payload: { timestamp: timestamp, place: place, postalAddress: postalAddress } as const, + } as const), + } as const, + } as const; + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + announce(call = defaultCall): Promise { + const data = encode(client).announce(); + return call(client, address, data, false, (data: Uint8Array | undefined) => { + return decode(client, data).announce(); + }); + }, + } as const, + listeners: { + Init( + callback: ( + err?: Error, + event?: { + eventId: Buffer; + intervalId: Buffer; + eventAddress: string; + namespace: string; + name: string; + controller: string; + threshold: number; + metadata: string; + }, + ) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream { + return client.listen( + ['5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1'], + address, + (err?: Error, event?: Event) => { + if (err) { + return callback(err); + } + return callback( + undefined, + decode(client, event?.getLog()?.getData_asU8(), event?.getLog()?.getTopicsList_asU8()).Init(), + ); + }, + start, + end, + ); + }, + MonoRampage( + callback: ( + err?: Error, + event?: { + timestamp: number; + place: string; + postalAddress: string; + }, + ) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream { + return client.listen( + ['6A35688E78094E86AC7DD4593423FA89415105DC68A0766B27106861EF410214'], + address, + (err?: Error, event?: Event) => { + if (err) { + return callback(err); + } + return callback( + undefined, + decode(client, event?.getLog()?.getData_asU8(), event?.getLog()?.getTopicsList_asU8()).MonoRampage(), + ); + }, + start, + end, + ); + }, + } as const, + listenerFor: (eventNames: T[]): TypedListener => + (listenerFor(client, address, events, decode, eventNames) as unknown) as TypedListener, + listener: listenerFor( + client, + address, + events, + decode, + Object.keys(events) as EventName[], + ) as TypedListener, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + announce: () => { + return codec.encodeFunctionData('8F970DF0'); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + Init: (): { + eventId: Buffer; + intervalId: Buffer; + eventAddress: string; + namespace: string; + name: string; + controller: string; + threshold: number; + metadata: string; + } => { + const [ + eventId, + intervalId, + eventAddress, + namespace, + name, + controller, + threshold, + metadata, + ] = codec.decodeEventLog('5F20DF97EE573AB8B43581CF3FF905F3507AD2329B7EFE6F92E802B4FAD031C1', data, topics); + return { + eventId: eventId, + intervalId: intervalId, + eventAddress: eventAddress, + namespace: namespace, + name: name, + controller: controller, + threshold: threshold, + metadata: metadata, + }; + }, + MonoRampage: (): { + timestamp: number; + place: string; + postalAddress: string; + } => { + const [timestamp, place, postalAddress] = codec.decodeEventLog( + '6A35688E78094E86AC7DD4593423FA89415105DC68A0766B27106861EF410214', + data, + topics, + ); + return { timestamp: timestamp, place: place, postalAddress: postalAddress }; + }, + announce: (): void => { + return; + }, + }; + }; +} diff --git a/js/src/solts/sol/Event.sol b/js/src/solts/sol/Eventer.sol similarity index 74% rename from js/src/solts/sol/Event.sol rename to js/src/solts/sol/Eventer.sol index 9f7063022..1c5510cd1 100644 --- a/js/src/solts/sol/Event.sol +++ b/js/src/solts/sol/Eventer.sol @@ -1,6 +1,12 @@ pragma solidity >=0.0.0; -contract Contract { +contract Eventer { + event MonoRampage( + int indexed timestamp, + string place, + string postalAddress + ); + event Init( bytes32 indexed eventId, bytes32 indexed intervalId, @@ -13,6 +19,7 @@ contract Contract { ); function announce() public { + emit MonoRampage(123, "Santa Eularia", "Sant Juame"); emit Init(bytes32("event1"), bytes32("interval2"), 0x59C99d4EbF520619ee7F806f11d90a9cac02CE06, diff --git a/js/src/solts/sol/MultipleReturns.abi.ts b/js/src/solts/sol/MultipleReturns.abi.ts index bc2dbf87e..44802ff0d 100644 --- a/js/src/solts/sol/MultipleReturns.abi.ts +++ b/js/src/solts/sol/MultipleReturns.abi.ts @@ -1,67 +1,84 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; +} +export namespace Multiple { + export const contactName = 'Multiple'; + export const abi = + '[{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = + '6080604052348015600f57600080fd5b5060ab8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80636d4ce63c14602d575b600080fd5b60336057565b60405180848152602001838152602001828152602001935050505060405180910390f35b600080600060016002600382925081915080905092509250925090919256fea265627a7a72315820db13aa50f1e1ad94cdc8fb2b158b105d0d0812d53d9851ed8a9bfa5447e3c17864736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + get(call = defaultCall): Promise<[number, number, number]> { + const data = encode(client).get(); + return call<[number, number, number]>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).get(); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + get: () => { + return codec.encodeFunctionData('6D4CE63C'); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + get: (): [number, number, number] => { + return codec.decodeFunctionResult('6D4CE63C', data); + }, + }; + }; } -export module Multiple { - export const abi = '[{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; - export const bytecode = '6080604052348015600f57600080fd5b5060ab8061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80636d4ce63c14602d575b600080fd5b60336057565b60405180848152602001838152602001828152602001935050505060405180910390f35b600080600060016002600382925081915080905092509250925090919256fea265627a7a72315820db13aa50f1e1ad94cdc8fb2b158b105d0d0812d53d9851ed8a9bfa5447e3c17864736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - get(): Promise<[ - number, - number, - number - ]> { - const data = encode(this.client).get(); - return call<[ - number, - number, - number - ]>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).get(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - get: () => { return codec.encodeFunctionData("6D4CE63C"); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - get: (): [ - number, - number, - number - ] => { return codec.decodeFunctionResult ("6D4CE63C", data); } - }; }; -} \ No newline at end of file diff --git a/js/src/solts/sol/Storage.abi.ts b/js/src/solts/sol/Storage.abi.ts index 51a88241e..cd6966519 100644 --- a/js/src/solts/sol/Storage.abi.ts +++ b/js/src/solts/sol/Storage.abi.ts @@ -1,72 +1,105 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; } -export module Storage { - export const abi = '[{"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"ret","type":"int256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; - export const bytecode = '608060405234801561001057600080fd5b506040516101203803806101208339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505060c68061005a6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80636d4ce63c146037578063e5c19b2d146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea265627a7a72315820f111616fdeed05afb6d4e43891f3c42451310bf48d12f72b2864c23005c11fb664736f6c63430005110032'; - export function deploy(client: Provider, x: number): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy(x)]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - get(): Promise<{ - ret: number; +export namespace Storage { + export const contactName = 'Storage'; + export const abi = + '[{"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"payable":false,"stateMutability":"nonpayable","type":"constructor"},{"constant":true,"inputs":[],"name":"get","outputs":[{"internalType":"int256","name":"ret","type":"int256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"int256","name":"x","type":"int256"}],"name":"set","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}]'; + export const bytecode = + '608060405234801561001057600080fd5b506040516101203803806101208339818101604052602081101561003357600080fd5b8101908080519060200190929190505050806000819055505060c68061005a6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c80636d4ce63c146037578063e5c19b2d146053575b600080fd5b603d607e565b6040518082815260200191505060405180910390f35b607c60048036036020811015606757600080fd5b81019080803590602001909291905050506087565b005b60008054905090565b806000819055505056fea265627a7a72315820f111616fdeed05afb6d4e43891f3c42451310bf48d12f72b2864c23005c11fb664736f6c63430005110032'; + export function deploy(client: Provider, x: number): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy(x)]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider, x: number): Promise { + const address = await deploy(client, x); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + get( + call = defaultCall, + ): Promise<{ + ret: number; }> { - const data = encode(this.client).get(); - return call<{ - ret: number; - }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).get(); - }); - } - set(x: number): Promise { - const data = encode(this.client).set(x); - return call(this.client, this.address, data, false, (data: Uint8Array | undefined) => { - return decode(this.client, data).set(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - get: () => { return codec.encodeFunctionData("6D4CE63C"); }, - set: (x: number) => { return codec.encodeFunctionData("E5C19B2D", x); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - get: (): { + const data = encode(client).get(); + return call<{ ret: number; - } => { - const [ret] = codec.decodeFunctionResult ("6D4CE63C", data); - return { ret: ret }; + }>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).get(); + }); + }, + set(x: number, call = defaultCall): Promise { + const data = encode(client).set(x); + return call(client, address, data, false, (data: Uint8Array | undefined) => { + return decode(client, data).set(); + }); }, - set: (): void => { return; } - }; }; -} \ No newline at end of file + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + get: () => { + return codec.encodeFunctionData('6D4CE63C'); + }, + set: (x: number) => { + return codec.encodeFunctionData('E5C19B2D', x); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + get: (): { + ret: number; + } => { + const [ret] = codec.decodeFunctionResult('6D4CE63C', data); + return { ret: ret }; + }, + set: (): void => { + return; + }, + }; + }; +} diff --git a/js/src/solts/sol/Unnamed.abi.ts b/js/src/solts/sol/Unnamed.abi.ts index 84e8aae16..3ba22926d 100644 --- a/js/src/solts/sol/Unnamed.abi.ts +++ b/js/src/solts/sol/Unnamed.abi.ts @@ -1,64 +1,94 @@ //Code generated by solts. DO NOT EDIT. -import { Readable } from "stream"; -import { Address, BlockRange, CallTx, ContractCodec, EndOfStream, EventStream, LogEvent, Result } from "../../index"; +import { Address, CallTx, ContractCodec, Event, EventStream, Signal } from '../../index'; interface Provider { - deploy(msg: CallTx): Promise
; - call(msg: CallTx): Promise; - callSim(msg: CallTx): Promise; - listen(signature: string, address: string, callback: (err?: Error | EndOfStream, log?: LogEvent) => void, range?: BlockRange): EventStream; - payload(data: string | Uint8Array, address?: string): CallTx; - contractCodec(contractABI: string): ContractCodec; + deploy(msg: CallTx): Promise
; + call(msg: CallTx): Promise; + callSim(msg: CallTx): Promise; + listen( + signatures: string[], + address: string, + callback: (err?: Error, event?: Event) => Signal | void, + start?: 'first' | 'latest' | 'stream' | number, + end?: 'first' | 'latest' | 'stream' | number, + ): EventStream; + payload(data: string | Uint8Array, address?: string): CallTx; + contractCodec(contractABI: string): ContractCodec; } -async function call(client: Provider, addr: string, data: Uint8Array, isSim: boolean, callback: (exec: Uint8Array | undefined) => Output): Promise { - const payload = client.payload(data, addr); - const txe = await (isSim ? client.callSim(payload) : client.call(payload)); - return callback(txe); +export type Caller = typeof defaultCall; +export async function defaultCall( + client: Provider, + addr: string, + data: Uint8Array, + isSim: boolean, + callback: (returnData: Uint8Array | undefined) => Output, +): Promise { + const payload = client.payload(data, addr); + const returnData = await (isSim ? client.callSim(payload) : client.call(payload)); + return callback(returnData); } function linker(bytecode: string, name: string, address: string): string { - address = address + Array(40 - address.length + 1).join("0"); - const truncated = name.slice(0, 36); - const label = "__" + truncated + Array(37 - truncated.length).join("_") + "__"; - while (bytecode.indexOf(label) >= 0) - bytecode = bytecode.replace(label, address); - return bytecode; + address = address + Array(40 - address.length + 1).join('0'); + const truncated = name.slice(0, 36); + const label = '__' + truncated + Array(37 - truncated.length).join('_') + '__'; + while (bytecode.indexOf(label) >= 0) { + bytecode = bytecode.replace(label, address); + } + return bytecode; } -export module Unnamed { - export const abi = '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"name":"set","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; - export const bytecode = '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806304c402f414602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600082830190509291505056fea265627a7a72315820c692aa5593a39c498f58c4a0221aa0b6ca567436b02c62f90b9214af7b7c390064736f6c63430005110032'; - export function deploy(client: Provider): Promise { - const codec = client.contractCodec(abi); - let linkedBytecode = bytecode; - const data = Buffer.concat([Buffer.from(linkedBytecode, "hex"), codec.encodeDeploy()]); - const payload = client.payload(data); - return client.deploy(payload); - } - export class Contract { - private client: Provider; - public address: string; - constructor(client: Provider, address: string) { - this.client = client; - this.address = address; - } - set(a: number): Promise<{ - sum: number; +export namespace Unnamed { + export const contactName = 'Unnamed'; + export const abi = + '[{"constant":true,"inputs":[{"internalType":"int256","name":"a","type":"int256"},{"internalType":"int256","name":"","type":"int256"}],"name":"set","outputs":[{"internalType":"int256","name":"sum","type":"int256"}],"payable":false,"stateMutability":"pure","type":"function"}]'; + export const bytecode = + '608060405234801561001057600080fd5b5060b88061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c806304c402f414602d575b600080fd5b606060048036036040811015604157600080fd5b8101908080359060200190929190803590602001909291905050506076565b6040518082815260200191505060405180910390f35b600082830190509291505056fea265627a7a72315820c692aa5593a39c498f58c4a0221aa0b6ca567436b02c62f90b9214af7b7c390064736f6c63430005110032'; + export function deploy(client: Provider): Promise { + const codec = client.contractCodec(abi); + const linkedBytecode = bytecode; + const data = Buffer.concat([Buffer.from(linkedBytecode, 'hex'), codec.encodeDeploy()]); + const payload = client.payload(data); + return client.deploy(payload); + } + export async function deployContract(client: Provider): Promise { + const address = await deploy(client); + return contract(client, address); + } + export type Contract = ReturnType; + export const contract = (client: Provider, address: string) => + ({ + address, + functions: { + set( + a: number, + call = defaultCall, + ): Promise<{ + sum: number; }> { - const data = encode(this.client).set(a); - return call<{ - sum: number; - }>(this.client, this.address, data, true, (data: Uint8Array | undefined) => { - return decode(this.client, data).set(); - }); - } - } - export const encode = (client: Provider) => { const codec = client.contractCodec(abi); return { - set: (a: number) => { return codec.encodeFunctionData("04C402F4", a); } - }; }; - export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { const codec = client.contractCodec(abi); return { - set: (): { + const data = encode(client).set(a); + return call<{ sum: number; - } => { - const [sum] = codec.decodeFunctionResult ("04C402F4", data); - return { sum: sum }; - } - }; }; -} \ No newline at end of file + }>(client, address, data, true, (data: Uint8Array | undefined) => { + return decode(client, data).set(); + }); + }, + } as const, + } as const); + export const encode = (client: Provider) => { + const codec = client.contractCodec(abi); + return { + set: (a: number) => { + return codec.encodeFunctionData('04C402F4', a); + }, + }; + }; + export const decode = (client: Provider, data: Uint8Array | undefined, topics: Uint8Array[] = []) => { + const codec = client.contractCodec(abi); + return { + set: (): { + sum: number; + } => { + const [sum] = codec.decodeFunctionResult('04C402F4', data); + return { sum: sum }; + }, + }; + }; +} diff --git a/js/src/test/solts.test.ts b/js/src/test/solts.test.ts index 763d0cbf5..dfe1c8fdb 100644 --- a/js/src/test/solts.test.ts +++ b/js/src/test/solts.test.ts @@ -1,28 +1,43 @@ import * as assert from 'assert'; import { readEvents } from '../events'; import { Addition } from '../solts/sol/Addition.abi'; -import { Contract } from '../solts/sol/Event.abi'; +import { Eventer } from '../solts/sol/Eventer.abi'; import { burrow } from './test'; describe('solts', () => { it('can deploy and call from codegen', async () => { const address = await Addition.deploy(burrow); - const add = new Addition.Contract(burrow, address); - const { sum } = await add.add(2342, 23432); + const add = Addition.contract(burrow, address); + const { sum } = await add.functions.add(2342, 23432); assert.strictEqual(sum, 25774); }); it('can receive events', async () => { - const address = await Contract.deploy(burrow); - const eventer = new Contract.Contract(burrow, address); - const height = await burrow.latestHeight(); - await eventer.announce(); - await eventer.announce(); - await eventer.announce(); - const events = await readEvents(eventer.Init.bind(eventer)); + const eventer = Eventer.contract(burrow, await Eventer.deploy(burrow)); + await eventer.functions.announce(); + await eventer.functions.announce(); + await eventer.functions.announce(); + const events = await readEvents(eventer.listeners.Init); assert.strictEqual(events.length, 3); const event = events[0]; assert.strictEqual(event.controller, 'C9F239591C593CB8EE192B0009C6A0F2C9F8D768'); assert.strictEqual(event.metadata, 'bacon,beans,eggs,tomato'); }); + + it('can listen to multiple events', async () => { + const eventer = Eventer.contract(burrow, await Eventer.deploy(burrow)); + await eventer.functions.announce(); + await eventer.functions.announce(); + const listener = eventer.listenerFor(['MonoRampage', 'Init']); + const events = await readEvents(listener); + assert.strictEqual(events.length, 4); + // Look ma, type narrowing! + events.map((event) => { + if (event.name === 'Init') { + assert.strictEqual(event.payload.eventId, '6576656E74310000000000000000000000000000000000000000000000000000'); + } else if (event.name === 'MonoRampage') { + assert.strictEqual(event.payload.timestamp, 123); + } + }); + }); }); diff --git a/project/history.go b/project/history.go index 169d84b0e..d2814ecd1 100644 --- a/project/history.go +++ b/project/history.go @@ -47,7 +47,20 @@ func FullVersion() string { // To cut a new release add a release to the front of this slice then run the // release tagging script: ./scripts/tag_release.sh var History relic.ImmutableHistory = relic.NewHistory("Hyperledger Burrow", "https://github.com/hyperledger/burrow"). - MustDeclareReleases("0.32.1 - 2021-05-15", + MustDeclareReleases("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 - [JS] Return byte arrays as Buffers from decode (only return fixed-width byteNN types as hex strings)