aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLeonid Logvinov <logvinov.leon@gmail.com>2018-06-27 16:48:01 +0800
committerLeonid Logvinov <logvinov.leon@gmail.com>2018-06-29 22:52:53 +0800
commit2adc299c78f17712dfea55cf8257c1cb237479cb (patch)
treed0439649a9fa50e947976a351ff552c41edd931a
parent3aef323c1334e577905855cac50a6f41245563c6 (diff)
downloaddexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar.gz
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar.bz2
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar.lz
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar.xz
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.tar.zst
dexon-sol-tools-2adc299c78f17712dfea55cf8257c1cb237479cb.zip
Implement ERC721 token wrapper and token transfer proxy with tests
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/erc721_proxy_wrapper.ts73
-rw-r--r--packages/contract-wrappers/src/contract_wrappers/erc721_token_wrapper.ts439
-rw-r--r--packages/contract-wrappers/test/erc721_proxy_wrapper_test.ts36
-rw-r--r--packages/contract-wrappers/test/erc721_wrapper_test.ts460
4 files changed, 1008 insertions, 0 deletions
diff --git a/packages/contract-wrappers/src/contract_wrappers/erc721_proxy_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/erc721_proxy_wrapper.ts
new file mode 100644
index 000000000..fbe32a7c5
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/erc721_proxy_wrapper.ts
@@ -0,0 +1,73 @@
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import { ContractAbi } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { artifacts } from '../artifacts';
+import { assert } from '../utils/assert';
+
+import { ContractWrapper } from './contract_wrapper';
+import { ERC721ProxyContract } from './generated/erc721_proxy';
+
+/**
+ * This class includes the functionality related to interacting with the ERC721Proxy contract.
+ */
+export class ERC721ProxyWrapper extends ContractWrapper {
+ public abi: ContractAbi = artifacts.ERC20Proxy.compilerOutput.abi;
+ private _erc721ProxyContractIfExists?: ERC721ProxyContract;
+ private _contractAddressIfExists?: string;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, contractAddressIfExists?: string) {
+ super(web3Wrapper, networkId);
+ this._contractAddressIfExists = contractAddressIfExists;
+ }
+ /**
+ * Check if the Exchange contract address is authorized by the ERC721Proxy contract.
+ * @param exchangeContractAddress The hex encoded address of the Exchange contract to call.
+ * @return Whether the exchangeContractAddress is authorized.
+ */
+ public async isAuthorizedAsync(exchangeContractAddress: string): Promise<boolean> {
+ assert.isETHAddressHex('exchangeContractAddress', exchangeContractAddress);
+ const normalizedExchangeContractAddress = exchangeContractAddress.toLowerCase();
+ const ERC721ProxyContractInstance = await this._getERC721ProxyContractAsync();
+ const isAuthorized = await ERC721ProxyContractInstance.authorized.callAsync(normalizedExchangeContractAddress);
+ return isAuthorized;
+ }
+ /**
+ * Get the list of all Exchange contract addresses authorized by the ERC721Proxy contract.
+ * @return The list of authorized addresses.
+ */
+ public async getAuthorizedAddressesAsync(): Promise<string[]> {
+ const ERC721ProxyContractInstance = await this._getERC721ProxyContractAsync();
+ const authorizedAddresses = await ERC721ProxyContractInstance.getAuthorizedAddresses.callAsync();
+ return authorizedAddresses;
+ }
+ /**
+ * Retrieves the Ethereum address of the ERC721Proxy contract deployed on the network
+ * that the user-passed web3 provider is connected to.
+ * @returns The Ethereum address of the ERC721Proxy contract being used.
+ */
+ public getContractAddress(): string {
+ const contractAddress = this._getContractAddress(artifacts.ERC721Proxy, this._contractAddressIfExists);
+ return contractAddress;
+ }
+ // tslint:disable-next-line:no-unused-variable
+ private _invalidateContractInstance(): void {
+ delete this._erc721ProxyContractIfExists;
+ }
+ private async _getERC721ProxyContractAsync(): Promise<ERC721ProxyContract> {
+ if (!_.isUndefined(this._erc721ProxyContractIfExists)) {
+ return this._erc721ProxyContractIfExists;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.ERC721Proxy,
+ this._contractAddressIfExists,
+ );
+ const contractInstance = new ERC721ProxyContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ this._erc721ProxyContractIfExists = contractInstance;
+ return this._erc721ProxyContractIfExists;
+ }
+}
diff --git a/packages/contract-wrappers/src/contract_wrappers/erc721_token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/erc721_token_wrapper.ts
new file mode 100644
index 000000000..8164e3df3
--- /dev/null
+++ b/packages/contract-wrappers/src/contract_wrappers/erc721_token_wrapper.ts
@@ -0,0 +1,439 @@
+import { schemas } from '@0xproject/json-schemas';
+import { BigNumber } from '@0xproject/utils';
+import { Web3Wrapper } from '@0xproject/web3-wrapper';
+import { ContractAbi, LogWithDecodedArgs } from 'ethereum-types';
+import * as _ from 'lodash';
+
+import { constants } from '../../test/utils/constants';
+import { artifacts } from '../artifacts';
+import {
+ BlockRange,
+ ContractWrappersError,
+ EventCallback,
+ IndexedFilterValues,
+ MethodOpts,
+ TransactionOpts,
+} from '../types';
+import { assert } from '../utils/assert';
+
+import { ContractWrapper } from './contract_wrapper';
+import { ERC721ProxyWrapper } from './erc721_proxy_wrapper';
+import { ERC721TokenContract, ERC721TokenEventArgs, ERC721TokenEvents } from './generated/erc721_token';
+
+/**
+ * This class includes all the functionality related to interacting with ERC721 token contracts.
+ * All ERC721 method calls are supported, along with some convenience methods for getting/setting allowances
+ * to the 0x ERC721 Proxy smart contract.
+ */
+export class ERC721TokenWrapper extends ContractWrapper {
+ public abi: ContractAbi = artifacts.ERC721Token.compilerOutput.abi;
+ private _tokenContractsByAddress: { [address: string]: ERC721TokenContract };
+ private _erc721ProxyWrapper: ERC721ProxyWrapper;
+ constructor(web3Wrapper: Web3Wrapper, networkId: number, erc721ProxyWrapper: ERC721ProxyWrapper) {
+ super(web3Wrapper, networkId);
+ this._tokenContractsByAddress = {};
+ this._erc721ProxyWrapper = erc721ProxyWrapper;
+ }
+ /**
+ * Count all NFTs assigned to an owner
+ * NFTs assigned to the zero address are considered invalid, and this function throws for queries about the zero address.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address whose balance you would like to check.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The number of NFTs owned by `ownerAddress`, possibly zero
+ */
+ public async getTokenCountAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ methodOpts?: MethodOpts,
+ ): Promise<BigNumber> {
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ let balance = await tokenContract.balanceOf.callAsync(normalizedOwnerAddress, txData, defaultBlock);
+ // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
+ balance = new BigNumber(balance);
+ return balance;
+ }
+ /**
+ * Find the owner of an NFT
+ * NFTs assigned to zero address are considered invalid, and queries about them do throw.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param tokenId The identifier for an NFT
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The address of the owner of the NFT
+ */
+ public async getOwnerOfAsync(tokenAddress: string, tokenId: BigNumber, methodOpts?: MethodOpts): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isBigNumber('tokenId', tokenId);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ try {
+ const tokenOwner = await tokenContract.ownerOf.callAsync(tokenId, txData, defaultBlock);
+ return tokenOwner;
+ } catch (err) {
+ throw new Error(ContractWrappersError.ERC721OwnerNotFound);
+ }
+ }
+ /**
+ * Query if an address is an authorized operator for all NFT's of `ownerAddress`
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address of the token owner.
+ * @param operatorAddress The hex encoded user Ethereum address of the operator you'd like to check if approved.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return True if `operatorAddress` is an approved operator for `ownerAddress`, false otherwise
+ */
+ public async isApprovedForAllAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ operatorAddress: string,
+ methodOpts?: MethodOpts,
+ ): Promise<boolean> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('operatorAddress', operatorAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ const normalizedOperatorAddress = operatorAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ const isApprovedForAll = await tokenContract.isApprovedForAll.callAsync(
+ normalizedOwnerAddress,
+ normalizedOperatorAddress,
+ txData,
+ defaultBlock,
+ );
+ return isApprovedForAll;
+ }
+ /**
+ * Query if 0x proxy is an authorized operator for all NFT's of `ownerAddress`
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address of the token owner.
+ * @param methodOpts Optional arguments this method accepts.
+ * @return True if `operatorAddress` is an approved operator for `ownerAddress`, false otherwise
+ */
+ public async isProxyApprovedForAllAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ methodOpts?: MethodOpts,
+ ): Promise<boolean> {
+ const proxyAddress = this._erc721ProxyWrapper.getContractAddress();
+ const isProxyApprovedForAll = await this.isApprovedForAllAsync(
+ tokenAddress,
+ ownerAddress,
+ proxyAddress,
+ methodOpts,
+ );
+ return isProxyApprovedForAll;
+ }
+ /**
+ * Get the approved address for a single NFT
+ * Throws if `_tokenId` is not a valid NFT
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param tokenId The identifier for an NFT
+ * @param methodOpts Optional arguments this method accepts.
+ * @return The approved address for this NFT, or the zero address if there is none
+ */
+ public async getApprovedAsync(
+ tokenAddress: string,
+ tokenId: BigNumber,
+ methodOpts?: MethodOpts,
+ ): Promise<string | undefined> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isBigNumber('tokenId', tokenId);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
+ const txData = {};
+ const approvedAddress = await tokenContract.getApproved.callAsync(tokenId, txData, defaultBlock);
+ if (approvedAddress === constants.NULL_ADDRESS) {
+ return undefined;
+ }
+ return approvedAddress;
+ }
+ /**
+ * Checks if 0x proxy is approved for a single NFT
+ * Throws if `_tokenId` is not a valid NFT
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param tokenId The identifier for an NFT
+ * @param methodOpts Optional arguments this method accepts.
+ * @return True if 0x proxy is approved
+ */
+ public async isProxyApprovedAsync(
+ tokenAddress: string,
+ tokenId: BigNumber,
+ methodOpts?: MethodOpts,
+ ): Promise<boolean> {
+ const proxyAddress = this._erc721ProxyWrapper.getContractAddress();
+ const approvedAddress = await this.getApprovedAsync(tokenAddress, tokenId, methodOpts);
+ const isProxyApproved = approvedAddress === proxyAddress;
+ return isProxyApproved;
+ }
+ /**
+ * Enable or disable approval for a third party ("operator") to manage all of `ownerAddress`'s assets.
+ * Throws if `_tokenId` is not a valid NFT
+ * Emits the ApprovalForAll event.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address of the token owner.
+ * @param operatorAddress The hex encoded user Ethereum address of the operator you'd like to set approval for.
+ * @param isApproved The boolean variable to set the approval to.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setApprovalForAllAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ operatorAddress: string,
+ isApproved: boolean,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('ownerAddress', ownerAddress);
+ assert.isETHAddressHex('operatorAddress', operatorAddress);
+ assert.isBoolean('isApproved', isApproved);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedOwnerAddress = ownerAddress.toLowerCase();
+ const normalizedOperatorAddress = operatorAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const txHash = await tokenContract.setApprovalForAll.sendTransactionAsync(
+ normalizedOperatorAddress,
+ isApproved,
+ {
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ from: normalizedOwnerAddress,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Enable or disable approval for a third party ("operator") to manage all of `ownerAddress`'s assets.
+ * Throws if `_tokenId` is not a valid NFT
+ * Emits the ApprovalForAll event.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param ownerAddress The hex encoded user Ethereum address of the token owner.
+ * @param operatorAddress The hex encoded user Ethereum address of the operator you'd like to set approval for.
+ * @param isApproved The boolean variable to set the approval to.
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setProxyApprovalForAllAsync(
+ tokenAddress: string,
+ ownerAddress: string,
+ isApproved: boolean,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ const proxyAddress = this._erc721ProxyWrapper.getContractAddress();
+ const txHash = await this.setApprovalForAllAsync(tokenAddress, ownerAddress, proxyAddress, isApproved, txOpts);
+ return txHash;
+ }
+ /**
+ * Set or reaffirm the approved address for an NFT
+ * The zero address indicates there is no approved address. Throws unless `msg.sender` is the current NFT owner,
+ * or an authorized operator of the current owner.
+ * Throws if `_tokenId` is not a valid NFT
+ * Emits the Approval event.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param approvedAddress The hex encoded user Ethereum address you'd like to set approval for.
+ * @param tokenId The identifier for an NFT
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setApprovalAsync(
+ tokenAddress: string,
+ approvedAddress: string,
+ tokenId: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('approvedAddress', approvedAddress);
+ assert.isBigNumber('tokenId', tokenId);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedApprovedAddress = approvedAddress.toLowerCase();
+
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const tokenOwnerAddress = await tokenContract.ownerOf.callAsync(tokenId);
+ const txHash = await tokenContract.approve.sendTransactionAsync(normalizedApprovedAddress, tokenId, {
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ from: tokenOwnerAddress,
+ });
+ return txHash;
+ }
+ /**
+ * Set or reaffirm 0x proxy as an approved address for an NFT
+ * Throws unless `msg.sender` is the current NFT owner, or an authorized operator of the current owner.
+ * Throws if `_tokenId` is not a valid NFT
+ * Emits the Approval event.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param tokenId The identifier for an NFT
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async setProxyApprovalAsync(
+ tokenAddress: string,
+ tokenId: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ const proxyAddress = this._erc721ProxyWrapper.getContractAddress();
+ const txHash = await this.setApprovalAsync(tokenAddress, proxyAddress, tokenId, txOpts);
+ return txHash;
+ }
+ /**
+ * Enable or disable approval for a third party ("operator") to manage all of `ownerAddress`'s assets.
+ * Throws if `_tokenId` is not a valid NFT
+ * Emits the ApprovalForAll event.
+ * @param tokenAddress The hex encoded contract Ethereum address where the ERC721 token is deployed.
+ * @param receiverAddress The hex encoded Ethereum address of the user to send the NFT to.
+ * @param senderAddress The hex encoded Ethereum address of the user to send the NFT to.
+ * @param tokenId The identifier for an NFT
+ * @param txOpts Transaction parameters.
+ * @return Transaction hash.
+ */
+ public async transferFromAsync(
+ tokenAddress: string,
+ receiverAddress: string,
+ senderAddress: string,
+ tokenId: BigNumber,
+ txOpts: TransactionOpts = {},
+ ): Promise<string> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ assert.isETHAddressHex('receiverAddress', receiverAddress);
+ assert.isETHAddressHex('senderAddress', senderAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ const normalizedReceiverAddress = receiverAddress.toLowerCase();
+ const normalizedSenderAddress = senderAddress.toLowerCase();
+ const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
+ const ownerAddress = await this.getOwnerOfAsync(tokenAddress, tokenId);
+ const isApprovedForAll = await this.isApprovedForAllAsync(
+ normalizedTokenAddress,
+ ownerAddress,
+ normalizedSenderAddress,
+ );
+ if (!isApprovedForAll) {
+ const approved = await this.getApprovedAsync(normalizedTokenAddress, tokenId);
+ if (approved !== senderAddress) {
+ throw new Error(ContractWrappersError.ERC721NoApproval);
+ }
+ }
+ const txHash = await tokenContract.transferFrom.sendTransactionAsync(
+ ownerAddress,
+ normalizedReceiverAddress,
+ tokenId,
+ {
+ gas: txOpts.gasLimit,
+ gasPrice: txOpts.gasPrice,
+ from: normalizedSenderAddress,
+ },
+ );
+ return txHash;
+ }
+ /**
+ * Subscribe to an event type emitted by the Token contract.
+ * @param tokenAddress The hex encoded address where the ERC721 token is deployed.
+ * @param eventName The token contract event you would like to subscribe to.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{maker: aUserAddressHex}`
+ * @param callback Callback that gets called when a log is added/removed
+ * @return Subscription token used later to unsubscribe
+ */
+ public subscribe<ArgsType extends ERC721TokenEventArgs>(
+ tokenAddress: string,
+ eventName: ERC721TokenEvents,
+ indexFilterValues: IndexedFilterValues,
+ callback: EventCallback<ArgsType>,
+ ): string {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, ERC721TokenEvents);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ assert.isFunction('callback', callback);
+ const subscriptionToken = this._subscribe<ArgsType>(
+ normalizedTokenAddress,
+ eventName,
+ indexFilterValues,
+ artifacts.ERC721Token.compilerOutput.abi,
+ callback,
+ );
+ return subscriptionToken;
+ }
+ /**
+ * Cancel a subscription
+ * @param subscriptionToken Subscription token returned by `subscribe()`
+ */
+ public unsubscribe(subscriptionToken: string): void {
+ this._unsubscribe(subscriptionToken);
+ }
+ /**
+ * Cancels all existing subscriptions
+ */
+ public unsubscribeAll(): void {
+ super._unsubscribeAll();
+ }
+ /**
+ * Gets historical logs without creating a subscription
+ * @param tokenAddress An address of the token that emitted the logs.
+ * @param eventName The token contract event you would like to subscribe to.
+ * @param blockRange Block range to get logs from.
+ * @param indexFilterValues An object where the keys are indexed args returned by the event and
+ * the value is the value you are interested in. E.g `{_from: aUserAddressHex}`
+ * @return Array of logs that match the parameters
+ */
+ public async getLogsAsync<ArgsType extends ERC721TokenEventArgs>(
+ tokenAddress: string,
+ eventName: ERC721TokenEvents,
+ blockRange: BlockRange,
+ indexFilterValues: IndexedFilterValues,
+ ): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
+ assert.isETHAddressHex('tokenAddress', tokenAddress);
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ assert.doesBelongToStringEnum('eventName', eventName, ERC721TokenEvents);
+ assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
+ assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
+ const logs = await this._getLogsAsync<ArgsType>(
+ normalizedTokenAddress,
+ eventName,
+ blockRange,
+ indexFilterValues,
+ artifacts.ERC721Token.compilerOutput.abi,
+ );
+ return logs;
+ }
+ // tslint:disable-next-line:no-unused-variable
+ private _invalidateContractInstances(): void {
+ this.unsubscribeAll();
+ this._tokenContractsByAddress = {};
+ }
+ private async _getTokenContractAsync(tokenAddress: string): Promise<ERC721TokenContract> {
+ const normalizedTokenAddress = tokenAddress.toLowerCase();
+ let tokenContract = this._tokenContractsByAddress[normalizedTokenAddress];
+ if (!_.isUndefined(tokenContract)) {
+ return tokenContract;
+ }
+ const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
+ artifacts.ERC721Token,
+ normalizedTokenAddress,
+ );
+ const contractInstance = new ERC721TokenContract(
+ abi,
+ address,
+ this._web3Wrapper.getProvider(),
+ this._web3Wrapper.getContractDefaults(),
+ );
+ tokenContract = contractInstance;
+ this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract;
+ return tokenContract;
+ }
+}
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<ERC721TokenTransferEventArgs>) => {
+ 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<ERC721TokenApprovalEventArgs>) => {
+ 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<ERC721TokenApprovalEventArgs>) => {
+ 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<ERC721TokenApprovalForAllEventArgs>) => {
+ 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<ERC721TokenApprovalForAllEventArgs>(
+ 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<ERC721TokenApprovalForAllEventArgs>(
+ 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;
+}