diff options
-rw-r--r-- | src/0x.ts | 7 | ||||
-rw-r--r-- | src/contract_wrappers/ether_token_wrapper.ts | 75 | ||||
-rw-r--r-- | src/contract_wrappers/token_wrapper.ts | 1 | ||||
-rw-r--r-- | src/types.ts | 8 | ||||
-rw-r--r-- | src/web3_wrapper.ts | 12 | ||||
-rw-r--r-- | test/ether_token_wrapper_test.ts | 106 |
6 files changed, 205 insertions, 4 deletions
@@ -12,6 +12,7 @@ import {utils} from './utils/utils'; import {assert} from './utils/assert'; import {ExchangeWrapper} from './contract_wrappers/exchange_wrapper'; import {TokenRegistryWrapper} from './contract_wrappers/token_registry_wrapper'; +import {EtherTokenWrapper} from './contract_wrappers/ether_token_wrapper'; import {ecSignatureSchema} from './schemas/ec_signature_schema'; import {TokenWrapper} from './contract_wrappers/token_wrapper'; import {ECSignature, ZeroExError, Order, SignedOrder, Web3Provider} from './types'; @@ -46,6 +47,11 @@ export class ZeroEx { * An instance of the TokenWrapper class containing methods for interacting with any ERC20 token smart contract. */ public token: TokenWrapper; + /** + * An instance of the EtherTokenWrapper class containing methods for interacting with the + * wrapped ETH ERC20 token smart contract. + */ + public etherToken: EtherTokenWrapper; private _web3Wrapper: Web3Wrapper; /** * Verifies that the elliptic curve signature `signature` was generated @@ -145,6 +151,7 @@ export class ZeroEx { this.token = new TokenWrapper(this._web3Wrapper); this.exchange = new ExchangeWrapper(this._web3Wrapper, this.token); this.tokenRegistry = new TokenRegistryWrapper(this._web3Wrapper); + this.etherToken = new EtherTokenWrapper(this._web3Wrapper, this.token); } /** * Sets a new provider for the web3 instance used by 0x.js. Updating the provider will stop all diff --git a/src/contract_wrappers/ether_token_wrapper.ts b/src/contract_wrappers/ether_token_wrapper.ts new file mode 100644 index 000000000..76e7289b7 --- /dev/null +++ b/src/contract_wrappers/ether_token_wrapper.ts @@ -0,0 +1,75 @@ +import * as _ from 'lodash'; +import {Web3Wrapper} from '../web3_wrapper'; +import {ContractWrapper} from './contract_wrapper'; +import {TokenWrapper} from './token_wrapper'; +import {EtherTokenContract, ZeroExError} from '../types'; +import {assert} from '../utils/assert'; +import * as EtherTokenArtifacts from '../artifacts/EtherToken.json'; + +/** + * This class includes all the functionality related to interacting with a wrapped Ether ERC20 token contract. + * The caller can convert ETH into the equivalent number of wrapped ETH ERC20 tokens and back. + */ +export class EtherTokenWrapper extends ContractWrapper { + private _etherTokenContractIfExists?: EtherTokenContract; + private _tokenWrapper: TokenWrapper; + constructor(web3Wrapper: Web3Wrapper, tokenWrapper: TokenWrapper) { + super(web3Wrapper); + this._tokenWrapper = tokenWrapper; + } + /** + * Deposit ETH into the Wrapped ETH smart contract and issues the equivalent number of wrapped ETH tokens + * to the depositor address. These wrapped ETH tokens can be used in 0x trades and are redeemable for 1-to-1 + * for ETH. + * @param amountInWei Amount of ETH in Wei the caller wishes to deposit. + * @param depositor The hex encoded user Ethereum address that would like to make the deposit. + */ + public async depositAsync(amountInWei: BigNumber.BigNumber, depositor: string): Promise<void> { + assert.isBigNumber('amountInWei', amountInWei); + await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); + + const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(depositor); + assert.assert(ethBalanceInWei.gte(amountInWei), ZeroExError.INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT); + + const wethContract = await this._getEtherTokenContractAsync(); + await wethContract.deposit({ + from: depositor, + value: amountInWei, + }); + } + /** + * Withdraw ETH to the withdrawer's address from the wrapped ETH smart contract in exchange for the + * equivalent number of wrapped ETH tokens. + * @param amountInWei Amount of ETH in Wei the caller wishes to withdraw. + * @param withdrawer The hex encoded user Ethereum address that would like to make the withdrawl. + */ + public async withdrawAsync(amountInWei: BigNumber.BigNumber, withdrawer: string): Promise<void> { + assert.isBigNumber('amountInWei', amountInWei); + await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); + + const wethContractAddress = await this.getContractAddressAsync(); + const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync(wethContractAddress, withdrawer); + assert.assert(WETHBalanceInBaseUnits.gte(amountInWei), ZeroExError.INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL); + + const wethContract = await this._getEtherTokenContractAsync(); + await wethContract.withdraw(amountInWei, { + from: withdrawer, + }); + } + /** + * Retrieves the Wrapped Ether token contract address + * @return The Wrapped Ether token contract address + */ + public async getContractAddressAsync(): Promise<string> { + const wethContract = await this._getEtherTokenContractAsync(); + return wethContract.address; + } + private async _getEtherTokenContractAsync(): Promise<EtherTokenContract> { + if (!_.isUndefined(this._etherTokenContractIfExists)) { + return this._etherTokenContractIfExists; + } + const contractInstance = await this._instantiateContractIfExistsAsync((EtherTokenArtifacts as any)); + this._etherTokenContractIfExists = contractInstance as EtherTokenContract; + return this._etherTokenContractIfExists; + } +} diff --git a/src/contract_wrappers/token_wrapper.ts b/src/contract_wrappers/token_wrapper.ts index 29f9b2d1c..e34c624ab 100644 --- a/src/contract_wrappers/token_wrapper.ts +++ b/src/contract_wrappers/token_wrapper.ts @@ -28,6 +28,7 @@ export class TokenWrapper extends ContractWrapper { * 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. + * @return The owner's ERC20 token balance in base units. */ public async getBalanceAsync(tokenAddress: string, ownerAddress: string): Promise<BigNumber.BigNumber> { assert.isETHAddressHex('ownerAddress', ownerAddress); diff --git a/src/types.ts b/src/types.ts index 2b7fba226..200e65d56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,8 @@ export const ZeroExError = strEnum([ 'ZRX_NOT_IN_TOKEN_REGISTRY', 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', 'INSUFFICIENT_BALANCE_FOR_TRANSFER', + 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT', + 'INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL', 'INVALID_JUMP', 'OUT_OF_GAS', ]); @@ -140,6 +142,11 @@ export interface TokenRegistryContract extends ContractInstance { }; } +export interface EtherTokenContract extends ContractInstance { + deposit: (txOpts: TxOpts) => Promise<void>; + withdraw: (amount: BigNumber.BigNumber, txOpts: TxOpts) => Promise<void>; +} + export const SolidityTypes = strEnum([ 'address', 'uint256', @@ -255,6 +262,7 @@ export interface Token { export interface TxOpts { from: string; gas?: number; + value?: BigNumber.BigNumber; } export interface TokenAddressBySymbol { diff --git a/src/web3_wrapper.ts b/src/web3_wrapper.ts index 6bdca499f..630f0bef3 100644 --- a/src/web3_wrapper.ts +++ b/src/web3_wrapper.ts @@ -34,10 +34,14 @@ export class Web3Wrapper { return undefined; } } - public async getBalanceInEthAsync(owner: string): Promise<BigNumber.BigNumber> { - const balanceInWei = await promisify(this.web3.eth.getBalance)(owner); - const balanceEth = this.web3.fromWei(balanceInWei, 'ether'); - return balanceEth; + public toWei(ethAmount: BigNumber.BigNumber): BigNumber.BigNumber { + const balanceWei = this.web3.toWei(ethAmount, 'ether'); + return balanceWei; + } + public async getBalanceInWeiAsync(owner: string): Promise<BigNumber.BigNumber> { + let balanceInWei = await promisify(this.web3.eth.getBalance)(owner); + balanceInWei = new BigNumber(balanceInWei); + return balanceInWei; } public async doesContractExistAtAddressAsync(address: string): Promise<boolean> { const code = await promisify(this.web3.eth.getCode)(address); diff --git a/test/ether_token_wrapper_test.ts b/test/ether_token_wrapper_test.ts new file mode 100644 index 000000000..ebce81e97 --- /dev/null +++ b/test/ether_token_wrapper_test.ts @@ -0,0 +1,106 @@ +import 'mocha'; +import * as chai from 'chai'; +import {chaiSetup} from './utils/chai_setup'; +import * as Web3 from 'web3'; +import * as BigNumber from 'bignumber.js'; +import promisify = require('es6-promisify'); +import {web3Factory} from './utils/web3_factory'; +import {ZeroEx, ZeroExError, Token} from '../src'; +import {BlockchainLifecycle} from './utils/blockchain_lifecycle'; + +chaiSetup.configure(); +const expect = chai.expect; +const blockchainLifecycle = new BlockchainLifecycle(); + +// Since the address depositing/withdrawing ETH/WETH also needs to pay gas costs for the transaction, +// a small amount of ETH will be used to pay this gas cost. We therefore check that the difference between +// the expected balance and actual balance (given the amount of ETH deposited), only deviates by the amount +// required to pay gas costs. +const MAX_REASONABLE_GAS_COST_IN_WEI = 62237; + +describe('EtherTokenWrapper', () => { + let web3: Web3; + let zeroEx: ZeroEx; + let userAddresses: string[]; + let addressWithETH: string; + let wethContractAddress: string; + let depositWeiAmount: BigNumber.BigNumber; + let decimalPlaces: number; + before(async () => { + web3 = web3Factory.create(); + zeroEx = new ZeroEx(web3.currentProvider); + userAddresses = await promisify(web3.eth.getAccounts)(); + addressWithETH = userAddresses[0]; + wethContractAddress = await zeroEx.etherToken.getContractAddressAsync(); + depositWeiAmount = (zeroEx as any)._web3Wrapper.toWei(new BigNumber(5)); + decimalPlaces = 7; + }); + beforeEach(async () => { + await blockchainLifecycle.startAsync(); + }); + afterEach(async () => { + await blockchainLifecycle.revertAsync(); + }); + describe('#depositAsync', () => { + it('should successfully deposit ETH and issue Wrapped ETH tokens', async () => { + const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH); + expect(preETHBalance).to.be.bignumber.gt(0); + expect(preWETHBalance).to.be.bignumber.equal(0); + + await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH); + + const postETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH); + + expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(depositWeiAmount); + const remainingETHInWei = preETHBalance.minus(depositWeiAmount); + const gasCost = remainingETHInWei.minus(postETHBalanceInWei); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + }); + it('should throw if user has insufficient ETH balance for deposit', async () => { + const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + + const extraETHBalance = (zeroEx as any)._web3Wrapper.toWei(5, 'ether'); + const overETHBalanceinWei = preETHBalance.add(extraETHBalance); + + return expect( + zeroEx.etherToken.depositAsync(overETHBalanceinWei, addressWithETH), + ).to.be.rejectedWith(ZeroExError.INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT); + }); + }); + describe('#withdrawAsync', () => { + it('should successfully withdraw ETH in return for Wrapped ETH tokens', async () => { + const ETHBalanceInWei = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + + await zeroEx.etherToken.depositAsync(depositWeiAmount, addressWithETH); + + const expectedPreETHBalance = ETHBalanceInWei.minus(depositWeiAmount); + const preETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH); + let gasCost = expectedPreETHBalance.minus(preETHBalance); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + expect(preWETHBalance).to.be.bignumber.equal(depositWeiAmount); + + await zeroEx.etherToken.withdrawAsync(depositWeiAmount, addressWithETH); + + const postETHBalance = await (zeroEx as any)._web3Wrapper.getBalanceInWeiAsync(addressWithETH); + const postWETHBalanceInBaseUnits = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH); + + expect(postWETHBalanceInBaseUnits).to.be.bignumber.equal(0); + const expectedETHBalance = preETHBalance.add(depositWeiAmount).round(decimalPlaces); + gasCost = expectedETHBalance.minus(postETHBalance); + expect(gasCost).to.be.bignumber.lte(MAX_REASONABLE_GAS_COST_IN_WEI); + }); + it('should throw if user has insufficient WETH balance for withdrawl', async () => { + const preWETHBalance = await zeroEx.token.getBalanceAsync(wethContractAddress, addressWithETH); + expect(preWETHBalance).to.be.bignumber.equal(0); + + const overWETHBalance = preWETHBalance.add(999999999); + + return expect( + zeroEx.etherToken.withdrawAsync(overWETHBalance, addressWithETH), + ).to.be.rejectedWith(ZeroExError.INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL); + }); + }); +}); |