aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/0x.ts7
-rw-r--r--src/contract_wrappers/ether_token_wrapper.ts75
-rw-r--r--src/contract_wrappers/token_wrapper.ts1
-rw-r--r--src/types.ts8
-rw-r--r--src/web3_wrapper.ts12
-rw-r--r--test/ether_token_wrapper_test.ts106
6 files changed, 205 insertions, 4 deletions
diff --git a/src/0x.ts b/src/0x.ts
index 3a06c7b5a..d7a01ba70 100644
--- a/src/0x.ts
+++ b/src/0x.ts
@@ -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);
+ });
+ });
+});