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 --- .../src/contract_wrappers/erc721_proxy_wrapper.ts | 73 ++++ .../src/contract_wrappers/erc721_token_wrapper.ts | 439 +++++++++++++++++++++ 2 files changed, 512 insertions(+) create mode 100644 packages/contract-wrappers/src/contract_wrappers/erc721_proxy_wrapper.ts create mode 100644 packages/contract-wrappers/src/contract_wrappers/erc721_token_wrapper.ts (limited to 'packages/contract-wrappers/src/contract_wrappers') 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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( + tokenAddress: string, + eventName: ERC721TokenEvents, + indexFilterValues: IndexedFilterValues, + callback: EventCallback, + ): 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( + 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( + tokenAddress: string, + eventName: ERC721TokenEvents, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + ): Promise>> { + 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( + 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 { + 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; + } +} -- cgit v1.2.3