From 2adc299c78f17712dfea55cf8257c1cb237479cb Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 27 Jun 2018 11:48:01 +0300 Subject: Implement ERC721 token wrapper and token transfer proxy with tests --- .../test/erc721_proxy_wrapper_test.ts | 36 ++ .../contract-wrappers/test/erc721_wrapper_test.ts | 460 +++++++++++++++++++++ 2 files changed, 496 insertions(+) create mode 100644 packages/contract-wrappers/test/erc721_proxy_wrapper_test.ts create mode 100644 packages/contract-wrappers/test/erc721_wrapper_test.ts (limited to 'packages/contract-wrappers/test') diff --git a/packages/contract-wrappers/test/erc721_proxy_wrapper_test.ts b/packages/contract-wrappers/test/erc721_proxy_wrapper_test.ts new file mode 100644 index 000000000..7d1a5b8bb --- /dev/null +++ b/packages/contract-wrappers/test/erc721_proxy_wrapper_test.ts @@ -0,0 +1,36 @@ +import * as chai from 'chai'; +import 'make-promises-safe'; + +import { ContractWrappers } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { provider } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('ERC721ProxyWrapper', () => { + let contractWrappers: ContractWrappers; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + }); + describe('#isAuthorizedAsync', () => { + it('should return false if the address is not authorized', async () => { + const isAuthorized = await contractWrappers.erc721Proxy.isAuthorizedAsync(constants.NULL_ADDRESS); + expect(isAuthorized).to.be.false(); + }); + }); + describe('#getAuthorizedAddressesAsync', () => { + it('should return the list of authorized addresses', async () => { + const authorizedAddresses = await contractWrappers.erc721Proxy.getAuthorizedAddressesAsync(); + for (const authorizedAddress of authorizedAddresses) { + const isAuthorized = await contractWrappers.erc721Proxy.isAuthorizedAsync(authorizedAddress); + expect(isAuthorized).to.be.true(); + } + }); + }); +}); diff --git a/packages/contract-wrappers/test/erc721_wrapper_test.ts b/packages/contract-wrappers/test/erc721_wrapper_test.ts new file mode 100644 index 000000000..170442690 --- /dev/null +++ b/packages/contract-wrappers/test/erc721_wrapper_test.ts @@ -0,0 +1,460 @@ +import { BlockchainLifecycle, callbackErrorReporter } from '@0xproject/dev-utils'; +import { EmptyWalletSubprovider } from '@0xproject/subproviders'; +import { DoneCallback } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import { Provider } from 'ethereum-types'; +import 'make-promises-safe'; +import 'mocha'; +import Web3ProviderEngine = require('web3-provider-engine'); + +import { + BlockParamLiteral, + BlockRange, + ContractWrappers, + ContractWrappersError, + DecodedLogEvent, + ERC721TokenApprovalEventArgs, + ERC721TokenApprovalForAllEventArgs, + ERC721TokenEvents, + ERC721TokenTransferEventArgs, +} from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { constants } from './utils/constants'; +import { tokenUtils } from './utils/token_utils'; +import { provider, web3Wrapper } from './utils/web3_wrapper'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(web3Wrapper); + +describe('ERC721Wrapper', () => { + let contractWrappers: ContractWrappers; + let userAddresses: string[]; + let tokens: string[]; + let ownerAddress: string; + let tokenAddress: string; + let anotherOwnerAddress: string; + let operatorAddress: string; + let approvedAddress: string; + let receiverAddress: string; + const config = { + networkId: constants.TESTRPC_NETWORK_ID, + }; + before(async () => { + contractWrappers = new ContractWrappers(provider, config); + userAddresses = await web3Wrapper.getAvailableAddressesAsync(); + tokens = tokenUtils.getDummyERC721TokenAddresses(); + tokenAddress = tokens[0]; + [ownerAddress, operatorAddress, anotherOwnerAddress, approvedAddress, receiverAddress] = userAddresses; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#transferFromAsync', () => { + it('should fail to transfer NFT if fromAddress has no approvals set', async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + return expect( + contractWrappers.erc721Token.transferFromAsync(tokenAddress, receiverAddress, approvedAddress, tokenId), + ).to.be.rejectedWith(ContractWrappersError.ERC721NoApproval); + }); + it('should successfully transfer tokens when sender is an approved address', async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + let txHash = await contractWrappers.erc721Token.setApprovalAsync(tokenAddress, approvedAddress, tokenId); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const owner = await contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, tokenId); + expect(owner).to.be.equal(ownerAddress); + txHash = await contractWrappers.erc721Token.transferFromAsync( + tokenAddress, + receiverAddress, + approvedAddress, + tokenId, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const newOwner = await contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, tokenId); + expect(newOwner).to.be.equal(receiverAddress); + }); + it('should successfully transfer tokens when sender is an approved operator', async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + const isApprovedForAll = true; + let txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + isApprovedForAll, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const owner = await contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, tokenId); + expect(owner).to.be.equal(ownerAddress); + txHash = await contractWrappers.erc721Token.transferFromAsync( + tokenAddress, + receiverAddress, + operatorAddress, + tokenId, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const newOwner = await contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, tokenId); + expect(newOwner).to.be.equal(receiverAddress); + }); + }); + describe('#getTokenCountAsync', () => { + describe('With provider with accounts', () => { + it('should return the count for an existing ERC721 token', async () => { + let tokenCount = await contractWrappers.erc721Token.getTokenCountAsync(tokenAddress, ownerAddress); + expect(tokenCount).to.be.bignumber.equal(0); + await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + tokenCount = await contractWrappers.erc721Token.getTokenCountAsync(tokenAddress, ownerAddress); + expect(tokenCount).to.be.bignumber.equal(1); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + return expect( + contractWrappers.erc721Token.getTokenCountAsync(nonExistentTokenAddress, ownerAddress), + ).to.be.rejectedWith(ContractWrappersError.ERC721TokenContractDoesNotExist); + }); + it('should return a balance of 0 for a non-existent owner address', async () => { + const nonExistentOwner = '0x198c6ad858f213fb31b6fe809e25040e6b964593'; + const balance = await contractWrappers.erc721Token.getTokenCountAsync(tokenAddress, nonExistentOwner); + const expectedBalance = new BigNumber(0); + return expect(balance).to.be.bignumber.equal(expectedBalance); + }); + }); + describe('With provider without accounts', () => { + let zeroExContractWithoutAccounts: ContractWrappers; + before(async () => { + const emptyWalletProvider = addEmptyWalletSubprovider(provider); + zeroExContractWithoutAccounts = new ContractWrappers(emptyWalletProvider, config); + }); + it('should return balance even when called with provider instance without addresses', async () => { + const balance = await zeroExContractWithoutAccounts.erc721Token.getTokenCountAsync( + tokenAddress, + ownerAddress, + ); + return expect(balance).to.be.bignumber.equal(0); + }); + }); + }); + describe('#getOwnerOfAsync', () => { + it('should return the owner for an existing ERC721 token', async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + const tokenOwner = await contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, tokenId); + expect(tokenOwner).to.be.bignumber.equal(ownerAddress); + }); + it('should throw a CONTRACT_DOES_NOT_EXIST error for a non-existent token contract', async () => { + const nonExistentTokenAddress = '0x9dd402f14d67e001d8efbe6583e51bf9706aa065'; + const fakeTokenId = new BigNumber(42); + return expect( + contractWrappers.erc721Token.getOwnerOfAsync(nonExistentTokenAddress, fakeTokenId), + ).to.be.rejectedWith(ContractWrappersError.ERC721TokenContractDoesNotExist); + }); + it('should return undefined not 0 for a non-existent ERC721', async () => { + const fakeTokenId = new BigNumber(42); + return expect(contractWrappers.erc721Token.getOwnerOfAsync(tokenAddress, fakeTokenId)).to.be.rejectedWith( + ContractWrappersError.ERC721OwnerNotFound, + ); + }); + }); + describe('#setApprovalForAllAsync/isApprovedForAllAsync', () => { + it('should check if operator address is approved', async () => { + let isApprovedForAll = await contractWrappers.erc721Token.isApprovedForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + ); + expect(isApprovedForAll).to.be.false(); + // set + let txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + isApprovedForAll = await contractWrappers.erc721Token.isApprovedForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + ); + expect(isApprovedForAll).to.be.true(); + // usnset + txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + false, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + isApprovedForAll = await contractWrappers.erc721Token.isApprovedForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + ); + expect(isApprovedForAll).to.be.false(); + }); + }); + describe('#setProxyApprovalForAllAsync/isProxyApprovedForAllAsync', () => { + it('should check if proxy address is approved', async () => { + const txHash = await contractWrappers.erc721Token.setProxyApprovalForAllAsync( + tokenAddress, + ownerAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const isApprovedForAll = await contractWrappers.erc721Token.isProxyApprovedForAllAsync( + tokenAddress, + ownerAddress, + ); + expect(isApprovedForAll).to.be.true(); + }); + }); + describe('#setApprovalAsync/getApprovedAsync', () => { + it("should set the spender's approval", async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + + const approvalBeforeSet = await contractWrappers.erc721Token.getApprovedAsync(tokenAddress, tokenId); + expect(approvalBeforeSet).to.be.undefined(); + await contractWrappers.erc721Token.setApprovalAsync(tokenAddress, approvedAddress, tokenId); + const approvalAfterSet = await contractWrappers.erc721Token.getApprovedAsync(tokenAddress, tokenId); + expect(approvalAfterSet).to.be.equal(approvedAddress); + }); + }); + describe('#setProxyApprovalAsync/isProxyApprovedAsync', () => { + it('should set the proxy approval', async () => { + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + + const approvalBeforeSet = await contractWrappers.erc721Token.isProxyApprovedAsync(tokenAddress, tokenId); + expect(approvalBeforeSet).to.be.false(); + await contractWrappers.erc721Token.setProxyApprovalAsync(tokenAddress, tokenId); + const approvalAfterSet = await contractWrappers.erc721Token.isProxyApprovedAsync(tokenAddress, tokenId); + expect(approvalAfterSet).to.be.true(); + }); + }); + describe('#subscribe', () => { + const indexFilterValues = {}; + afterEach(() => { + contractWrappers.erc721Token.unsubscribeAll(); + }); + // Hack: Mocha does not allow a test to be both async and have a `done` callback + // Since we need to await the receipt of the event in the `subscribe` callback, + // we do need both. A hack is to make the top-level a sync fn w/ a done callback and then + // wrap the rest of the test in an async block + // Source: https://github.com/mochajs/mocha/issues/2407 + it('Should receive the Transfer event when tokens are transfered', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + expect(logEvent.isRemoved).to.be.false(); + expect(logEvent.log.logIndex).to.be.equal(0); + expect(logEvent.log.transactionIndex).to.be.equal(0); + expect(logEvent.log.blockNumber).to.be.a('number'); + const args = logEvent.log.args; + expect(args._from).to.be.equal(ownerAddress); + expect(args._to).to.be.equal(receiverAddress); + expect(args._tokenId).to.be.bignumber.equal(tokenId); + }, + ); + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + const isApprovedForAll = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + isApprovedForAll, + ), + ); + contractWrappers.erc721Token.subscribe( + tokenAddress, + ERC721TokenEvents.Transfer, + indexFilterValues, + callback, + ); + await web3Wrapper.awaitTransactionSuccessAsync( + await contractWrappers.erc721Token.transferFromAsync( + tokenAddress, + receiverAddress, + operatorAddress, + tokenId, + ), + ); + })().catch(done); + }); + it('Should receive the Approval event when allowance is being set', (done: DoneCallback) => { + (async () => { + const callback = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + expect(logEvent).to.not.be.undefined(); + expect(logEvent.isRemoved).to.be.false(); + const args = logEvent.log.args; + expect(args._owner).to.be.equal(ownerAddress); + expect(args._approved).to.be.equal(approvedAddress); + expect(args._tokenId).to.be.bignumber.equal(tokenId); + }, + ); + contractWrappers.erc721Token.subscribe( + tokenAddress, + ERC721TokenEvents.Approval, + indexFilterValues, + callback, + ); + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await contractWrappers.erc721Token.setApprovalAsync(tokenAddress, approvedAddress, tokenId), + ); + })().catch(done); + }); + it('Outstanding subscriptions are cancelled when contractWrappers.setProvider called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + contractWrappers.erc721Token.subscribe( + tokenAddress, + ERC721TokenEvents.Transfer, + indexFilterValues, + callbackNeverToBeCalled, + ); + const callbackToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)(); + contractWrappers.setProvider(provider, constants.TESTRPC_NETWORK_ID); + contractWrappers.erc721Token.subscribe( + tokenAddress, + ERC721TokenEvents.Approval, + indexFilterValues, + callbackToBeCalled, + ); + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + await web3Wrapper.awaitTransactionSuccessAsync( + await contractWrappers.erc721Token.setApprovalAsync(tokenAddress, approvedAddress, tokenId), + ); + done(); + })().catch(done); + }); + it('Should cancel subscription when unsubscribe called', (done: DoneCallback) => { + (async () => { + const callbackNeverToBeCalled = callbackErrorReporter.reportNodeCallbackErrors(done)( + (logEvent: DecodedLogEvent) => { + done(new Error('Expected this subscription to have been cancelled')); + }, + ); + const subscriptionToken = contractWrappers.erc721Token.subscribe( + tokenAddress, + ERC721TokenEvents.ApprovalForAll, + indexFilterValues, + callbackNeverToBeCalled, + ); + contractWrappers.erc721Token.unsubscribe(subscriptionToken); + + const tokenId = await tokenUtils.mintDummyERC721Async(tokenAddress, ownerAddress); + const isApproved = true; + await web3Wrapper.awaitTransactionSuccessAsync( + await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + isApproved, + ), + ); + done(); + })().catch(done); + }); + }); + describe('#getLogsAsync', () => { + let tokenTransferProxyAddress: string; + const blockRange: BlockRange = { + fromBlock: 0, + toBlock: BlockParamLiteral.Latest, + }; + let txHash: string; + before(() => { + tokenTransferProxyAddress = contractWrappers.erc721Proxy.getContractAddress(); + }); + it('should get logs with decoded args emitted by ApprovalForAll', async () => { + txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const eventName = ERC721TokenEvents.ApprovalForAll; + const indexFilterValues = {}; + const logs = await contractWrappers.erc721Token.getLogsAsync( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(logs[0].event).to.be.equal(eventName); + expect(args._owner).to.be.equal(ownerAddress); + expect(args._operator).to.be.equal(operatorAddress); + expect(args._approved).to.be.equal(true); + }); + it('should only get the logs with the correct event name', async () => { + txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const differentEventName = ERC721TokenEvents.Transfer; + const indexFilterValues = {}; + const logs = await contractWrappers.erc721Token.getLogsAsync( + tokenAddress, + differentEventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(0); + }); + it('should only get the logs with the correct indexed fields', async () => { + txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + ownerAddress, + operatorAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + txHash = await contractWrappers.erc721Token.setApprovalForAllAsync( + tokenAddress, + anotherOwnerAddress, + operatorAddress, + true, + ); + await web3Wrapper.awaitTransactionSuccessAsync(txHash); + const eventName = ERC721TokenEvents.ApprovalForAll; + const indexFilterValues = { + _owner: anotherOwnerAddress, + }; + const logs = await contractWrappers.erc721Token.getLogsAsync( + tokenAddress, + eventName, + blockRange, + indexFilterValues, + ); + expect(logs).to.have.length(1); + const args = logs[0].args; + expect(args._owner).to.be.equal(anotherOwnerAddress); + }); + }); +}); +// tslint:disable:max-file-line-count + +function addEmptyWalletSubprovider(p: Provider): Provider { + const providerEngine = new Web3ProviderEngine(); + providerEngine.addProvider(new EmptyWalletSubprovider()); + const currentSubproviders = (p as any)._providers; + for (const subprovider of currentSubproviders) { + providerEngine.addProvider(subprovider); + } + providerEngine.start(); + return providerEngine; +} -- cgit v1.2.3