import { DevUtilsContract } from '@0x/contracts-dev-utils';
import {
    artifacts as erc1155Artifacts,
    DummyERC1155ReceiverBatchTokenReceivedEventArgs,
    DummyERC1155ReceiverContract,
    ERC1155MintableContract,
    Erc1155Wrapper,
} from '@0x/contracts-erc1155';
import {
    chaiSetup,
    constants,
    expectTransactionFailedAsync,
    expectTransactionFailedWithoutReasonAsync,
    provider,
    txDefaults,
    web3Wrapper,
} from '@0x/contracts-test-utils';
import { SafeMathRevertErrors } from '@0x/contracts-utils';
import { BlockchainLifecycle } from '@0x/dev-utils';
import { AssetProxyId, RevertReason } from '@0x/types';
import { BigNumber } from '@0x/utils';
import * as chai from 'chai';
import { LogWithDecodedArgs } from 'ethereum-types';
import * as ethUtil from 'ethereumjs-util';
import * as _ from 'lodash';

import { ERC1155ProxyWrapper } from '../src/erc1155_proxy_wrapper';
import { ERC1155ProxyContract, IAssetDataContract } from '../src/wrappers';

import { artifacts } from './artifacts';

chaiSetup.configure();
const expect = chai.expect;
const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper);

// tslint:disable:no-unnecessary-type-assertion
describe('ERC1155Proxy', () => {
    // constant values used in transfer tests
    const nftOwnerBalance = new BigNumber(1);
    const nftNotOwnerBalance = new BigNumber(0);
    const spenderInitialFungibleBalance = constants.INITIAL_ERC1155_FUNGIBLE_BALANCE;
    const receiverInitialFungibleBalance = constants.INITIAL_ERC1155_FUNGIBLE_BALANCE;
    const receiverContractInitialFungibleBalance = new BigNumber(0);
    const fungibleValueToTransferSmall = spenderInitialFungibleBalance.div(100);
    const fungibleValueToTransferLarge = spenderInitialFungibleBalance.div(4);
    const valueMultiplierSmall = new BigNumber(2);
    const valueMultiplierNft = new BigNumber(1);
    const nonFungibleValueToTransfer = nftOwnerBalance;
    const receiverCallbackData = '0x01020304';
    // addresses
    let owner: string;
    let notAuthorized: string;
    let authorized: string;
    let spender: string;
    let receiver: string;
    let receiverContract: string;
    // contracts & wrappers
    let erc1155Proxy: ERC1155ProxyContract;
    let erc1155Receiver: DummyERC1155ReceiverContract;
    let erc1155ProxyWrapper: ERC1155ProxyWrapper;
    let erc1155Contract: ERC1155MintableContract;
    let erc1155Wrapper: Erc1155Wrapper;
    // tokens
    let fungibleTokens: BigNumber[];
    let nonFungibleTokensOwnedBySpender: BigNumber[];
    // devUtils for encoding and decoding assetData
    let devUtils: DevUtilsContract;
    // tests
    before(async () => {
        await blockchainLifecycle.startAsync();
    });
    after(async () => {
        await blockchainLifecycle.revertAsync();
    });
    before(async () => {
        /// deploy & configure ERC1155Proxy
        const accounts = await web3Wrapper.getAvailableAddressesAsync();
        const usedAddresses = ([owner, notAuthorized, authorized, spender, receiver] = _.slice(accounts, 0, 5));
        erc1155ProxyWrapper = new ERC1155ProxyWrapper(provider, usedAddresses, owner);
        erc1155Proxy = await erc1155ProxyWrapper.deployProxyAsync();
        await erc1155Proxy.addAuthorizedAddress(authorized).awaitTransactionSuccessAsync({ from: owner });
        await erc1155Proxy.addAuthorizedAddress(erc1155Proxy.address).awaitTransactionSuccessAsync({ from: owner });
        // deploy & configure ERC1155 tokens and receiver
        [erc1155Wrapper] = await erc1155ProxyWrapper.deployDummyContractsAsync();
        erc1155Contract = erc1155Wrapper.getContract();
        erc1155Receiver = await DummyERC1155ReceiverContract.deployFrom0xArtifactAsync(
            erc1155Artifacts.DummyERC1155Receiver,
            provider,
            txDefaults,
            artifacts,
        );
        receiverContract = erc1155Receiver.address;
        await erc1155ProxyWrapper.setBalancesAndAllowancesAsync();
        fungibleTokens = erc1155ProxyWrapper.getFungibleTokenIds();
        const nonFungibleTokens = erc1155ProxyWrapper.getNonFungibleTokenIds();
        const tokenBalances = await erc1155ProxyWrapper.getBalancesAsync();
        nonFungibleTokensOwnedBySpender = [];
        _.each(nonFungibleTokens, (nonFungibleToken: BigNumber) => {
            const nonFungibleTokenAsString = nonFungibleToken.toString();
            const nonFungibleTokenHeldBySpender =
                tokenBalances.nonFungible[spender][erc1155Contract.address][nonFungibleTokenAsString][0];
            nonFungibleTokensOwnedBySpender.push(nonFungibleTokenHeldBySpender);
        });
        // set up devUtils
        devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider, { from: owner });
    });
    beforeEach(async () => {
        await blockchainLifecycle.startAsync();
    });
    afterEach(async () => {
        await blockchainLifecycle.revertAsync();
    });
    describe('general', () => {
        it('should revert if undefined function is called', async () => {
            const undefinedSelector = '0x01020304';
            await expectTransactionFailedWithoutReasonAsync(
                web3Wrapper.sendTransactionAsync({
                    from: owner,
                    to: erc1155Proxy.address,
                    value: constants.ZERO_AMOUNT,
                    data: undefinedSelector,
                }),
            );
        });
        it('should have an id of 0xa7cb5fb7', async () => {
            const proxyId = await erc1155Proxy.getProxyId().callAsync();
            const expectedProxyId = AssetProxyId.ERC1155;
            expect(proxyId).to.equal(expectedProxyId);
        });
    });
    describe('transferFrom', () => {
        it('should successfully transfer value for a single, fungible token', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const totalValueTransferred = valuesToTransfer[0].times(valueMultiplier);
            const expectedFinalBalances = [
                spenderInitialFungibleBalance.minus(totalValueTransferred),
                receiverInitialFungibleBalance.plus(totalValueTransferred),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value for the same fungible token several times', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokenToTransfer = fungibleTokens[0];
            const tokensToTransfer = [tokenToTransfer, tokenToTransfer, tokenToTransfer];
            const valuesToTransfer = [
                fungibleValueToTransferSmall.plus(10),
                fungibleValueToTransferSmall.plus(20),
                fungibleValueToTransferSmall.plus(30),
            ];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                spenderInitialFungibleBalance,
                // receiver
                receiverInitialFungibleBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, [tokenToTransfer], expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            let totalValueTransferred = _.reduce(valuesToTransfer, (sum: BigNumber, value: BigNumber) => {
                return sum.plus(value);
            }) as BigNumber;
            totalValueTransferred = totalValueTransferred.times(valueMultiplier);
            const expectedFinalBalances = [
                // spender
                spenderInitialFungibleBalance.minus(totalValueTransferred),
                // receiver
                receiverInitialFungibleBalance.plus(totalValueTransferred),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, [tokenToTransfer], expectedFinalBalances);
        });
        it('should successfully transfer value for several fungible tokens', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 3);
            const valuesToTransfer = [
                fungibleValueToTransferSmall.plus(10),
                fungibleValueToTransferSmall.plus(20),
                fungibleValueToTransferSmall.plus(30),
            ];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                spenderInitialFungibleBalance,
                spenderInitialFungibleBalance,
                spenderInitialFungibleBalance,
                // receiver
                receiverInitialFungibleBalance,
                receiverInitialFungibleBalance,
                receiverInitialFungibleBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            const expectedFinalBalances = [
                // spender
                spenderInitialFungibleBalance.minus(totalValuesTransferred[0]),
                spenderInitialFungibleBalance.minus(totalValuesTransferred[1]),
                spenderInitialFungibleBalance.minus(totalValuesTransferred[2]),
                // receiver
                receiverInitialFungibleBalance.plus(totalValuesTransferred[0]),
                receiverInitialFungibleBalance.plus(totalValuesTransferred[1]),
                receiverInitialFungibleBalance.plus(totalValuesTransferred[2]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer a non-fungible token', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1);
            const valuesToTransfer = [nonFungibleValueToTransfer];
            const valueMultiplier = valueMultiplierNft;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const expectedFinalBalances = [
                // spender
                nftNotOwnerBalance,
                // receiver
                nftOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer multiple non-fungible tokens', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 3);
            const valuesToTransfer = [
                nonFungibleValueToTransfer,
                nonFungibleValueToTransfer,
                nonFungibleValueToTransfer,
            ];
            const valueMultiplier = valueMultiplierNft;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                nftOwnerBalance,
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
                nftNotOwnerBalance,
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const expectedFinalBalances = [
                // spender
                nftNotOwnerBalance,
                nftNotOwnerBalance,
                nftNotOwnerBalance,
                // receiver
                nftOwnerBalance,
                nftOwnerBalance,
                nftOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value for a combination of several fungible/non-fungible tokens', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const fungibleTokensToTransfer = fungibleTokens.slice(0, 3);
            const nonFungibleTokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 2);
            const tokensToTransfer = fungibleTokensToTransfer.concat(nonFungibleTokensToTransfer);
            const valuesToTransfer = [
                fungibleValueToTransferLarge,
                fungibleValueToTransferSmall,
                fungibleValueToTransferSmall,
                nonFungibleValueToTransfer,
                nonFungibleValueToTransfer,
            ];
            const valueMultiplier = valueMultiplierNft;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                spenderInitialFungibleBalance,
                spenderInitialFungibleBalance,
                spenderInitialFungibleBalance,
                nftOwnerBalance,
                nftOwnerBalance,
                // receiver
                receiverInitialFungibleBalance,
                receiverInitialFungibleBalance,
                receiverInitialFungibleBalance,
                nftNotOwnerBalance,
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            const expectedFinalBalances = [
                // spender
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].minus(totalValuesTransferred[1]),
                expectedInitialBalances[2].minus(totalValuesTransferred[2]),
                expectedInitialBalances[3].minus(totalValuesTransferred[3]),
                expectedInitialBalances[4].minus(totalValuesTransferred[4]),
                // receiver
                expectedInitialBalances[5].plus(totalValuesTransferred[0]),
                expectedInitialBalances[6].plus(totalValuesTransferred[1]),
                expectedInitialBalances[7].plus(totalValuesTransferred[2]),
                expectedInitialBalances[8].plus(totalValuesTransferred[3]),
                expectedInitialBalances[9].plus(totalValuesTransferred[4]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value to a smart contract and trigger its callback', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(receiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value to a smart contract and trigger its callback, when callback `data` is NULL', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            const nullReceiverCallbackData = '0x';
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                nullReceiverCallbackData,
                authorized,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(nullReceiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value to a smart contract and trigger its callback, when callback `data` is one word', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // create word of callback data
            const customReceiverCallbackData = '0x0102030405060708091001020304050607080910010203040506070809100102';
            const customReceiverCallbackDataAsBuffer = ethUtil.toBuffer(customReceiverCallbackData);
            const oneWordInBytes = 32;
            expect(customReceiverCallbackDataAsBuffer.byteLength).to.be.equal(oneWordInBytes);
            // execute transfer
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                customReceiverCallbackData,
                authorized,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(customReceiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value to a smart contract and trigger its callback, when callback `data` is multiple words', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // create word of callback data
            const scalar = 5;
            const customReceiverCallbackData = `0x${'0102030405060708091001020304050607080910010203040506070809100102'.repeat(
                scalar,
            )}`;
            const customReceiverCallbackDataAsBuffer = ethUtil.toBuffer(customReceiverCallbackData);
            const oneWordInBytes = 32;
            expect(customReceiverCallbackDataAsBuffer.byteLength).to.be.equal(oneWordInBytes * scalar);
            // execute transfer
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                customReceiverCallbackData,
                authorized,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(customReceiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value to a smart contract and trigger its callback, when callback `data` is multiple words but not word-aligned', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // create word of callback data
            const scalar = 5;
            const customReceiverCallbackData = `0x${'0102030405060708091001020304050607080910010203040506070809100102'.repeat(
                scalar,
            )}090807`;
            const customReceiverCallbackDataAsBuffer = ethUtil.toBuffer(customReceiverCallbackData);
            const oneWordInBytes = 32;
            expect(customReceiverCallbackDataAsBuffer.byteLength).to.be.greaterThan(oneWordInBytes * scalar);
            expect(customReceiverCallbackDataAsBuffer.byteLength).to.be.lessThan(oneWordInBytes * (scalar + 1));
            // execute transfer
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                customReceiverCallbackData,
                authorized,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(customReceiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer value and ignore extra assetData', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const totalValuesTransferred = _.map(valuesToTransfer, (value: BigNumber) => {
                return value.times(valueMultiplier);
            });
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            const extraData = '0102030405060708091001020304050607080910010203040506070809100102';
            const assetDataWithExtraData = `${assetData}${extraData}`;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetDataWithExtraData,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(1);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(totalValuesTransferred[0]);
            // note - if the `extraData` is ignored then the receiver log should ignore it as well.
            expect(receiverLog.args.data).to.be.deep.equal(receiverCallbackData);
            // check balances after transfer
            const expectedFinalBalances = [
                expectedInitialBalances[0].minus(totalValuesTransferred[0]),
                expectedInitialBalances[1].plus(totalValuesTransferred[0]),
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should successfully transfer if token ids and values are abi encoded to same entry in calldata', async () => {
            /**
             * Suppose the `tokensToTransfer` and `valuesToTransfer` are identical; their offsets in
             * the ABI-encoded asset data may be the same. E.g. token IDs [1, 2] and values [1, 2].
             * Suppose we scale by a factor of 2, then we expect to trade token IDs [1, 2] and values [2, 4].
             * This test ensures that scaling the values does not simultaneously scale the token IDs.
             */
            ///// Step 1/5 /////
            // Create tokens with ids [1, 2, 3, 4] and mint a balance of 4 for the `spender`
            const tokensToCreate = [new BigNumber(1), new BigNumber(2), new BigNumber(3), new BigNumber(4)];
            const spenderInitialBalance = new BigNumber(4);
            const receiverInitialBalance = new BigNumber(0);
            const tokenUri = '';
            for (const tokenToCreate of tokensToCreate) {
                // create token
                await erc1155Wrapper
                    .getContract()
                    .createWithType(tokenToCreate, tokenUri)
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });

                // mint balance for spender
                await erc1155Wrapper
                    .getContract()
                    .mintFungible(tokenToCreate, [spender], [spenderInitialBalance])
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });
            }
            ///// Step 2/5 /////
            // Check balances before transfer
            const balanceHolders = [spender, spender, spender, spender, receiver, receiver, receiver, receiver];
            const balanceTokens = tokensToCreate.concat(tokensToCreate);
            const initialBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedInitialBalances = [
                spenderInitialBalance, // Token ID 1 / Spender Balance
                spenderInitialBalance, // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance, // Token ID 1 / Receiver Balance
                receiverInitialBalance, // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(initialBalances).to.be.deep.equal(expectedInitialBalances);
            ///// Step 3/5 /////
            // Create optimized calldata. We expect it to be formatted like the table below.
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082      // ERC1155 contract address
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080      // Offset to token IDs
            // 0x40       0000000000000000000000000000000000000000000000000000000000000080      // Offset to token values (same as IDs)
            // 0x60       00000000000000000000000000000000000000000000000000000000000000e0      // Offset to data
            // 0x80       0000000000000000000000000000000000000000000000000000000000000002      // Length of token Ids / token values
            // 0xA0       0000000000000000000000000000000000000000000000000000000000000001      // First Token ID / Token value
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000002      // Second Token ID / Token value
            // 0xE0       0000000000000000000000000000000000000000000000000000000000000004      // Length of callback data
            // 0x100      0102030400000000000000000000000000000000000000000000000000000000      // Callback data
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const tokensToTransfer = [new BigNumber(1), new BigNumber(2)];
            const valuesToTransfer = tokensToTransfer;
            const valueMultiplier = new BigNumber(2);

            // hand encode optimized assetData because our tooling (based on LibAssetData.sol/encodeERC1155AssetData) does not use optimized encoding
            const assetDataContract = new IAssetDataContract(constants.NULL_ADDRESS, provider);
            const selector = assetDataContract.getSelector('ERC1155Assets');
            const assetDataWithoutContractAddress =
                '0000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000040102030400000000000000000000000000000000000000000000000000000000';
            const assetData = `${selector}000000000000000000000000${erc1155ContractAddress.substr(
                2,
            )}${assetDataWithoutContractAddress}`;

            ///// Step 4/5 /////
            // Transfer token IDs [1, 2] and amounts [1, 2] with a multiplier of 2;
            // the expected trade will be token IDs [1, 2] and amounts [2, 4]
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetData,
            );
            ///// Step 5/5 /////
            // Validate final balances
            const finalBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedAmountsTransferred = _.map(valuesToTransfer, value => {
                return value.times(valueMultiplier);
            });
            const expectedFinalBalances = [
                spenderInitialBalance.minus(expectedAmountsTransferred[0]), // Token ID 1 / Spender Balance
                spenderInitialBalance.minus(expectedAmountsTransferred[1]), // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[0]), // Token ID 1 / Receiver Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[1]), // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(finalBalances).to.be.deep.equal(expectedFinalBalances);
        });
        it('should successfully transfer if token values and data are abi encoded to same entry in calldata', async () => {
            /**
             * This test ensures that scaling the values does not simultaneously scale the data.
             * Note that this test is slightly more contrived than the test above, as asset data must be
             * intentionally hand-modified to produce this result: a functioning abi encoder will not produce it.
             */
            ///// Step 1/5 /////
            // Create tokens with ids [1, 2, 3, 4] and mint a balance of 4 for the `spender`
            const tokensToCreate = [new BigNumber(1), new BigNumber(2), new BigNumber(3), new BigNumber(4)];
            const spenderInitialBalance = new BigNumber(4);
            const receiverInitialBalance = new BigNumber(0);
            const tokenUri = '';
            for (const tokenToCreate of tokensToCreate) {
                // create token
                await erc1155Wrapper
                    .getContract()
                    .createWithType(tokenToCreate, tokenUri)
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });

                // mint balance for spender
                await erc1155Wrapper
                    .getContract()
                    .mintFungible(tokenToCreate, [spender], [spenderInitialBalance])
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });
            }
            ///// Step 2/5 /////
            // Check balances before transfer
            const balanceHolders = [
                spender,
                spender,
                spender,
                spender,
                receiverContract,
                receiverContract,
                receiverContract,
                receiverContract,
            ];
            const balanceTokens = tokensToCreate.concat(tokensToCreate);
            const initialBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedInitialBalances = [
                spenderInitialBalance, // Token ID 1 / Spender Balance
                spenderInitialBalance, // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance, // Token ID 1 / Receiver Balance
                receiverInitialBalance, // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(initialBalances).to.be.deep.equal(expectedInitialBalances);
            ///// Step 3/5 /////
            // Create optimized calldata. We format like the table below.
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082      // ERC1155 contract address
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080      // Offset to token IDs
            // 0x40       00000000000000000000000000000000000000000000000000000000000000e0      // Offset to token values
            // 0x60       00000000000000000000000000000000000000000000000000000000000000e0      // Offset to data (same as values)
            // 0x80       0000000000000000000000000000000000000000000000000000000000000002      // Length of token Ids
            // 0xA0       0000000000000000000000000000000000000000000000000000000000000001      // First Token ID
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000002      // Second Token ID
            // 0xE0       0000000000000000000000000000000000000000000000000000000000000002      // Length of values (Length of data)
            // 0x100      0000000000000000000000000000000000000000000000000000000000000002      // First Value
            // 0x120      0000000000000000000000000000000000000000000000000000000000000002      // Second Value
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const tokensToTransfer = [new BigNumber(1), new BigNumber(2)];
            const valuesToTransfer = [new BigNumber(2), new BigNumber(2)];
            const valueMultiplier = new BigNumber(2);
            // create callback data that is the encoded version of `valuesToTransfer`
            const generatedAssetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // remove the function selector and contract address from check, as these change on each test
            const offsetToTokenIds = 74;
            const assetDataSelectorAndContractAddress = generatedAssetData.substr(0, offsetToTokenIds);
            const assetDataParameters =
                '000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002';
            const assetData = `${assetDataSelectorAndContractAddress}${assetDataParameters}`;
            ///// Step 4/5 /////
            // Transfer tokens
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetData,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(2);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenIds[1]).to.be.bignumber.equal(tokensToTransfer[1]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(2);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(valuesToTransfer[0].times(valueMultiplier));
            expect(receiverLog.args.tokenValues[1]).to.be.bignumber.equal(valuesToTransfer[1].times(valueMultiplier));
            expect(receiverLog.args.data).to.be.deep.equal('0x0000');
            ///// Step 5/5 /////
            // Validate final balances
            const finalBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedAmountsTransferred = _.map(valuesToTransfer, value => {
                return value.times(valueMultiplier);
            });
            const expectedFinalBalances = [
                spenderInitialBalance.minus(expectedAmountsTransferred[0]), // Token ID 1 / Spender Balance
                spenderInitialBalance.minus(expectedAmountsTransferred[1]), // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[0]), // Token ID 1 / Receiver Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[1]), // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(finalBalances).to.be.deep.equal(expectedFinalBalances);
        });
        it('should successfully transfer if token ids, values and data are abi encoded to same entry in calldata', async () => {
            /**
             * This test combines the two tests above.
             * Similar to the test above, the asset data must be manually constructed.
             */
            ///// Step 1/5 /////
            // Create tokens with ids [1, 2, 3, 4] and mint a balance of 4 for the `spender`
            const tokensToCreate = [new BigNumber(1), new BigNumber(2), new BigNumber(3), new BigNumber(4)];
            const spenderInitialBalance = new BigNumber(4);
            const receiverInitialBalance = new BigNumber(0);
            const tokenUri = '';
            for (const tokenToCreate of tokensToCreate) {
                // create token
                await erc1155Wrapper
                    .getContract()
                    .createWithType(tokenToCreate, tokenUri)
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });

                // mint balance for spender
                await erc1155Wrapper
                    .getContract()
                    .mintFungible(tokenToCreate, [spender], [spenderInitialBalance])
                    .awaitTransactionSuccessAsync({
                        from: owner,
                    });
            }
            ///// Step 2/5 /////
            // Check balances before transfer
            const balanceHolders = [
                spender,
                spender,
                spender,
                spender,
                receiverContract,
                receiverContract,
                receiverContract,
                receiverContract,
            ];
            const balanceTokens = tokensToCreate.concat(tokensToCreate);
            const initialBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedInitialBalances = [
                spenderInitialBalance, // Token ID 1 / Spender Balance
                spenderInitialBalance, // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance, // Token ID 1 / Receiver Balance
                receiverInitialBalance, // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(initialBalances).to.be.deep.equal(expectedInitialBalances);
            ///// Step 3/5 /////
            // Create optimized calldata. We format like the table below.
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082      // ERC1155 contract address
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080      // Offset to token IDs
            // 0x40       0000000000000000000000000000000000000000000000000000000000000080      // Offset to token values
            // 0x60       0000000000000000000000000000000000000000000000000000000000000080      // Offset to data (same as values)
            // 0x80       0000000000000000000000000000000000000000000000000000000000000002      // Length of token Ids (Length of values / data)
            // 0xA0       0000000000000000000000000000000000000000000000000000000000000001      // First Token ID (First Value)
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000002      // Second Token ID (Second Value)
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const tokensToTransfer = [new BigNumber(1), new BigNumber(2)];
            const valuesToTransfer = [new BigNumber(1), new BigNumber(2)];
            const valueMultiplier = new BigNumber(2);
            // create callback data that is the encoded version of `valuesToTransfer`
            const generatedAssetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // remove the function selector and contract address from check, as these change on each test
            const offsetToTokenIds = 74;
            const assetDataSelectorAndContractAddress = generatedAssetData.substr(0, offsetToTokenIds);
            const assetDataParameters =
                '000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002';
            const assetData = `${assetDataSelectorAndContractAddress}${assetDataParameters}`;
            ///// Step 4/5 /////
            // Transfer tokens
            const txReceipt = await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetData,
            );
            // check receiver log ignored extra asset data
            expect(txReceipt.logs.length).to.be.equal(2);
            const receiverLog = txReceipt.logs[1] as LogWithDecodedArgs<
                DummyERC1155ReceiverBatchTokenReceivedEventArgs
            >;
            expect(receiverLog.args.operator).to.be.equal(erc1155Proxy.address);
            expect(receiverLog.args.from).to.be.equal(spender);
            expect(receiverLog.args.tokenIds.length).to.be.deep.equal(2);
            expect(receiverLog.args.tokenIds[0]).to.be.bignumber.equal(tokensToTransfer[0]);
            expect(receiverLog.args.tokenIds[1]).to.be.bignumber.equal(tokensToTransfer[1]);
            expect(receiverLog.args.tokenValues.length).to.be.deep.equal(2);
            expect(receiverLog.args.tokenValues[0]).to.be.bignumber.equal(valuesToTransfer[0].times(valueMultiplier));
            expect(receiverLog.args.tokenValues[1]).to.be.bignumber.equal(valuesToTransfer[1].times(valueMultiplier));
            expect(receiverLog.args.data).to.be.deep.equal('0x0000');
            ///// Step 5/5 /////
            // Validate final balances
            const finalBalances = await erc1155Wrapper.getBalancesAsync(balanceHolders, balanceTokens);
            const expectedAmountsTransferred = _.map(valuesToTransfer, value => {
                return value.times(valueMultiplier);
            });
            const expectedFinalBalances = [
                spenderInitialBalance.minus(expectedAmountsTransferred[0]), // Token ID 1 / Spender Balance
                spenderInitialBalance.minus(expectedAmountsTransferred[1]), // Token ID 2 / Spender Balance
                spenderInitialBalance, // Token ID 3 / Spender Balance
                spenderInitialBalance, // Token ID 4 / Spender Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[0]), // Token ID 1 / Receiver Balance
                receiverInitialBalance.plus(expectedAmountsTransferred[1]), // Token ID 2 / Receiver Balance
                receiverInitialBalance, // Token ID 3 / Receiver Balance
                receiverInitialBalance, // Token ID 4 / Receiver Balance
            ];
            expect(finalBalances).to.be.deep.equal(expectedFinalBalances);
        });
        it('should revert if token ids resolves to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080 // offset to token ids
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token ids to point outside the calldata.
            const encodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000080';
            const badEncodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000180';
            const assetDataWithBadTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenIds,
                badEncodedOffsetToTokenIds,
            );
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenIdsOffset,
                ),
            );
        });
        it('should revert if an element of token ids lies to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080 // offset to token ids
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token ids to the end of calldata.
            // Then we'll add an invalid length: we encode length of 2 but only add 1 element.
            const encodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000080';
            const newEcodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithNewTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenIds,
                newEcodedOffsetToTokenIds,
            );
            const encodedTokenIdsLength = '0000000000000000000000000000000000000000000000000000000000000002';
            const encodedTokenIdValues = '0000000000000000000000000000000000000000000000000000000000000001';
            const assetDataWithBadTokenIds = `${assetDataWithNewTokenIdsOffset}${encodedTokenIdsLength}${encodedTokenIdValues}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenIds,
                ),
            );
        });
        it('should revert token ids length overflows', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080 // offset to token ids
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token ids to point to the end of calldata
            const encodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000080';
            const badEncodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithBadTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenIds,
                badEncodedOffsetToTokenIds,
            );
            // We want a length that will overflow when converted to bytes - ie, multiplied by 32.
            const encodedIdsLengthOverflow = '0800000000000000000000000000000000000000000000000000000000000001';
            const buffer = '0'.repeat(64 * 10);
            const assetDataWithOverflow = `${assetDataWithBadTokenIdsOffset}${encodedIdsLengthOverflow}${buffer}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithOverflow,
                ),
            );
        });
        it('should revert token values length overflows', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0 // offset to token values
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token values to point to the end of calldata
            const encodedOffsetToTokenIds = '00000000000000000000000000000000000000000000000000000000000000c0';
            const badEncodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithBadTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenIds,
                badEncodedOffsetToTokenIds,
            );
            // We want a length that will overflow when converted to bytes - ie, multiplied by 32.
            const encodedIdsLengthOverflow = '0800000000000000000000000000000000000000000000000000000000000001';
            const buffer = '0'.repeat(64 * 10);
            const assetDataWithOverflow = `${assetDataWithBadTokenIdsOffset}${encodedIdsLengthOverflow}${buffer}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithOverflow,
                ),
            );
        });
        it('should revert token data length overflows', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100 // offset to token data
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token ids to point to the end of calldata,
            // which we'll extend with a bad length.
            const encodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000100';
            const badEncodedOffsetToTokenIds = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithBadTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenIds,
                badEncodedOffsetToTokenIds,
            );
            // We want a length that will overflow when converted to bytes - ie, multiplied by 32.
            const encodedIdsLengthOverflow = '0800000000000000000000000000000000000000000000000000000000000001';
            const buffer = '0'.repeat(64 * 10);
            const assetDataWithOverflow = `${assetDataWithBadTokenIdsOffset}${encodedIdsLengthOverflow}${buffer}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithOverflow,
                ),
            );
        });
        it('should revert if token values resolves to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0 // offset to token values
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token values to point outside the calldata.
            const encodedOffsetToTokenValues = '00000000000000000000000000000000000000000000000000000000000000c0';
            const badEncodedOffsetToTokenValues = '00000000000000000000000000000000000000000000000000000000000001c0';
            const assetDataWithBadTokenIdsOffset = assetData.replace(
                encodedOffsetToTokenValues,
                badEncodedOffsetToTokenValues,
            );
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenIdsOffset,
                ),
            );
        });
        it('should revert if an element of token values lies to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0 // offset to token values
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token values to the end of calldata.
            // Then we'll add an invalid length: we encode length of 2 but only add 1 element.
            const encodedOffsetToTokenValues = '00000000000000000000000000000000000000000000000000000000000000c0';
            const newEcodedOffsetToTokenValues = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithNewTokenValuesOffset = assetData.replace(
                encodedOffsetToTokenValues,
                newEcodedOffsetToTokenValues,
            );
            const encodedTokenValuesLength = '0000000000000000000000000000000000000000000000000000000000000002';
            const encodedTokenValuesElements = '0000000000000000000000000000000000000000000000000000000000000001';
            const assetDataWithBadTokenIds = `${assetDataWithNewTokenValuesOffset}${encodedTokenValuesLength}${encodedTokenValuesElements}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenIds,
                ),
            );
        });
        it('should revert if token data resolves to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100 // offset to token data
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token data to point outside the calldata.
            const encodedOffsetToTokenData = '0000000000000000000000000000000000000000000000000000000000000100';
            const badEncodedOffsetToTokenData = '00000000000000000000000000000000000000000000000000000000000001c0';
            const assetDataWithBadTokenDataOffset = assetData.replace(
                encodedOffsetToTokenData,
                badEncodedOffsetToTokenData,
            );
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenDataOffset,
                ),
            );
        });
        it('should revert if an element of token data lies to outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            // The asset data we just generated will look like this:
            // a7cb5fb7
            // 0x         0000000000000000000000000b1ba0af832d7c05fd64161e0db78e85978e8082
            // 0x20       0000000000000000000000000000000000000000000000000000000000000080
            // 0x40       00000000000000000000000000000000000000000000000000000000000000c0
            // 0x60       0000000000000000000000000000000000000000000000000000000000000100 // offset to token data
            // 0x80       0000000000000000000000000000000000000000000000000000000000000001
            // 0xA0       0000000000000000000000000000000100000000000000000000000000000000
            // 0xC0       0000000000000000000000000000000000000000000000000000000000000001
            // 0xE0       0000000000000000000000000000000000000000000000878678326eac900000
            // 0x100      0000000000000000000000000000000000000000000000000000000000000004
            // 0x120      0102030400000000000000000000000000000000000000000000000000000000
            //
            // We want to change the offset to token data to the end of calldata.
            // Then we'll add an invalid length: we encode length of 33 but only add 32 elements.
            const encodedOffsetToTokenData = '0000000000000000000000000000000000000000000000000000000000000100';
            const newEcodedOffsetToTokenData = '0000000000000000000000000000000000000000000000000000000000000140';
            const assetDataWithNewTokenDataOffset = assetData.replace(
                encodedOffsetToTokenData,
                newEcodedOffsetToTokenData,
            );
            const encodedTokenDataLength = '0000000000000000000000000000000000000000000000000000000000000021';
            const encodedTokenDataElements = '0000000000000000000000000000000000000000000000000000000000000001';
            const assetDataWithBadTokenData = `${assetDataWithNewTokenDataOffset}${encodedTokenDataLength}${encodedTokenDataElements}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetDataWithBadTokenData,
                ),
            );
        });
        it('should revert if asset data lies outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            const txData = await erc1155ProxyWrapper.getTransferFromAbiEncodedTxDataAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetData,
            );
            const offsetToAssetData = '0000000000000000000000000000000000000000000000000000000000000080';
            const invalidOffsetToAssetData = '0000000000000000000000000000000000000000000000000000000000000180';
            const badTxData = txData.replace(offsetToAssetData, invalidOffsetToAssetData);
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromRawAsync(badTxData, authorized),
            );
        });
        it('should revert if asset data lies outside the bounds of calldata', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            const erc1155ContractAddress = erc1155Wrapper.getContract().address;
            const assetData = await devUtils
                .encodeERC1155AssetData(
                    erc1155ContractAddress,
                    tokensToTransfer,
                    valuesToTransfer,
                    receiverCallbackData,
                )
                .callAsync();
            const txData = await erc1155ProxyWrapper.getTransferFromAbiEncodedTxDataAsync(
                spender,
                receiverContract,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
                assetData,
            );
            // append asset data to end of tx data with a length of 0x300 bytes, which will extend past actual calldata.
            const offsetToAssetData = '0000000000000000000000000000000000000000000000000000000000000080';
            const invalidOffsetToAssetData = '0000000000000000000000000000000000000000000000000000000000000200';
            const newAssetData = '0000000000000000000000000000000000000000000000000000000000000304';
            const badTxData = `${txData.replace(offsetToAssetData, invalidOffsetToAssetData)}${newAssetData}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromRawAsync(badTxData, authorized),
            );
        });
        it('should revert if length of assetData is less than 132 bytes', async () => {
            // setup test parameters
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            // we'll construct asset data that has a 4 byte selector plus
            // 96 byte payload. This results in asset data that is 100 bytes
            // long and will trigger the `invalid length` error.
            // we must be sure to use a # of bytes that is still %32
            // so that we know the error is not triggered by another check in the code.
            const zeros96Bytes = '0'.repeat(188);
            const assetData131Bytes = `${AssetProxyId.ERC1155}${zeros96Bytes}`;
            // execute transfer
            await expectTransactionFailedWithoutReasonAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                    assetData131Bytes,
                ),
            );
        });
        it('should transfer nothing if value is zero', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [new BigNumber(0)];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should transfer nothing if value multiplier is zero', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = new BigNumber(0);
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should transfer nothing if there are no tokens in asset data', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer: BigNumber[] = [];
            const valuesToTransfer: BigNumber[] = [];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            // check balances after transfer
            const expectedFinalBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedFinalBalances);
        });
        it('should propagate revert reason from erc1155 contract failure', async () => {
            // disable transfers
            const shouldRejectTransfer = true;
            await erc1155Receiver.setRejectTransferFlag(shouldRejectTransfer).awaitTransactionSuccessAsync({
                from: owner,
            });
            // setup test parameters
            const tokenHolders = [spender, receiverContract];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverContractInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiverContract,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
                RevertReason.TransferRejected,
            );
        });
        it('should revert if transferring the same non-fungible token more than once', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const nftToTransfer = nonFungibleTokensOwnedBySpender[0];
            const tokensToTransfer = [nftToTransfer, nftToTransfer];
            const valuesToTransfer = [nonFungibleValueToTransfer, nonFungibleValueToTransfer];
            const valueMultiplier = valueMultiplierNft;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
                RevertReason.NFTNotOwnedByFromAddress,
            );
        });
        it('should revert if there is a multiplication overflow', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 3);
            const maxUintValue = new BigNumber(2).pow(256).minus(1);
            const valuesToTransfer = [nonFungibleValueToTransfer, maxUintValue, nonFungibleValueToTransfer];
            const valueMultiplier = new BigNumber(2);
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                nftOwnerBalance,
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
                nftNotOwnerBalance,
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
                SafeMathRevertErrors.BinOpErrorCodes.MultiplicationOverflow,
                maxUintValue,
                valueMultiplier,
            );
            // execute transfer
            // note - this will overflow because we are trying to transfer `maxUintValue * 2` of the 2nd token
            await expect(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
            ).to.revertWith(expectedError);
        });
        it('should revert if transferring > 1 instances of a non-fungible token (valueMultiplier field >1)', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1);
            const valuesToTransfer = [nonFungibleValueToTransfer];
            const valueMultiplier = new BigNumber(2);
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
                RevertReason.AmountEqualToOneRequired,
            );
        });
        it('should revert if transferring > 1 instances of a non-fungible token (`valuesToTransfer` field >1)', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = nonFungibleTokensOwnedBySpender.slice(0, 1);
            const valuesToTransfer = [new BigNumber(2)];
            const valueMultiplier = valueMultiplierNft;
            // check balances before transfer
            const expectedInitialBalances = [
                // spender
                nftOwnerBalance,
                // receiver
                nftNotOwnerBalance,
            ];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
                RevertReason.AmountEqualToOneRequired,
            );
        });
        it('should revert if sender balance is insufficient', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valueGreaterThanSpenderBalance = spenderInitialFungibleBalance.plus(1);
            const valuesToTransfer = [valueGreaterThanSpenderBalance];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            const expectedError = new SafeMathRevertErrors.Uint256BinOpError(
                SafeMathRevertErrors.BinOpErrorCodes.SubtractionUnderflow,
                spenderInitialFungibleBalance,
                valuesToTransfer[0].times(valueMultiplier),
            );
            // execute transfer
            const tx = erc1155ProxyWrapper.transferFromAsync(
                spender,
                receiver,
                erc1155Contract.address,
                tokensToTransfer,
                valuesToTransfer,
                valueMultiplier,
                receiverCallbackData,
                authorized,
            );
            return expect(tx).to.revertWith(expectedError);
        });
        it('should revert if sender allowance is insufficient', async () => {
            // dremove allowance for ERC1155 proxy
            const wrapper = erc1155ProxyWrapper.getContractWrapper(erc1155Contract.address);
            const isApproved = false;
            await wrapper.setApprovalForAllAsync(spender, erc1155Proxy.address, isApproved);
            const isApprovedActualValue = await wrapper.isApprovedForAllAsync(spender, erc1155Proxy.address);
            expect(isApprovedActualValue).to.be.equal(isApproved);
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    authorized,
                ),
                RevertReason.InsufficientAllowance,
            );
        });
        it('should revert if caller is not authorized', async () => {
            // setup test parameters
            const tokenHolders = [spender, receiver];
            const tokensToTransfer = fungibleTokens.slice(0, 1);
            const valuesToTransfer = [fungibleValueToTransferLarge];
            const valueMultiplier = valueMultiplierSmall;
            // check balances before transfer
            const expectedInitialBalances = [spenderInitialFungibleBalance, receiverInitialFungibleBalance];
            await erc1155Wrapper.assertBalancesAsync(tokenHolders, tokensToTransfer, expectedInitialBalances);
            // execute transfer
            await expectTransactionFailedAsync(
                erc1155ProxyWrapper.transferFromAsync(
                    spender,
                    receiver,
                    erc1155Contract.address,
                    tokensToTransfer,
                    valuesToTransfer,
                    valueMultiplier,
                    receiverCallbackData,
                    notAuthorized,
                ),
                RevertReason.SenderNotAuthorized,
            );
        });
    });
});
// tslint:enable:no-unnecessary-type-assertion
// tslint:disable:max-file-line-count
