diff --git a/contracts/utils/compiler.json b/contracts/utils/compiler.json index 01ab211056..b0857f00ba 100644 --- a/contracts/utils/compiler.json +++ b/contracts/utils/compiler.json @@ -30,6 +30,7 @@ "src/ReentrancyGuard.sol", "src/SafeMath.sol", "src/interfaces/IOwnable.sol", + "test/TestAbi.sol", "test/TestConstants.sol", "test/TestLibAddressArray.sol", "test/TestLibBytes.sol", diff --git a/contracts/utils/contracts/test/TestAbi.sol b/contracts/utils/contracts/test/TestAbi.sol new file mode 100644 index 0000000000..e64b17b2db --- /dev/null +++ b/contracts/utils/contracts/test/TestAbi.sol @@ -0,0 +1,141 @@ +/* + + Copyright 2018 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.5.5; +pragma experimental ABIEncoderV2; + + +contract TestAbi { + + struct ComplexInput { + uint256 foo; + bytes bar; + string car; + } + + struct ComplexOutput { + ComplexInput input; + bytes lorem; + bytes ipsum; + string dolor; + } + + function noInputNoOutput() + public + pure + { + // NOP + require(true == true); + } + + function noInputSimpleOutput() + public + pure + returns (uint256) + { + return 1991; + } + + function simpleInputNoOutput(uint256) + public + pure + { + // NOP + require(true == true); + } + + function simpleInputSimpleOutput(uint256) + public + pure + returns (uint256) + { + return 1991; + } + + function complexInputComplexOutput(ComplexInput memory complexInput) + public + pure + returns (ComplexOutput memory) + { + return ComplexOutput({ + input: complexInput, + lorem: hex'12345678', + ipsum: hex'87654321', + dolor: "amet" + }); + } + + function multiInputMultiOutput( + uint256, + bytes memory, + string memory + ) + public + pure + returns ( + bytes memory, + bytes memory, + string memory + ) + { + return ( + hex'12345678', + hex'87654321', + "amet" + ); + } + + function () + external + { + address addr = address(this); + assembly { + // copy calldata to memory + calldatacopy( + 0x0, + 0x0, + calldatasize() + ) + // execute transaction + let success := call( + gas, // send all gas. + addr, // call into this contract. + 0, // don't send any ether. + 0x0, // input is `txData`. + calldatasize(), // input length is that of `txData`. + 0x0, // any return data goes at mem address 0x0. + 0 // there is no fixed return value size. + ) + + // copy return data to memory + returndatacopy( + 0x0, + 0x0, + returndatasize() + ) + + // rethrow any exceptions + if iszero(success) { + revert(0, returndatasize()) + } + + // return call results + return(0, returndatasize()) + } + } +} diff --git a/contracts/utils/package.json b/contracts/utils/package.json index 58dfca0e6e..42a94f15ce 100644 --- a/contracts/utils/package.json +++ b/contracts/utils/package.json @@ -34,7 +34,7 @@ "lint-contracts": "solhint -c ../.solhint.json contracts/**/**/**/**/*.sol" }, "config": { - "abis": "./generated-artifacts/@(Address|IOwnable|LibBytes|Ownable|ReentrancyGuard|SafeMath|TestConstants|TestLibAddressArray|TestLibBytes|TestLogDecoding|TestLogDecodingDownstream).json", + "abis": "./generated-artifacts/@(Address|IOwnable|LibBytes|Ownable|ReentrancyGuard|SafeMath|TestAbi|TestConstants|TestLibAddressArray|TestLibBytes|TestLogDecoding|TestLogDecodingDownstream).json", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually." }, "repository": { diff --git a/contracts/utils/src/artifacts.ts b/contracts/utils/src/artifacts.ts index e2b2104257..99bc3cf7e6 100644 --- a/contracts/utils/src/artifacts.ts +++ b/contracts/utils/src/artifacts.ts @@ -11,6 +11,7 @@ import * as LibBytes from '../generated-artifacts/LibBytes.json'; import * as Ownable from '../generated-artifacts/Ownable.json'; import * as ReentrancyGuard from '../generated-artifacts/ReentrancyGuard.json'; import * as SafeMath from '../generated-artifacts/SafeMath.json'; +import * as TestAbi from '../generated-artifacts/TestAbi.json'; import * as TestConstants from '../generated-artifacts/TestConstants.json'; import * as TestLibAddressArray from '../generated-artifacts/TestLibAddressArray.json'; import * as TestLibBytes from '../generated-artifacts/TestLibBytes.json'; @@ -28,4 +29,5 @@ export const artifacts = { TestLibBytes: TestLibBytes as ContractArtifact, TestLogDecoding: TestLogDecoding as ContractArtifact, TestLogDecodingDownstream: TestLogDecodingDownstream as ContractArtifact, + TestAbi: TestAbi as ContractArtifact, }; diff --git a/contracts/utils/src/wrappers.ts b/contracts/utils/src/wrappers.ts index be616b060f..0439bc28b1 100644 --- a/contracts/utils/src/wrappers.ts +++ b/contracts/utils/src/wrappers.ts @@ -9,6 +9,7 @@ export * from '../generated-wrappers/lib_bytes'; export * from '../generated-wrappers/ownable'; export * from '../generated-wrappers/reentrancy_guard'; export * from '../generated-wrappers/safe_math'; +export * from '../generated-wrappers/test_abi'; export * from '../generated-wrappers/test_constants'; export * from '../generated-wrappers/test_lib_address_array'; export * from '../generated-wrappers/test_lib_bytes'; diff --git a/contracts/utils/test/abi.ts b/contracts/utils/test/abi.ts new file mode 100644 index 0000000000..fcabea04ba --- /dev/null +++ b/contracts/utils/test/abi.ts @@ -0,0 +1,95 @@ +import { chaiSetup, provider, txDefaults, web3Wrapper } from '@0x/contracts-test-utils'; +import { BlockchainLifecycle } from '@0x/dev-utils'; +import { BigNumber } from '@0x/utils'; +import * as chai from 'chai'; +import { DecodedLogArgs, LogWithDecodedArgs } from 'ethereum-types'; + +import { artifacts, TestAbiContract } from '../src'; + +chaiSetup.configure(); +const expect = chai.expect; + +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe.only('TestAbi', () => { + let testAbi: TestAbiContract; + const runTestAsync = async (contractMethod: any, input: any, output: any) => { + const transaction = contractMethod.getABIEncodedTransactionData(input); + // try decoding transaction + const decodedInput = contractMethod.getABIDecodedTransactionData(transaction); + expect(decodedInput, 'decoded input').to.be.deep.equal(input); + // execute transaction + const rawOutput = await web3Wrapper.callAsync({ + to: testAbi.address, + data: transaction, + }); + // try decoding output + const decodedOutput = contractMethod.getABIDecodedReturnData(rawOutput); + expect(decodedOutput, 'decoded output').to.be.deep.equal(output); + }; + before(async () => { + testAbi = await TestAbiContract.deployFrom0xArtifactAsync(artifacts.TestAbi, provider, txDefaults, artifacts); + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('Encoding/Decoding Transaction Data and Return Values', () => { + it('should successfully encode/decode (no input / no output)', async () => { + const input = undefined; + const output = undefined; + await runTestAsync(testAbi.noInputNoOutput, input, output); + }); + it('should successfully encode/decode (no input / simple output)', async () => { + const input = undefined; + const output = new BigNumber(1991); + await runTestAsync(testAbi.noInputSimpleOutput, input, output); + }); + it('should successfully encode/decode (simple input / no output)', async () => { + const input = new BigNumber(1991); + const output = undefined; + await runTestAsync(testAbi.simpleInputNoOutput, input, output); + }); + it('should successfully encode/decode (simple input / simple output)', async () => { + const input = new BigNumber(16); + const output = new BigNumber(1991); + await runTestAsync(testAbi.simpleInputSimpleOutput, input, output); + }); + it('should successfully encode/decode (complex input / complex output)', async () => { + const input = { + foo: new BigNumber(1991), + bar: '0x1234', + car: 'zoom zoom', + }; + const output = { + input: input, + lorem: '0x12345678', + ipsum: '0x87654321', + dolor: 'amet', + }; + await runTestAsync(testAbi.complexInputComplexOutput, input, output); + }); + it('should successfully encode/decode (complex input / complex output)', async () => { + const input = [new BigNumber(1991), '0x1234', 'zoom zoom']; + const output = ['0x12345678', '0x87654321', 'amet']; + const transaction = testAbi.multiInputMultiOutput.getABIEncodedTransactionData( + input[0] as BigNumber, + input[1] as string, + input[2] as string, + ); + // try decoding transaction + const decodedInput = testAbi.multiInputMultiOutput.getABIDecodedTransactionData(transaction); + expect(decodedInput, 'decoded input').to.be.deep.equal(input); + // execute transaction + const rawOutput = await web3Wrapper.callAsync({ + to: testAbi.address, + data: transaction, + }); + // try decoding output + const decodedOutput = testAbi.multiInputMultiOutput.getABIDecodedReturnData(rawOutput); + expect(decodedOutput, 'decoded output').to.be.deep.equal(output); + }); + }); +}); diff --git a/contracts/utils/tsconfig.json b/contracts/utils/tsconfig.json index bb3f9566be..a339d3631a 100644 --- a/contracts/utils/tsconfig.json +++ b/contracts/utils/tsconfig.json @@ -9,6 +9,7 @@ "generated-artifacts/Ownable.json", "generated-artifacts/ReentrancyGuard.json", "generated-artifacts/SafeMath.json", + "generated-artifacts/TestAbi.json", "generated-artifacts/TestConstants.json", "generated-artifacts/TestLibAddressArray.json", "generated-artifacts/TestLibBytes.json", diff --git a/packages/abi-gen-templates/partials/callAsync.handlebars b/packages/abi-gen-templates/partials/callAsync.handlebars index adaf7098d9..8c362fd51e 100644 --- a/packages/abi-gen-templates/partials/callAsync.handlebars +++ b/packages/abi-gen-templates/partials/callAsync.handlebars @@ -44,3 +44,21 @@ getABIEncodedTransactionData( const abiEncodedTransactionData = self._strictEncodeArguments('{{this.functionSignature}}', [{{> normalized_params inputs=inputs}}]); return abiEncodedTransactionData; }, +getABIDecodedTransactionData( + returnData: string +): ({{> return_type inputs=inputs ~}}) { + const self = this as any as {{contractName}}Contract; + const abiEncoder = self._lookupAbiEncoder('{{this.functionSignature}}'); + // tslint:disable boolean-naming + const abiDecodedReturnData = abiEncoder.strictDecode<{{> return_type inputs=inputs}}>(returnData); + return abiDecodedReturnData; +}, +getABIDecodedReturnData( + returnData: string +): ({{> return_type outputs=outputs ~}}) { + const self = this as any as {{contractName}}Contract; + const abiEncoder = self._lookupAbiEncoder('{{this.functionSignature}}'); + // tslint:disable boolean-naming + const abiDecodedReturnData = abiEncoder.strictDecodeReturnValue<{{> return_type outputs=outputs}}>(returnData); + return abiDecodedReturnData; +}, diff --git a/packages/utils/src/abi_encoder/evm_data_types/method.ts b/packages/utils/src/abi_encoder/evm_data_types/method.ts index 93746fa002..803fe77d89 100644 --- a/packages/utils/src/abi_encoder/evm_data_types/method.ts +++ b/packages/utils/src/abi_encoder/evm_data_types/method.ts @@ -34,6 +34,19 @@ export class MethodDataType extends AbstractSetDataType { return value; } + public strictDecode(calldata: string, rules?: DecodingRules): T { + const value = super.decode(calldata, rules, this._methodSelector); + const valueAsArray: any = _.isObject(value) ? _.values(value) : [value]; + switch (valueAsArray.length) { + case 0: + return undefined as any; + case 1: + return valueAsArray[0]; + default: + return valueAsArray; + } + } + public encodeReturnValues(value: any, rules?: EncodingRules): string { const returnData = this._returnDataType.encode(value, rules); return returnData;