abi-gen/Py: fix incorrect method return types and other small issues (#2345)

* .gitignore gen'd Python staking contract wrappers

* abi-gen/test-cli: check Python type hints in lint

* sra_client.py: Update doc for replicating examples

* abi-gen/Py: fix call() return type incl. tx hash

Previously, generated wrappers for contract methods were including type
hints that suggested that a call() (as opposed to a send_transaction())
might return either the underlying return type or a transaction hash.
This doesn't make sense because a call() will never return a TX hash.
Now, the type hint just has the return type of the underlying method.

* abi-gen: fix test_cli:lint checking wrong code

test_cli:lint is meant to be a rudimentary test of the code generated by
abi-gen.  However, previously, this script was incorporated into `yarn
lint`, and in CircleCI `static-tests` runs independently of `build`.
Consequently, the runs of test_cli:lint were checking the OLD code,
which was previously generated and checked in to git, NOT the code
generated with the version of abi-gen represented by the git repo.  Now,
test_cli:lint happens during `yarn test` rather than `yarn lint`,
because `yarn test` IS dependent on `yarn build`.

* contract_wrappers.py: fix misplaced doc

Previously, the routines `order_to_jsdict()` and `jsdict_to_order()`
were moved from contract_wrappers.exchange.types to
contract_wrappers.order_conversions.  However, the module-level
docstring describing those routines was accidentally left behind in
exchange.types.

* abi-gen/Py: stop documenting return types for TXs

Previously the send_transaction() interface included docstring
documentation for the return types of the contract method, but that
doesn't make any sense because send_transaction() returns a transaction
hash rather than any actual return values.

* abi-gen/Py: stop gen'ing send_tx for const methods

* abi-gen/Py: add build_tx to contract methods

* abi-gen/Py: fix incorrect method return types

Fixes #2298 .

* abi-gen/Py: rm validator arg to no-input methods

* abi-gen: mv Py Handlebars helpers to own module

Move all existing Python-related Handlebars helpers to the newly created
python_handlebars_helpers module.

* abi-gen: refactor internal interface

No functionality is changed.  Sole purpose of this commit is to
facilitate an upcoming commit.

* abi-gen: refactor internal interface

No functionality is changed.  Sole purpose of this commit is to
facilitate an upcoming commit.

* abi-gen/Py: name tuples w/internalType, not hash

Use the new `internalType` field on the `DataItem`s in the contract
artifact to give generated tuple classes a better name than just hashing
their component field names.

* Fix CI errors

* abi-gen/Py/wrapper: make internal member private

* Update CHANGELOGs
This commit is contained in:
F. Eugene Aumson 2019-11-15 18:27:45 -05:00 committed by GitHub
parent 9e3cc379ed
commit df97b20913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 679 additions and 847 deletions

2
.gitignore vendored
View File

@ -175,6 +175,8 @@ python-packages/contract_wrappers/src/zero_ex/contract_wrappers/i_validator/__in
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/i_wallet/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/multi_asset_proxy/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/order_validator/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/staking/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/staking_proxy/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/static_call_proxy/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/weth9/__init__.py
python-packages/contract_wrappers/src/zero_ex/contract_wrappers/zrx_token/__init__.py

View File

@ -5,6 +5,26 @@
{
"note": "Refactored TS wrapper templates to result in a more succint interface. See https://github.com/0xProject/0x-monorepo/pull/2325 for details.",
"pr": 2284
},
{
"note": "Python: Corrected return types and values for call() interface to generated method wrappers. (Fixes #2298)",
"pr": 2345
},
{
"note": "Python: Stopped generating send_transaction() interface for constant (view/pure) methods",
"pr": 2345
},
{
"note": "Python: Added a build_transaction() interface to contract method classes",
"pr": 2345
},
{
"note": "Python: Removed `validator` argument to contract method classes for methods that don't have any inputs",
"pr": 2345
},
{
"note": "Python: Changed the names of generated tuples to use the `internalType` field in the ABI, if it's present, resulting in human readable struct names rather than hashes of component field names.",
"pr": 2345
}
]
},

View File

@ -8,7 +8,7 @@
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"scripts": {
"lint": "tslint --format stylish --project . && yarn test_cli:lint",
"lint": "tslint --format stylish --project .",
"fix": "tslint --fix --format stylish --project . && yarn lint-contracts",
"clean": "shx rm -rf lib && yarn test_cli:clean",
"build": "tsc -b && yarn generate_contract_wrappers && yarn prettier_contract_wrappers && yarn test_cli:build",
@ -18,15 +18,16 @@
"run_mocha": "(uname -s | grep -q Darwin && echo 'HACK! skipping mocha run due to https://github.com/0xProject/0x-monorepo/issues/2000') || mocha --require source-map-support/register --require make-promises-safe lib/test/*_test.js --timeout 100000 --bail --exit",
"test:coverage": "nyc npm run test --all && yarn coverage:report:lcov",
"coverage:report:lcov": "nyc report --reporter=text-lcov > coverage/lcov.info",
"test_cli": "run-s test_cli:test_typescript diff_contract_wrappers",
"test_cli": "run-p test_cli:test_typescript diff_contract_wrappers test_cli:lint",
"test_cli:clean": "rm -rf test-cli/test_typescript/lib",
"test_cli:build": "tsc --project test-cli/tsconfig.json",
"test_cli:test_typescript": "mocha --require source-map-support/register --require make-promises-safe test-cli/test_typescript/lib/**/*_test.js --timeout 100000 --bail --exit",
"test_cli:lint": "run-p test_cli:lint_solidity test_cli:lint_python # test_cli:lint_typescript # HACK: typescript lint disabled because prettier fails",
"test_cli:lint_solidity": "solhint -c ../../contracts/.solhint.json test-cli/fixtures/contracts/*.sol",
"test_cli:lint_typescript": "prettier --check ./test-cli/output/typescript/* --config ../../.prettierrc",
"test_cli:lint_python": "pip install -r test-cli/fixtures/python-requirements.txt && run-p test_cli:lint_python:black test_cli:lint_python:pylint",
"test_cli:lint_python": "pip install -r test-cli/fixtures/python-requirements.txt && run-p test_cli:lint_python:black test_cli:lint_python:mypy test_cli:lint_python:pylint",
"test_cli:lint_python:black": "black --line-length=79 --check ./test-cli/output/python/*",
"test_cli:lint_python:mypy": "MYPYPATH=./stubs mypy ./test-cli/output/python",
"test_cli:lint_python:pylint": "PYTHONPATH=../../python-packages/contract_wrappers/src pylint --rcfile=test-cli/fixtures/pylintrc ./test-cli/output/python/*",
"rebuild_and_test": "run-s build test",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",

View File

@ -4,22 +4,14 @@ import chalk from 'chalk';
import * as changeCase from 'change-case';
import { execSync } from 'child_process';
import * as cliFormat from 'cli-format';
import {
AbiDefinition,
ConstructorAbi,
ContractAbi,
DataItem,
DevdocOutput,
EventAbi,
MethodAbi,
} from 'ethereum-types';
import { AbiDefinition, ConstructorAbi, ContractAbi, DevdocOutput, EventAbi, MethodAbi } from 'ethereum-types';
import { sync as globSync } from 'glob';
import * as Handlebars from 'handlebars';
import * as _ from 'lodash';
import * as mkdirp from 'mkdirp';
import toposort = require('toposort');
import * as yargs from 'yargs';
import { registerPythonHelpers } from './python_handlebars_helpers';
import { ContextData, ContractsBackend, ParamKind } from './types';
import { utils } from './utils';
@ -146,154 +138,6 @@ function registerTypeScriptHelpers(): void {
);
}
function registerPythonHelpers(): void {
Handlebars.registerHelper('equal', (lhs: any, rhs: any) => {
return lhs === rhs;
});
Handlebars.registerHelper('safeString', (str: string) => new Handlebars.SafeString(str));
Handlebars.registerHelper('parameterType', utils.solTypeToPyType.bind(utils));
Handlebars.registerHelper('returnType', utils.solTypeToPyType.bind(utils));
Handlebars.registerHelper('toPythonIdentifier', utils.toPythonIdentifier.bind(utils));
Handlebars.registerHelper('sanitizeDevdocDetails', (_methodName: string, devdocDetails: string, indent: number) => {
// wrap to 80 columns, assuming given indent, so that generated
// docstrings can pass pycodestyle checks. also, replace repeated
// spaces, likely caused by leading indents in the Solidity, because
// they cause repeated spaces in the output, and in particular they may
// cause repeated spaces at the beginning of a line in the docstring,
// which leads to "unexpected indent" errors when generating
// documentation.
if (devdocDetails === undefined || devdocDetails.length === 0) {
return '';
}
const columnsPerRow = 80;
return new Handlebars.SafeString(
`\n${cliFormat.wrap(devdocDetails.replace(/ +/g, ' ') || '', {
paddingLeft: ' '.repeat(indent),
width: columnsPerRow,
ansi: false,
})}\n`,
);
});
Handlebars.registerHelper('makeParameterDocstringRole', (name: string, description: string, indent: number) => {
let docstring = `:param ${name}:`;
if (description && description.length > 0) {
docstring = `${docstring} ${description}`;
}
return new Handlebars.SafeString(utils.wrapPythonDocstringRole(docstring, indent));
});
Handlebars.registerHelper(
'makeReturnDocstringRole',
(description: string, indent: number) =>
new Handlebars.SafeString(
utils.wrapPythonDocstringRole(`:returns: ${description.replace(/ +/g, ' ')}`, indent),
),
);
Handlebars.registerHelper(
'makeEventParameterDocstringRole',
(eventName: string, indent: number) =>
new Handlebars.SafeString(
utils.wrapPythonDocstringRole(
`:param tx_hash: hash of transaction emitting ${eventName} event`,
indent,
),
),
);
Handlebars.registerHelper('tupleDefinitions', (abisJSON: string) => {
const abis: AbiDefinition[] = JSON.parse(abisJSON);
// build an array of objects, each of which has one key, the Python
// name of a tuple, with a string value holding the body of a Python
// class representing that tuple. Using a key-value object conveniently
// filters duplicate references to the same tuple.
const tupleBodies: { [pythonTupleName: string]: string } = {};
// build an array of tuple dependencies, whose format conforms to the
// expected input to toposort, a function to do a topological sort,
// which will help us declare tuples in the proper order, avoiding
// references to tuples that haven't been declared yet.
const tupleDependencies: Array<[string, string]> = [];
for (const abi of abis) {
let parameters: DataItem[] = [];
if (abi.hasOwnProperty('inputs')) {
// HACK(feuGeneA): using "as MethodAbi" below, but abi
// could just as well be ConstructorAbi, EventAbi, etc. We
// just need to tell the TypeScript compiler that it's NOT
// FallbackAbi, or else it would complain, "Property
// 'inputs' does not exist on type 'AbiDefinition'.
// Property 'inputs' does not exist on type
// 'FallbackAbi'.", despite the enclosing if statement.
// tslint:disable:no-unnecessary-type-assertion
parameters = parameters.concat((abi as MethodAbi).inputs);
}
if (abi.hasOwnProperty('outputs')) {
// HACK(feuGeneA): same as described above, except here we
// KNOW that it's a MethodAbi, given the enclosing if
// statement, because that's the only AbiDefinition subtype
// that actually has an outputs field.
parameters = parameters.concat((abi as MethodAbi).outputs);
}
for (const parameter of parameters) {
utils.extractTuples(parameter, tupleBodies, tupleDependencies);
}
}
// build up a list of tuples to declare. the order they're pushed into
// this array is the order they will be declared.
const tuplesToDeclare = [];
// first push the ones that have dependencies
tuplesToDeclare.push(...toposort(tupleDependencies));
// then push any remaining bodies (the ones that DON'T have
// dependencies)
for (const pythonTupleName in tupleBodies) {
if (!tuplesToDeclare.includes(pythonTupleName)) {
tuplesToDeclare.push(pythonTupleName);
}
}
// now iterate over those ordered tuples-to-declare, and prefix the
// corresponding class bodies with their class headers, to form full
// class declarations.
const tupleDeclarations = [];
for (const pythonTupleName of tuplesToDeclare) {
if (tupleBodies[pythonTupleName]) {
tupleDeclarations.push(
`class ${pythonTupleName}(TypedDict):\n """Python representation of a tuple or struct.\n\n Solidity compiler output does not include the names of structs that appear\n in method definitions. A tuple found in an ABI may have been written in\n Solidity as a literal, anonymous tuple, or it may have been written as a\n named \`struct\`:code:, but there is no way to tell from the compiler\n output. This class represents a tuple that appeared in a method\n definition. Its name is derived from a hash of that tuple's field names,\n and every method whose ABI refers to a tuple with that same list of field\n names will have a generated wrapper method that refers to this class.\n\n Any members of type \`bytes\`:code: should be encoded as UTF-8, which can be\n accomplished via \`str.encode("utf_8")\`:code:\n """${
tupleBodies[pythonTupleName]
}`,
);
}
}
// finally, join the class declarations together for the output file
return new Handlebars.SafeString(tupleDeclarations.join('\n\n\n'));
});
Handlebars.registerHelper('docBytesIfNecessary', (abisJSON: string) => {
const abis: AbiDefinition[] = JSON.parse(abisJSON);
// see if any ABIs accept params of type bytes, and if so then emit
// explanatory documentation string.
for (const abi of abis) {
if (abi.hasOwnProperty('inputs')) {
// HACK(feuGeneA): using "as MethodAbi" below, but abi
// could just as well be ConstructorAbi, EventAbi, etc. We
// just need to tell the TypeScript compiler that it's NOT
// FallbackAbi, or else it would complain, "Property
// 'inputs' does not exist on type 'AbiDefinition'.
// Property 'inputs' does not exist on type
// 'FallbackAbi'.", despite the enclosing if statement.
// tslint:disable:no-unnecessary-type-assertion
if ((abi as MethodAbi).inputs) {
for (const input of (abi as MethodAbi).inputs) {
if (input.type === 'bytes') {
return new Handlebars.SafeString(
'\n\n All method parameters of type `bytes`:code: should be encoded as UTF-8,\n which can be accomplished via `str.encode("utf_8")`:code:.\n ',
);
}
}
}
}
}
return '';
});
Handlebars.registerHelper(
'toPythonClassname',
(sourceName: string) => new Handlebars.SafeString(changeCase.pascal(sourceName)),
);
}
if (args.language === 'TypeScript') {
registerTypeScriptHelpers();
} else if (args.language === 'Python') {

View File

@ -0,0 +1,205 @@
import * as changeCase from 'change-case';
import * as cliFormat from 'cli-format';
import * as Handlebars from 'handlebars';
import toposort = require('toposort');
import { AbiDefinition, DataItem, MethodAbi } from 'ethereum-types';
import { utils } from './utils';
/**
* Register all Python-related Handlebars helpers
*/
export function registerPythonHelpers(): void {
Handlebars.registerHelper('equal', (lhs: any, rhs: any) => {
return lhs === rhs;
});
Handlebars.registerHelper('safeString', (str: string) => new Handlebars.SafeString(str));
Handlebars.registerHelper('parameterType', utils.solTypeToPyType.bind(utils));
Handlebars.registerHelper('returnType', utils.solTypeToPyType.bind(utils));
Handlebars.registerHelper('toPythonIdentifier', utils.toPythonIdentifier.bind(utils));
Handlebars.registerHelper('sanitizeDevdocDetails', (_methodName: string, devdocDetails: string, indent: number) => {
// wrap to 80 columns, assuming given indent, so that generated
// docstrings can pass pycodestyle checks. also, replace repeated
// spaces, likely caused by leading indents in the Solidity, because
// they cause repeated spaces in the output, and in particular they may
// cause repeated spaces at the beginning of a line in the docstring,
// which leads to "unexpected indent" errors when generating
// documentation.
if (devdocDetails === undefined || devdocDetails.length === 0) {
return '';
}
const columnsPerRow = 80;
return new Handlebars.SafeString(
`\n${cliFormat.wrap(devdocDetails.replace(/ +/g, ' ') || '', {
paddingLeft: ' '.repeat(indent),
width: columnsPerRow,
ansi: false,
})}\n`,
);
});
Handlebars.registerHelper('makeParameterDocstringRole', (name: string, description: string, indent: number) => {
let docstring = `:param ${name}:`;
if (description && description.length > 0) {
docstring = `${docstring} ${description}`;
}
return new Handlebars.SafeString(utils.wrapPythonDocstringRole(docstring, indent));
});
Handlebars.registerHelper(
'makeReturnDocstringRole',
(description: string, indent: number) =>
new Handlebars.SafeString(
utils.wrapPythonDocstringRole(`:returns: ${description.replace(/ +/g, ' ')}`, indent),
),
);
Handlebars.registerHelper(
'makeEventParameterDocstringRole',
(eventName: string, indent: number) =>
new Handlebars.SafeString(
utils.wrapPythonDocstringRole(
`:param tx_hash: hash of transaction emitting ${eventName} event`,
indent,
),
),
);
Handlebars.registerHelper('tupleDefinitions', (abisJSON: string) => {
const abis: AbiDefinition[] = JSON.parse(abisJSON);
// build an array of objects, each of which has one key, the Python
// name of a tuple, with a string value holding the body of a Python
// class representing that tuple. Using a key-value object conveniently
// filters duplicate references to the same tuple.
const tupleBodies: { [pythonTupleName: string]: string } = {};
// build an array of tuple dependencies, whose format conforms to the
// expected input to toposort, a function to do a topological sort,
// which will help us declare tuples in the proper order, avoiding
// references to tuples that haven't been declared yet.
const tupleDependencies: Array<[string, string]> = [];
for (const abi of abis) {
let parameters: DataItem[] = [];
if (abi.hasOwnProperty('inputs')) {
// HACK(feuGeneA): using "as MethodAbi" below, but abi
// could just as well be ConstructorAbi, EventAbi, etc. We
// just need to tell the TypeScript compiler that it's NOT
// FallbackAbi, or else it would complain, "Property
// 'inputs' does not exist on type 'AbiDefinition'.
// Property 'inputs' does not exist on type
// 'FallbackAbi'.", despite the enclosing if statement.
// tslint:disable:no-unnecessary-type-assertion
parameters = parameters.concat((abi as MethodAbi).inputs);
}
if (abi.hasOwnProperty('outputs')) {
// HACK(feuGeneA): same as described above, except here we
// KNOW that it's a MethodAbi, given the enclosing if
// statement, because that's the only AbiDefinition subtype
// that actually has an outputs field.
parameters = parameters.concat((abi as MethodAbi).outputs);
}
for (const parameter of parameters) {
utils.extractTuples(parameter, tupleBodies, tupleDependencies);
}
}
// build up a list of tuples to declare. the order they're pushed into
// this array is the order they will be declared.
const tuplesToDeclare = [];
// first push the ones that have dependencies
tuplesToDeclare.push(...toposort(tupleDependencies));
// then push any remaining bodies (the ones that DON'T have
// dependencies)
for (const pythonTupleName in tupleBodies) {
if (!tuplesToDeclare.includes(pythonTupleName)) {
tuplesToDeclare.push(pythonTupleName);
}
}
// now iterate over those ordered tuples-to-declare, and prefix the
// corresponding class bodies with their class headers, to form full
// class declarations.
const tupleDeclarations = [];
for (const pythonTupleName of tuplesToDeclare) {
if (tupleBodies[pythonTupleName]) {
tupleDeclarations.push(
`class ${pythonTupleName}(TypedDict):\n """Python representation of a tuple or struct.\n\n Solidity compiler output does not include the names of structs that appear\n in method definitions. A tuple found in an ABI may have been written in\n Solidity as a literal, anonymous tuple, or it may have been written as a\n named \`struct\`:code:, but there is no way to tell from the compiler\n output. This class represents a tuple that appeared in a method\n definition. Its name is derived from a hash of that tuple's field names,\n and every method whose ABI refers to a tuple with that same list of field\n names will have a generated wrapper method that refers to this class.\n\n Any members of type \`bytes\`:code: should be encoded as UTF-8, which can be\n accomplished via \`str.encode("utf_8")\`:code:\n """${
tupleBodies[pythonTupleName]
}`,
);
}
}
// finally, join the class declarations together for the output file
return new Handlebars.SafeString(tupleDeclarations.join('\n\n\n'));
});
Handlebars.registerHelper('docBytesIfNecessary', (abisJSON: string) => {
const abis: AbiDefinition[] = JSON.parse(abisJSON);
// see if any ABIs accept params of type bytes, and if so then emit
// explanatory documentation string.
for (const abi of abis) {
if (abi.hasOwnProperty('inputs')) {
// HACK(feuGeneA): using "as MethodAbi" below, but abi
// could just as well be ConstructorAbi, EventAbi, etc. We
// just need to tell the TypeScript compiler that it's NOT
// FallbackAbi, or else it would complain, "Property
// 'inputs' does not exist on type 'AbiDefinition'.
// Property 'inputs' does not exist on type
// 'FallbackAbi'.", despite the enclosing if statement.
// tslint:disable:no-unnecessary-type-assertion
if ((abi as MethodAbi).inputs) {
for (const input of (abi as MethodAbi).inputs) {
if (input.type === 'bytes') {
return new Handlebars.SafeString(
'\n\n All method parameters of type `bytes`:code: should be encoded as UTF-8,\n which can be accomplished via `str.encode("utf_8")`:code:.\n ',
);
}
}
}
}
}
return '';
});
Handlebars.registerHelper(
'toPythonClassname',
(sourceName: string) => new Handlebars.SafeString(changeCase.pascal(sourceName)),
);
Handlebars.registerHelper(
'makeOutputsValue',
/**
* Produces a Python expression representing the return value from a
* Solidity function.
* @param pythonVariable the name of the Python variable holding the value
* to be used to populate the output expression.
* @param abiOutputs the "outputs" object of the function's ABI.
*/
(pythonVariable: string, abiOutputs: DataItem[]) => {
if (abiOutputs.length === 1) {
return new Handlebars.SafeString(solValueToPyValue(pythonVariable, abiOutputs[0]));
} else {
let tupleValue = '(';
for (let i = 0; i < abiOutputs.length; i++) {
tupleValue += `${pythonVariable}[${i}],`;
}
tupleValue += ')';
return new Handlebars.SafeString(tupleValue);
}
},
);
}
function solValueToPyValue(pythonVariable: string, abiItem: DataItem): string {
const pythonTypeName = utils.solTypeToPyType(abiItem);
if (pythonTypeName.match(/List\[.*\]/) !== null) {
return `[${solValueToPyValue('element', {
...abiItem,
type: abiItem.type.replace('[]', ''),
})} for element in ${pythonVariable}]`;
} else {
let pyValue = `${pythonTypeName}(`;
if (abiItem.components) {
let i = 0;
for (const component of abiItem.components) {
pyValue += `${component.name}=${pythonVariable}[${i}],`;
i++;
}
} else {
pyValue += pythonVariable;
}
pyValue += ')';
return pyValue;
}
}

View File

@ -102,11 +102,14 @@ export const utils = {
throw new Error(`Unknown Solidity type found: ${solType}`);
}
},
solTypeToPyType(solType: string, components?: DataItem[]): string {
solTypeToPyType(dataItem: DataItem): string {
const solType = dataItem.type;
const trailingArrayRegex = /\[\d*\]$/;
if (solType.match(trailingArrayRegex)) {
const arrayItemSolType = solType.replace(trailingArrayRegex, '');
const arrayItemPyType = utils.solTypeToPyType(arrayItemSolType, components);
const arrayItemPyType = utils.solTypeToPyType({
...dataItem,
type: dataItem.type.replace(trailingArrayRegex, ''),
});
const arrayPyType = `List[${arrayItemPyType}]`;
return arrayPyType;
} else {
@ -125,7 +128,7 @@ export const utils = {
}
const TUPLE_TYPE_REGEX = '^tuple$';
if (solType.match(TUPLE_TYPE_REGEX)) {
return utils.makePythonTupleName(components as DataItem[]);
return utils.makePythonTupleName(dataItem);
}
throw new Error(`Unknown Solidity type found: ${solType}`);
}
@ -187,13 +190,21 @@ export const utils = {
* simply concatenate all of the names of the components, and convert that
* concatenation into PascalCase to conform to Python convention.
*/
makePythonTupleName(tupleComponents: DataItem[]): string {
makePythonTupleName(tuple: DataItem): string {
if (tuple.internalType !== undefined) {
return tuple.internalType
.replace('struct ', '')
.replace('.', '')
.replace('[]', '');
} else {
const tupleComponents = tuple.components;
const lengthOfHashSuffix = 8;
return `Tuple0x${createHash('MD5')
.update(_.map(tupleComponents, component => component.name).join('_'))
.digest()
.toString('hex')
.substring(0, lengthOfHashSuffix)}`;
}
},
/**
* @returns a string that is a Python code snippet that's intended to be
@ -203,10 +214,7 @@ export const utils = {
makePythonTupleClassBody(tupleComponents: DataItem[]): string {
let toReturn: string = '';
for (const tupleComponent of tupleComponents) {
toReturn = `${toReturn}\n\n ${tupleComponent.name}: ${utils.solTypeToPyType(
tupleComponent.type,
tupleComponent.components,
)}`;
toReturn = `${toReturn}\n\n ${tupleComponent.name}: ${utils.solTypeToPyType(tupleComponent)}`;
}
toReturn = `${toReturn}`;
return toReturn;
@ -361,12 +369,12 @@ export const utils = {
// Argument of type 'DataItem[] | undefined' is not assignable to parameter of type 'DataItem[]'.
// Type 'undefined' is not assignable to type 'DataItem[]'
// when the code below tries to access tupleDataItem.components.
const pythonTupleName = utils.makePythonTupleName(tupleDataItem.components);
const pythonTupleName = utils.makePythonTupleName(tupleDataItem);
tupleBodies[pythonTupleName] = utils.makePythonTupleClassBody(tupleDataItem.components);
for (const component of tupleDataItem.components) {
if (component.type === 'tuple' || component.type === 'tuple[]') {
tupleDependencies.push([
utils.makePythonTupleName((component as TupleDataItem).components), // tslint:disable-line:no-unnecessary-type-assertion
utils.makePythonTupleName(component as TupleDataItem), // tslint:disable-line:no-unnecessary-type-assertion
pythonTupleName,
]);
utils.extractTuples(component, tupleBodies, tupleDependencies);

View File

@ -0,0 +1 @@
class Account: ...

View File

@ -0,0 +1,3 @@
class LocalAccount:
address: str
...

View File

@ -0,0 +1 @@
class HexBytes: ...

View File

@ -0,0 +1,67 @@
from typing import Any, Callable, Dict, List, Optional, Union
from hexbytes import HexBytes
from eth_account.local import LocalAccount
from web3 import datastructures
from web3.contract import Contract
from web3.providers.base import BaseProvider
class Web3:
class HTTPProvider(BaseProvider):
...
def __init__(self, provider: BaseProvider) -> None: ...
@staticmethod
def sha3(
primitive: Optional[Union[bytes, int, None]] = None,
text: Optional[str] = None,
hexstr: Optional[str] = None
) -> bytes: ...
@staticmethod
def isAddress(address: str) -> bool: ...
class middleware_stack:
@staticmethod
def get(key: str) -> Callable: ...
def inject(
self, middleware_func: object, layer: object
) -> None: ...
...
middleware_onion: middleware_stack
class net:
version: str
...
class Eth:
defaultAccount: str
accounts: List[str]
chainId: int
...
class account:
@staticmethod
def privateKeyToAccount(private_key: str) -> LocalAccount: ...
...
@staticmethod
def getTransactionReceipt(tx_hash: Union[HexBytes, bytes]) -> Any: ...
@staticmethod
def contract(address: str, abi: Dict) -> Contract: ...
...
@staticmethod
def isAddress(address: str) -> bool: ...
...
eth: Eth
...

View File

@ -0,0 +1,17 @@
from typing import Any
class Contract:
def call(self): ...
functions: Any
events: Any
...
class ContractFunction:
def __call__(self, *args, **kwargs):
...
...

View File

@ -0,0 +1,5 @@
class NamedElementOnion:
...
class AttributeDict:
...

View File

@ -0,0 +1,2 @@
class BadFunctionCallOutput(Exception):
...

View File

@ -0,0 +1,2 @@
class BaseProvider:
...

View File

@ -115,7 +115,7 @@ class {{contractName}}:
functions = self._web3_eth.contract(address=to_checksum_address(contract_address), abi={{contractName}}.abi()).functions
{{#each methods}}
self.{{toPythonIdentifier this.languageSpecificName}} = {{toPythonClassname this.languageSpecificName}}Method(web3_or_provider, contract_address, functions.{{this.name}}, validator)
self.{{toPythonIdentifier this.languageSpecificName}} = {{toPythonClassname this.languageSpecificName}}Method(web3_or_provider, contract_address, functions.{{this.name}}{{#if this.inputs}}, validator{{/if}})
{{/each}}
{{/if}}

View File

@ -0,0 +1,11 @@
{{~#if outputs~}}
{{#if outputs.length}}
{{#singleReturnValue}}
{{#returnType outputs.[0]}}{{~/returnType~}}
{{/singleReturnValue}}
{{^singleReturnValue}}
Tuple[{{#each outputs}}{{#returnType this}}{{/returnType}}{{#unless @last}}, {{/unless}}{{/each}}]
{{~/singleReturnValue}}
{{else}}None
{{/if}}
{{else}}None{{/if~}}

View File

@ -2,10 +2,10 @@
class {{toPythonClassname this.languageSpecificName}}Method(ContractMethod):
"""Various interfaces to the {{this.name}} method."""
def __init__(self, web3_or_provider: Union[Web3, BaseProvider], contract_address: str, contract_function: ContractFunction, validator: Validator=None):
def __init__(self, web3_or_provider: Union[Web3, BaseProvider], contract_address: str, contract_function: ContractFunction{{#if inputs}}, validator: Validator=None{{/if}}):
"""Persist instance data."""
super().__init__(web3_or_provider, contract_address, validator)
self.underlying_method = contract_function
super().__init__(web3_or_provider, contract_address{{#if inputs}}, validator{{/if}})
self._underlying_method = contract_function
{{#if inputs}}
def validate_and_normalize_inputs(self, {{> typed_params inputs=inputs}}):
@ -26,7 +26,7 @@ class {{toPythonClassname this.languageSpecificName}}Method(ContractMethod):
return ({{> params }})
{{/if}}
def call(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> {{> return_type outputs=outputs type='call'~}}:
def call(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> {{> call_return_type outputs=outputs type='call'~}}:
"""Execute underlying contract method via eth_call.
{{sanitizeDevdocDetails this.name this.devdoc.details 8}}{{~#if this.devdoc.params~}}{{#each this.devdoc.params}}
{{makeParameterDocstringRole @key this 8}}{{/each}}{{/if}}
@ -42,28 +42,37 @@ class {{toPythonClassname this.languageSpecificName}}Method(ContractMethod):
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).call(tx_params.as_dict())
{{#hasReturnValue}}returned = {{/hasReturnValue}}self._underlying_method({{> params}}).call(tx_params.as_dict())
{{#hasReturnValue}}
return {{makeOutputsValue 'returned' outputs}}
{{/hasReturnValue}}
{{^if this.constant}}
def send_transaction(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
{{sanitizeDevdocDetails this.name this.devdoc.details 8}}{{~#if this.devdoc.params~}}{{#each this.devdoc.params}}
{{makeParameterDocstringRole @key this 8}}{{/each}}{{/if}}
:param tx_params: transaction parameters
{{#if this.constant~}}
{{#if this.devdoc.return}}
{{makeReturnDocstringRole this.devdoc.return 8}}{{/if}}
{{/if}}
"""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).transact(tx_params.as_dict())
return self._underlying_method({{> params}}).transact(tx_params.as_dict())
def build_transaction(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> dict:
"""Construct calldata to be used as input to the method."""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self._underlying_method({{> params}}).buildTransaction(tx_params.as_dict())
{{/if}}
def estimate_gas(self, {{#if inputs}}{{> typed_params inputs=inputs}}, {{/if}}tx_params: Optional[TxParams] = None) -> int:
"""Estimate gas consumption of method call."""
{{#if inputs}}
({{> params }}) = self.validate_and_normalize_inputs({{> params}})
{{/if}}
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method({{> params}}).estimateGas(tx_params.as_dict())
return self._underlying_method({{> params}}).estimateGas(tx_params.as_dict())

View File

@ -4,10 +4,10 @@ Union[
{{~/if~}}
{{#if outputs.length}}
{{#singleReturnValue}}
{{#returnType outputs.0.type outputs.0.components}}{{~/returnType~}}
{{#returnType outputs.[0]}}{{~/returnType~}}
{{/singleReturnValue}}
{{^singleReturnValue}}
Tuple[{{#each outputs}}{{#returnType type components}}{{/returnType}}{{#unless @last}}, {{/unless}}{{/each}}]
Tuple[{{#each outputs}}{{#returnType this}}{{/returnType}}{{#unless @last}}, {{/unless}}{{/each}}]
{{~/singleReturnValue}}
{{else}}None
{{/if}}{{^if this.constant}}, Union[HexBytes, bytes]]{{/if~}}

View File

@ -1,3 +1,3 @@
{{#each inputs}}
{{toPythonIdentifier name}}: {{#parameterType type components}}{{/parameterType}}{{^if @last}}, {{/if~}}
{{toPythonIdentifier name}}: {{#parameterType this}}{{/parameterType}}{{^if @last}}, {{/if~}}
{{/each~}}

View File

@ -1,6 +1,8 @@
black
eth_utils
hexbytes
mypy
mypy_extensions
pylint
web3
../../python-packages/contract_wrappers

View File

@ -58,7 +58,7 @@ class PublicAddConstantMethod(ContractMethod):
):
"""Persist instance data."""
super().__init__(web3_or_provider, contract_address, validator)
self.underlying_method = contract_function
self._underlying_method = contract_function
def validate_and_normalize_inputs(self, x: int):
"""Validate the inputs to the publicAddConstant method."""
@ -79,19 +79,8 @@ class PublicAddConstantMethod(ContractMethod):
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).call(tx_params.as_dict())
def send_transaction(
self, x: int, tx_params: Optional[TxParams] = None
) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).transact(tx_params.as_dict())
returned = self._underlying_method(x).call(tx_params.as_dict())
return int(returned)
def estimate_gas(
self, x: int, tx_params: Optional[TxParams] = None
@ -99,7 +88,7 @@ class PublicAddConstantMethod(ContractMethod):
"""Estimate gas consumption of method call."""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).estimateGas(tx_params.as_dict())
return self._underlying_method(x).estimateGas(tx_params.as_dict())
class PublicAddOneMethod(ContractMethod):
@ -114,7 +103,7 @@ class PublicAddOneMethod(ContractMethod):
):
"""Persist instance data."""
super().__init__(web3_or_provider, contract_address, validator)
self.underlying_method = contract_function
self._underlying_method = contract_function
def validate_and_normalize_inputs(self, x: int):
"""Validate the inputs to the publicAddOne method."""
@ -133,19 +122,8 @@ class PublicAddOneMethod(ContractMethod):
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).call(tx_params.as_dict())
def send_transaction(
self, x: int, tx_params: Optional[TxParams] = None
) -> Union[HexBytes, bytes]:
"""Execute underlying contract method via eth_sendTransaction.
:param tx_params: transaction parameters
"""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).transact(tx_params.as_dict())
returned = self._underlying_method(x).call(tx_params.as_dict())
return int(returned)
def estimate_gas(
self, x: int, tx_params: Optional[TxParams] = None
@ -153,7 +131,7 @@ class PublicAddOneMethod(ContractMethod):
"""Estimate gas consumption of method call."""
(x) = self.validate_and_normalize_inputs(x)
tx_params = super().normalize_tx_params(tx_params)
return self.underlying_method(x).estimateGas(tx_params.as_dict())
return self._underlying_method(x).estimateGas(tx_params.as_dict())
# pylint: disable=too-many-public-methods,too-many-instance-attributes

View File

@ -134,6 +134,7 @@ export interface EventAbi {
export interface DataItem {
name: string;
type: string;
internalType?: string;
components?: DataItem[];
}

View File

@ -9,6 +9,11 @@
- Moved methods `jsdict_to_order()` and `order_to_jsdict()` from `zero_ex.contract_wrappers.exchange.types` to `zero_ex.contract_wrappers.order_conversions`.
- Changed field name `zero_ex.contract_wrappers.tx_params.TxParams.gasPrice` to `.gas_price`.
- Migrated to new version of 0x-contract-addresses.
- Made the `underlying_method` field on ContractMethod private by prefixing its name with an underscore.
- Corrected return types and values for call() interface to generated method wrappers. (Fixes #2298.)
- Removed `send_transaction()` method from ContractMethod instances for underlying Solidity methods that are const (view/pure).
- Added a `build_transaction()` method to instances of ContractMethod for non-const Solidity methods.
- Removed `validator` argument from ContractMethod instances for underlying Solidity methods that lack inputs.
## 1.1.0 - 2019-08-14

View File

@ -201,23 +201,23 @@ zero_ex.contract_wrappers.exchange.types
zero_ex.contract_wrappers.exchange: Generated Tuples
====================================================
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x6ca34a6f
.. autoclass:: zero_ex.contract_wrappers.exchange.LibOrderOrder
This is the generated class representing `the Order struct <https://0x.org/docs/contracts#structs-Order>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x735c43e3
.. autoclass:: zero_ex.contract_wrappers.exchange.LibFillResultsFillResults
This is the generated class representing `the FillResults struct <https://0x.org/docs/contracts#structs-FillResults>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0x4c5ca29b
.. autoclass:: zero_ex.contract_wrappers.exchange.LibFillResultsMatchedFillResults
This is the generated class representing `the MatchedFillResults struct <https://0x.org/docs/contracts#structs-MatchedFillResults>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xb1e4a1ae
.. autoclass:: zero_ex.contract_wrappers.exchange.LibOrderOrderInfo
This is the generated class representing `the OrderInfo struct <https://0x.org/docs/contracts#structs-OrderInfo>`_.
.. autoclass:: zero_ex.contract_wrappers.exchange.Tuple0xdabc15fe
.. autoclass:: zero_ex.contract_wrappers.exchange.LibZeroExTransactionZeroExTransaction
This is the generated class representing `the ZeroExTransaction struct <https://0x.org/docs/contracts#structs-ZeroExTransaction>`_.

View File

@ -171,7 +171,7 @@ Now we'll have our Taker fill the order.
But before filling an order, one may wish to check that it's actually fillable:
>>> from zero_ex.contract_wrappers.exchange.types import OrderStatus
>>> OrderStatus(exchange.get_order_info.call(order)[0])
>>> OrderStatus(exchange.get_order_info.call(order)["orderStatus"])
<OrderStatus.FILLABLE: 3>
The `takerAssetAmount`:code: parameter specifies the amount of tokens (in this
@ -181,13 +181,18 @@ completely, but partial fills are possible too.
One may wish to first call the method in a read-only way, to ensure that it
will not revert, and to validate that the return data is as expected:
>>> exchange.fill_order.call(
>>> from pprint import pprint
>>> pprint(exchange.fill_order.call(
... order=order,
... taker_asset_fill_amount=order["takerAssetAmount"],
... signature=maker_signature,
... tx_params=TxParams(from_=taker_address)
... )
(100000000000000000, 100000000000000000, 0, 0, 0)
... ))
{'makerAssetFilledAmount': 100000000000000000,
'makerFeePaid': 0,
'protocolFeePaid': 0,
'takerAssetFilledAmount': 100000000000000000,
'takerFeePaid': 0}
Finally, submit the transaction:
@ -203,7 +208,6 @@ the exchange wrapper:
>>> exchange.get_fill_event(tx_hash)
(AttributeDict({'args': ...({'makerAddress': ...}), 'event': 'Fill', ...}),)
>>> from pprint import pprint
>>> pprint(exchange.get_fill_event(tx_hash)[0].args.__dict__)
{'feeRecipientAddress': '0x0000000000000000000000000000000000000000',
'makerAddress': '0x...',

View File

@ -5,20 +5,16 @@ encountered in the Exchange contract's ABI. However, they have weird names,
containing hashes of the tuple's field names, because the name of a Solidity
`struct`:code: isn't conveyed through the ABI. This module provides type
aliases with human-friendly names.
Converting between the JSON wire format and the types accepted by Web3.py (eg
`bytes` vs `str`) can be onerous. This module provides conveniences for
converting Exchange structs between JSON and Python objects.
"""
from enum import auto, Enum
from . import (
Tuple0x735c43e3,
Tuple0x6ca34a6f,
Tuple0x4c5ca29b,
Tuple0xdabc15fe,
Tuple0xb1e4a1ae,
LibFillResultsFillResults,
LibOrderOrder,
LibFillResultsMatchedFillResults,
LibZeroExTransactionZeroExTransaction,
LibOrderOrderInfo,
)
@ -29,43 +25,43 @@ from . import (
# of each of these classes.
class FillResults(Tuple0x735c43e3):
class FillResults(LibFillResultsFillResults):
"""The `FillResults`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0x735c43e3`:py:class:.
`zero_ex.contract_wrappers.exchange.LibFillResultsFillResults`:py:class:.
"""
class Order(Tuple0x6ca34a6f):
class Order(LibOrderOrder):
"""The `Order`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0x6ca34a6f`:py:class:.
`zero_ex.contract_wrappers.exchange.LibOrderOrder`:py:class:.
"""
class MatchedFillResults(Tuple0x4c5ca29b):
class MatchedFillResults(LibFillResultsMatchedFillResults):
"""The `MatchedFillResults`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0x4c5ca29b`:py:class:.
`zero_ex.contract_wrappers.exchange.LibFillResultsMatchedFillResults`:py:class:.
"""
class ZeroExTransaction(Tuple0xdabc15fe):
class ZeroExTransaction(LibZeroExTransactionZeroExTransaction):
"""The `ZeroExTransaction`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0xdabc15fe`:py:class:.
`zero_ex.contract_wrappers.exchange.LibZeroExTransactionZeroExTransaction`:py:class:.
"""
class OrderInfo(Tuple0xb1e4a1ae):
class OrderInfo(LibOrderOrderInfo):
"""The `OrderInfo`:code: Solidity struct.
Also known as
`zero_ex.contract_wrappers.exchange.Tuple0xb1e4a1ae`:py:class:.
`zero_ex.contract_wrappers.exchange.LibOrderOrderInfo`:py:class:.
"""

View File

@ -1,4 +1,9 @@
"""Utilities to convert between JSON and Python-native objects."""
"""Utilities to convert between JSON and Python-native objects.
Converting between the JSON wire format and the types accepted by Web3.py (eg
`bytes` vs `str`) can be onerous. This module provides conveniences for
converting Exchange structs between JSON and Python objects.
"""
from copy import copy
from typing import cast, Dict, Union

View File

@ -91,11 +91,11 @@ def test_exchange_wrapper__fill_order(
signature=order_signature,
tx_params=TxParams(from_=taker),
)
assert fill_results[0] == 1
assert fill_results[1] == 1
assert fill_results[2] == 0
assert fill_results[3] == 0
assert fill_results[4] == 0
assert fill_results["makerAssetFilledAmount"] == 1
assert fill_results["takerAssetFilledAmount"] == 1
assert fill_results["makerFeePaid"] == 0
assert fill_results["takerFeePaid"] == 0
assert fill_results["protocolFeePaid"] == 0
tx_hash = exchange_wrapper.fill_order.send_transaction(
order=order,

View File

@ -32,37 +32,12 @@ configured to connect to any JSON-RPC endpoint, on any network. The examples
below assume that Launch Kit is connected to a Ganache development network
accessible at `http://localhost:8545`:code:.
To replicate this setup, one could run the following commands:
::
docker run -d -p 8545:8545 0xorg/ganache-cli
docker run -d -p 60557:60557 --network host \
-e ETHEREUM_RPC_URL=http://localhost:8545 \
-e ETHEREUM_NETWORK_ID=50 \
-e ETHEREUM_CHAIN_ID=1337 \
-e USE_BOOTSTRAP_LIST=false \
-e VERBOSITY=3 \
-e PRIVATE_KEY_PATH= \
-e BLOCK_POLLING_INTERVAL=5s \
-e P2P_LISTEN_PORT=60557
0xorg/mesh:6.0.0-beta-0xv3
docker run -d --network host \
-e RPC_URL=http://localhost:8545 \
-e CHAIN_ID=1337 \
-e FEE_RECIPIENT=0x0000000000000000000000000000000000000001 \
-e MAKER_FEE_UNIT_AMOUNT=0 \
-e TAKER_FEE_UNIT_AMOUNT=0
-e MESH_ENDPOINT=ws://localhost:60557
-e WHITELIST_ALL_TOKENS=True \
0xorg/launch-kit-ci
(Note: This will only work on Linux, because `--network host`:code: only works
on Linux. For other platforms one would have to clone `the 0x-launch-kit-backend
repository <https://github.com/0xProject/0x-launch-kit-backend>`_ and build and start
the server.)
These examples are automatically verified by spinning up docker images
`0xorg/ganache-cli`, `0xorg/mesh`, and `0xorg/launch-kit-backend`. You can
replicate this environment yourself by using `this docker-compose.yml file
<https://github.com/0xProject/0x-monorepo/blob/development/python-packages/sra_client/test/relayer/docker-compose.yml>`_.
(Note: This will only work on Linux, because it uses `network_mode:
"host"`:code:, which only works on Linux.)
Configure and create an API client instance
-------------------------------------------