import {
    blockchainTests,
    constants,
    expect,
    getRandomInteger,
    getRandomPortion,
    Numberish,
    randomAddress,
    verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { ETH_TOKEN_ADDRESS } from '@0x/order-utils';
import { AbiEncoder, hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
import { DecodedLogEntry } from 'ethereum-types';
import * as ethjs from 'ethereumjs-util';

import { generateCallDataHashSignature, signCallData } from '../../src/signed_call_data';
import { IZeroExContract, TransformERC20FeatureContract } from '../../src/wrappers';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
import {
    FlashWalletContract,
    ITokenSpenderFeatureContract,
    TestMintableERC20TokenContract,
    TestMintTokenERC20TransformerContract,
    TestMintTokenERC20TransformerEvents,
    TestMintTokenERC20TransformerMintTransformEventArgs,
    TransformERC20FeatureEvents,
} from '../wrappers';

const { NULL_ADDRESS, NULL_BYTES, NULL_BYTES32 } = constants;

type MintTokenTransformerEvent = DecodedLogEntry<TestMintTokenERC20TransformerMintTransformEventArgs>;

blockchainTests.resets('TransformERC20 feature', env => {
    const callDataSignerKey = hexUtils.random();
    const callDataSigner = ethjs.bufferToHex(ethjs.privateToAddress(ethjs.toBuffer(callDataSignerKey)));
    let owner: string;
    let taker: string;
    let sender: string;
    let transformerDeployer: string;
    let zeroEx: IZeroExContract;
    let feature: TransformERC20FeatureContract;
    let wallet: FlashWalletContract;
    let allowanceTarget: string;

    before(async () => {
        [owner, taker, sender, transformerDeployer] = await env.getAccountAddressesAsync();
        zeroEx = await fullMigrateAsync(
            owner,
            env.provider,
            env.txDefaults,
            {
                transformERC20: (await TransformERC20FeatureContract.deployFrom0xArtifactAsync(
                    artifacts.TestTransformERC20,
                    env.provider,
                    env.txDefaults,
                    artifacts,
                )).address,
            },
            { transformerDeployer },
        );
        feature = new TransformERC20FeatureContract(
            zeroEx.address,
            env.provider,
            { ...env.txDefaults, from: sender },
            abis,
        );
        wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults);
        allowanceTarget = await new ITokenSpenderFeatureContract(zeroEx.address, env.provider, env.txDefaults)
            .getAllowanceTarget()
            .callAsync();
        await feature.setQuoteSigner(callDataSigner).awaitTransactionSuccessAsync({ from: owner });
    });

    const { MAX_UINT256, ZERO_AMOUNT } = constants;

    describe('wallets', () => {
        it('createTransformWallet() replaces the current wallet', async () => {
            const newWalletAddress = await feature.createTransformWallet().callAsync({ from: owner });
            expect(newWalletAddress).to.not.eq(wallet.address);
            await feature.createTransformWallet().awaitTransactionSuccessAsync({ from: owner });
            return expect(feature.getTransformWallet().callAsync()).to.eventually.eq(newWalletAddress);
        });

        it('createTransformWallet() cannot be called by non-owner', async () => {
            const notOwner = randomAddress();
            const tx = feature.createTransformWallet().callAsync({ from: notOwner });
            return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner));
        });
    });

    describe('transformer deployer', () => {
        it('`getTransformerDeployer()` returns the transformer deployer', async () => {
            const actualDeployer = await feature.getTransformerDeployer().callAsync();
            expect(actualDeployer).to.eq(transformerDeployer);
        });

        it('owner can set the transformer deployer with `setTransformerDeployer()`', async () => {
            const newDeployer = randomAddress();
            const receipt = await feature
                .setTransformerDeployer(newDeployer)
                .awaitTransactionSuccessAsync({ from: owner });
            verifyEventsFromLogs(
                receipt.logs,
                [{ transformerDeployer: newDeployer }],
                TransformERC20FeatureEvents.TransformerDeployerUpdated,
            );
            const actualDeployer = await feature.getTransformerDeployer().callAsync();
            expect(actualDeployer).to.eq(newDeployer);
        });

        it('non-owner cannot set the transformer deployer with `setTransformerDeployer()`', async () => {
            const newDeployer = randomAddress();
            const notOwner = randomAddress();
            const tx = feature.setTransformerDeployer(newDeployer).callAsync({ from: notOwner });
            return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner));
        });
    });

    describe('quote signer', () => {
        it('`getQuoteSigner()` returns the quote signer', async () => {
            const actualSigner = await feature.getQuoteSigner().callAsync();
            expect(actualSigner).to.eq(callDataSigner);
        });

        it('owner can set the quote signer with `setQuoteSigner()`', async () => {
            const newSigner = randomAddress();
            const receipt = await feature.setQuoteSigner(newSigner).awaitTransactionSuccessAsync({ from: owner });
            verifyEventsFromLogs(
                receipt.logs,
                [{ quoteSigner: newSigner }],
                TransformERC20FeatureEvents.QuoteSignerUpdated,
            );
            const actualSigner = await feature.getQuoteSigner().callAsync();
            expect(actualSigner).to.eq(newSigner);
        });

        it('non-owner cannot set the quote signer with `setQuoteSigner()`', async () => {
            const newSigner = randomAddress();
            const notOwner = randomAddress();
            const tx = feature.setQuoteSigner(newSigner).callAsync({ from: notOwner });
            return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner));
        });
    });

    describe('_transformERC20()/transformERC20()', () => {
        let inputToken: TestMintableERC20TokenContract;
        let outputToken: TestMintableERC20TokenContract;
        let mintTransformer: TestMintTokenERC20TransformerContract;
        let transformerNonce: number;

        before(async () => {
            inputToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
                artifacts.TestMintableERC20Token,
                env.provider,
                env.txDefaults,
                artifacts,
            );
            outputToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
                artifacts.TestMintableERC20Token,
                env.provider,
                env.txDefaults,
                artifacts,
            );
            transformerNonce = await env.web3Wrapper.getAccountNonceAsync(transformerDeployer);
            mintTransformer = await TestMintTokenERC20TransformerContract.deployFrom0xArtifactAsync(
                artifacts.TestMintTokenERC20Transformer,
                env.provider,
                {
                    ...env.txDefaults,
                    from: transformerDeployer,
                },
                artifacts,
            );
            await inputToken.approve(allowanceTarget, MAX_UINT256).awaitTransactionSuccessAsync({ from: taker });
        });

        interface Transformation {
            deploymentNonce: number;
            data: string;
        }

        const transformDataEncoder = AbiEncoder.create([
            {
                name: 'data',
                type: 'tuple',
                components: [
                    { name: 'inputToken', type: 'address' },
                    { name: 'outputToken', type: 'address' },
                    { name: 'burnAmount', type: 'uint256' },
                    { name: 'mintAmount', type: 'uint256' },
                    { name: 'feeAmount', type: 'uint256' },
                ],
            },
        ]);

        function createMintTokenTransformation(
            opts: Partial<{
                transformer: string;
                outputTokenAddress: string;
                inputTokenAddress: string;
                inputTokenBurnAmunt: Numberish;
                outputTokenMintAmount: Numberish;
                outputTokenFeeAmount: Numberish;
                deploymentNonce: number;
            }> = {},
        ): Transformation {
            const _opts = {
                outputTokenAddress: outputToken.address,
                inputTokenAddress: inputToken.address,
                inputTokenBurnAmunt: ZERO_AMOUNT,
                outputTokenMintAmount: ZERO_AMOUNT,
                outputTokenFeeAmount: ZERO_AMOUNT,
                transformer: mintTransformer.address,
                deploymentNonce: transformerNonce,
                ...opts,
            };
            return {
                deploymentNonce: _opts.deploymentNonce,
                data: transformDataEncoder.encode([
                    {
                        inputToken: _opts.inputTokenAddress,
                        outputToken: _opts.outputTokenAddress,
                        burnAmount: _opts.inputTokenBurnAmunt,
                        mintAmount: _opts.outputTokenMintAmount,
                        feeAmount: _opts.outputTokenFeeAmount,
                    },
                ]),
            };
        }

        describe('_transformERC20()', () => {
            it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount", async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            taker,
                            inputTokenAmount,
                            outputTokenAmount: outputTokenMintAmount,
                            inputToken: inputToken.address,
                            outputToken: outputToken.address,
                        },
                    ],
                    TransformERC20FeatureEvents.TransformedERC20,
                );
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            sender,
                            taker,
                            callDataHash: NULL_BYTES32,
                            context: wallet.address,
                            caller: zeroEx.address,
                            data: transformation.data,
                            inputTokenBalance: inputTokenAmount,
                            ethBalance: callValue,
                        },
                    ],
                    TestMintTokenERC20TransformerEvents.MintTransform,
                );
            });

            it("succeeds if taker's output token balance increases by exactly minOutputTokenAmount, with ETH", async () => {
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = outputTokenMintAmount.times(2);
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                    outputTokenAddress: ETH_TOKEN_ADDRESS,
                });
                const startingOutputTokenBalance = await env.web3Wrapper.getBalanceInWeiAsync(taker);
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: ETH_TOKEN_ADDRESS,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            taker,
                            inputTokenAmount,
                            outputTokenAmount: outputTokenMintAmount,
                            inputToken: inputToken.address,
                            outputToken: ETH_TOKEN_ADDRESS,
                        },
                    ],
                    TransformERC20FeatureEvents.TransformedERC20,
                );
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            taker,
                            sender,
                            callDataHash: NULL_BYTES32,
                            context: wallet.address,
                            caller: zeroEx.address,
                            data: transformation.data,
                            inputTokenBalance: inputTokenAmount,
                            ethBalance: callValue,
                        },
                    ],
                    TestMintTokenERC20TransformerEvents.MintTransform,
                );
                expect(await env.web3Wrapper.getBalanceInWeiAsync(taker)).to.bignumber.eq(
                    startingOutputTokenBalance.plus(outputTokenMintAmount),
                );
            });

            it("succeeds if taker's output token balance increases by more than minOutputTokenAmount", async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount.plus(1);
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            taker,
                            inputTokenAmount,
                            outputTokenAmount: outputTokenMintAmount,
                            inputToken: inputToken.address,
                            outputToken: outputToken.address,
                        },
                    ],
                    TransformERC20FeatureEvents.TransformedERC20,
                );
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            sender,
                            taker,
                            callDataHash: NULL_BYTES32,
                            context: wallet.address,
                            caller: zeroEx.address,
                            data: transformation.data,
                            inputTokenBalance: inputTokenAmount,
                            ethBalance: callValue,
                        },
                    ],
                    TestMintTokenERC20TransformerEvents.MintTransform,
                );
            });

            it("throws if taker's output token balance increases by less than minOutputTokenAmount", async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount.minus(1);
                const callValue = getRandomInteger(1, '1e18');
                const tx = feature
                    ._transformERC20({
                        callDataHash: hexUtils.random(),
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        callDataSignature: NULL_BYTES,
                        transformations: [
                            createMintTokenTransformation({
                                outputTokenMintAmount,
                                inputTokenBurnAmunt: inputTokenAmount,
                            }),
                        ],
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                const expectedError = new ZeroExRevertErrors.TransformERC20.IncompleteTransformERC20Error(
                    outputToken.address,
                    outputTokenMintAmount,
                    minOutputTokenAmount,
                );
                return expect(tx).to.revertWith(expectedError);
            });

            it("throws if taker's output token balance decreases", async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = ZERO_AMOUNT;
                const outputTokenFeeAmount = 1;
                const callValue = getRandomInteger(1, '1e18');
                const tx = feature
                    ._transformERC20({
                        callDataHash: hexUtils.random(),
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        callDataSignature: NULL_BYTES,
                        transformations: [
                            createMintTokenTransformation({
                                outputTokenFeeAmount,
                                inputTokenBurnAmunt: inputTokenAmount,
                            }),
                        ],
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                const expectedError = new ZeroExRevertErrors.TransformERC20.NegativeTransformERC20OutputError(
                    outputToken.address,
                    outputTokenFeeAmount,
                );
                return expect(tx).to.revertWith(expectedError);
            });

            it('can call multiple transformers', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(2, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(2, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                // Split the total minting between two transformers.
                const transformations = [
                    createMintTokenTransformation({
                        inputTokenBurnAmunt: 1,
                        outputTokenMintAmount: 1,
                    }),
                    createMintTokenTransformation({
                        inputTokenBurnAmunt: inputTokenAmount.minus(1),
                        outputTokenMintAmount: outputTokenMintAmount.minus(1),
                    }),
                ];
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations,
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                verifyEventsFromLogs(
                    receipt.logs,
                    [
                        {
                            sender,
                            taker,
                            callDataHash: NULL_BYTES32,
                            context: wallet.address,
                            caller: zeroEx.address,
                            data: transformations[0].data,
                            inputTokenBalance: inputTokenAmount,
                            ethBalance: callValue,
                        },
                        {
                            sender,
                            taker,
                            callDataHash: NULL_BYTES32,
                            context: wallet.address,
                            caller: zeroEx.address,
                            data: transformations[1].data,
                            inputTokenBalance: inputTokenAmount.minus(1),
                            ethBalance: callValue,
                        },
                    ],
                    TestMintTokenERC20TransformerEvents.MintTransform,
                );
            });

            it('fails with invalid transformer nonce', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(2, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(2, '1e18');
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformations = [createMintTokenTransformation({ deploymentNonce: 1337 })];
                const tx = feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations,
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                return expect(tx).to.revertWith(
                    new ZeroExRevertErrors.TransformERC20.TransformerFailedError(
                        undefined,
                        transformations[0].data,
                        constants.NULL_BYTES,
                    ),
                );
            });

            it('passes the calldata hash to transformer with proper signature', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: generateCallDataHashSignature(callDataHash, callDataSignerKey),
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(callDataHash);
            });

            it('passes empty calldata hash to transformer with improper signature', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: generateCallDataHashSignature(callDataHash, hexUtils.random()),
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(NULL_BYTES32);
            });

            it('passes empty calldata hash to transformer with no signature', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const callDataHash = hexUtils.random();
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const receipt = await feature
                    ._transformERC20({
                        taker,
                        inputToken: inputToken.address,
                        outputToken: outputToken.address,
                        inputTokenAmount,
                        minOutputTokenAmount,
                        transformations: [transformation],
                        callDataHash,
                        callDataSignature: NULL_BYTES,
                    })
                    .awaitTransactionSuccessAsync({ value: callValue });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(NULL_BYTES32);
            });
        });

        describe('transformERC20()', () => {
            it('passes the calldata hash to transformer with properly signed calldata', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const bakedCall = feature.transformERC20(
                    inputToken.address,
                    outputToken.address,
                    inputTokenAmount,
                    minOutputTokenAmount,
                    [transformation],
                );
                const callData = bakedCall.getABIEncodedTransactionData();
                const signedCallData = signCallData(callData, callDataSignerKey);
                const receipt = await bakedCall.awaitTransactionSuccessAsync({
                    from: taker,
                    value: callValue,
                    data: signedCallData,
                });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(hexUtils.hash(callData));
            });

            it('passes the calldata hash to transformer when no quote signer configured', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const bakedCall = feature.transformERC20(
                    inputToken.address,
                    outputToken.address,
                    inputTokenAmount,
                    minOutputTokenAmount,
                    [transformation],
                );
                const callData = bakedCall.getABIEncodedTransactionData();
                await feature.setQuoteSigner(NULL_ADDRESS).awaitTransactionSuccessAsync({ from: owner });
                const receipt = await bakedCall.awaitTransactionSuccessAsync({
                    from: taker,
                    value: callValue,
                    data: callData,
                });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(hexUtils.hash(callData));
            });

            it('passes empty calldata hash to transformer with improperly signed calldata', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const bakedCall = feature.transformERC20(
                    inputToken.address,
                    outputToken.address,
                    inputTokenAmount,
                    minOutputTokenAmount,
                    [transformation],
                );
                const callData = bakedCall.getABIEncodedTransactionData();
                const signedCallData = signCallData(callData, hexUtils.random());
                const receipt = await bakedCall.awaitTransactionSuccessAsync({
                    from: taker,
                    value: callValue,
                    data: signedCallData,
                });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(NULL_BYTES32);
            });

            it('passes empty calldata hash to transformer with unsigned calldata', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const bakedCall = feature.transformERC20(
                    inputToken.address,
                    outputToken.address,
                    inputTokenAmount,
                    minOutputTokenAmount,
                    [transformation],
                );
                const callData = bakedCall.getABIEncodedTransactionData();
                const receipt = await bakedCall.awaitTransactionSuccessAsync({
                    from: taker,
                    value: callValue,
                    data: callData,
                });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(NULL_BYTES32);
            });

            it('passes empty calldata hash to transformer with calldata with malformed signature', async () => {
                const startingOutputTokenBalance = getRandomInteger(0, '100e18');
                const startingInputTokenBalance = getRandomInteger(0, '100e18');
                await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
                await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
                const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
                const minOutputTokenAmount = getRandomInteger(1, '1e18');
                const outputTokenMintAmount = minOutputTokenAmount;
                const callValue = getRandomInteger(1, '1e18');
                const transformation = createMintTokenTransformation({
                    outputTokenMintAmount,
                    inputTokenBurnAmunt: inputTokenAmount,
                });
                const bakedCall = feature.transformERC20(
                    inputToken.address,
                    outputToken.address,
                    inputTokenAmount,
                    minOutputTokenAmount,
                    [transformation],
                );
                const callData = bakedCall.getABIEncodedTransactionData();
                const signedCallData = hexUtils.concat(
                    hexUtils.slice(signCallData(callData, hexUtils.random()), 0, -1),
                    127,
                );
                const receipt = await bakedCall.awaitTransactionSuccessAsync({
                    from: taker,
                    value: callValue,
                    data: signedCallData,
                });
                const { callDataHash: actualCallDataHash } = (receipt.logs[0] as MintTokenTransformerEvent).args;
                expect(actualCallDataHash).to.eq(NULL_BYTES32);
            });
        });
    });
});
