435 lines
21 KiB
TypeScript
435 lines
21 KiB
TypeScript
import { schemas } from '@0xproject/json-schemas';
|
|
import { LogWithDecodedArgs } from '@0xproject/types';
|
|
import { AbiDecoder, BigNumber } from '@0xproject/utils';
|
|
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts } from '../artifacts';
|
|
import { BlockRange, EventCallback, IndexedFilterValues, MethodOpts, TransactionOpts, ZeroExError } from '../types';
|
|
import { assert } from '../utils/assert';
|
|
import { constants } from '../utils/constants';
|
|
|
|
import { ContractWrapper } from './contract_wrapper';
|
|
import { TokenContract, TokenContractEventArgs, TokenEvents } from './generated/token';
|
|
import { TokenTransferProxyWrapper } from './token_transfer_proxy_wrapper';
|
|
|
|
/**
|
|
* This class includes all the functionality related to interacting with ERC20 token contracts.
|
|
* All ERC20 method calls are supported, along with some convenience methods for getting/setting allowances
|
|
* to the 0x Proxy smart contract.
|
|
*/
|
|
export class TokenWrapper extends ContractWrapper {
|
|
public UNLIMITED_ALLOWANCE_IN_BASE_UNITS = constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS;
|
|
private _tokenContractsByAddress: { [address: string]: TokenContract };
|
|
private _tokenTransferProxyWrapper: TokenTransferProxyWrapper;
|
|
constructor(
|
|
web3Wrapper: Web3Wrapper,
|
|
networkId: number,
|
|
abiDecoder: AbiDecoder,
|
|
tokenTransferProxyWrapper: TokenTransferProxyWrapper,
|
|
) {
|
|
super(web3Wrapper, networkId, abiDecoder);
|
|
this._tokenContractsByAddress = {};
|
|
this._tokenTransferProxyWrapper = tokenTransferProxyWrapper;
|
|
}
|
|
/**
|
|
* Retrieves an owner's ERC20 token balance.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 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 owner's ERC20 token balance in base units.
|
|
*/
|
|
public async getBalanceAsync(
|
|
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;
|
|
}
|
|
/**
|
|
* Sets the spender's allowance to a specified number of baseUnits on behalf of the owner address.
|
|
* Equivalent to the ERC20 spec method `approve`.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
|
|
* for spenderAddress.
|
|
* @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
|
|
* @param amountInBaseUnits The allowance amount you would like to set.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async setAllowanceAsync(
|
|
tokenAddress: string,
|
|
ownerAddress: string,
|
|
spenderAddress: string,
|
|
amountInBaseUnits: BigNumber,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('spenderAddress', spenderAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedSpenderAddress = spenderAddress.toLowerCase();
|
|
const normalizedOwnerAddress = ownerAddress.toLowerCase();
|
|
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
|
|
|
const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
|
|
const txHash = await tokenContract.approve.sendTransactionAsync(normalizedSpenderAddress, amountInBaseUnits, {
|
|
from: normalizedOwnerAddress,
|
|
gas: txOpts.gasLimit,
|
|
gasPrice: txOpts.gasPrice,
|
|
});
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Sets the spender's allowance to an unlimited number of baseUnits on behalf of the owner address.
|
|
* Equivalent to the ERC20 spec method `approve`.
|
|
* Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
|
|
* allowances set to the max amount (e.g ZRX, WETH)
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address who would like to set an allowance
|
|
* for spenderAddress.
|
|
* @param spenderAddress The hex encoded user Ethereum address who will be able to spend the set allowance.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async setUnlimitedAllowanceAsync(
|
|
tokenAddress: string,
|
|
ownerAddress: string,
|
|
spenderAddress: string,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
assert.isETHAddressHex('spenderAddress', spenderAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedOwnerAddress = ownerAddress.toLowerCase();
|
|
const normalizedSpenderAddress = spenderAddress.toLowerCase();
|
|
const txHash = await this.setAllowanceAsync(
|
|
normalizedTokenAddress,
|
|
normalizedOwnerAddress,
|
|
normalizedSpenderAddress,
|
|
this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
|
|
txOpts,
|
|
);
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Retrieves the owners allowance in baseUnits set to the spender's address.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address whose allowance to spenderAddress
|
|
* you would like to retrieve.
|
|
* @param spenderAddress The hex encoded user Ethereum address who can spend the allowance you are fetching.
|
|
* @param methodOpts Optional arguments this method accepts.
|
|
*/
|
|
public async getAllowanceAsync(
|
|
tokenAddress: string,
|
|
ownerAddress: string,
|
|
spenderAddress: string,
|
|
methodOpts?: MethodOpts,
|
|
): Promise<BigNumber> {
|
|
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
assert.isETHAddressHex('spenderAddress', spenderAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedOwnerAddress = ownerAddress.toLowerCase();
|
|
const normalizedSpenderAddress = spenderAddress.toLowerCase();
|
|
|
|
const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
|
|
const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock;
|
|
const txData = {};
|
|
let allowanceInBaseUnits = await tokenContract.allowance.callAsync(
|
|
normalizedOwnerAddress,
|
|
normalizedSpenderAddress,
|
|
txData,
|
|
defaultBlock,
|
|
);
|
|
// Wrap BigNumbers returned from web3 with our own (later) version of BigNumber
|
|
allowanceInBaseUnits = new BigNumber(allowanceInBaseUnits);
|
|
return allowanceInBaseUnits;
|
|
}
|
|
/**
|
|
* Retrieves the owner's allowance in baseUnits set to the 0x proxy contract.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address whose proxy contract allowance we are retrieving.
|
|
* @param methodOpts Optional arguments this method accepts.
|
|
*/
|
|
public async getProxyAllowanceAsync(
|
|
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 proxyAddress = this._tokenTransferProxyWrapper.getContractAddress();
|
|
const allowanceInBaseUnits = await this.getAllowanceAsync(
|
|
normalizedTokenAddress,
|
|
normalizedOwnerAddress,
|
|
proxyAddress,
|
|
methodOpts,
|
|
);
|
|
return allowanceInBaseUnits;
|
|
}
|
|
/**
|
|
* Sets the 0x proxy contract's allowance to a specified number of a tokens' baseUnits on behalf
|
|
* of an owner address.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
|
|
* for the Proxy contract.
|
|
* @param amountInBaseUnits The allowance amount specified in baseUnits.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async setProxyAllowanceAsync(
|
|
tokenAddress: string,
|
|
ownerAddress: string,
|
|
amountInBaseUnits: BigNumber,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedOwnerAddress = ownerAddress.toLowerCase();
|
|
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
|
|
|
const proxyAddress = this._tokenTransferProxyWrapper.getContractAddress();
|
|
const txHash = await this.setAllowanceAsync(
|
|
normalizedTokenAddress,
|
|
normalizedOwnerAddress,
|
|
proxyAddress,
|
|
amountInBaseUnits,
|
|
txOpts,
|
|
);
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Sets the 0x proxy contract's allowance to a unlimited number of a tokens' baseUnits on behalf
|
|
* of an owner address.
|
|
* Setting an unlimited allowance will lower the gas cost for filling orders involving tokens that forego updating
|
|
* allowances set to the max amount (e.g ZRX, WETH)
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param ownerAddress The hex encoded user Ethereum address who is setting an allowance
|
|
* for the Proxy contract.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async setUnlimitedProxyAllowanceAsync(
|
|
tokenAddress: string,
|
|
ownerAddress: string,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('ownerAddress', ownerAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedOwnerAddress = ownerAddress.toLowerCase();
|
|
const txHash = await this.setProxyAllowanceAsync(
|
|
normalizedTokenAddress,
|
|
normalizedOwnerAddress,
|
|
this.UNLIMITED_ALLOWANCE_IN_BASE_UNITS,
|
|
txOpts,
|
|
);
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param fromAddress The hex encoded user Ethereum address that will send the funds.
|
|
* @param toAddress The hex encoded user Ethereum address that will receive the funds.
|
|
* @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async transferAsync(
|
|
tokenAddress: string,
|
|
fromAddress: string,
|
|
toAddress: string,
|
|
amountInBaseUnits: BigNumber,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
assert.isETHAddressHex('toAddress', toAddress);
|
|
await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedFromAddress = fromAddress.toLowerCase();
|
|
const normalizedToAddress = toAddress.toLowerCase();
|
|
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
|
|
|
const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
|
|
|
|
const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress);
|
|
if (fromAddressBalance.lessThan(amountInBaseUnits)) {
|
|
throw new Error(ZeroExError.InsufficientBalanceForTransfer);
|
|
}
|
|
|
|
const txHash = await tokenContract.transfer.sendTransactionAsync(normalizedToAddress, amountInBaseUnits, {
|
|
from: normalizedFromAddress,
|
|
gas: txOpts.gasLimit,
|
|
gasPrice: txOpts.gasPrice,
|
|
});
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Transfers `amountInBaseUnits` ERC20 tokens from `fromAddress` to `toAddress`.
|
|
* Requires the fromAddress to have sufficient funds and to have approved an allowance of
|
|
* `amountInBaseUnits` to `senderAddress`.
|
|
* @param tokenAddress The hex encoded contract Ethereum address where the ERC20 token is deployed.
|
|
* @param fromAddress The hex encoded user Ethereum address whose funds are being sent.
|
|
* @param toAddress The hex encoded user Ethereum address that will receive the funds.
|
|
* @param senderAddress The hex encoded user Ethereum address whose initiates the fund transfer. The
|
|
* `fromAddress` must have set an allowance to the `senderAddress`
|
|
* before this call.
|
|
* @param amountInBaseUnits The amount (specified in baseUnits) of the token to transfer.
|
|
* @param txOpts Transaction parameters.
|
|
* @return Transaction hash.
|
|
*/
|
|
public async transferFromAsync(
|
|
tokenAddress: string,
|
|
fromAddress: string,
|
|
toAddress: string,
|
|
senderAddress: string,
|
|
amountInBaseUnits: BigNumber,
|
|
txOpts: TransactionOpts = {},
|
|
): Promise<string> {
|
|
assert.isETHAddressHex('toAddress', toAddress);
|
|
assert.isETHAddressHex('fromAddress', fromAddress);
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper);
|
|
const normalizedToAddress = toAddress.toLowerCase();
|
|
const normalizedFromAddress = fromAddress.toLowerCase();
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
const normalizedSenderAddress = senderAddress.toLowerCase();
|
|
assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits);
|
|
|
|
const tokenContract = await this._getTokenContractAsync(normalizedTokenAddress);
|
|
|
|
const fromAddressAllowance = await this.getAllowanceAsync(
|
|
normalizedTokenAddress,
|
|
normalizedFromAddress,
|
|
normalizedSenderAddress,
|
|
);
|
|
if (fromAddressAllowance.lessThan(amountInBaseUnits)) {
|
|
throw new Error(ZeroExError.InsufficientAllowanceForTransfer);
|
|
}
|
|
|
|
const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress);
|
|
if (fromAddressBalance.lessThan(amountInBaseUnits)) {
|
|
throw new Error(ZeroExError.InsufficientBalanceForTransfer);
|
|
}
|
|
|
|
const txHash = await tokenContract.transferFrom.sendTransactionAsync(
|
|
normalizedFromAddress,
|
|
normalizedToAddress,
|
|
amountInBaseUnits,
|
|
{
|
|
from: normalizedSenderAddress,
|
|
gas: txOpts.gasLimit,
|
|
gasPrice: txOpts.gasPrice,
|
|
},
|
|
);
|
|
return txHash;
|
|
}
|
|
/**
|
|
* Subscribe to an event type emitted by the Token contract.
|
|
* @param tokenAddress The hex encoded address where the ERC20 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 TokenContractEventArgs>(
|
|
tokenAddress: string,
|
|
eventName: TokenEvents,
|
|
indexFilterValues: IndexedFilterValues,
|
|
callback: EventCallback<ArgsType>,
|
|
): string {
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
|
|
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
|
assert.isFunction('callback', callback);
|
|
const subscriptionToken = this._subscribe<ArgsType>(
|
|
normalizedTokenAddress,
|
|
eventName,
|
|
indexFilterValues,
|
|
artifacts.TokenArtifact.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 TokenContractEventArgs>(
|
|
tokenAddress: string,
|
|
eventName: TokenEvents,
|
|
blockRange: BlockRange,
|
|
indexFilterValues: IndexedFilterValues,
|
|
): Promise<Array<LogWithDecodedArgs<ArgsType>>> {
|
|
assert.isETHAddressHex('tokenAddress', tokenAddress);
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
assert.doesBelongToStringEnum('eventName', eventName, TokenEvents);
|
|
assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema);
|
|
assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema);
|
|
const logs = await this._getLogsAsync<ArgsType>(
|
|
normalizedTokenAddress,
|
|
eventName,
|
|
blockRange,
|
|
indexFilterValues,
|
|
artifacts.TokenArtifact.abi,
|
|
);
|
|
return logs;
|
|
}
|
|
private _invalidateContractInstances(): void {
|
|
this.unsubscribeAll();
|
|
this._tokenContractsByAddress = {};
|
|
}
|
|
private async _getTokenContractAsync(tokenAddress: string): Promise<TokenContract> {
|
|
const normalizedTokenAddress = tokenAddress.toLowerCase();
|
|
let tokenContract = this._tokenContractsByAddress[normalizedTokenAddress];
|
|
if (!_.isUndefined(tokenContract)) {
|
|
return tokenContract;
|
|
}
|
|
const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync(
|
|
artifacts.TokenArtifact,
|
|
normalizedTokenAddress,
|
|
);
|
|
const contractInstance = new TokenContract(this._web3Wrapper, abi, address);
|
|
tokenContract = contractInstance;
|
|
this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract;
|
|
return tokenContract;
|
|
}
|
|
}
|