diff --git a/.circleci/config.yml b/.circleci/config.yml index 9ab5e60baf..bae6464463 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -77,7 +77,7 @@ jobs: - restore_cache: keys: - repo-{{ .Environment.CIRCLE_SHA1 }} - - run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-asset-proxy @0x/contracts-exchange-forwarder @0x/contracts-coordinator @0x/contracts-erc20-bridge-sampler @0x/contracts-zero-ex + - run: yarn wsrun test:circleci @0x/contracts-multisig @0x/contracts-utils @0x/contracts-exchange-libs @0x/contracts-erc20 @0x/contracts-erc721 @0x/contracts-erc1155 @0x/contracts-asset-proxy @0x/contracts-exchange-forwarder @0x/contracts-coordinator @0x/contracts-erc20-bridge-sampler @0x/contracts-broker @0x/contracts-zero-ex test-publish: resource_class: medium+ docker: diff --git a/contracts/broker/CHANGELOG.json b/contracts/broker/CHANGELOG.json index 658b70f40a..a7a7c5f293 100644 --- a/contracts/broker/CHANGELOG.json +++ b/contracts/broker/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "1.1.5", + "changes": [ + { + "note": "Fix broken tests.", + "pr": 2591 + } + ] + }, { "timestamp": 1583220306, "version": "1.1.4", diff --git a/contracts/broker/test/gods_unchained_validator_test.ts b/contracts/broker/test/gods_unchained_validator_test.ts index 1e17f9bbad..965d6a977a 100644 --- a/contracts/broker/test/gods_unchained_validator_test.ts +++ b/contracts/broker/test/gods_unchained_validator_test.ts @@ -44,13 +44,13 @@ blockchainTests.resets('GodsUnchainedValidator unit tests', env => { const tokenId = getRandomInteger(0, constants.MAX_UINT256); await godsUnchained.setTokenProperties(tokenId, proto.plus(1), quality).awaitTransactionSuccessAsync(); const tx = validator.checkBrokerAsset(tokenId, propertyData).callAsync(); - expect(tx).to.revertWith('PROTO_MISMATCH'); + expect(tx).to.revertWith('GodsUnchainedValidator/PROTO_MISMATCH'); }); it("reverts if assetData quality doesn't match proeprtyData", async () => { const tokenId = getRandomInteger(0, constants.MAX_UINT256); await godsUnchained.setTokenProperties(tokenId, proto, quality.plus(1)).awaitTransactionSuccessAsync(); const tx = validator.checkBrokerAsset(tokenId, propertyData).callAsync(); - expect(tx).to.revertWith('QUALITY_MISMATCH'); + expect(tx).to.revertWith('GodsUnchainedValidator/QUALITY_MISMATCH'); }); }); }); diff --git a/contracts/zero-ex/src/transformer_data_encoders.ts b/contracts/zero-ex/src/transformer_data_encoders.ts index 26c29abff3..00a7831f61 100644 --- a/contracts/zero-ex/src/transformer_data_encoders.ts +++ b/contracts/zero-ex/src/transformer_data_encoders.ts @@ -69,6 +69,13 @@ export function encodeFillQuoteTransformerData(data: FillQuoteTransformerData): return fillQuoteTransformerDataEncoder.encode([data]); } +/** + * ABI-decode a `FillQuoteTransformer.TransformData` type. + */ +export function decodeFillQuoteTransformerData(encoded: string): FillQuoteTransformerData { + return fillQuoteTransformerDataEncoder.decode(encoded).data; +} + /** * ABI encoder for `WethTransformer.TransformData` */ @@ -95,6 +102,13 @@ export function encodeWethTransformerData(data: WethTransformerData): string { return wethTransformerDataEncoder.encode([data]); } +/** + * ABI-decode a `WethTransformer.TransformData` type. + */ +export function decodeWethTransformerData(encoded: string): WethTransformerData { + return wethTransformerDataEncoder.decode(encoded).data; +} + /** * ABI encoder for `PayTakerTransformer.TransformData` */ @@ -121,6 +135,13 @@ export function encodePayTakerTransformerData(data: PayTakerTransformerData): st return payTakerTransformerDataEncoder.encode([data]); } +/** + * ABI-decode a `PayTakerTransformer.TransformData` type. + */ +export function decodePayTakerTransformerData(encoded: string): PayTakerTransformerData { + return payTakerTransformerDataEncoder.decode(encoded).data; +} + /** * ABI encoder for `PayTakerTransformer.TransformData` */ @@ -157,3 +178,10 @@ export interface AffiliateFeeTransformerData { export function encodeAffiliateFeeTransformerData(data: AffiliateFeeTransformerData): string { return affiliateFeeTransformerDataEncoder.encode(data); } + +/** + * ABI-decode a `AffiliateFeeTransformer.TransformData` type. + */ +export function decodeAffiliateFeeTransformerData(encoded: string): AffiliateFeeTransformerData { + return affiliateFeeTransformerDataEncoder.decode(encoded).data; +} diff --git a/packages/abi-gen/CHANGELOG.json b/packages/abi-gen/CHANGELOG.json index a05ef6f9a3..62cb5095b7 100644 --- a/packages/abi-gen/CHANGELOG.json +++ b/packages/abi-gen/CHANGELOG.json @@ -1,4 +1,13 @@ [ + { + "version": "5.3.0", + "changes": [ + { + "note": "Allow identifiers with leading underscores in python wrappers", + "pr": 2591 + } + ] + }, { "version": "5.2.2", "changes": [ diff --git a/packages/abi-gen/src/index.ts b/packages/abi-gen/src/index.ts index be65fa6475..8d89354072 100644 --- a/packages/abi-gen/src/index.ts +++ b/packages/abi-gen/src/index.ts @@ -146,7 +146,16 @@ if (args.language === 'TypeScript') { registerPartials(); function makeLanguageSpecificName(methodName: string): string { - return args.language === 'Python' ? changeCase.snake(methodName) : methodName; + if (args.language === 'Python') { + let snakeCased = changeCase.snake(methodName); + // Move leading underscores to the end. + const m = /^(_*).+?(_*)$/.exec(methodName); + if (m) { + snakeCased = `${snakeCased}${m[1] || m[2]}`; + } + return snakeCased; + } + return methodName; } if (_.isEmpty(abiFileNames)) { diff --git a/packages/abi-gen/src/python_handlebars_helpers.ts b/packages/abi-gen/src/python_handlebars_helpers.ts index f98cd9b62e..b64164bfc1 100644 --- a/packages/abi-gen/src/python_handlebars_helpers.ts +++ b/packages/abi-gen/src/python_handlebars_helpers.ts @@ -153,10 +153,15 @@ export function registerPythonHelpers(): void { } return ''; }); - Handlebars.registerHelper( - 'toPythonClassname', - (sourceName: string) => new Handlebars.SafeString(changeCase.pascal(sourceName)), - ); + Handlebars.registerHelper('toPythonClassname', (sourceName: string) => { + let pascalCased = changeCase.pascal(sourceName); + // Retain trailing underscores. + const m = /^.+?(_*)$/.exec(sourceName); + if (m) { + pascalCased = `${pascalCased}${m[1]}`; + } + return new Handlebars.SafeString(pascalCased); + }); Handlebars.registerHelper( 'makeOutputsValue', /** diff --git a/packages/abi-gen/src/utils.ts b/packages/abi-gen/src/utils.ts index de82de8b90..b5623014c9 100644 --- a/packages/abi-gen/src/utils.ts +++ b/packages/abi-gen/src/utils.ts @@ -331,13 +331,14 @@ export const utils = { 'max', 'round', ]; - if ( - pythonReservedWords.includes(snakeCased) || - pythonBuiltins.includes(snakeCased) || - /*changeCase strips leading underscores :(*/ input[0] === '_' - ) { + if (pythonReservedWords.includes(snakeCased) || pythonBuiltins.includes(snakeCased)) { snakeCased = `_${snakeCased}`; } + // Retain trailing underscores. + const m = /^.+?(_*)$/.exec(input); + if (m) { + snakeCased = `${snakeCased}${m[1]}`; + } return snakeCased; }, /** diff --git a/packages/abi-gen/templates/Python/partials/method_class.handlebars b/packages/abi-gen/templates/Python/partials/method_class.handlebars index 945009edb3..9bdf47c61f 100644 --- a/packages/abi-gen/templates/Python/partials/method_class.handlebars +++ b/packages/abi-gen/templates/Python/partials/method_class.handlebars @@ -1,5 +1,5 @@ -class {{toPythonClassname this.languageSpecificName}}Method(ContractMethod): +class {{toPythonClassname this.languageSpecificName}}Method(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the {{this.name}} method.""" def __init__(self, web3_or_provider: Union[Web3, BaseProvider], contract_address: str, contract_function: ContractFunction{{#if inputs}}, validator: Validator=None{{/if}}): diff --git a/packages/abi-gen/test-cli/output/python/abi_gen_dummy/__init__.py b/packages/abi-gen/test-cli/output/python/abi_gen_dummy/__init__.py index 866128d746..f047e399a6 100644 --- a/packages/abi-gen/test-cli/output/python/abi_gen_dummy/__init__.py +++ b/packages/abi-gen/test-cli/output/python/abi_gen_dummy/__init__.py @@ -178,7 +178,9 @@ class AbiGenDummyNestedStruct(TypedDict): description: str -class AcceptsAnArrayOfBytesMethod(ContractMethod): +class AcceptsAnArrayOfBytesMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the acceptsAnArrayOfBytes method.""" def __init__( @@ -225,7 +227,7 @@ def estimate_gas( return self._underlying_method(a).estimateGas(tx_params.as_dict()) -class AcceptsBytesMethod(ContractMethod): +class AcceptsBytesMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the acceptsBytes method.""" def __init__( @@ -267,7 +269,9 @@ def estimate_gas( return self._underlying_method(a).estimateGas(tx_params.as_dict()) -class ComplexInputComplexOutputMethod(ContractMethod): +class ComplexInputComplexOutputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the complexInputComplexOutput method.""" def __init__( @@ -329,7 +333,7 @@ def estimate_gas( ) -class EcrecoverFnMethod(ContractMethod): +class EcrecoverFnMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the ecrecoverFn method.""" def __init__( @@ -414,7 +418,7 @@ def estimate_gas( ) -class EmitSimpleEventMethod(ContractMethod): +class EmitSimpleEventMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the emitSimpleEvent method.""" def __init__( @@ -457,7 +461,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class MethodAcceptingArrayOfArrayOfStructsMethod(ContractMethod): +class MethodAcceptingArrayOfArrayOfStructsMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the methodAcceptingArrayOfArrayOfStructs method.""" def __init__( @@ -509,7 +515,9 @@ def estimate_gas( ) -class MethodAcceptingArrayOfStructsMethod(ContractMethod): +class MethodAcceptingArrayOfStructsMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the methodAcceptingArrayOfStructs method.""" def __init__( @@ -559,7 +567,9 @@ def estimate_gas( ) -class MethodReturningArrayOfStructsMethod(ContractMethod): +class MethodReturningArrayOfStructsMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the methodReturningArrayOfStructs method.""" def __init__( @@ -598,7 +608,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class MethodReturningMultipleValuesMethod(ContractMethod): +class MethodReturningMultipleValuesMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the methodReturningMultipleValues method.""" def __init__( @@ -632,7 +644,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: class MethodUsingNestedStructWithInnerStructNotUsedElsewhereMethod( ContractMethod -): +): # pylint: disable=invalid-name """Various interfaces to the methodUsingNestedStructWithInnerStructNotUsedElsewhere method.""" def __init__( @@ -665,7 +677,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class MultiInputMultiOutputMethod(ContractMethod): +class MultiInputMultiOutputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the multiInputMultiOutput method.""" def __init__( @@ -747,7 +761,7 @@ def estimate_gas( ) -class NestedStructInputMethod(ContractMethod): +class NestedStructInputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the nestedStructInput method.""" def __init__( @@ -791,7 +805,7 @@ def estimate_gas( return self._underlying_method(n).estimateGas(tx_params.as_dict()) -class NestedStructOutputMethod(ContractMethod): +class NestedStructOutputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the nestedStructOutput method.""" def __init__( @@ -824,7 +838,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class NoInputNoOutputMethod(ContractMethod): +class NoInputNoOutputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the noInputNoOutput method.""" def __init__( @@ -854,7 +868,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class NoInputSimpleOutputMethod(ContractMethod): +class NoInputSimpleOutputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the noInputSimpleOutput method.""" def __init__( @@ -885,7 +901,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class NonPureMethodMethod(ContractMethod): +class NonPureMethodMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the nonPureMethod method.""" def __init__( @@ -929,7 +945,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class NonPureMethodThatReturnsNothingMethod(ContractMethod): +class NonPureMethodThatReturnsNothingMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the nonPureMethodThatReturnsNothing method.""" def __init__( @@ -972,7 +990,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class OverloadedMethod2Method(ContractMethod): +class OverloadedMethod2Method(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the overloadedMethod method.""" def __init__( @@ -1014,7 +1032,7 @@ def estimate_gas( return self._underlying_method(a).estimateGas(tx_params.as_dict()) -class OverloadedMethod1Method(ContractMethod): +class OverloadedMethod1Method(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the overloadedMethod method.""" def __init__( @@ -1056,7 +1074,9 @@ def estimate_gas( return self._underlying_method(a).estimateGas(tx_params.as_dict()) -class PureFunctionWithConstantMethod(ContractMethod): +class PureFunctionWithConstantMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the pureFunctionWithConstant method.""" def __init__( @@ -1085,7 +1105,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class RequireWithConstantMethod(ContractMethod): +class RequireWithConstantMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the requireWithConstant method.""" def __init__( @@ -1113,7 +1135,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class RevertWithConstantMethod(ContractMethod): +class RevertWithConstantMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the revertWithConstant method.""" def __init__( @@ -1141,7 +1163,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class SimpleInputNoOutputMethod(ContractMethod): +class SimpleInputNoOutputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the simpleInputNoOutput method.""" def __init__( @@ -1189,7 +1213,9 @@ def estimate_gas( ) -class SimpleInputSimpleOutputMethod(ContractMethod): +class SimpleInputSimpleOutputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the simpleInputSimpleOutput method.""" def __init__( @@ -1238,7 +1264,7 @@ def estimate_gas( ) -class SimplePureFunctionMethod(ContractMethod): +class SimplePureFunctionMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the simplePureFunction method.""" def __init__( @@ -1267,7 +1293,9 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class SimplePureFunctionWithInputMethod(ContractMethod): +class SimplePureFunctionWithInputMethod( + ContractMethod +): # pylint: disable=invalid-name """Various interfaces to the simplePureFunctionWithInput method.""" def __init__( @@ -1312,7 +1340,7 @@ def estimate_gas( return self._underlying_method(x).estimateGas(tx_params.as_dict()) -class SimpleRequireMethod(ContractMethod): +class SimpleRequireMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the simpleRequire method.""" def __init__( @@ -1340,7 +1368,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class SimpleRevertMethod(ContractMethod): +class SimpleRevertMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the simpleRevert method.""" def __init__( @@ -1368,7 +1396,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class StructInputMethod(ContractMethod): +class StructInputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the structInput method.""" def __init__( @@ -1410,7 +1438,7 @@ def estimate_gas( return self._underlying_method(s).estimateGas(tx_params.as_dict()) -class StructOutputMethod(ContractMethod): +class StructOutputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the structOutput method.""" def __init__( @@ -1446,7 +1474,7 @@ def estimate_gas(self, tx_params: Optional[TxParams] = None) -> int: return self._underlying_method().estimateGas(tx_params.as_dict()) -class WithAddressInputMethod(ContractMethod): +class WithAddressInputMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the withAddressInput method.""" def __init__( @@ -1537,7 +1565,7 @@ def estimate_gas( ) -class WithdrawMethod(ContractMethod): +class WithdrawMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the withdraw method.""" def __init__( diff --git a/packages/abi-gen/test-cli/output/python/lib_dummy/__init__.py b/packages/abi-gen/test-cli/output/python/lib_dummy/__init__.py index 6c1375710e..12e96c147f 100644 --- a/packages/abi-gen/test-cli/output/python/lib_dummy/__init__.py +++ b/packages/abi-gen/test-cli/output/python/lib_dummy/__init__.py @@ -90,7 +90,7 @@ def __init__( try: for middleware in MIDDLEWARE: web3.middleware_onion.inject( - middleware["function"], layer=middleware["layer"] + middleware["function"], layer=middleware["layer"], ) except ValueError as value_error: if value_error.args == ( diff --git a/packages/abi-gen/test-cli/output/python/test_lib_dummy/__init__.py b/packages/abi-gen/test-cli/output/python/test_lib_dummy/__init__.py index 11c40fa5d5..9fd3ea6e65 100644 --- a/packages/abi-gen/test-cli/output/python/test_lib_dummy/__init__.py +++ b/packages/abi-gen/test-cli/output/python/test_lib_dummy/__init__.py @@ -46,7 +46,7 @@ class TestLibDummyValidator( # type: ignore pass -class PublicAddConstantMethod(ContractMethod): +class PublicAddConstantMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the publicAddConstant method.""" def __init__( @@ -91,7 +91,7 @@ def estimate_gas( return self._underlying_method(x).estimateGas(tx_params.as_dict()) -class PublicAddOneMethod(ContractMethod): +class PublicAddOneMethod(ContractMethod): # pylint: disable=invalid-name """Various interfaces to the publicAddOne method.""" def __init__( diff --git a/packages/asset-swapper/CHANGELOG.json b/packages/asset-swapper/CHANGELOG.json index 65cfc37de6..6ffdadba42 100644 --- a/packages/asset-swapper/CHANGELOG.json +++ b/packages/asset-swapper/CHANGELOG.json @@ -89,6 +89,10 @@ { "note": "Fix Uniswap V2 path ordering", "pr": 2601 + }, + { + "note": "Add exchange proxy support", + "pr": 2591 } ] }, diff --git a/packages/asset-swapper/package.json b/packages/asset-swapper/package.json index 2aab8d3e5f..1788cc8811 100644 --- a/packages/asset-swapper/package.json +++ b/packages/asset-swapper/package.json @@ -49,6 +49,7 @@ "@0x/assert": "^3.0.7", "@0x/contract-addresses": "^4.9.0", "@0x/contract-wrappers": "^13.6.3", + "@0x/contracts-zero-ex": "^0.1.0", "@0x/json-schemas": "^5.0.7", "@0x/order-utils": "^10.2.4", "@0x/orderbook": "^2.2.5", @@ -56,6 +57,7 @@ "@0x/web3-wrapper": "^7.0.7", "axios": "^0.19.2", "axios-mock-adapter": "^1.18.1", + "ethereumjs-util": "^5.1.1", "heartbeats": "^5.0.1", "lodash": "^4.17.11" }, diff --git a/packages/asset-swapper/src/index.ts b/packages/asset-swapper/src/index.ts index 187f8df134..2428c35a91 100644 --- a/packages/asset-swapper/src/index.ts +++ b/packages/asset-swapper/src/index.ts @@ -60,6 +60,7 @@ export { SwapQuoteConsumerError, SignedOrderWithFillableAmounts, SwapQuoteOrdersBreakdown, + ExchangeProxyContractOpts, } from './types'; export { ERC20BridgeSource, diff --git a/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts new file mode 100644 index 0000000000..1337ab787c --- /dev/null +++ b/packages/asset-swapper/src/quote_consumers/exchange_proxy_swap_quote_consumer.ts @@ -0,0 +1,211 @@ +import { ContractAddresses } from '@0x/contract-addresses'; +import { ITransformERC20Contract } from '@0x/contract-wrappers'; +import { + encodeFillQuoteTransformerData, + encodePayTakerTransformerData, + encodeWethTransformerData, + ETH_TOKEN_ADDRESS, + FillQuoteTransformerSide, +} from '@0x/contracts-zero-ex'; +import { assetDataUtils, ERC20AssetData } from '@0x/order-utils'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber, providerUtils } from '@0x/utils'; +import { SupportedProvider, ZeroExProvider } from '@0x/web3-wrapper'; +import * as ethjs from 'ethereumjs-util'; +import * as _ from 'lodash'; + +import { constants } from '../constants'; +import { + CalldataInfo, + MarketBuySwapQuote, + MarketOperation, + MarketSellSwapQuote, + SwapQuote, + SwapQuoteConsumerBase, + SwapQuoteConsumerOpts, + SwapQuoteExecutionOpts, + SwapQuoteGetOutputOpts, +} from '../types'; +import { assert } from '../utils/assert'; + +// tslint:disable-next-line:custom-no-magic-numbers +const MAX_UINT256 = new BigNumber(2).pow(256).minus(1); +const { NULL_ADDRESS } = constants; +const MAX_NONCE_GUESSES = 2048; + +export class ExchangeProxySwapQuoteConsumer implements SwapQuoteConsumerBase { + public readonly provider: ZeroExProvider; + public readonly chainId: number; + public readonly transformerNonces: { + wethTransformer: number; + payTakerTransformer: number; + fillQuoteTransformer: number; + }; + + private readonly _transformFeature: ITransformERC20Contract; + + constructor( + supportedProvider: SupportedProvider, + public readonly contractAddresses: ContractAddresses, + options: Partial = {}, + ) { + const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); + assert.isNumber('chainId', chainId); + const provider = providerUtils.standardizeOrThrow(supportedProvider); + this.provider = provider; + this.chainId = chainId; + this.contractAddresses = contractAddresses; + this._transformFeature = new ITransformERC20Contract(contractAddresses.exchangeProxy, supportedProvider); + this.transformerNonces = { + wethTransformer: findTransformerNonce( + contractAddresses.transformers.wethTransformer, + contractAddresses.exchangeProxyTransformerDeployer, + ), + payTakerTransformer: findTransformerNonce( + contractAddresses.transformers.payTakerTransformer, + contractAddresses.exchangeProxyTransformerDeployer, + ), + fillQuoteTransformer: findTransformerNonce( + contractAddresses.transformers.fillQuoteTransformer, + contractAddresses.exchangeProxyTransformerDeployer, + ), + }; + } + + public async getCalldataOrThrowAsync( + quote: MarketBuySwapQuote | MarketSellSwapQuote, + opts: Partial = {}, + ): Promise { + assert.isValidSwapQuote('quote', quote); + const { isFromETH, isToETH } = { + ...constants.DEFAULT_FORWARDER_SWAP_QUOTE_GET_OPTS, + extensionContractOpts: { + isFromETH: false, + isToETH: false, + }, + ...opts, + }.extensionContractOpts; + + const sellToken = getTokenFromAssetData(quote.takerAssetData); + const buyToken = getTokenFromAssetData(quote.makerAssetData); + + // Build up the transforms. + const transforms = []; + if (isFromETH) { + // Create a WETH wrapper if coming from ETH. + transforms.push({ + deploymentNonce: this.transformerNonces.wethTransformer, + data: encodeWethTransformerData({ + token: ETH_TOKEN_ADDRESS, + amount: quote.worstCaseQuoteInfo.totalTakerAssetAmount, + }), + }); + } + + // This transformer will fill the quote. + transforms.push({ + deploymentNonce: this.transformerNonces.fillQuoteTransformer, + data: encodeFillQuoteTransformerData({ + sellToken, + buyToken, + side: isBuyQuote(quote) ? FillQuoteTransformerSide.Buy : FillQuoteTransformerSide.Sell, + fillAmount: isBuyQuote(quote) ? quote.makerAssetFillAmount : quote.takerAssetFillAmount, + maxOrderFillAmounts: [], + orders: quote.orders, + signatures: quote.orders.map(o => o.signature), + }), + }); + + if (isToETH) { + // Create a WETH unwrapper if going to ETH. + transforms.push({ + deploymentNonce: this.transformerNonces.wethTransformer, + data: encodeWethTransformerData({ + token: this.contractAddresses.etherToken, + amount: MAX_UINT256, + }), + }); + } + + // The final transformer will send all funds to the taker. + transforms.push({ + deploymentNonce: this.transformerNonces.payTakerTransformer, + data: encodePayTakerTransformerData({ + tokens: [sellToken, buyToken, ETH_TOKEN_ADDRESS], + amounts: [], + }), + }); + + const calldataHexString = this._transformFeature + .transformERC20( + isFromETH ? ETH_TOKEN_ADDRESS : sellToken, + isToETH ? ETH_TOKEN_ADDRESS : buyToken, + quote.worstCaseQuoteInfo.totalTakerAssetAmount, + quote.worstCaseQuoteInfo.makerAssetAmount, + transforms, + ) + .getABIEncodedTransactionData(); + + let ethAmount = quote.worstCaseQuoteInfo.protocolFeeInWeiAmount; + if (isFromETH) { + ethAmount = ethAmount.plus(quote.worstCaseQuoteInfo.takerAssetAmount); + } + + return { + calldataHexString, + ethAmount, + toAddress: this._transformFeature.address, + allowanceTarget: this.contractAddresses.exchangeProxyAllowanceTarget, + }; + } + + // tslint:disable-next-line:prefer-function-over-method + public async executeSwapQuoteOrThrowAsync( + _quote: SwapQuote, + _opts: Partial, + ): Promise { + throw new Error('Execution not supported for Exchange Proxy quotes'); + } +} + +function isBuyQuote(quote: SwapQuote): quote is MarketBuySwapQuote { + return quote.type === MarketOperation.Buy; +} + +function getTokenFromAssetData(assetData: string): string { + const data = assetDataUtils.decodeAssetDataOrThrow(assetData); + if (data.assetProxyId !== AssetProxyId.ERC20) { + throw new Error(`Unsupported exchange proxy quote asset type: ${data.assetProxyId}`); + } + // tslint:disable-next-line:no-unnecessary-type-assertion + return (data as ERC20AssetData).tokenAddress; +} + +/** + * Find the nonce for a transformer given its deployer. + * If `deployer` is the null address, zero will always be returned. + */ +export function findTransformerNonce(transformer: string, deployer: string = NULL_ADDRESS): number { + if (deployer === NULL_ADDRESS) { + return 0; + } + const lowercaseTransformer = transformer.toLowerCase(); + // Try to guess the nonce. + for (let nonce = 0; nonce < MAX_NONCE_GUESSES; ++nonce) { + const deployedAddress = getTransformerAddress(deployer, nonce); + if (deployedAddress === lowercaseTransformer) { + return nonce; + } + } + throw new Error(`${deployer} did not deploy ${transformer}!`); +} + +/** + * Compute the deployed address for a transformer given a deployer and nonce. + */ +export function getTransformerAddress(deployer: string, nonce: number): string { + return ethjs.bufferToHex( + // tslint:disable-next-line: custom-no-magic-numbers + ethjs.rlphash([deployer, nonce] as any).slice(12), + ); +} diff --git a/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts index 3fefdd3740..99168ed10c 100644 --- a/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/exchange_swap_quote_consumer.ts @@ -25,7 +25,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase { constructor( supportedProvider: SupportedProvider, - contractAddresses: ContractAddresses, + public readonly contractAddresses: ContractAddresses, options: Partial = {}, ) { const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); @@ -59,6 +59,7 @@ export class ExchangeSwapQuoteConsumer implements SwapQuoteConsumerBase { calldataHexString, ethAmount: quote.worstCaseQuoteInfo.protocolFeeInWeiAmount, toAddress: this._exchangeContract.address, + allowanceTarget: this.contractAddresses.erc20Proxy, }; } diff --git a/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts index 6284fcd2d9..2f8fee221b 100644 --- a/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/forwarder_swap_quote_consumer.ts @@ -19,16 +19,17 @@ import { affiliateFeeUtils } from '../utils/affiliate_fee_utils'; import { assert } from '../utils/assert'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; +const { NULL_ADDRESS } = constants; + export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { public readonly provider: ZeroExProvider; public readonly chainId: number; - private readonly _contractAddresses: ContractAddresses; private readonly _forwarder: ForwarderContract; constructor( supportedProvider: SupportedProvider, - contractAddresses: ContractAddresses, + public readonly contractAddresses: ContractAddresses, options: Partial = {}, ) { const { chainId } = _.merge({}, constants.DEFAULT_SWAP_QUOTER_OPTS, options); @@ -36,7 +37,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { const provider = providerUtils.standardizeOrThrow(supportedProvider); this.provider = provider; this.chainId = chainId; - this._contractAddresses = contractAddresses; this._forwarder = new ForwarderContract(contractAddresses.forwarder, supportedProvider); } @@ -90,6 +90,7 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { calldataHexString, toAddress: this._forwarder.address, ethAmount: ethAmountWithFees, + allowanceTarget: NULL_ADDRESS, }; } @@ -160,6 +161,6 @@ export class ForwarderSwapQuoteConsumer implements SwapQuoteConsumerBase { } private _getEtherTokenAssetDataOrThrow(): string { - return assetDataUtils.encodeERC20AssetData(this._contractAddresses.etherToken); + return assetDataUtils.encodeERC20AssetData(this.contractAddresses.etherToken); } } diff --git a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts index be11f56270..029663978c 100644 --- a/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts +++ b/packages/asset-swapper/src/quote_consumers/swap_quote_consumer.ts @@ -17,6 +17,7 @@ import { import { assert } from '../utils/assert'; import { swapQuoteConsumerUtils } from '../utils/swap_quote_consumer_utils'; +import { ExchangeProxySwapQuoteConsumer } from './exchange_proxy_swap_quote_consumer'; import { ExchangeSwapQuoteConsumer } from './exchange_swap_quote_consumer'; import { ForwarderSwapQuoteConsumer } from './forwarder_swap_quote_consumer'; @@ -27,6 +28,7 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { private readonly _exchangeConsumer: ExchangeSwapQuoteConsumer; private readonly _forwarderConsumer: ForwarderSwapQuoteConsumer; private readonly _contractAddresses: ContractAddresses; + private readonly _exchangeProxyConsumer: ExchangeProxySwapQuoteConsumer; public static getSwapQuoteConsumer( supportedProvider: SupportedProvider, @@ -45,6 +47,11 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { this._contractAddresses = options.contractAddresses || getContractAddressesForChainOrThrow(chainId); this._exchangeConsumer = new ExchangeSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); this._forwarderConsumer = new ForwarderSwapQuoteConsumer(supportedProvider, this._contractAddresses, options); + this._exchangeProxyConsumer = new ExchangeProxySwapQuoteConsumer( + supportedProvider, + this._contractAddresses, + options, + ); } /** @@ -93,9 +100,13 @@ export class SwapQuoteConsumer implements SwapQuoteConsumerBase { } private async _getConsumerForSwapQuoteAsync(opts: Partial): Promise { - if (opts.useExtensionContract === ExtensionContractType.Forwarder) { - return this._forwarderConsumer; + switch (opts.useExtensionContract) { + case ExtensionContractType.Forwarder: + return this._forwarderConsumer; + case ExtensionContractType.ExchangeProxy: + return this._exchangeProxyConsumer; + default: + return this._exchangeConsumer; } - return this._exchangeConsumer; } } diff --git a/packages/asset-swapper/src/types.ts b/packages/asset-swapper/src/types.ts index 2168991dad..bebe297fe2 100644 --- a/packages/asset-swapper/src/types.ts +++ b/packages/asset-swapper/src/types.ts @@ -50,19 +50,22 @@ export interface SignedOrderWithFillableAmounts extends SignedOrder { * calldataHexString: The hexstring of the calldata. * toAddress: The contract address to call. * ethAmount: The eth amount in wei to send with the smart contract call. + * allowanceTarget: The address the taker should grant an allowance to. */ export interface CalldataInfo { calldataHexString: string; toAddress: string; ethAmount: BigNumber; + allowanceTarget: string; } /** * Represents the varying smart contracts that can consume a valid swap quote */ export enum ExtensionContractType { - Forwarder = 'FORWARDER', None = 'NONE', + Forwarder = 'FORWARDER', + ExchangeProxy = 'EXCHANGE_PROXY', } /** @@ -97,7 +100,7 @@ export interface SwapQuoteConsumerOpts { */ export interface SwapQuoteGetOutputOpts { useExtensionContract: ExtensionContractType; - extensionContractOpts?: ForwarderExtensionContractOpts | any; + extensionContractOpts?: ForwarderExtensionContractOpts | ExchangeProxyContractOpts | any; } /** @@ -112,7 +115,6 @@ export interface SwapQuoteExecutionOpts extends SwapQuoteGetOutputOpts { } /** - * ethAmount: The amount of eth (in Wei) sent to the forwarder contract. * feePercentage: percentage (up to 5%) of the taker asset paid to feeRecipient * feeRecipient: address of the receiver of the feePercentage of taker asset */ @@ -121,6 +123,15 @@ export interface ForwarderExtensionContractOpts { feeRecipient: string; } +/** + * @param isFromETH Whether the input token is ETH. + * @param isToETH Whether the output token is ETH. + */ +export interface ExchangeProxyContractOpts { + isFromETH: boolean; + isToETH: boolean; +} + export type SwapQuote = MarketBuySwapQuote | MarketSellSwapQuote; export interface GetExtensionContractTypeOpts { diff --git a/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts new file mode 100644 index 0000000000..9f09e29879 --- /dev/null +++ b/packages/asset-swapper/test/exchange_proxy_swap_quote_consumer_test.ts @@ -0,0 +1,268 @@ +import { getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; +import { constants as contractConstants, getRandomInteger, Numberish, randomAddress } from '@0x/contracts-test-utils'; +import { + decodeFillQuoteTransformerData, + decodePayTakerTransformerData, + decodeWethTransformerData, + ETH_TOKEN_ADDRESS, + FillQuoteTransformerSide, +} from '@0x/contracts-zero-ex'; +import { assetDataUtils } from '@0x/order-utils'; +import { Order } from '@0x/types'; +import { AbiEncoder, BigNumber, hexUtils } from '@0x/utils'; +import * as chai from 'chai'; +import * as _ from 'lodash'; +import 'mocha'; + +import { constants } from '../src/constants'; +import { + ExchangeProxySwapQuoteConsumer, + getTransformerAddress, +} from '../src/quote_consumers/exchange_proxy_swap_quote_consumer'; +import { MarketBuySwapQuote, MarketOperation, MarketSellSwapQuote } from '../src/types'; +import { OptimizedMarketOrder } from '../src/utils/market_operation_utils/types'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +const { NULL_ADDRESS } = constants; +const { MAX_UINT256 } = contractConstants; + +// tslint:disable: custom-no-magic-numbers + +describe('ExchangeProxySwapQuoteConsumer', () => { + const CHAIN_ID = 1; + const TAKER_TOKEN = randomAddress(); + const MAKER_TOKEN = randomAddress(); + const TRANSFORMER_DEPLOYER = randomAddress(); + const contractAddresses = { + ...getContractAddressesForChainOrThrow(CHAIN_ID), + exchangeProxy: randomAddress(), + exchangeProxyAllowanceTarget: randomAddress(), + exchangeProxyTransformerDeployer: TRANSFORMER_DEPLOYER, + transformers: { + wethTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 1), + payTakerTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 2), + fillQuoteTransformer: getTransformerAddress(TRANSFORMER_DEPLOYER, 3), + }, + }; + let consumer: ExchangeProxySwapQuoteConsumer; + + before(async () => { + const fakeProvider = { + async sendAsync(): Promise { + /* noop */ + }, + }; + consumer = new ExchangeProxySwapQuoteConsumer(fakeProvider, contractAddresses, { chainId: CHAIN_ID }); + }); + + function getRandomAmount(maxAmount: Numberish = '1e18'): BigNumber { + return getRandomInteger(1, maxAmount); + } + + function createAssetData(token?: string): string { + return assetDataUtils.encodeERC20AssetData(token || randomAddress()); + } + + function getRandomOrder(): OptimizedMarketOrder { + return { + fillableMakerAssetAmount: getRandomAmount(), + fillableTakerFeeAmount: getRandomAmount(), + fillableTakerAssetAmount: getRandomAmount(), + fills: [], + chainId: CHAIN_ID, + exchangeAddress: contractAddresses.exchange, + expirationTimeSeconds: getRandomInteger(1, 2e9), + feeRecipientAddress: randomAddress(), + makerAddress: randomAddress(), + makerAssetAmount: getRandomAmount(), + takerAssetAmount: getRandomAmount(), + makerFee: getRandomAmount(), + takerFee: getRandomAmount(), + salt: getRandomAmount(2e9), + signature: hexUtils.random(66), + senderAddress: NULL_ADDRESS, + takerAddress: NULL_ADDRESS, + makerAssetData: createAssetData(MAKER_TOKEN), + takerAssetData: createAssetData(TAKER_TOKEN), + makerFeeAssetData: createAssetData(), + takerFeeAssetData: createAssetData(), + }; + } + + function getRandomQuote(side: MarketOperation): MarketBuySwapQuote | MarketSellSwapQuote { + return { + gasPrice: getRandomInteger(1, 1e9), + type: side, + makerAssetData: createAssetData(MAKER_TOKEN), + takerAssetData: createAssetData(TAKER_TOKEN), + orders: [getRandomOrder()], + bestCaseQuoteInfo: { + feeTakerAssetAmount: getRandomAmount(), + makerAssetAmount: getRandomAmount(), + gas: Math.floor(Math.random() * 8e6), + protocolFeeInWeiAmount: getRandomAmount(), + takerAssetAmount: getRandomAmount(), + totalTakerAssetAmount: getRandomAmount(), + }, + worstCaseQuoteInfo: { + feeTakerAssetAmount: getRandomAmount(), + makerAssetAmount: getRandomAmount(), + gas: Math.floor(Math.random() * 8e6), + protocolFeeInWeiAmount: getRandomAmount(), + takerAssetAmount: getRandomAmount(), + totalTakerAssetAmount: getRandomAmount(), + }, + ...(side === MarketOperation.Buy + ? { makerAssetFillAmount: getRandomAmount() } + : { takerAssetFillAmount: getRandomAmount() }), + } as any; + } + + function getRandomSellQuote(): MarketSellSwapQuote { + return getRandomQuote(MarketOperation.Sell) as MarketSellSwapQuote; + } + + function getRandomBuyQuote(): MarketBuySwapQuote { + return getRandomQuote(MarketOperation.Buy) as MarketBuySwapQuote; + } + + type PlainOrder = Exclude; + + function cleanOrders(orders: OptimizedMarketOrder[]): PlainOrder[] { + return orders.map( + o => + _.omit(o, [ + 'chainId', + 'exchangeAddress', + 'fillableMakerAssetAmount', + 'fillableTakerAssetAmount', + 'fillableTakerFeeAmount', + 'fills', + 'signature', + ]) as PlainOrder, + ); + } + + const callDataEncoder = AbiEncoder.createMethod('transformERC20', [ + { type: 'address', name: 'inputToken' }, + { type: 'address', name: 'outputToken' }, + { type: 'uint256', name: 'inputTokenAmount' }, + { type: 'uint256', name: 'minOutputTokenAmount' }, + { + type: 'tuple[]', + name: 'transformations', + components: [{ type: 'uint32', name: 'deploymentNonce' }, { type: 'bytes', name: 'data' }], + }, + ]); + + interface CallArgs { + inputToken: string; + outputToken: string; + inputTokenAmount: BigNumber; + minOutputTokenAmount: BigNumber; + transformations: Array<{ + deploymentNonce: BigNumber; + data: string; + }>; + } + + describe('getCalldataOrThrow()', () => { + it('can produce a sell quote', async () => { + const quote = getRandomSellQuote(); + const callInfo = await consumer.getCalldataOrThrowAsync(quote); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + expect(callArgs.inputToken).to.eq(TAKER_TOKEN); + expect(callArgs.outputToken).to.eq(MAKER_TOKEN); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount); + expect(callArgs.transformations).to.be.length(2); + expect( + callArgs.transformations[0].deploymentNonce.toNumber() === + consumer.transformerNonces.fillQuoteTransformer, + ); + expect( + callArgs.transformations[1].deploymentNonce.toNumber() === + consumer.transformerNonces.payTakerTransformer, + ); + const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); + expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Sell); + expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.takerAssetFillAmount); + expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders)); + expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature)); + expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); + expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); + const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data); + expect(payTakerTransformerData.amounts).to.deep.eq([]); + expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]); + }); + + it('can produce a buy quote', async () => { + const quote = getRandomBuyQuote(); + const callInfo = await consumer.getCalldataOrThrowAsync(quote); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + expect(callArgs.inputToken).to.eq(TAKER_TOKEN); + expect(callArgs.outputToken).to.eq(MAKER_TOKEN); + expect(callArgs.inputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); + expect(callArgs.minOutputTokenAmount).to.bignumber.eq(quote.worstCaseQuoteInfo.makerAssetAmount); + expect(callArgs.transformations).to.be.length(2); + expect( + callArgs.transformations[0].deploymentNonce.toNumber() === + consumer.transformerNonces.fillQuoteTransformer, + ); + expect( + callArgs.transformations[1].deploymentNonce.toNumber() === + consumer.transformerNonces.payTakerTransformer, + ); + const fillQuoteTransformerData = decodeFillQuoteTransformerData(callArgs.transformations[0].data); + expect(fillQuoteTransformerData.side).to.eq(FillQuoteTransformerSide.Buy); + expect(fillQuoteTransformerData.fillAmount).to.bignumber.eq(quote.makerAssetFillAmount); + expect(fillQuoteTransformerData.orders).to.deep.eq(cleanOrders(quote.orders)); + expect(fillQuoteTransformerData.signatures).to.deep.eq(quote.orders.map(o => o.signature)); + expect(fillQuoteTransformerData.sellToken).to.eq(TAKER_TOKEN); + expect(fillQuoteTransformerData.buyToken).to.eq(MAKER_TOKEN); + const payTakerTransformerData = decodePayTakerTransformerData(callArgs.transformations[1].data); + expect(payTakerTransformerData.amounts).to.deep.eq([]); + expect(payTakerTransformerData.tokens).to.deep.eq([TAKER_TOKEN, MAKER_TOKEN, ETH_TOKEN_ADDRESS]); + }); + + it('ERC20 -> ERC20 does not have a WETH transformer', async () => { + const quote = getRandomSellQuote(); + const callInfo = await consumer.getCalldataOrThrowAsync(quote); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + const nonces = callArgs.transformations.map(t => t.deploymentNonce); + expect(nonces).to.not.include(consumer.transformerNonces.wethTransformer); + }); + + it('ETH -> ERC20 has a WETH transformer before the fill', async () => { + const quote = getRandomSellQuote(); + const callInfo = await consumer.getCalldataOrThrowAsync(quote, { + extensionContractOpts: { isFromETH: true }, + }); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + expect(callArgs.transformations[0].deploymentNonce.toNumber()).to.eq( + consumer.transformerNonces.wethTransformer, + ); + const wethTransformerData = decodeWethTransformerData(callArgs.transformations[0].data); + expect(wethTransformerData.amount).to.bignumber.eq(quote.worstCaseQuoteInfo.totalTakerAssetAmount); + expect(wethTransformerData.token).to.eq(ETH_TOKEN_ADDRESS); + }); + + it('ERC20 -> ETH has a WETH transformer after the fill', async () => { + const quote = getRandomSellQuote(); + const callInfo = await consumer.getCalldataOrThrowAsync(quote, { + extensionContractOpts: { isToETH: true }, + }); + const callArgs = callDataEncoder.decode(callInfo.calldataHexString) as CallArgs; + expect(callArgs.transformations[1].deploymentNonce.toNumber()).to.eq( + consumer.transformerNonces.wethTransformer, + ); + const wethTransformerData = decodeWethTransformerData(callArgs.transformations[1].data); + expect(wethTransformerData.amount).to.bignumber.eq(MAX_UINT256); + expect(wethTransformerData.token).to.eq(contractAddresses.etherToken); + }); + }); +}); diff --git a/packages/contract-addresses/CHANGELOG.json b/packages/contract-addresses/CHANGELOG.json index 5b9a0ca596..a0b1dca24a 100644 --- a/packages/contract-addresses/CHANGELOG.json +++ b/packages/contract-addresses/CHANGELOG.json @@ -65,6 +65,10 @@ { "note": "Add UniswapV2Bridge address on Mainnet (new field)", "pr": 2599 + }, + { + "note": "Add Exchange Proxy addresses", + "pr": 2591 } ] }, diff --git a/packages/contract-addresses/addresses.json b/packages/contract-addresses/addresses.json index 95f0784e5a..0b6287cbb7 100644 --- a/packages/contract-addresses/addresses.json +++ b/packages/contract-addresses/addresses.json @@ -32,7 +32,16 @@ "curveBridge": "0x6dc7950423ada9f56fb2c93a23edb787f1e29088", "maximumGasPrice": "0xe2bfd35306495d11e3c9db0d8de390cda24563cf", "dexForwarderBridge": "0x5591360f8c7640fea5771c9682d6b5ecb776e1f8", - "multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1" + "multiBridge": "0xc03117a8c9bde203f70aa911cb64a7a0df5ba1e1", + "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", + "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", + "exchangeProxyTransformerDeployer": "0x80a36559ab9a497fb658325ed771a584eb0f13da", + "transformers": { + "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", + "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", + "fillQuoteTransformer": "0x9b81a08ef144e7aa4925f7fd77da1e1b3990e59a" + } }, "3": { "erc20Proxy": "0xb1408f4c245a23c31b98d2c626777d4c0d766caa", @@ -67,7 +76,16 @@ "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x407b4128e9ecad8769b2332312a9f655cb9f5f3a", "dexForwarderBridge": "0x3be8e59038d8c4e8d8776ca40ef2f024bad95ad1", - "multiBridge": "0x0000000000000000000000000000000000000000" + "multiBridge": "0x0000000000000000000000000000000000000000", + "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", + "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", + "exchangeProxyTransformerDeployer": "0x80a36559ab9a497fb658325ed771a584eb0f13da", + "transformers": { + "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", + "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", + "fillQuoteTransformer": "0x9b81a08ef144e7aa4925f7fd77da1e1b3990e59a" + } }, "4": { "exchangeV2": "0xbff9493f92a3df4b0429b6d00743b3cfb4c85831", @@ -102,7 +120,16 @@ "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x47697b44bd89051e93b4d5857ba8e024800a74ac", "dexForwarderBridge": "0x0000000000000000000000000000000000000000", - "multiBridge": "0x0000000000000000000000000000000000000000" + "multiBridge": "0x0000000000000000000000000000000000000000", + "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", + "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", + "exchangeProxyTransformerDeployer": "0x80a36559ab9a497fb658325ed771a584eb0f13da", + "transformers": { + "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", + "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", + "fillQuoteTransformer": "0x9b81a08ef144e7aa4925f7fd77da1e1b3990e59a" + } }, "42": { "erc20Proxy": "0xf1ec01d6236d3cd881a0bf0130ea25fe4234003e", @@ -137,7 +164,16 @@ "curveBridge": "0x90c62c91a9f655f4f739e6cee85c84f9ccf47323", "maximumGasPrice": "0x67a094cf028221ffdd93fc658f963151d05e2a74", "dexForwarderBridge": "0x6cce442a48ab07635462a40594054f34f44195ff", - "multiBridge": "0x0000000000000000000000000000000000000000" + "multiBridge": "0x0000000000000000000000000000000000000000", + "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", + "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", + "exchangeProxyTransformerDeployer": "0x80a36559ab9a497fb658325ed771a584eb0f13da", + "transformers": { + "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", + "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", + "fillQuoteTransformer": "0x9b81a08ef144e7aa4925f7fd77da1e1b3990e59a" + } }, "1337": { "erc20Proxy": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48", @@ -172,6 +208,15 @@ "curveBridge": "0x0000000000000000000000000000000000000000", "maximumGasPrice": "0x0000000000000000000000000000000000000000", "dexForwarderBridge": "0x0000000000000000000000000000000000000000", - "multiBridge": "0x0000000000000000000000000000000000000000" + "multiBridge": "0x0000000000000000000000000000000000000000", + "exchangeProxyGovernor": "0x618f9c67ce7bf1a50afa1e7e0238422601b0ff6e", + "exchangeProxy": "0xdef1c0ded9bec7f1a1670819833240f027b25eff", + "exchangeProxyAllowanceTarget": "0xf740b67da229f2f10bcbd38a7979992fcc71b8eb", + "exchangeProxyTransformerDeployer": "0x80a36559ab9a497fb658325ed771a584eb0f13da", + "transformers": { + "wethTransformer": "0x7bab5f7299e1ca123bb44eb71e6c89be7e558cc8", + "payTakerTransformer": "0xe8c07a119452b55eee2f999478aab97f3656d841", + "fillQuoteTransformer": "0x9b81a08ef144e7aa4925f7fd77da1e1b3990e59a" + } } } diff --git a/packages/contract-addresses/src/index.ts b/packages/contract-addresses/src/index.ts index 627f03527e..1a7830d01e 100644 --- a/packages/contract-addresses/src/index.ts +++ b/packages/contract-addresses/src/index.ts @@ -34,6 +34,15 @@ export interface ContractAddresses { maximumGasPrice: string; dexForwarderBridge: string; multiBridge: string; + exchangeProxyGovernor: string; + exchangeProxy: string; + exchangeProxyAllowanceTarget: string; + exchangeProxyTransformerDeployer: string; + transformers: { + wethTransformer: string; + payTakerTransformer: string; + fillQuoteTransformer: string; + }; } export enum ChainId { diff --git a/packages/contract-artifacts/CHANGELOG.json b/packages/contract-artifacts/CHANGELOG.json index fa3d6e3aa9..18105a0e6d 100644 --- a/packages/contract-artifacts/CHANGELOG.json +++ b/packages/contract-artifacts/CHANGELOG.json @@ -29,6 +29,10 @@ { "note": "Added `ERC20BridgeSampler.sampleSellsFromMultiBridge", "pr": 2593 + }, + { + "note": "Added `ITransformERC20`", + "pr": 2591 } ] }, diff --git a/packages/contract-artifacts/artifacts/ITransformERC20.json b/packages/contract-artifacts/artifacts/ITransformERC20.json new file mode 100644 index 0000000000..877edab6c4 --- /dev/null +++ b/packages/contract-artifacts/artifacts/ITransformERC20.json @@ -0,0 +1,171 @@ +{ + "schemaVersion": "2.0.0", + "contractName": "ITransformERC20", + "compilerOutput": { + "abi": [ + { + "anonymous": false, + "inputs": [ + { "indexed": true, "internalType": "address", "name": "taker", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "inputToken", "type": "address" }, + { "indexed": false, "internalType": "address", "name": "outputToken", "type": "address" }, + { "indexed": false, "internalType": "uint256", "name": "inputTokenAmount", "type": "uint256" }, + { "indexed": false, "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" } + ], + "name": "TransformedERC20", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { "indexed": false, "internalType": "address", "name": "transformerDeployer", "type": "address" } + ], + "name": "TransformerDeployerUpdated", + "type": "event" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "callDataHash", "type": "bytes32" }, + { "internalType": "address payable", "name": "taker", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "inputToken", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "inputTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minOutputTokenAmount", "type": "uint256" }, + { + "components": [ + { "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct ITransformERC20.Transformation[]", + "name": "transformations", + "type": "tuple[]" + } + ], + "name": "_transformERC20", + "outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "createTransformWallet", + "outputs": [{ "internalType": "contract IFlashWallet", "name": "wallet", "type": "address" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getTransformWallet", + "outputs": [{ "internalType": "contract IFlashWallet", "name": "wallet", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getTransformerDeployer", + "outputs": [{ "internalType": "address", "name": "deployer", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [{ "internalType": "address", "name": "transformerDeployer", "type": "address" }], + "name": "setTransformerDeployer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "contract IERC20TokenV06", "name": "inputToken", "type": "address" }, + { "internalType": "contract IERC20TokenV06", "name": "outputToken", "type": "address" }, + { "internalType": "uint256", "name": "inputTokenAmount", "type": "uint256" }, + { "internalType": "uint256", "name": "minOutputTokenAmount", "type": "uint256" }, + { + "components": [ + { "internalType": "uint32", "name": "deploymentNonce", "type": "uint32" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct ITransformERC20.Transformation[]", + "name": "transformations", + "type": "tuple[]" + } + ], + "name": "transformERC20", + "outputs": [{ "internalType": "uint256", "name": "outputTokenAmount", "type": "uint256" }], + "stateMutability": "payable", + "type": "function" + } + ], + "devdoc": { + "details": "Feature to composably transform between ERC20 tokens.", + "methods": { + "_transformERC20(bytes32,address,address,address,uint256,uint256,(uint32,bytes)[])": { + "details": "Internal version of `transformERC20()`. Only callable from within.", + "params": { + "callDataHash": "Hash of the ingress calldata.", + "inputToken": "The token being provided by the taker. If `0xeee...`, ETH is implied and should be provided with the call.`", + "inputTokenAmount": "The amount of `inputToken` to take from the taker.", + "minOutputTokenAmount": "The minimum amount of `outputToken` the taker must receive for the entire transformation to succeed.", + "outputToken": "The token to be acquired by the taker. `0xeee...` implies ETH.", + "taker": "The taker address.", + "transformations": "The transformations to execute on the token balance(s) in sequence." + }, + "returns": { "outputTokenAmount": "The amount of `outputToken` received by the taker." } + }, + "createTransformWallet()": { + "details": "Deploy a new flash wallet instance and replace the current one with it. Useful if we somehow break the current wallet instance. Anyone can call this.", + "returns": { "wallet": "The new wallet instance." } + }, + "getTransformWallet()": { + "details": "Return the current wallet instance that will serve as the execution context for transformations.", + "returns": { "wallet": "The wallet instance." } + }, + "getTransformerDeployer()": { + "details": "Return the allowed deployer for transformers.", + "returns": { "deployer": "The transform deployer address." } + }, + "setTransformerDeployer(address)": { + "details": "Replace the allowed deployer for transformers. Only callable by the owner.", + "params": { "transformerDeployer": "The address of the trusted deployer for transformers." } + }, + "transformERC20(address,address,uint256,uint256,(uint32,bytes)[])": { + "details": "Executes a series of transformations to convert an ERC20 `inputToken` to an ERC20 `outputToken`.", + "params": { + "inputToken": "The token being provided by the sender. If `0xeee...`, ETH is implied and should be provided with the call.`", + "inputTokenAmount": "The amount of `inputToken` to take from the sender.", + "minOutputTokenAmount": "The minimum amount of `outputToken` the sender must receive for the entire transformation to succeed.", + "outputToken": "The token to be acquired by the sender. `0xeee...` implies ETH.", + "transformations": "The transformations to execute on the token balance(s) in sequence." + }, + "returns": { "outputTokenAmount": "The amount of `outputToken` received by the sender." } + } + } + }, + "evm": { "bytecode": { "object": "0x" }, "deployedBytecode": { "immutableReferences": {}, "object": "0x" } } + }, + "compiler": { + "name": "solc", + "version": "0.6.9+commit.3e3065ac", + "settings": { + "optimizer": { + "enabled": true, + "runs": 1000000, + "details": { "yul": true, "deduplicate": true, "cse": true, "constantOptimizer": true } + }, + "outputSelection": { + "*": { + "*": [ + "abi", + "devdoc", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + }, + "evmVersion": "istanbul" + } + }, + "chains": {} +} diff --git a/packages/contract-artifacts/src/index.ts b/packages/contract-artifacts/src/index.ts index 78d92a62ba..e51c6ff302 100644 --- a/packages/contract-artifacts/src/index.ts +++ b/packages/contract-artifacts/src/index.ts @@ -26,6 +26,7 @@ import * as ZRXToken from '../artifacts/ZRXToken.json'; import * as ERC20BridgeProxy from '../artifacts/ERC20BridgeProxy.json'; import * as ZrxVault from '../artifacts/ZrxVault.json'; import * as IERC20BridgeSampler from '../artifacts/IERC20BridgeSampler.json'; +import * as ITransformERC20 from '../artifacts/ITransformERC20.json'; export { AssetProxyOwner, @@ -56,4 +57,5 @@ export { StakingProxy, ZrxVault, IERC20BridgeSampler, + ITransformERC20, }; diff --git a/packages/contract-artifacts/tsconfig.json b/packages/contract-artifacts/tsconfig.json index 1c174c6d3b..7f6106b0a3 100644 --- a/packages/contract-artifacts/tsconfig.json +++ b/packages/contract-artifacts/tsconfig.json @@ -36,6 +36,7 @@ "./artifacts/StakingProxy.json", "./artifacts/ZrxVault.json", "./artifacts/ERC20BridgeProxy.json", - "./artifacts/IERC20BridgeSampler.json" + "./artifacts/IERC20BridgeSampler.json", + "./artifacts/ITransformERC20.json" ] } diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index 5b703ba9fa..93ac32215a 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -33,6 +33,10 @@ { "note": "Added `ERC20BridgeSampler.sampleSellsFromMultiBridge", "pr": 2593 + }, + { + "note": "Add `ITransformERC20`", + "pr": 2591 } ] }, diff --git a/packages/contract-wrappers/package.json b/packages/contract-wrappers/package.json index bd73ccb222..b5e220c31d 100644 --- a/packages/contract-wrappers/package.json +++ b/packages/contract-wrappers/package.json @@ -32,7 +32,7 @@ "wrappers:generate": "abi-gen --abis ${npm_package_config_abis} --output src/generated-wrappers --backend ethers" }, "config": { - "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice).json" + "abis": "../contract-artifacts/artifacts/@(DevUtils|ERC20Token|ERC721Token|Exchange|Forwarder|IAssetData|LibTransactionDecoder|WETH9|Coordinator|Staking|StakingProxy|IERC20BridgeSampler|ERC20BridgeSampler|GodsUnchainedValidator|Broker|ILiquidityProvider|ILiquidityProviderRegistry|MaximumGasPrice|ITransformERC20).json" }, "gitpkg": { "registry": "git@github.com:0xProject/gitpkg-registry.git" diff --git a/packages/contract-wrappers/src/generated-wrappers/i_transform_erc20.ts b/packages/contract-wrappers/src/generated-wrappers/i_transform_erc20.ts new file mode 100644 index 0000000000..1fa812fef1 --- /dev/null +++ b/packages/contract-wrappers/src/generated-wrappers/i_transform_erc20.ts @@ -0,0 +1,910 @@ +// tslint:disable:no-consecutive-blank-lines ordered-imports align trailing-comma enum-naming +// tslint:disable:whitespace no-unbound-method no-trailing-whitespace +// tslint:disable:no-unused-variable +import { + AwaitTransactionSuccessOpts, + ContractFunctionObj, + ContractTxFunctionObj, + SendTransactionOpts, + BaseContract, + SubscriptionManager, + PromiseWithTransactionHash, + methodAbiToFunctionSignature, + linkLibrariesInBytecode, +} from '@0x/base-contract'; +import { schemas } from '@0x/json-schemas'; +import { + BlockParam, + BlockParamLiteral, + BlockRange, + CallData, + ContractAbi, + ContractArtifact, + DecodedLogArgs, + LogWithDecodedArgs, + MethodAbi, + TransactionReceiptWithDecodedLogs, + TxData, + TxDataPayable, + SupportedProvider, +} from 'ethereum-types'; +import { BigNumber, classUtils, hexUtils, logUtils, providerUtils } from '@0x/utils'; +import { EventCallback, IndexedFilterValues, SimpleContractArtifact } from '@0x/types'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import { assert } from '@0x/assert'; +import * as ethers from 'ethers'; +// tslint:enable:no-unused-variable + +export type ITransformERC20EventArgs = + | ITransformERC20TransformedERC20EventArgs + | ITransformERC20TransformerDeployerUpdatedEventArgs; + +export enum ITransformERC20Events { + TransformedERC20 = 'TransformedERC20', + TransformerDeployerUpdated = 'TransformerDeployerUpdated', +} + +export interface ITransformERC20TransformedERC20EventArgs extends DecodedLogArgs { + taker: string; + inputToken: string; + outputToken: string; + inputTokenAmount: BigNumber; + outputTokenAmount: BigNumber; +} + +export interface ITransformERC20TransformerDeployerUpdatedEventArgs extends DecodedLogArgs { + transformerDeployer: string; +} + +/* istanbul ignore next */ +// tslint:disable:array-type +// tslint:disable:no-parameter-reassignment +// tslint:disable-next-line:class-name +export class ITransformERC20Contract extends BaseContract { + /** + * @ignore + */ + public static deployedBytecode: string | undefined; + public static contractName = 'ITransformERC20'; + private readonly _methodABIIndex: { [name: string]: number } = {}; + private readonly _subscriptionManager: SubscriptionManager; + public static async deployFrom0xArtifactAsync( + artifact: ContractArtifact | SimpleContractArtifact, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const bytecode = artifact.compilerOutput.evm.bytecode.object; + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + return ITransformERC20Contract.deployAsync(bytecode, abi, provider, txDefaults, logDecodeDependenciesAbiOnly); + } + + public static async deployWithLibrariesFrom0xArtifactAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractArtifact | SimpleContractArtifact }, + ): Promise { + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + if (artifact.compilerOutput === undefined) { + throw new Error('Compiler output not found in the artifact file'); + } + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const abi = artifact.compilerOutput.abi; + const logDecodeDependenciesAbiOnly: { [contractName: string]: ContractAbi } = {}; + if (Object.keys(logDecodeDependencies) !== undefined) { + for (const key of Object.keys(logDecodeDependencies)) { + logDecodeDependenciesAbiOnly[key] = logDecodeDependencies[key].compilerOutput.abi; + } + } + const libraryAddresses = await ITransformERC20Contract._deployLibrariesAsync( + artifact, + libraryArtifacts, + new Web3Wrapper(provider), + txDefaults, + ); + const bytecode = linkLibrariesInBytecode(artifact, libraryAddresses); + return ITransformERC20Contract.deployAsync(bytecode, abi, provider, txDefaults, logDecodeDependenciesAbiOnly); + } + + public static async deployAsync( + bytecode: string, + abi: ContractAbi, + supportedProvider: SupportedProvider, + txDefaults: Partial, + logDecodeDependencies: { [contractName: string]: ContractAbi }, + ): Promise { + assert.isHexString('bytecode', bytecode); + assert.doesConformToSchema('txDefaults', txDefaults, schemas.txDataSchema, [ + schemas.addressSchema, + schemas.numberSchema, + schemas.jsNumber, + ]); + const provider = providerUtils.standardizeOrThrow(supportedProvider); + const constructorAbi = BaseContract._lookupConstructorAbi(abi); + [] = BaseContract._formatABIDataItemList(constructorAbi.inputs, [], BaseContract._bigNumberToString); + const iface = new ethers.utils.Interface(abi); + const deployInfo = iface.deployFunction; + const txData = deployInfo.encode(bytecode, []); + const web3Wrapper = new Web3Wrapper(provider); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: txData, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const txReceipt = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`ITransformERC20 successfully deployed at ${txReceipt.contractAddress}`); + const contractInstance = new ITransformERC20Contract( + txReceipt.contractAddress as string, + provider, + txDefaults, + logDecodeDependencies, + ); + contractInstance.constructorArgs = []; + return contractInstance; + } + + /** + * @returns The contract ABI + */ + public static ABI(): ContractAbi { + const abi = [ + { + anonymous: false, + inputs: [ + { + name: 'taker', + type: 'address', + indexed: true, + }, + { + name: 'inputToken', + type: 'address', + indexed: false, + }, + { + name: 'outputToken', + type: 'address', + indexed: false, + }, + { + name: 'inputTokenAmount', + type: 'uint256', + indexed: false, + }, + { + name: 'outputTokenAmount', + type: 'uint256', + indexed: false, + }, + ], + name: 'TransformedERC20', + outputs: [], + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + name: 'transformerDeployer', + type: 'address', + indexed: false, + }, + ], + name: 'TransformerDeployerUpdated', + outputs: [], + type: 'event', + }, + { + inputs: [ + { + name: 'callDataHash', + type: 'bytes32', + }, + { + name: 'taker', + type: 'address', + }, + { + name: 'inputToken', + type: 'address', + }, + { + name: 'outputToken', + type: 'address', + }, + { + name: 'inputTokenAmount', + type: 'uint256', + }, + { + name: 'minOutputTokenAmount', + type: 'uint256', + }, + { + name: 'transformations', + type: 'tuple[]', + components: [ + { + name: 'deploymentNonce', + type: 'uint32', + }, + { + name: 'data', + type: 'bytes', + }, + ], + }, + ], + name: '_transformERC20', + outputs: [ + { + name: 'outputTokenAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [], + name: 'createTransformWallet', + outputs: [ + { + name: 'wallet', + type: 'address', + }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getTransformWallet', + outputs: [ + { + name: 'wallet', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getTransformerDeployer', + outputs: [ + { + name: 'deployer', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + name: 'transformerDeployer', + type: 'address', + }, + ], + name: 'setTransformerDeployer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + name: 'inputToken', + type: 'address', + }, + { + name: 'outputToken', + type: 'address', + }, + { + name: 'inputTokenAmount', + type: 'uint256', + }, + { + name: 'minOutputTokenAmount', + type: 'uint256', + }, + { + name: 'transformations', + type: 'tuple[]', + components: [ + { + name: 'deploymentNonce', + type: 'uint32', + }, + { + name: 'data', + type: 'bytes', + }, + ], + }, + ], + name: 'transformERC20', + outputs: [ + { + name: 'outputTokenAmount', + type: 'uint256', + }, + ], + stateMutability: 'payable', + type: 'function', + }, + ] as ContractAbi; + return abi; + } + + protected static async _deployLibrariesAsync( + artifact: ContractArtifact, + libraryArtifacts: { [libraryName: string]: ContractArtifact }, + web3Wrapper: Web3Wrapper, + txDefaults: Partial, + libraryAddresses: { [libraryName: string]: string } = {}, + ): Promise<{ [libraryName: string]: string }> { + const links = artifact.compilerOutput.evm.bytecode.linkReferences; + // Go through all linked libraries, recursively deploying them if necessary. + for (const link of Object.values(links)) { + for (const libraryName of Object.keys(link)) { + if (!libraryAddresses[libraryName]) { + // Library not yet deployed. + const libraryArtifact = libraryArtifacts[libraryName]; + if (!libraryArtifact) { + throw new Error(`Missing artifact for linked library "${libraryName}"`); + } + // Deploy any dependent libraries used by this library. + await ITransformERC20Contract._deployLibrariesAsync( + libraryArtifact, + libraryArtifacts, + web3Wrapper, + txDefaults, + libraryAddresses, + ); + // Deploy this library. + const linkedLibraryBytecode = linkLibrariesInBytecode(libraryArtifact, libraryAddresses); + const txDataWithDefaults = await BaseContract._applyDefaultsToContractTxDataAsync( + { + data: linkedLibraryBytecode, + ...txDefaults, + }, + web3Wrapper.estimateGasAsync.bind(web3Wrapper), + ); + const txHash = await web3Wrapper.sendTransactionAsync(txDataWithDefaults); + logUtils.log(`transactionHash: ${txHash}`); + const { contractAddress } = await web3Wrapper.awaitTransactionSuccessAsync(txHash); + logUtils.log(`${libraryArtifact.contractName} successfully deployed at ${contractAddress}`); + libraryAddresses[libraryArtifact.contractName] = contractAddress as string; + } + } + } + return libraryAddresses; + } + + public getFunctionSignature(methodName: string): string { + const index = this._methodABIIndex[methodName]; + const methodAbi = ITransformERC20Contract.ABI()[index] as MethodAbi; // tslint:disable-line:no-unnecessary-type-assertion + const functionSignature = methodAbiToFunctionSignature(methodAbi); + return functionSignature; + } + + public getABIDecodedTransactionData(methodName: string, callData: string): T { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as ITransformERC20Contract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + const abiDecodedCallData = abiEncoder.strictDecode(callData); + return abiDecodedCallData; + } + + public getABIDecodedReturnData(methodName: string, callData: string): T { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as ITransformERC20Contract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + const abiDecodedCallData = abiEncoder.strictDecodeReturnValue(callData); + return abiDecodedCallData; + } + + public getSelector(methodName: string): string { + const functionSignature = this.getFunctionSignature(methodName); + const self = (this as any) as ITransformERC20Contract; + const abiEncoder = self._lookupAbiEncoder(functionSignature); + return abiEncoder.getSelector(); + } + + /** + * Internal version of `transformERC20()`. Only callable from within. + * @param callDataHash Hash of the ingress calldata. + * @param taker The taker address. + * @param inputToken The token being provided by the taker. If + * `0xeee...`, ETH is implied and should be provided with the call.` + * @param outputToken The token to be acquired by the taker. `0xeee...` + * implies ETH. + * @param inputTokenAmount The amount of `inputToken` to take from the taker. + * @param minOutputTokenAmount The minimum amount of `outputToken` the taker + * must receive for the entire transformation to succeed. + * @param transformations The transformations to execute on the token + * balance(s) in sequence. + */ + public _transformERC20( + callDataHash: string, + taker: string, + inputToken: string, + outputToken: string, + inputTokenAmount: BigNumber, + minOutputTokenAmount: BigNumber, + transformations: Array<{ deploymentNonce: number | BigNumber; data: string }>, + ): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + assert.isString('callDataHash', callDataHash); + assert.isString('taker', taker); + assert.isString('inputToken', inputToken); + assert.isString('outputToken', outputToken); + assert.isBigNumber('inputTokenAmount', inputTokenAmount); + assert.isBigNumber('minOutputTokenAmount', minOutputTokenAmount); + assert.isArray('transformations', transformations); + const functionSignature = '_transformERC20(bytes32,address,address,address,uint256,uint256,(uint32,bytes)[])'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + callDataHash, + taker.toLowerCase(), + inputToken.toLowerCase(), + outputToken.toLowerCase(), + inputTokenAmount, + minOutputTokenAmount, + transformations, + ]); + }, + }; + } + /** + * Deploy a new flash wallet instance and replace the current one with it. + * Useful if we somehow break the current wallet instance. + * Anyone can call this. + */ + public createTransformWallet(): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + const functionSignature = 'createTransformWallet()'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, []); + }, + }; + } + /** + * Return the current wallet instance that will serve as the execution + * context for transformations. + */ + public getTransformWallet(): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + const functionSignature = 'getTransformWallet()'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, []); + }, + }; + } + /** + * Return the allowed deployer for transformers. + */ + public getTransformerDeployer(): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + const functionSignature = 'getTransformerDeployer()'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, []); + }, + }; + } + /** + * Replace the allowed deployer for transformers. + * Only callable by the owner. + * @param transformerDeployer The address of the trusted deployer for + * transformers. + */ + public setTransformerDeployer(transformerDeployer: string): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + assert.isString('transformerDeployer', transformerDeployer); + const functionSignature = 'setTransformerDeployer(address)'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [transformerDeployer.toLowerCase()]); + }, + }; + } + /** + * Executes a series of transformations to convert an ERC20 `inputToken` + * to an ERC20 `outputToken`. + * @param inputToken The token being provided by the sender. If + * `0xeee...`, ETH is implied and should be provided with the call.` + * @param outputToken The token to be acquired by the sender. `0xeee...` + * implies ETH. + * @param inputTokenAmount The amount of `inputToken` to take from the sender. + * @param minOutputTokenAmount The minimum amount of `outputToken` the sender + * must receive for the entire transformation to succeed. + * @param transformations The transformations to execute on the token + * balance(s) in sequence. + */ + public transformERC20( + inputToken: string, + outputToken: string, + inputTokenAmount: BigNumber, + minOutputTokenAmount: BigNumber, + transformations: Array<{ deploymentNonce: number | BigNumber; data: string }>, + ): ContractTxFunctionObj { + const self = (this as any) as ITransformERC20Contract; + assert.isString('inputToken', inputToken); + assert.isString('outputToken', outputToken); + assert.isBigNumber('inputTokenAmount', inputTokenAmount); + assert.isBigNumber('minOutputTokenAmount', minOutputTokenAmount); + assert.isArray('transformations', transformations); + const functionSignature = 'transformERC20(address,address,uint256,uint256,(uint32,bytes)[])'; + + return { + async sendTransactionAsync( + txData?: Partial | undefined, + opts: SendTransactionOpts = { shouldValidate: true }, + ): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync( + { ...txData, data: this.getABIEncodedTransactionData() }, + this.estimateGasAsync.bind(this), + ); + if (opts.shouldValidate !== false) { + await this.callAsync(txDataWithDefaults); + } + return self._web3Wrapper.sendTransactionAsync(txDataWithDefaults); + }, + awaitTransactionSuccessAsync( + txData?: Partial, + opts: AwaitTransactionSuccessOpts = { shouldValidate: true }, + ): PromiseWithTransactionHash { + return self._promiseWithTransactionHash(this.sendTransactionAsync(txData, opts), opts); + }, + async estimateGasAsync(txData?: Partial | undefined): Promise { + const txDataWithDefaults = await self._applyDefaultsToTxDataAsync({ + ...txData, + data: this.getABIEncodedTransactionData(), + }); + return self._web3Wrapper.estimateGasAsync(txDataWithDefaults); + }, + async callAsync(callData: Partial = {}, defaultBlock?: BlockParam): Promise { + BaseContract._assertCallParams(callData, defaultBlock); + const rawCallResult = await self._performCallAsync( + { ...callData, data: this.getABIEncodedTransactionData() }, + defaultBlock, + ); + const abiEncoder = self._lookupAbiEncoder(functionSignature); + BaseContract._throwIfUnexpectedEmptyCallResult(rawCallResult, abiEncoder); + return abiEncoder.strictDecodeReturnValue(rawCallResult); + }, + getABIEncodedTransactionData(): string { + return self._strictEncodeArguments(functionSignature, [ + inputToken.toLowerCase(), + outputToken.toLowerCase(), + inputTokenAmount, + minOutputTokenAmount, + transformations, + ]); + }, + }; + } + + /** + * Subscribe to an event type emitted by the ITransformERC20 contract. + * @param eventName The ITransformERC20 contract event you would like to subscribe to. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{maker: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed + * @param isVerbose Enable verbose subscription warnings (e.g recoverable network issues encountered) + * @return Subscription token used later to unsubscribe + */ + public subscribe( + eventName: ITransformERC20Events, + indexFilterValues: IndexedFilterValues, + callback: EventCallback, + isVerbose: boolean = false, + blockPollingIntervalMs?: number, + ): string { + assert.doesBelongToStringEnum('eventName', eventName, ITransformERC20Events); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const subscriptionToken = this._subscriptionManager.subscribe( + this.address, + eventName, + indexFilterValues, + ITransformERC20Contract.ABI(), + callback, + isVerbose, + blockPollingIntervalMs, + ); + return subscriptionToken; + } + + /** + * Cancel a subscription + * @param subscriptionToken Subscription token returned by `subscribe()` + */ + public unsubscribe(subscriptionToken: string): void { + this._subscriptionManager.unsubscribe(subscriptionToken); + } + + /** + * Cancels all existing subscriptions + */ + public unsubscribeAll(): void { + this._subscriptionManager.unsubscribeAll(); + } + + /** + * Gets historical logs without creating a subscription + * @param eventName The ITransformERC20 contract event you would like to subscribe to. + * @param blockRange Block range to get logs from. + * @param indexFilterValues An object where the keys are indexed args returned by the event and + * the value is the value you are interested in. E.g `{_from: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync( + eventName: ITransformERC20Events, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + ): Promise>> { + assert.doesBelongToStringEnum('eventName', eventName, ITransformERC20Events); + assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const logs = await this._subscriptionManager.getLogsAsync( + this.address, + eventName, + blockRange, + indexFilterValues, + ITransformERC20Contract.ABI(), + ); + return logs; + } + + constructor( + address: string, + supportedProvider: SupportedProvider, + txDefaults?: Partial, + logDecodeDependencies?: { [contractName: string]: ContractAbi }, + deployedBytecode: string | undefined = ITransformERC20Contract.deployedBytecode, + ) { + super( + 'ITransformERC20', + ITransformERC20Contract.ABI(), + address, + supportedProvider, + txDefaults, + logDecodeDependencies, + deployedBytecode, + ); + classUtils.bindAll(this, ['_abiEncoderByFunctionSignature', 'address', '_web3Wrapper']); + this._subscriptionManager = new SubscriptionManager( + ITransformERC20Contract.ABI(), + this._web3Wrapper, + ); + ITransformERC20Contract.ABI().forEach((item, index) => { + if (item.type === 'function') { + const methodAbi = item as MethodAbi; + this._methodABIIndex[methodAbi.name] = index; + } + }); + } +} + +// tslint:disable:max-file-line-count +// tslint:enable:no-unbound-method no-parameter-reassignment no-consecutive-blank-lines ordered-imports align +// tslint:enable:trailing-comma whitespace no-trailing-whitespace diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index 091bfb06b5..b6b0d03d94 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -83,6 +83,13 @@ export { StakingProxyStakingContractAttachedToProxyEventArgs, StakingProxyStakingContractDetachedFromProxyEventArgs, } from './generated-wrappers/staking_proxy'; +export { + ITransformERC20Contract, + ITransformERC20EventArgs, + ITransformERC20Events, + ITransformERC20TransformerDeployerUpdatedEventArgs, + ITransformERC20TransformedERC20EventArgs, +} from './generated-wrappers/i_transform_erc20'; export { BlockRange, SupportedProvider, diff --git a/packages/instant/CHANGELOG.json b/packages/instant/CHANGELOG.json index 150d379a9f..2b420e0bcb 100644 --- a/packages/instant/CHANGELOG.json +++ b/packages/instant/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Remove `slippagePercentage` SwapQuoter config.", "pr": 2513 + }, + { + "note": "Pin `asset-swapper` dependency", + "pr": 2591 } ] }, diff --git a/packages/instant/package.json b/packages/instant/package.json index deb006f1bb..5bd2758b7e 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -44,7 +44,7 @@ "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "dependencies": { "@0x/assert": "^3.0.7", - "@0x/asset-swapper": "^4.4.0", + "@0x/asset-swapper": "4.4.0", "@0x/contract-wrappers": "^13.6.3", "@0x/json-schemas": "^5.0.7", "@0x/subproviders": "^6.0.8", diff --git a/packages/migrations/CHANGELOG.json b/packages/migrations/CHANGELOG.json index 7b684c83d1..80a22888a3 100644 --- a/packages/migrations/CHANGELOG.json +++ b/packages/migrations/CHANGELOG.json @@ -13,6 +13,10 @@ { "note": "Added `UniswapV2Bridge` address on Mainnet", "pr": 2599 + }, + { + "note": "Return empty Exchange Proxy addresses", + "pr": 2591 } ] }, diff --git a/packages/migrations/src/migration.ts b/packages/migrations/src/migration.ts index 94561d79bf..ebd106fcee 100644 --- a/packages/migrations/src/migration.ts +++ b/packages/migrations/src/migration.ts @@ -49,6 +49,8 @@ const allArtifacts = { ...erc20BridgeSamplerArtifacts, }; +const { NULL_ADDRESS } = constants; + /** * Creates and deploys all the contracts that are required for the latest * version of the 0x protocol. @@ -202,8 +204,8 @@ export async function runMigrationsAsync( txDefaults, allArtifacts, exchange.address, - constants.NULL_ADDRESS, - constants.NULL_ADDRESS, + NULL_ADDRESS, + NULL_ADDRESS, ); // tslint:disable-next-line:no-unused-variable @@ -278,7 +280,7 @@ export async function runMigrationsAsync( txDefaults, allArtifacts, exchange.address, - exchangeV2Address || constants.NULL_ADDRESS, + exchangeV2Address || NULL_ADDRESS, etherToken.address, ); @@ -297,33 +299,42 @@ export async function runMigrationsAsync( zrxToken: zrxToken.address, etherToken: etherToken.address, exchange: exchange.address, - assetProxyOwner: constants.NULL_ADDRESS, + assetProxyOwner: NULL_ADDRESS, erc20BridgeProxy: erc20BridgeProxy.address, - zeroExGovernor: constants.NULL_ADDRESS, + zeroExGovernor: NULL_ADDRESS, forwarder: forwarder.address, coordinatorRegistry: coordinatorRegistry.address, coordinator: coordinator.address, multiAssetProxy: multiAssetProxy.address, staticCallProxy: staticCallProxy.address, devUtils: devUtils.address, - exchangeV2: exchangeV2Address || constants.NULL_ADDRESS, + exchangeV2: exchangeV2Address || NULL_ADDRESS, zrxVault: zrxVault.address, staking: stakingLogic.address, stakingProxy: stakingProxy.address, - uniswapBridge: constants.NULL_ADDRESS, - uniswapV2Bridge: constants.NULL_ADDRESS, - eth2DaiBridge: constants.NULL_ADDRESS, - kyberBridge: constants.NULL_ADDRESS, + uniswapBridge: NULL_ADDRESS, + eth2DaiBridge: NULL_ADDRESS, + kyberBridge: NULL_ADDRESS, erc20BridgeSampler: erc20BridgeSampler.address, - chaiBridge: constants.NULL_ADDRESS, - dydxBridge: constants.NULL_ADDRESS, - curveBridge: constants.NULL_ADDRESS, - godsUnchainedValidator: constants.NULL_ADDRESS, - broker: constants.NULL_ADDRESS, - chainlinkStopLimit: constants.NULL_ADDRESS, - maximumGasPrice: constants.NULL_ADDRESS, - dexForwarderBridge: constants.NULL_ADDRESS, - multiBridge: constants.NULL_ADDRESS, + chaiBridge: NULL_ADDRESS, + dydxBridge: NULL_ADDRESS, + curveBridge: NULL_ADDRESS, + uniswapV2Bridge: NULL_ADDRESS, + godsUnchainedValidator: NULL_ADDRESS, + broker: NULL_ADDRESS, + chainlinkStopLimit: NULL_ADDRESS, + maximumGasPrice: NULL_ADDRESS, + dexForwarderBridge: NULL_ADDRESS, + multiBridge: NULL_ADDRESS, + exchangeProxyGovernor: NULL_ADDRESS, + exchangeProxy: NULL_ADDRESS, + exchangeProxyAllowanceTarget: NULL_ADDRESS, + exchangeProxyTransformerDeployer: NULL_ADDRESS, + transformers: { + wethTransformer: NULL_ADDRESS, + payTakerTransformer: NULL_ADDRESS, + fillQuoteTransformer: NULL_ADDRESS, + }, }; return contractAddresses; } diff --git a/packages/sol-compiler/CHANGELOG.json b/packages/sol-compiler/CHANGELOG.json index 9f3dca62a9..851cc0e6c8 100644 --- a/packages/sol-compiler/CHANGELOG.json +++ b/packages/sol-compiler/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Filter `receive` functions from 0.6 ABIs", "pr": 2540 + }, + { + "note": "Fix `CompilerOptions` schema", + "pr": 2591 } ] }, diff --git a/packages/sol-compiler/src/schemas/compiler_options_schema.ts b/packages/sol-compiler/src/schemas/compiler_options_schema.ts index 4b34ff1477..12b655cf68 100644 --- a/packages/sol-compiler/src/schemas/compiler_options_schema.ts +++ b/packages/sol-compiler/src/schemas/compiler_options_schema.ts @@ -3,7 +3,7 @@ export const compilerOptionsSchema = { properties: { contractsDir: { type: 'string' }, artifactsDir: { type: 'string' }, - solcVersion: { type: 'string', pattern: '^\\d+.\\d+.\\d+$' }, + solcVersion: { type: 'string', pattern: '^\\d+.\\d+.\\d+\\+commit\\.[a-f0-9]{8}$' }, compilerSettings: { type: 'object' }, contracts: { oneOf: [