diff options
author | Fabio Berger <me@fabioberger.com> | 2018-05-12 01:11:27 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-12 01:11:27 +0800 |
commit | 6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b (patch) | |
tree | 8d374bf8f4f70a40d75c8bf4b35fe9a169250909 /packages/contract-wrappers/src | |
parent | f42f608f3f97a5244f09f17ae5ee184c0f775de3 (diff) | |
parent | ad8e12eeaebd40fdafac7cbef96121d4454909df (diff) | |
download | dexon-sol-tools-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.gz dexon-sol-tools-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.tar.zst dexon-sol-tools-6aed4fb1ae27dabed027c855f2cbdc0bfb4f3b6b.zip |
Merge pull request #579 from 0xProject/breakUp0xjs
Split 0x.js into: contract-wrappers & order-watcher
Diffstat (limited to 'packages/contract-wrappers/src')
33 files changed, 5094 insertions, 0 deletions
diff --git a/packages/contract-wrappers/src/artifacts.ts b/packages/contract-wrappers/src/artifacts.ts new file mode 100644 index 000000000..13587984c --- /dev/null +++ b/packages/contract-wrappers/src/artifacts.ts @@ -0,0 +1,18 @@ +import { Artifact } from '@0xproject/types'; + +import * as DummyToken from './compact_artifacts/DummyToken.json'; +import * as EtherToken from './compact_artifacts/EtherToken.json'; +import * as Exchange from './compact_artifacts/Exchange.json'; +import * as Token from './compact_artifacts/Token.json'; +import * as TokenRegistry from './compact_artifacts/TokenRegistry.json'; +import * as TokenTransferProxy from './compact_artifacts/TokenTransferProxy.json'; +import * as ZRX from './compact_artifacts/ZRX.json'; +export const artifacts = { + ZRX: (ZRX as any) as Artifact, + DummyToken: (DummyToken as any) as Artifact, + Token: (Token as any) as Artifact, + Exchange: (Exchange as any) as Artifact, + EtherToken: (EtherToken as any) as Artifact, + TokenRegistry: (TokenRegistry as any) as Artifact, + TokenTransferProxy: (TokenTransferProxy as any) as Artifact, +}; diff --git a/packages/contract-wrappers/src/compact_artifacts/DummyToken.json b/packages/contract-wrappers/src/compact_artifacts/DummyToken.json new file mode 100644 index 000000000..f64a8cd3d --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/DummyToken.json @@ -0,0 +1,22 @@ +{ + "contract_name": "DummyToken", + "abi": [ + { + "constant": false, + "inputs": [ + { + "name": "_target", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "setBalance", + "outputs": [], + "payable": false, + "type": "function" + } + ] +} diff --git a/packages/contract-wrappers/src/compact_artifacts/EtherToken.json b/packages/contract-wrappers/src/compact_artifacts/EtherToken.json new file mode 100644 index 000000000..26cca57cd --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/EtherToken.json @@ -0,0 +1,287 @@ +{ + "contract_name": "EtherToken", + "abi": [ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "amount", + "type": "uint256" + } + ], + "name": "withdraw", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [], + "name": "deposit", + "outputs": [], + "payable": true, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "payable": true, + "type": "fallback" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + } + ], + "networks": { + "1": { + "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + }, + "3": { + "address": "0xc00fd9820cd2898cc4c054b7bf142de637ad129a" + }, + "4": { + "address": "0xc778417e063141139fce010982780140aa0cd5ab" + }, + "42": { + "address": "0x653e49e301e508a13237c0ddc98ae7d4cd2667a1" + }, + "50": { + "address": "0x871dd7c2b4b25e1aa18728e9d5f2af4c4e431f5c" + } + } +} diff --git a/packages/contract-wrappers/src/compact_artifacts/Exchange.json b/packages/contract-wrappers/src/compact_artifacts/Exchange.json new file mode 100644 index 000000000..af8db7360 --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/Exchange.json @@ -0,0 +1,610 @@ +{ + "contract_name": "Exchange", + "abi": [ + { + "constant": true, + "inputs": [ + { + "name": "numerator", + "type": "uint256" + }, + { + "name": "denominator", + "type": "uint256" + }, + { + "name": "target", + "type": "uint256" + } + ], + "name": "isRoundingError", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "name": "filled", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "name": "cancelled", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "fillOrdersUpTo", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "cancelTakerTokenAmount", + "type": "uint256" + } + ], + "name": "cancelOrder", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "ZRX_TOKEN_CONTRACT", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmounts", + "type": "uint256[]" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "batchFillOrKillOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "fillOrKillOrder", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "getUnavailableTakerTokenAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "signer", + "type": "address" + }, + { + "name": "hash", + "type": "bytes32" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "isValidSignature", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "numerator", + "type": "uint256" + }, + { + "name": "denominator", + "type": "uint256" + }, + { + "name": "target", + "type": "uint256" + } + ], + "name": "getPartialAmount", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "TOKEN_TRANSFER_PROXY_CONTRACT", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "fillTakerTokenAmounts", + "type": "uint256[]" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8[]" + }, + { + "name": "r", + "type": "bytes32[]" + }, + { + "name": "s", + "type": "bytes32[]" + } + ], + "name": "batchFillOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5][]" + }, + { + "name": "orderValues", + "type": "uint256[6][]" + }, + { + "name": "cancelTakerTokenAmounts", + "type": "uint256[]" + } + ], + "name": "batchCancelOrders", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + }, + { + "name": "fillTakerTokenAmount", + "type": "uint256" + }, + { + "name": "shouldThrowOnInsufficientBalanceOrAllowance", + "type": "bool" + }, + { + "name": "v", + "type": "uint8" + }, + { + "name": "r", + "type": "bytes32" + }, + { + "name": "s", + "type": "bytes32" + } + ], + "name": "fillOrder", + "outputs": [ + { + "name": "filledTakerTokenAmount", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "orderAddresses", + "type": "address[5]" + }, + { + "name": "orderValues", + "type": "uint256[6]" + } + ], + "name": "getOrderHash", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "EXTERNAL_QUERY_GAS_LIMIT", + "outputs": [ + { + "name": "", + "type": "uint16" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "type": "function" + }, + { + "inputs": [ + { + "name": "_zrxToken", + "type": "address" + }, + { + "name": "_tokenTransferProxy", + "type": "address" + } + ], + "payable": false, + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "name": "taker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "filledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "filledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidMakerFee", + "type": "uint256" + }, + { + "indexed": false, + "name": "paidTakerFee", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogFill", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "name": "feeRecipient", + "type": "address" + }, + { + "indexed": false, + "name": "makerToken", + "type": "address" + }, + { + "indexed": false, + "name": "takerToken", + "type": "address" + }, + { + "indexed": false, + "name": "cancelledMakerTokenAmount", + "type": "uint256" + }, + { + "indexed": false, + "name": "cancelledTakerTokenAmount", + "type": "uint256" + }, + { + "indexed": true, + "name": "tokens", + "type": "bytes32" + }, + { + "indexed": false, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogCancel", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "errorId", + "type": "uint8" + }, + { + "indexed": true, + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "LogError", + "type": "event" + } + ], + "networks": { + "1": { + "address": "0x12459c951127e0c374ff9105dda097662a027093" + }, + "3": { + "address": "0x479cc461fecd078f766ecc58533d6f69580cf3ac" + }, + "4": { + "address": "0x1d16ef40fac01cec8adac2ac49427b9384192c05" + }, + "42": { + "address": "0x90fe2af704b34e0224bf2299c838e04d4dcf1364" + }, + "50": { + "address": "0x48bacb9266a570d521063ef5dd96e61686dbe788" + } + } +} diff --git a/packages/contract-wrappers/src/compact_artifacts/Token.json b/packages/contract-wrappers/src/compact_artifacts/Token.json new file mode 100644 index 000000000..3b5a86ae0 --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/Token.json @@ -0,0 +1,172 @@ +{ + "contract_name": "Token", + "abi": [ + { + "constant": false, + "inputs": [ + { + "name": "_spender", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "supply", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_from", + "type": "address" + }, + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "balance", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_to", + "type": "address" + }, + { + "name": "_value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "success", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_owner", + "type": "address" + }, + { + "name": "_spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "remaining", + "type": "uint256" + } + ], + "payable": false, + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_from", + "type": "address" + }, + { + "indexed": true, + "name": "_to", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "_owner", + "type": "address" + }, + { + "indexed": true, + "name": "_spender", + "type": "address" + }, + { + "indexed": false, + "name": "_value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } + ] +} diff --git a/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json b/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json new file mode 100644 index 000000000..0f583628c --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/TokenRegistry.json @@ -0,0 +1,547 @@ +{ + "contract_name": "TokenRegistry", + "abi": [ + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_index", + "type": "uint256" + } + ], + "name": "removeToken", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_name", + "type": "string" + } + ], + "name": "getTokenAddressByName", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_symbol", + "type": "string" + } + ], + "name": "getTokenAddressBySymbol", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_swarmHash", + "type": "bytes" + } + ], + "name": "setTokenSwarmHash", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_token", + "type": "address" + } + ], + "name": "getTokenMetaData", + "outputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "uint8" + }, + { + "name": "", + "type": "bytes" + }, + { + "name": "", + "type": "bytes" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_name", + "type": "string" + }, + { + "name": "_symbol", + "type": "string" + }, + { + "name": "_decimals", + "type": "uint8" + }, + { + "name": "_ipfsHash", + "type": "bytes" + }, + { + "name": "_swarmHash", + "type": "bytes" + } + ], + "name": "addToken", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_name", + "type": "string" + } + ], + "name": "setTokenName", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "tokens", + "outputs": [ + { + "name": "token", + "type": "address" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "decimals", + "type": "uint8" + }, + { + "name": "ipfsHash", + "type": "bytes" + }, + { + "name": "swarmHash", + "type": "bytes" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "tokenAddresses", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_name", + "type": "string" + } + ], + "name": "getTokenByName", + "outputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "uint8" + }, + { + "name": "", + "type": "bytes" + }, + { + "name": "", + "type": "bytes" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getTokenAddresses", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_ipfsHash", + "type": "bytes" + } + ], + "name": "setTokenIpfsHash", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "_symbol", + "type": "string" + } + ], + "name": "getTokenBySymbol", + "outputs": [ + { + "name": "", + "type": "address" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "string" + }, + { + "name": "", + "type": "uint8" + }, + { + "name": "", + "type": "bytes" + }, + { + "name": "", + "type": "bytes" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_token", + "type": "address" + }, + { + "name": "_symbol", + "type": "string" + } + ], + "name": "setTokenSymbol", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "name", + "type": "string" + }, + { + "indexed": false, + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "name": "ipfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "swarmHash", + "type": "bytes" + } + ], + "name": "LogAddToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "name", + "type": "string" + }, + { + "indexed": false, + "name": "symbol", + "type": "string" + }, + { + "indexed": false, + "name": "decimals", + "type": "uint8" + }, + { + "indexed": false, + "name": "ipfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "swarmHash", + "type": "bytes" + } + ], + "name": "LogRemoveToken", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldName", + "type": "string" + }, + { + "indexed": false, + "name": "newName", + "type": "string" + } + ], + "name": "LogTokenNameChange", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldSymbol", + "type": "string" + }, + { + "indexed": false, + "name": "newSymbol", + "type": "string" + } + ], + "name": "LogTokenSymbolChange", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldIpfsHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "newIpfsHash", + "type": "bytes" + } + ], + "name": "LogTokenIpfsHashChange", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "token", + "type": "address" + }, + { + "indexed": false, + "name": "oldSwarmHash", + "type": "bytes" + }, + { + "indexed": false, + "name": "newSwarmHash", + "type": "bytes" + } + ], + "name": "LogTokenSwarmHashChange", + "type": "event" + } + ], + "networks": { + "1": { + "address": "0x926a74c5c36adf004c87399e65f75628b0f98d2c" + }, + "3": { + "address": "0x6b1a50f0bb5a7995444bd3877b22dc89c62843ed" + }, + "4": { + "address": "0x4e9aad8184de8833365fea970cd9149372fdf1e6" + }, + "42": { + "address": "0xf18e504561f4347bea557f3d4558f559dddbae7f" + }, + "50": { + "address": "0x0b1ba0af832d7c05fd64161e0db78e85978e8082" + } + } +} diff --git a/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json b/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json new file mode 100644 index 000000000..8cf551ddb --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/TokenTransferProxy.json @@ -0,0 +1,187 @@ +{ + "contract_name": "TokenTransferProxy", + "abi": [ + { + "constant": false, + "inputs": [ + { + "name": "token", + "type": "address" + }, + { + "name": "from", + "type": "address" + }, + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "target", + "type": "address" + } + ], + "name": "addAuthorizedAddress", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "uint256" + } + ], + "name": "authorities", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "target", + "type": "address" + } + ], + "name": "removeAuthorizedAddress", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "owner", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "", + "type": "address" + } + ], + "name": "authorized", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getAuthorizedAddresses", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "payable": false, + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "target", + "type": "address" + }, + { + "indexed": true, + "name": "caller", + "type": "address" + } + ], + "name": "LogAuthorizedAddressAdded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "target", + "type": "address" + }, + { + "indexed": true, + "name": "caller", + "type": "address" + } + ], + "name": "LogAuthorizedAddressRemoved", + "type": "event" + } + ], + "networks": { + "1": { + "address": "0x8da0d80f5007ef1e431dd2127178d224e32c2ef4" + }, + "3": { + "address": "0x4e9aad8184de8833365fea970cd9149372fdf1e6" + }, + "4": { + "address": "0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d" + }, + "42": { + "address": "0x087eed4bc1ee3de49befbd66c662b434b15d49d4" + }, + "50": { + "address": "0x1dc4c1cefef38a777b15aa20260a54e584b16c48" + } + } +} diff --git a/packages/contract-wrappers/src/compact_artifacts/ZRX.json b/packages/contract-wrappers/src/compact_artifacts/ZRX.json new file mode 100644 index 000000000..e40b8f268 --- /dev/null +++ b/packages/contract-wrappers/src/compact_artifacts/ZRX.json @@ -0,0 +1,20 @@ +{ + "contract_name": "ZRX", + "networks": { + "1": { + "address": "0xe41d2489571d322189246dafa5ebde1f4699f498" + }, + "3": { + "address": "0xa8e9fa8f91e5ae138c74648c9c304f1c75003a8d" + }, + "4": { + "address": "0x00f58d6d585f84b2d7267940cede30ce2fe6eae8" + }, + "42": { + "address": "0x6ff6c0ff1d68b964901f986d4c9fa3ac68346570" + }, + "50": { + "address": "0x1d7022f5b17d2f8b695918fb48fa1089c9f85401" + } + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers.ts b/packages/contract-wrappers/src/contract_wrappers.ts new file mode 100644 index 000000000..2ef050f27 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers.ts @@ -0,0 +1,112 @@ +import { Provider } from '@0xproject/types'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { artifacts } from './artifacts'; +import { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper'; +import { ExchangeWrapper } from './contract_wrappers/exchange_wrapper'; +import { TokenRegistryWrapper } from './contract_wrappers/token_registry_wrapper'; +import { TokenTransferProxyWrapper } from './contract_wrappers/token_transfer_proxy_wrapper'; +import { TokenWrapper } from './contract_wrappers/token_wrapper'; +import { ContractWrappersConfigSchema } from './schemas/contract_wrappers_config_schema'; +import { contractWrappersPrivateNetworkConfigSchema } from './schemas/contract_wrappers_private_network_config_schema'; +import { contractWrappersPublicNetworkConfigSchema } from './schemas/contract_wrappers_public_network_config_schema'; +import { ContractWrappersConfig } from './types'; +import { assert } from './utils/assert'; +/** + * The ContractWrappers class contains smart contract wrappers helpful when building on 0x protocol. + */ +export class ContractWrappers { + /** + * An instance of the ExchangeWrapper class containing methods for interacting with the 0x Exchange smart contract. + */ + public exchange: ExchangeWrapper; + /** + * An instance of the TokenRegistryWrapper class containing methods for interacting with the 0x + * TokenRegistry smart contract. + */ + public tokenRegistry: TokenRegistryWrapper; + /** + * 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; + /** + * An instance of the TokenTransferProxyWrapper class containing methods for interacting with the + * tokenTransferProxy smart contract. + */ + public proxy: TokenTransferProxyWrapper; + private _web3Wrapper: Web3Wrapper; + /** + * Instantiates a new ContractWrappers instance. + * @param provider The Provider instance you would like the 0x.js library to use for interacting with + * the Ethereum network. + * @param config The configuration object. Look up the type for the description. + * @return An instance of the ContractWrappers class. + */ + constructor(provider: Provider, config: ContractWrappersConfig) { + assert.isWeb3Provider('provider', provider); + assert.doesConformToSchema('config', config, ContractWrappersConfigSchema, [ + contractWrappersPrivateNetworkConfigSchema, + contractWrappersPublicNetworkConfigSchema, + ]); + const artifactJSONs = _.values(artifacts); + const abiArrays = _.map(artifactJSONs, artifact => artifact.abi); + const txDefaults = { + gasPrice: config.gasPrice, + }; + this._web3Wrapper = new Web3Wrapper(provider, txDefaults); + _.forEach(abiArrays, abi => { + this._web3Wrapper.abiDecoder.addABI(abi); + }); + this.proxy = new TokenTransferProxyWrapper( + this._web3Wrapper, + config.networkId, + config.tokenTransferProxyContractAddress, + ); + this.token = new TokenWrapper(this._web3Wrapper, config.networkId, this.proxy); + this.exchange = new ExchangeWrapper( + this._web3Wrapper, + config.networkId, + this.token, + config.exchangeContractAddress, + config.zrxContractAddress, + ); + this.tokenRegistry = new TokenRegistryWrapper( + this._web3Wrapper, + config.networkId, + config.tokenRegistryContractAddress, + ); + this.etherToken = new EtherTokenWrapper(this._web3Wrapper, config.networkId, this.token); + } + /** + * Sets a new web3 provider for 0x.js. Updating the provider will stop all + * subscriptions so you will need to re-subscribe to all events relevant to your app after this call. + * @param provider The Web3Provider you would like the 0x.js library to use from now on. + * @param networkId The id of the network your provider is connected to + */ + public setProvider(provider: Provider, networkId: number): void { + this._web3Wrapper.setProvider(provider); + (this.exchange as any)._invalidateContractInstances(); + (this.exchange as any)._setNetworkId(networkId); + (this.tokenRegistry as any)._invalidateContractInstance(); + (this.tokenRegistry as any)._setNetworkId(networkId); + (this.token as any)._invalidateContractInstances(); + (this.token as any)._setNetworkId(networkId); + (this.proxy as any)._invalidateContractInstance(); + (this.proxy as any)._setNetworkId(networkId); + (this.etherToken as any)._invalidateContractInstance(); + (this.etherToken as any)._setNetworkId(networkId); + } + /** + * Get the provider instance currently used by 0x.js + * @return Web3 provider instance + */ + public getProvider(): Provider { + return this._web3Wrapper.getProvider(); + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts new file mode 100644 index 000000000..f255ced62 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/contract_wrapper.ts @@ -0,0 +1,208 @@ +import { + Artifact, + BlockParamLiteral, + ContractAbi, + FilterObject, + LogEntry, + LogWithDecodedArgs, + RawLog, +} from '@0xproject/types'; +import { AbiDecoder, intervalUtils } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Block, BlockAndLogStreamer } from 'ethereumjs-blockstream'; +import * as _ from 'lodash'; + +import { + BlockRange, + ContractEventArgs, + ContractEvents, + ContractWrappersError, + EventCallback, + IndexedFilterValues, + InternalContractWrappersError, +} from '../types'; +import { constants } from '../utils/constants'; +import { filterUtils } from '../utils/filter_utils'; + +const CONTRACT_NAME_TO_NOT_FOUND_ERROR: { + [contractName: string]: ContractWrappersError; +} = { + ZRX: ContractWrappersError.ZRXContractDoesNotExist, + EtherToken: ContractWrappersError.EtherTokenContractDoesNotExist, + Token: ContractWrappersError.TokenContractDoesNotExist, + TokenRegistry: ContractWrappersError.TokenRegistryContractDoesNotExist, + TokenTransferProxy: ContractWrappersError.TokenTransferProxyContractDoesNotExist, + Exchange: ContractWrappersError.ExchangeContractDoesNotExist, +}; + +export class ContractWrapper { + protected _web3Wrapper: Web3Wrapper; + protected _networkId: number; + private _blockAndLogStreamerIfExists?: BlockAndLogStreamer; + private _blockAndLogStreamIntervalIfExists?: NodeJS.Timer; + private _filters: { [filterToken: string]: FilterObject }; + private _filterCallbacks: { + [filterToken: string]: EventCallback<ContractEventArgs>; + }; + private _onLogAddedSubscriptionToken: string | undefined; + private _onLogRemovedSubscriptionToken: string | undefined; + constructor(web3Wrapper: Web3Wrapper, networkId: number) { + this._web3Wrapper = web3Wrapper; + this._networkId = networkId; + this._filters = {}; + this._filterCallbacks = {}; + this._blockAndLogStreamerIfExists = undefined; + this._onLogAddedSubscriptionToken = undefined; + this._onLogRemovedSubscriptionToken = undefined; + } + protected _unsubscribeAll(): void { + const filterTokens = _.keys(this._filterCallbacks); + _.each(filterTokens, filterToken => { + this._unsubscribe(filterToken); + }); + } + protected _unsubscribe(filterToken: string, err?: Error): void { + if (_.isUndefined(this._filters[filterToken])) { + throw new Error(ContractWrappersError.SubscriptionNotFound); + } + if (!_.isUndefined(err)) { + const callback = this._filterCallbacks[filterToken]; + callback(err, undefined); + } + delete this._filters[filterToken]; + delete this._filterCallbacks[filterToken]; + if (_.isEmpty(this._filters)) { + this._stopBlockAndLogStream(); + } + } + protected _subscribe<ArgsType extends ContractEventArgs>( + address: string, + eventName: ContractEvents, + indexFilterValues: IndexedFilterValues, + abi: ContractAbi, + callback: EventCallback<ArgsType>, + ): string { + const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi); + if (_.isUndefined(this._blockAndLogStreamerIfExists)) { + this._startBlockAndLogStream(); + } + const filterToken = filterUtils.generateUUID(); + this._filters[filterToken] = filter; + this._filterCallbacks[filterToken] = callback as EventCallback<ContractEventArgs>; + return filterToken; + } + protected async _getLogsAsync<ArgsType extends ContractEventArgs>( + address: string, + eventName: ContractEvents, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + abi: ContractAbi, + ): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + const filter = filterUtils.getFilter(address, eventName, indexFilterValues, abi, blockRange); + const logs = await this._web3Wrapper.getLogsAsync(filter); + const logsWithDecodedArguments = _.map(logs, this._tryToDecodeLogOrNoop.bind(this)); + return logsWithDecodedArguments; + } + protected _tryToDecodeLogOrNoop<ArgsType extends ContractEventArgs>( + log: LogEntry, + ): LogWithDecodedArgs<ArgsType> | RawLog { + if (_.isUndefined(this._web3Wrapper.abiDecoder)) { + throw new Error(InternalContractWrappersError.NoAbiDecoder); + } + const logWithDecodedArgs = this._web3Wrapper.abiDecoder.tryToDecodeLogOrNoop(log); + return logWithDecodedArgs; + } + protected async _getContractAbiAndAddressFromArtifactsAsync( + artifact: Artifact, + addressIfExists?: string, + ): Promise<[ContractAbi, string]> { + let contractAddress: string; + if (_.isUndefined(addressIfExists)) { + if (_.isUndefined(artifact.networks[this._networkId])) { + throw new Error(ContractWrappersError.ContractNotDeployedOnNetwork); + } + contractAddress = artifact.networks[this._networkId].address.toLowerCase(); + } else { + contractAddress = addressIfExists; + } + const doesContractExist = await this._web3Wrapper.doesContractExistAtAddressAsync(contractAddress); + if (!doesContractExist) { + throw new Error(CONTRACT_NAME_TO_NOT_FOUND_ERROR[artifact.contract_name]); + } + const abiAndAddress: [ContractAbi, string] = [artifact.abi, contractAddress]; + return abiAndAddress; + } + protected _getContractAddress(artifact: Artifact, addressIfExists?: string): string { + if (_.isUndefined(addressIfExists)) { + const contractAddress = artifact.networks[this._networkId].address; + if (_.isUndefined(contractAddress)) { + throw new Error(ContractWrappersError.ExchangeContractDoesNotExist); + } + return contractAddress; + } else { + return addressIfExists; + } + } + private _onLogStateChanged<ArgsType extends ContractEventArgs>(isRemoved: boolean, log: LogEntry): void { + _.forEach(this._filters, (filter: FilterObject, filterToken: string) => { + if (filterUtils.matchesFilter(log, filter)) { + const decodedLog = this._tryToDecodeLogOrNoop(log) as LogWithDecodedArgs<ArgsType>; + const logEvent = { + log: decodedLog, + isRemoved, + }; + this._filterCallbacks[filterToken](null, logEvent); + } + }); + } + private _startBlockAndLogStream(): void { + if (!_.isUndefined(this._blockAndLogStreamerIfExists)) { + throw new Error(ContractWrappersError.SubscriptionAlreadyPresent); + } + this._blockAndLogStreamerIfExists = new BlockAndLogStreamer( + this._web3Wrapper.getBlockAsync.bind(this._web3Wrapper), + this._web3Wrapper.getLogsAsync.bind(this._web3Wrapper), + ); + const catchAllLogFilter = {}; + this._blockAndLogStreamerIfExists.addLogFilter(catchAllLogFilter); + this._blockAndLogStreamIntervalIfExists = intervalUtils.setAsyncExcludingInterval( + this._reconcileBlockAsync.bind(this), + constants.DEFAULT_BLOCK_POLLING_INTERVAL, + this._onReconcileBlockError.bind(this), + ); + let isRemoved = false; + this._onLogAddedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogAdded( + this._onLogStateChanged.bind(this, isRemoved), + ); + isRemoved = true; + this._onLogRemovedSubscriptionToken = this._blockAndLogStreamerIfExists.subscribeToOnLogRemoved( + this._onLogStateChanged.bind(this, isRemoved), + ); + } + private _onReconcileBlockError(err: Error): void { + const filterTokens = _.keys(this._filterCallbacks); + _.each(filterTokens, filterToken => { + this._unsubscribe(filterToken, err); + }); + } + private _setNetworkId(networkId: number): void { + this._networkId = networkId; + } + private _stopBlockAndLogStream(): void { + if (_.isUndefined(this._blockAndLogStreamerIfExists)) { + throw new Error(ContractWrappersError.SubscriptionNotFound); + } + this._blockAndLogStreamerIfExists.unsubscribeFromOnLogAdded(this._onLogAddedSubscriptionToken as string); + this._blockAndLogStreamerIfExists.unsubscribeFromOnLogRemoved(this._onLogRemovedSubscriptionToken as string); + intervalUtils.clearAsyncExcludingInterval(this._blockAndLogStreamIntervalIfExists as NodeJS.Timer); + delete this._blockAndLogStreamerIfExists; + } + private async _reconcileBlockAsync(): Promise<void> { + const latestBlock = await this._web3Wrapper.getBlockAsync(BlockParamLiteral.Latest); + // We need to coerce to Block type cause Web3.Block includes types for mempool blocks + if (!_.isUndefined(this._blockAndLogStreamerIfExists)) { + // If we clear the interval while fetching the block - this._blockAndLogStreamer will be undefined + await this._blockAndLogStreamerIfExists.reconcileNewBlock((latestBlock as any) as Block); + } + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts new file mode 100644 index 000000000..1bd65270b --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/ether_token_wrapper.ts @@ -0,0 +1,207 @@ +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, ContractWrappersError, EventCallback, IndexedFilterValues, TransactionOpts } from '../types'; +import { assert } from '../utils/assert'; + +import { ContractWrapper } from './contract_wrapper'; +import { EtherTokenContract, EtherTokenContractEventArgs, EtherTokenEvents } from './generated/ether_token'; +import { TokenWrapper } from './token_wrapper'; + +/** + * 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 _etherTokenContractsByAddress: { + [address: string]: EtherTokenContract; + } = {}; + private _tokenWrapper: TokenWrapper; + constructor(web3Wrapper: Web3Wrapper, networkId: number, tokenWrapper: TokenWrapper) { + super(web3Wrapper, networkId); + 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 etherTokenAddress EtherToken address you wish to deposit into. + * @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. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async depositAsync( + etherTokenAddress: string, + amountInWei: BigNumber, + depositor: string, + txOpts: TransactionOpts = {}, + ): Promise<string> { + assert.isETHAddressHex('etherTokenAddress', etherTokenAddress); + assert.isValidBaseUnitAmount('amountInWei', amountInWei); + await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper); + const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase(); + const normalizedDepositorAddress = depositor.toLowerCase(); + + const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(normalizedDepositorAddress); + assert.assert(ethBalanceInWei.gte(amountInWei), ContractWrappersError.InsufficientEthBalanceForDeposit); + + const wethContract = await this._getEtherTokenContractAsync(normalizedEtherTokenAddress); + const txHash = await wethContract.deposit.sendTransactionAsync({ + from: normalizedDepositorAddress, + value: amountInWei, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }); + return txHash; + } + /** + * Withdraw ETH to the withdrawer's address from the wrapped ETH smart contract in exchange for the + * equivalent number of wrapped ETH tokens. + * @param etherTokenAddress EtherToken address you wish to withdraw from. + * @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 withdrawal. + * @param txOpts Transaction parameters. + * @return Transaction hash. + */ + public async withdrawAsync( + etherTokenAddress: string, + amountInWei: BigNumber, + withdrawer: string, + txOpts: TransactionOpts = {}, + ): Promise<string> { + assert.isValidBaseUnitAmount('amountInWei', amountInWei); + assert.isETHAddressHex('etherTokenAddress', etherTokenAddress); + await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper); + const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase(); + const normalizedWithdrawerAddress = withdrawer.toLowerCase(); + + const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync( + normalizedEtherTokenAddress, + normalizedWithdrawerAddress, + ); + assert.assert( + WETHBalanceInBaseUnits.gte(amountInWei), + ContractWrappersError.InsufficientWEthBalanceForWithdrawal, + ); + + const wethContract = await this._getEtherTokenContractAsync(normalizedEtherTokenAddress); + const txHash = await wethContract.withdraw.sendTransactionAsync(amountInWei, { + from: normalizedWithdrawerAddress, + gas: txOpts.gasLimit, + gasPrice: txOpts.gasPrice, + }); + return txHash; + } + /** + * Gets historical logs without creating a subscription + * @param etherTokenAddress An address of the ether token that emitted the logs. + * @param eventName The ether 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 `{_owner: aUserAddressHex}` + * @return Array of logs that match the parameters + */ + public async getLogsAsync<ArgsType extends EtherTokenContractEventArgs>( + etherTokenAddress: string, + eventName: EtherTokenEvents, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + ): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + assert.isETHAddressHex('etherTokenAddress', etherTokenAddress); + const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase(); + assert.doesBelongToStringEnum('eventName', eventName, EtherTokenEvents); + assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const logs = await this._getLogsAsync<ArgsType>( + normalizedEtherTokenAddress, + eventName, + blockRange, + indexFilterValues, + artifacts.EtherToken.abi, + ); + return logs; + } + /** + * Subscribe to an event type emitted by the Token contract. + * @param etherTokenAddress The hex encoded address where the ether token is deployed. + * @param eventName The ether 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 `{_owner: aUserAddressHex}` + * @param callback Callback that gets called when a log is added/removed + * @return Subscription token used later to unsubscribe + */ + public subscribe<ArgsType extends EtherTokenContractEventArgs>( + etherTokenAddress: string, + eventName: EtherTokenEvents, + indexFilterValues: IndexedFilterValues, + callback: EventCallback<ArgsType>, + ): string { + assert.isETHAddressHex('etherTokenAddress', etherTokenAddress); + const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase(); + assert.doesBelongToStringEnum('eventName', eventName, EtherTokenEvents); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const subscriptionToken = this._subscribe<ArgsType>( + normalizedEtherTokenAddress, + eventName, + indexFilterValues, + artifacts.EtherToken.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(); + } + /** + * Retrieves the Ethereum address of the EtherToken contract deployed on the network + * that the user-passed web3 provider is connected to. If it's not Kovan, Ropsten, Rinkeby, Mainnet or TestRPC + * (networkId: 50), it will return undefined (e.g a private network). + * @returns The Ethereum address of the EtherToken contract or undefined. + */ + public getContractAddressIfExists(): string | undefined { + const networkSpecificArtifact = artifacts.EtherToken.networks[this._networkId]; + const contractAddressIfExists = _.isUndefined(networkSpecificArtifact) + ? undefined + : networkSpecificArtifact.address; + return contractAddressIfExists; + } + private _invalidateContractInstance(): void { + this.unsubscribeAll(); + this._etherTokenContractsByAddress = {}; + } + private async _getEtherTokenContractAsync(etherTokenAddress: string): Promise<EtherTokenContract> { + let etherTokenContract = this._etherTokenContractsByAddress[etherTokenAddress]; + if (!_.isUndefined(etherTokenContract)) { + return etherTokenContract; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.EtherToken, + etherTokenAddress, + ); + const contractInstance = new EtherTokenContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + etherTokenContract = contractInstance; + this._etherTokenContractsByAddress[etherTokenAddress] = etherTokenContract; + return etherTokenContract; + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts new file mode 100644 index 000000000..fc177dd42 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/exchange_wrapper.ts @@ -0,0 +1,939 @@ +import { schemas } from '@0xproject/json-schemas'; +import { formatters, getOrderHashHex, OrderStateUtils } from '@0xproject/order-utils'; +import { + BlockParamLiteral, + DecodedLogArgs, + ECSignature, + ExchangeContractErrs, + LogEntry, + LogWithDecodedArgs, + Order, + OrderAddresses, + OrderState, + OrderValues, + SignedOrder, +} from '@0xproject/types'; +import { AbiDecoder, BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { SimpleBalanceAndProxyAllowanceFetcher } from '../fetchers/simple_balance_and_proxy_allowance_fetcher'; +import { SimpleOrderFilledCancelledFetcher } from '../fetchers/simple_order_filled_cancelled_fetcher'; +import { + BlockRange, + EventCallback, + ExchangeContractErrCodes, + IndexedFilterValues, + MethodOpts, + OrderCancellationRequest, + OrderFillRequest, + OrderTransactionOpts, + ValidateOrderFillableOpts, +} from '../types'; +import { assert } from '../utils/assert'; +import { decorators } from '../utils/decorators'; +import { ExchangeTransferSimulator } from '../utils/exchange_transfer_simulator'; +import { OrderValidationUtils } from '../utils/order_validation_utils'; +import { utils } from '../utils/utils'; + +import { ContractWrapper } from './contract_wrapper'; +import { + ExchangeContract, + ExchangeContractEventArgs, + ExchangeEvents, + LogErrorContractEventArgs, +} from './generated/exchange'; +import { TokenWrapper } from './token_wrapper'; +const SHOULD_VALIDATE_BY_DEFAULT = true; + +interface ExchangeContractErrCodesToMsgs { + [exchangeContractErrCodes: number]: string; +} + +/** + * This class includes all the functionality related to calling methods and subscribing to + * events of the 0x Exchange smart contract. + */ +export class ExchangeWrapper extends ContractWrapper { + private _exchangeContractIfExists?: ExchangeContract; + private _orderValidationUtils: OrderValidationUtils; + private _tokenWrapper: TokenWrapper; + private _exchangeContractErrCodesToMsg: ExchangeContractErrCodesToMsgs = { + [ExchangeContractErrCodes.ERROR_FILL_EXPIRED]: ExchangeContractErrs.OrderFillExpired, + [ExchangeContractErrCodes.ERROR_CANCEL_EXPIRED]: ExchangeContractErrs.OrderFillExpired, + [ExchangeContractErrCodes.ERROR_FILL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero, + [ExchangeContractErrCodes.ERROR_CANCEL_NO_VALUE]: ExchangeContractErrs.OrderRemainingFillAmountZero, + [ExchangeContractErrCodes.ERROR_FILL_TRUNCATION]: ExchangeContractErrs.OrderFillRoundingError, + [ExchangeContractErrCodes.ERROR_FILL_BALANCE_ALLOWANCE]: ExchangeContractErrs.FillBalanceAllowanceError, + }; + private _contractAddressIfExists?: string; + private _zrxContractAddressIfExists?: string; + constructor( + web3Wrapper: Web3Wrapper, + networkId: number, + tokenWrapper: TokenWrapper, + contractAddressIfExists?: string, + zrxContractAddressIfExists?: string, + ) { + super(web3Wrapper, networkId); + this._tokenWrapper = tokenWrapper; + this._orderValidationUtils = new OrderValidationUtils(this); + this._contractAddressIfExists = contractAddressIfExists; + this._zrxContractAddressIfExists = zrxContractAddressIfExists; + } + /** + * Returns the unavailable takerAmount of an order. Unavailable amount is defined as the total + * amount that has been filled or cancelled. The remaining takerAmount can be calculated by + * subtracting the unavailable amount from the total order takerAmount. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the + * unavailable takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has either been filled or cancelled. + */ + public async getUnavailableTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + const txData = {}; + let unavailableTakerTokenAmount = await exchangeContract.getUnavailableTakerTokenAmount.callAsync( + orderHash, + txData, + defaultBlock, + ); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + unavailableTakerTokenAmount = new BigNumber(unavailableTakerTokenAmount); + return unavailableTakerTokenAmount; + } + /** + * Retrieve the takerAmount of an order that has already been filled. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the filled takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has already been filled. + */ + public async getFilledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + const txData = {}; + let fillAmountInBaseUnits = await exchangeContract.filled.callAsync(orderHash, txData, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + fillAmountInBaseUnits = new BigNumber(fillAmountInBaseUnits); + return fillAmountInBaseUnits; + } + /** + * Retrieve the takerAmount of an order that has been cancelled. + * @param orderHash The hex encoded orderHash for which you would like to retrieve the + * cancelled takerAmount. + * @param methodOpts Optional arguments this method accepts. + * @return The amount of the order (in taker tokens) that has been cancelled. + */ + public async getCancelledTakerAmountAsync(orderHash: string, methodOpts?: MethodOpts): Promise<BigNumber> { + assert.doesConformToSchema('orderHash', orderHash, schemas.orderHashSchema); + + const exchangeContract = await this._getExchangeContractAsync(); + const defaultBlock = _.isUndefined(methodOpts) ? undefined : methodOpts.defaultBlock; + const txData = {}; + let cancelledAmountInBaseUnits = await exchangeContract.cancelled.callAsync(orderHash, txData, defaultBlock); + // Wrap BigNumbers returned from web3 with our own (later) version of BigNumber + cancelledAmountInBaseUnits = new BigNumber(cancelledAmountInBaseUnits); + return cancelledAmountInBaseUnits; + } + /** + * Fills a signed order with an amount denominated in baseUnits of the taker token. + * Since the order in which transactions are included in the next block is indeterminate, race-conditions + * could arise where a users balance or allowance changes before the fillOrder executes. Because of this, + * we allow you to specify `shouldThrowOnInsufficientBalanceOrAllowance`. + * If false, the smart contract will not throw if the parties + * do not have sufficient balances/allowances, preserving gas costs. Setting it to true forgoes this check + * and causes the smart contract to throw (using all the gas supplied) instead. + * @param signedOrder An object that conforms to the SignedOrder interface. + * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that + * you wish to fill. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw + * if upon execution the tokens cannot be transferred. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Provider + * passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async fillOrderAsync( + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + + const exchangeInstance = await this._getExchangeContractAsync(); + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + + const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(signedOrder); + + const txHash: string = await exchangeInstance.fillOrder.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: normalizedTakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Sequentially and atomically fills signedOrders up to the specified takerTokenFillAmount. + * If the fill amount is reached - it succeeds and does not fill the rest of the orders. + * If fill amount is not reached - it fills as much of the fill amount as possible and succeeds. + * @param signedOrders The array of signedOrders that you would like to fill until + * takerTokenFillAmount is reached. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw if + * upon execution any of the tokens cannot be transferred. + * If set to false, the call will continue to fill subsequent + * signedOrders even when some cannot be filled. + * @param takerAddress The user Ethereum address who would like to fill these + * orders. Must be available via the supplied Provider + * passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async fillOrdersUpToAsync( + signedOrders: SignedOrder[], + fillTakerTokenAmount: BigNumber, + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('signedOrders', signedOrders, schemas.signedOrdersSchema); + const takerTokenAddresses = _.map(signedOrders, signedOrder => signedOrder.takerTokenAddress); + assert.hasAtMostOneUniqueValue( + takerTokenAddresses, + ExchangeContractErrs.MultipleTakerTokensInFillUpToDisallowed, + ); + const exchangeContractAddresses = _.map(signedOrders, signedOrder => signedOrder.exchangeContractAddress); + assert.hasAtMostOneUniqueValue( + exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress, + ); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + let filledTakerTokenAmount = new BigNumber(0); + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + for (const signedOrder of signedOrders) { + const singleFilledTakerTokenAmount = await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount.minus(filledTakerTokenAmount), + normalizedTakerAddress, + zrxTokenAddress, + ); + filledTakerTokenAmount = filledTakerTokenAmount.plus(singleFilledTakerTokenAmount); + if (filledTakerTokenAmount.eq(fillTakerTokenAmount)) { + break; + } + } + } + + if (_.isEmpty(signedOrders)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + + const orderAddressesValuesAndSignatureArray = _.map(signedOrders, signedOrder => { + return [ + ...formatters.getOrderAddressesAndValues(signedOrder), + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddressesArray, orderValuesArray, vArray, rArray, sArray] = _.unzip<any>( + orderAddressesValuesAndSignatureArray, + ); + + const exchangeInstance = await this._getExchangeContractAsync(); + const txHash = await exchangeInstance.fillOrdersUpTo.sendTransactionAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmount, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: normalizedTakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Batch version of fillOrderAsync. + * Executes multiple fills atomically in a single transaction. + * If shouldThrowOnInsufficientBalanceOrAllowance is set to false, it will continue filling subsequent orders even + * when earlier ones fail. + * When shouldThrowOnInsufficientBalanceOrAllowance is set to true, if any fill fails, the entire batch fails. + * @param orderFillRequests An array of objects that conform to the + * OrderFillRequest interface. + * @param shouldThrowOnInsufficientBalanceOrAllowance Whether or not you wish for the contract call to throw + * if upon execution any of the tokens cannot be + * transferred. If set to false, the call will continue to + * fill subsequent signedOrders even when some + * cannot be filled. + * @param takerAddress The user Ethereum address who would like to fill + * these orders. Must be available via the supplied + * Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async batchFillOrdersAsync( + orderFillRequests: OrderFillRequest[], + shouldThrowOnInsufficientBalanceOrAllowance: boolean, + takerAddress: string, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); + const exchangeContractAddresses = _.map( + orderFillRequests, + orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue( + exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress, + ); + assert.isBoolean('shouldThrowOnInsufficientBalanceOrAllowance', shouldThrowOnInsufficientBalanceOrAllowance); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + for (const orderFillRequest of orderFillRequests) { + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + orderFillRequest.signedOrder, + orderFillRequest.takerTokenFillAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + } + if (_.isEmpty(orderFillRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + + const orderAddressesValuesAmountsAndSignatureArray = _.map(orderFillRequests, orderFillRequest => { + return [ + ...formatters.getOrderAddressesAndValues(orderFillRequest.signedOrder), + orderFillRequest.takerTokenFillAmount, + orderFillRequest.signedOrder.ecSignature.v, + orderFillRequest.signedOrder.ecSignature.r, + orderFillRequest.signedOrder.ecSignature.s, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddressesArray, orderValuesArray, fillTakerTokenAmounts, vArray, rArray, sArray] = _.unzip<any>( + orderAddressesValuesAmountsAndSignatureArray, + ); + + const exchangeInstance = await this._getExchangeContractAsync(); + const txHash = await exchangeInstance.batchFillOrders.sendTransactionAsync( + orderAddressesArray, + orderValuesArray, + fillTakerTokenAmounts, + shouldThrowOnInsufficientBalanceOrAllowance, + vArray, + rArray, + sArray, + { + from: normalizedTakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Attempts to fill a specific amount of an order. If the entire amount specified cannot be filled, + * the fill order is abandoned. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async fillOrKillOrderAsync( + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + + const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(signedOrder); + const txHash = await exchangeInstance.fillOrKillOrder.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmount, + signedOrder.ecSignature.v, + signedOrder.ecSignature.r, + signedOrder.ecSignature.s, + { + from: normalizedTakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Batch version of fillOrKill. Allows a taker to specify a batch of orders that will either be atomically + * filled (each to the specified fillAmount) or aborted. + * @param orderFillRequests An array of objects that conform to the OrderFillRequest interface. + * @param takerAddress The user Ethereum address who would like to fill there orders. + * Must be available via the supplied Provider passed to 0x.js. + * @param orderTransactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async batchFillOrKillAsync( + orderFillRequests: OrderFillRequest[], + takerAddress: string, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('orderFillRequests', orderFillRequests, schemas.orderFillRequestsSchema); + const exchangeContractAddresses = _.map( + orderFillRequests, + orderFillRequest => orderFillRequest.signedOrder.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue( + exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress, + ); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + if (_.isEmpty(orderFillRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + for (const orderFillRequest of orderFillRequests) { + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + orderFillRequest.signedOrder, + orderFillRequest.takerTokenFillAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + } + + const orderAddressesValuesAndTakerTokenFillAmounts = _.map(orderFillRequests, request => { + return [ + ...formatters.getOrderAddressesAndValues(request.signedOrder), + request.takerTokenFillAmount, + request.signedOrder.ecSignature.v, + request.signedOrder.ecSignature.r, + request.signedOrder.ecSignature.s, + ]; + }); + + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddresses, orderValues, fillTakerTokenAmounts, vParams, rParams, sParams] = _.unzip<any>( + orderAddressesValuesAndTakerTokenFillAmounts, + ); + const txHash = await exchangeInstance.batchFillOrKillOrders.sendTransactionAsync( + orderAddresses, + orderValues, + fillTakerTokenAmounts, + vParams, + rParams, + sParams, + { + from: normalizedTakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Cancel a given fill amount of an order. Cancellations are cumulative. + * @param order An object that conforms to the Order or SignedOrder interface. + * The order you would like to cancel. + * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async cancelOrderAsync( + order: Order | SignedOrder, + cancelTakerTokenAmount: BigNumber, + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema('order', order, schemas.orderSchema); + assert.isValidBaseUnitAmount('takerTokenCancelAmount', cancelTakerTokenAmount); + await assert.isSenderAddressAsync('order.maker', order.maker, this._web3Wrapper); + const normalizedMakerAddress = order.maker.toLowerCase(); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + const orderHash = getOrderHashHex(order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + OrderValidationUtils.validateCancelOrderThrowIfInvalid( + order, + cancelTakerTokenAmount, + unavailableTakerTokenAmount, + ); + } + + const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(order); + const txHash = await exchangeInstance.cancelOrder.sendTransactionAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmount, + { + from: normalizedMakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Batch version of cancelOrderAsync. Atomically cancels multiple orders in a single transaction. + * All orders must be from the same maker. + * @param orderCancellationRequests An array of objects that conform to the OrderCancellationRequest + * interface. + * @param transactionOpts Optional arguments this method accepts. + * @return Transaction hash. + */ + @decorators.asyncZeroExErrorHandler + public async batchCancelOrdersAsync( + orderCancellationRequests: OrderCancellationRequest[], + orderTransactionOpts: OrderTransactionOpts = {}, + ): Promise<string> { + assert.doesConformToSchema( + 'orderCancellationRequests', + orderCancellationRequests, + schemas.orderCancellationRequestsSchema, + ); + const exchangeContractAddresses = _.map( + orderCancellationRequests, + orderCancellationRequest => orderCancellationRequest.order.exchangeContractAddress, + ); + assert.hasAtMostOneUniqueValue( + exchangeContractAddresses, + ExchangeContractErrs.BatchOrdersMustHaveSameExchangeAddress, + ); + const makers = _.map(orderCancellationRequests, cancellationRequest => cancellationRequest.order.maker); + assert.hasAtMostOneUniqueValue(makers, ExchangeContractErrs.MultipleMakersInSingleCancelBatchDisallowed); + const maker = makers[0]; + await assert.isSenderAddressAsync('maker', maker, this._web3Wrapper); + const normalizedMakerAddress = maker.toLowerCase(); + + const shouldValidate = _.isUndefined(orderTransactionOpts.shouldValidate) + ? SHOULD_VALIDATE_BY_DEFAULT + : orderTransactionOpts.shouldValidate; + if (shouldValidate) { + for (const orderCancellationRequest of orderCancellationRequests) { + const orderHash = getOrderHashHex(orderCancellationRequest.order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + OrderValidationUtils.validateCancelOrderThrowIfInvalid( + orderCancellationRequest.order, + orderCancellationRequest.takerTokenCancelAmount, + unavailableTakerTokenAmount, + ); + } + } + if (_.isEmpty(orderCancellationRequests)) { + throw new Error(ExchangeContractErrs.BatchOrdersMustHaveAtLeastOneItem); + } + const exchangeInstance = await this._getExchangeContractAsync(); + const orderAddressesValuesAndTakerTokenCancelAmounts = _.map(orderCancellationRequests, cancellationRequest => { + return [ + ...formatters.getOrderAddressesAndValues(cancellationRequest.order), + cancellationRequest.takerTokenCancelAmount, + ]; + }); + // We use _.unzip<any> because _.unzip doesn't type check if values have different types :'( + const [orderAddresses, orderValues, cancelTakerTokenAmounts] = _.unzip<any>( + orderAddressesValuesAndTakerTokenCancelAmounts, + ); + const txHash = await exchangeInstance.batchCancelOrders.sendTransactionAsync( + orderAddresses, + orderValues, + cancelTakerTokenAmounts, + { + from: normalizedMakerAddress, + gas: orderTransactionOpts.gasLimit, + gasPrice: orderTransactionOpts.gasPrice, + }, + ); + return txHash; + } + /** + * Subscribe to an event type emitted by the Exchange contract. + * @param eventName The exchange 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 ExchangeContractEventArgs>( + eventName: ExchangeEvents, + indexFilterValues: IndexedFilterValues, + callback: EventCallback<ArgsType>, + ): string { + assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + assert.isFunction('callback', callback); + const exchangeContractAddress = this.getContractAddress(); + const subscriptionToken = this._subscribe<ArgsType>( + exchangeContractAddress, + eventName, + indexFilterValues, + artifacts.Exchange.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 eventName The exchange 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 ExchangeContractEventArgs>( + eventName: ExchangeEvents, + blockRange: BlockRange, + indexFilterValues: IndexedFilterValues, + ): Promise<Array<LogWithDecodedArgs<ArgsType>>> { + assert.doesBelongToStringEnum('eventName', eventName, ExchangeEvents); + assert.doesConformToSchema('blockRange', blockRange, schemas.blockRangeSchema); + assert.doesConformToSchema('indexFilterValues', indexFilterValues, schemas.indexFilterValuesSchema); + const exchangeContractAddress = this.getContractAddress(); + const logs = await this._getLogsAsync<ArgsType>( + exchangeContractAddress, + eventName, + blockRange, + indexFilterValues, + artifacts.Exchange.abi, + ); + return logs; + } + /** + * Retrieves the Ethereum address of the Exchange contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the Exchange contract being used. + */ + public getContractAddress(): string { + const contractAddress = this._getContractAddress(artifacts.Exchange, this._contractAddressIfExists); + return contractAddress; + } + /** + * Checks if order is still fillable and throws an error otherwise. Useful for orderbook + * pruning where you want to remove stale orders without knowing who the taker will be. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to validate. + * @param opts An object that conforms to the ValidateOrderFillableOpts + * interface. Allows specifying a specific fillTakerTokenAmount + * to validate for. + */ + public async validateOrderFillableOrThrowAsync( + signedOrder: SignedOrder, + opts?: ValidateOrderFillableOpts, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + const zrxTokenAddress = this.getZRXTokenAddress(); + const expectedFillTakerTokenAmount = !_.isUndefined(opts) ? opts.expectedFillTakerTokenAmount : undefined; + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + await this._orderValidationUtils.validateOrderFillableOrThrowAsync( + exchangeTradeEmulator, + signedOrder, + zrxTokenAddress, + expectedFillTakerTokenAmount, + ); + } + /** + * Checks if order fill will succeed and throws an error otherwise. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Provider passed to 0x.js. + */ + public async validateFillOrderThrowIfInvalidAsync( + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + await this._orderValidationUtils.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + /** + * Checks if cancelling a given order will succeed and throws an informative error if it won't. + * @param order An object that conforms to the Order or SignedOrder interface. + * The order you would like to cancel. + * @param cancelTakerTokenAmount The amount (specified in taker tokens) that you would like to cancel. + */ + public async validateCancelOrderThrowIfInvalidAsync( + order: Order, + cancelTakerTokenAmount: BigNumber, + ): Promise<void> { + assert.doesConformToSchema('order', order, schemas.orderSchema); + assert.isValidBaseUnitAmount('cancelTakerTokenAmount', cancelTakerTokenAmount); + const orderHash = getOrderHashHex(order); + const unavailableTakerTokenAmount = await this.getUnavailableTakerAmountAsync(orderHash); + OrderValidationUtils.validateCancelOrderThrowIfInvalid( + order, + cancelTakerTokenAmount, + unavailableTakerTokenAmount, + ); + } + /** + * Checks if calling fillOrKill on a given order will succeed and throws an informative error if it won't. + * @param signedOrder An object that conforms to the SignedOrder interface. The + * signedOrder you wish to fill. + * @param fillTakerTokenAmount The total amount of the takerTokens you would like to fill. + * @param takerAddress The user Ethereum address who would like to fill this order. + * Must be available via the supplied Provider passed to 0x.js. + */ + public async validateFillOrKillOrderThrowIfInvalidAsync( + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + ): Promise<void> { + assert.doesConformToSchema('signedOrder', signedOrder, schemas.signedOrderSchema); + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + await assert.isSenderAddressAsync('takerAddress', takerAddress, this._web3Wrapper); + const normalizedTakerAddress = takerAddress.toLowerCase(); + const zrxTokenAddress = this.getZRXTokenAddress(); + const exchangeTradeEmulator = new ExchangeTransferSimulator(this._tokenWrapper, BlockParamLiteral.Latest); + await this._orderValidationUtils.validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + normalizedTakerAddress, + zrxTokenAddress, + ); + } + /** + * Checks if rounding error will be > 0.1% when computing makerTokenAmount by doing: + * `(fillTakerTokenAmount * makerTokenAmount) / takerTokenAmount`. + * 0x Protocol does not accept any trades that result in large rounding errors. This means that tokens with few or + * no decimals can only be filled in quantities and ratios that avoid large rounding errors. + * @param fillTakerTokenAmount The amount of the order (in taker tokens baseUnits) that you wish to fill. + * @param takerTokenAmount The order size on the taker side + * @param makerTokenAmount The order size on the maker side + */ + public async isRoundingErrorAsync( + fillTakerTokenAmount: BigNumber, + takerTokenAmount: BigNumber, + makerTokenAmount: BigNumber, + ): Promise<boolean> { + assert.isValidBaseUnitAmount('fillTakerTokenAmount', fillTakerTokenAmount); + assert.isValidBaseUnitAmount('takerTokenAmount', takerTokenAmount); + assert.isValidBaseUnitAmount('makerTokenAmount', makerTokenAmount); + const exchangeInstance = await this._getExchangeContractAsync(); + const isRoundingError = await exchangeInstance.isRoundingError.callAsync( + fillTakerTokenAmount, + takerTokenAmount, + makerTokenAmount, + ); + return isRoundingError; + } + /** + * Checks if logs contain LogError, which is emitted by Exchange contract on transaction failure. + * @param logs Transaction logs as returned by `zeroEx.awaitTransactionMinedAsync` + */ + public throwLogErrorsAsErrors(logs: Array<LogWithDecodedArgs<DecodedLogArgs> | LogEntry>): void { + const errLog = _.find(logs, { + event: ExchangeEvents.LogError, + }); + if (!_.isUndefined(errLog)) { + const logArgs = (errLog as LogWithDecodedArgs<LogErrorContractEventArgs>).args; + const errCode = logArgs.errorId; + const errMessage = this._exchangeContractErrCodesToMsg[errCode]; + throw new Error(errMessage); + } + } + /** + * Gets the latest OrderState of a signedOrder + * @param signedOrder The signedOrder + * @param stateLayer Optional, desired blockchain state layer (defaults to latest). + * @return OrderState of the signedOrder + */ + public async getOrderStateAsync( + signedOrder: SignedOrder, + stateLayer: BlockParamLiteral = BlockParamLiteral.Latest, + ): Promise<OrderState> { + const simpleBalanceAndProxyAllowanceFetcher = new SimpleBalanceAndProxyAllowanceFetcher( + this._tokenWrapper, + stateLayer, + ); + const simpleOrderFilledCancelledFetcher = new SimpleOrderFilledCancelledFetcher(this, stateLayer); + const orderStateUtils = new OrderStateUtils( + simpleBalanceAndProxyAllowanceFetcher, + simpleOrderFilledCancelledFetcher, + ); + const orderState = orderStateUtils.getOrderStateAsync(signedOrder); + return orderState; + } + /** + * Returns the ZRX token address used by the exchange contract. + * @return Address of ZRX token + */ + public getZRXTokenAddress(): string { + const contractAddress = this._getContractAddress(artifacts.ZRX, this._zrxContractAddressIfExists); + return contractAddress; + } + private _invalidateContractInstances(): void { + this.unsubscribeAll(); + delete this._exchangeContractIfExists; + } + private async _isValidSignatureUsingContractCallAsync( + dataHex: string, + ecSignature: ECSignature, + signerAddressHex: string, + ): Promise<boolean> { + assert.isHexString('dataHex', dataHex); + assert.doesConformToSchema('ecSignature', ecSignature, schemas.ecSignatureSchema); + assert.isETHAddressHex('signerAddressHex', signerAddressHex); + const normalizedSignerAddress = signerAddressHex.toLowerCase(); + + const exchangeInstance = await this._getExchangeContractAsync(); + + const isValidSignature = await exchangeInstance.isValidSignature.callAsync( + normalizedSignerAddress, + dataHex, + ecSignature.v, + ecSignature.r, + ecSignature.s, + ); + return isValidSignature; + } + private async _getOrderHashHexUsingContractCallAsync(order: Order | SignedOrder): Promise<string> { + const exchangeInstance = await this._getExchangeContractAsync(); + const [orderAddresses, orderValues] = formatters.getOrderAddressesAndValues(order); + const orderHashHex = await exchangeInstance.getOrderHash.callAsync(orderAddresses, orderValues); + return orderHashHex; + } + private async _getExchangeContractAsync(): Promise<ExchangeContract> { + if (!_.isUndefined(this._exchangeContractIfExists)) { + return this._exchangeContractIfExists; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.Exchange, + this._contractAddressIfExists, + ); + const contractInstance = new ExchangeContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + this._exchangeContractIfExists = contractInstance; + return this._exchangeContractIfExists; + } +} // tslint:disable:max-file-line-count diff --git a/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts new file mode 100644 index 000000000..e75973658 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/token_registry_wrapper.ts @@ -0,0 +1,131 @@ +import { Token } from '@0xproject/types'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { TokenMetadata } from '../types'; +import { assert } from '../utils/assert'; +import { constants } from '../utils/constants'; + +import { ContractWrapper } from './contract_wrapper'; +import { TokenRegistryContract } from './generated/token_registry'; + +/** + * This class includes all the functionality related to interacting with the 0x Token Registry smart contract. + */ +export class TokenRegistryWrapper extends ContractWrapper { + private _tokenRegistryContractIfExists?: TokenRegistryContract; + private _contractAddressIfExists?: string; + private static _createTokenFromMetadata(metadata: TokenMetadata): Token | undefined { + if (metadata[0] === constants.NULL_ADDRESS) { + return undefined; + } + const token = { + address: metadata[0], + name: metadata[1], + symbol: metadata[2], + decimals: metadata[3], + }; + return token; + } + constructor(web3Wrapper: Web3Wrapper, networkId: number, contractAddressIfExists?: string) { + super(web3Wrapper, networkId); + this._contractAddressIfExists = contractAddressIfExists; + } + /** + * Retrieves all the tokens currently listed in the Token Registry smart contract + * @return An array of objects that conform to the Token interface. + */ + public async getTokensAsync(): Promise<Token[]> { + const addresses = await this.getTokenAddressesAsync(); + const tokenPromises: Array<Promise<Token | undefined>> = _.map(addresses, async (address: string) => + this.getTokenIfExistsAsync(address), + ); + const tokens = await Promise.all(tokenPromises); + return tokens as Token[]; + } + /** + * Retrieves all the addresses of the tokens currently listed in the Token Registry smart contract + * @return An array of token addresses. + */ + public async getTokenAddressesAsync(): Promise<string[]> { + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addresses = await tokenRegistryContract.getTokenAddresses.callAsync(); + const lowerCaseAddresses = _.map(addresses, address => address.toLowerCase()); + return lowerCaseAddresses; + } + /** + * Retrieves a token by address currently listed in the Token Registry smart contract + * @return An object that conforms to the Token interface or undefined if token not found. + */ + public async getTokenIfExistsAsync(address: string): Promise<Token | undefined> { + assert.isETHAddressHex('address', address); + const normalizedAddress = address.toLowerCase(); + + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenMetaData.callAsync(normalizedAddress); + const token = TokenRegistryWrapper._createTokenFromMetadata(metadata); + return token; + } + public async getTokenAddressBySymbolIfExistsAsync(symbol: string): Promise<string | undefined> { + assert.isString('symbol', symbol); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addressIfExists = await tokenRegistryContract.getTokenAddressBySymbol.callAsync(symbol); + if (addressIfExists === constants.NULL_ADDRESS) { + return undefined; + } + return addressIfExists; + } + public async getTokenAddressByNameIfExistsAsync(name: string): Promise<string | undefined> { + assert.isString('name', name); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const addressIfExists = await tokenRegistryContract.getTokenAddressByName.callAsync(name); + if (addressIfExists === constants.NULL_ADDRESS) { + return undefined; + } + return addressIfExists; + } + public async getTokenBySymbolIfExistsAsync(symbol: string): Promise<Token | undefined> { + assert.isString('symbol', symbol); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenBySymbol.callAsync(symbol); + const token = TokenRegistryWrapper._createTokenFromMetadata(metadata); + return token; + } + public async getTokenByNameIfExistsAsync(name: string): Promise<Token | undefined> { + assert.isString('name', name); + const tokenRegistryContract = await this._getTokenRegistryContractAsync(); + const metadata = await tokenRegistryContract.getTokenByName.callAsync(name); + const token = TokenRegistryWrapper._createTokenFromMetadata(metadata); + return token; + } + /** + * Retrieves the Ethereum address of the TokenRegistry contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the TokenRegistry contract being used. + */ + public getContractAddress(): string { + const contractAddress = this._getContractAddress(artifacts.TokenRegistry, this._contractAddressIfExists); + return contractAddress; + } + private _invalidateContractInstance(): void { + delete this._tokenRegistryContractIfExists; + } + private async _getTokenRegistryContractAsync(): Promise<TokenRegistryContract> { + if (!_.isUndefined(this._tokenRegistryContractIfExists)) { + return this._tokenRegistryContractIfExists; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.TokenRegistry, + this._contractAddressIfExists, + ); + const contractInstance = new TokenRegistryContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + this._tokenRegistryContractIfExists = contractInstance; + return this._tokenRegistryContractIfExists; + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts new file mode 100644 index 000000000..02a2e19d0 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/token_transfer_proxy_wrapper.ts @@ -0,0 +1,72 @@ +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { artifacts } from '../artifacts'; +import { assert } from '../utils/assert'; + +import { ContractWrapper } from './contract_wrapper'; +import { TokenTransferProxyContract } from './generated/token_transfer_proxy'; + +/** + * This class includes the functionality related to interacting with the TokenTransferProxy contract. + */ +export class TokenTransferProxyWrapper extends ContractWrapper { + private _tokenTransferProxyContractIfExists?: TokenTransferProxyContract; + 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 TokenTransferProxy 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 tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync(); + const isAuthorized = await tokenTransferProxyContractInstance.authorized.callAsync( + normalizedExchangeContractAddress, + ); + return isAuthorized; + } + /** + * Get the list of all Exchange contract addresses authorized by the TokenTransferProxy contract. + * @return The list of authorized addresses. + */ + public async getAuthorizedAddressesAsync(): Promise<string[]> { + const tokenTransferProxyContractInstance = await this._getTokenTransferProxyContractAsync(); + const authorizedAddresses = await tokenTransferProxyContractInstance.getAuthorizedAddresses.callAsync(); + return authorizedAddresses; + } + /** + * Retrieves the Ethereum address of the TokenTransferProxy contract deployed on the network + * that the user-passed web3 provider is connected to. + * @returns The Ethereum address of the TokenTransferProxy contract being used. + */ + public getContractAddress(): string { + const contractAddress = this._getContractAddress(artifacts.TokenTransferProxy, this._contractAddressIfExists); + return contractAddress; + } + private _invalidateContractInstance(): void { + delete this._tokenTransferProxyContractIfExists; + } + private async _getTokenTransferProxyContractAsync(): Promise<TokenTransferProxyContract> { + if (!_.isUndefined(this._tokenTransferProxyContractIfExists)) { + return this._tokenTransferProxyContractIfExists; + } + const [abi, address] = await this._getContractAbiAndAddressFromArtifactsAsync( + artifacts.TokenTransferProxy, + this._contractAddressIfExists, + ); + const contractInstance = new TokenTransferProxyContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + this._tokenTransferProxyContractIfExists = contractInstance; + return this._tokenTransferProxyContractIfExists; + } +} diff --git a/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts b/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts new file mode 100644 index 000000000..844318c79 --- /dev/null +++ b/packages/contract-wrappers/src/contract_wrappers/token_wrapper.ts @@ -0,0 +1,441 @@ +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, + ContractWrappersError, + EventCallback, + IndexedFilterValues, + MethodOpts, + TransactionOpts, +} 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, tokenTransferProxyWrapper: TokenTransferProxyWrapper) { + super(web3Wrapper, networkId); + 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(ContractWrappersError.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(ContractWrappersError.InsufficientAllowanceForTransfer); + } + + const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress); + if (fromAddressBalance.lessThan(amountInBaseUnits)) { + throw new Error(ContractWrappersError.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.Token.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.Token.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.Token, + normalizedTokenAddress, + ); + const contractInstance = new TokenContract( + abi, + address, + this._web3Wrapper.getProvider(), + this._web3Wrapper.getContractDefaults(), + ); + tokenContract = contractInstance; + this._tokenContractsByAddress[normalizedTokenAddress] = tokenContract; + return tokenContract; + } +} diff --git a/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts b/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts new file mode 100644 index 000000000..5017d879a --- /dev/null +++ b/packages/contract-wrappers/src/fetchers/simple_balance_and_proxy_allowance_fetcher.ts @@ -0,0 +1,28 @@ +import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils'; +import { BlockParamLiteral } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { TokenWrapper } from '../contract_wrappers/token_wrapper'; + +export class SimpleBalanceAndProxyAllowanceFetcher implements AbstractBalanceAndProxyAllowanceFetcher { + private _tokenWrapper: TokenWrapper; + private _defaultBlock: BlockParamLiteral; + constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) { + this._tokenWrapper = token; + this._defaultBlock = defaultBlock; + } + public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const balance = this._tokenWrapper.getBalanceAsync(tokenAddress, userAddress, methodOpts); + return balance; + } + public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const proxyAllowance = this._tokenWrapper.getProxyAllowanceAsync(tokenAddress, userAddress, methodOpts); + return proxyAllowance; + } +} diff --git a/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts b/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts new file mode 100644 index 000000000..52d0a100c --- /dev/null +++ b/packages/contract-wrappers/src/fetchers/simple_order_filled_cancelled_fetcher.ts @@ -0,0 +1,36 @@ +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BlockParamLiteral } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper'; + +export class SimpleOrderFilledCancelledFetcher implements AbstractOrderFilledCancelledFetcher { + private _exchangeWrapper: ExchangeWrapper; + private _defaultBlock: BlockParamLiteral; + constructor(exchange: ExchangeWrapper, defaultBlock: BlockParamLiteral) { + this._exchangeWrapper = exchange; + this._defaultBlock = defaultBlock; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const filledTakerAmount = this._exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts); + return filledTakerAmount; + } + public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const cancelledTakerAmount = this._exchangeWrapper.getCancelledTakerAmountAsync(orderHash, methodOpts); + return cancelledTakerAmount; + } + public async getUnavailableTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const unavailableTakerAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + return unavailableTakerAmount; + } + public getZRXTokenAddress(): string { + const zrxToken = this._exchangeWrapper.getZRXTokenAddress(); + return zrxToken; + } +} diff --git a/packages/contract-wrappers/src/globals.d.ts b/packages/contract-wrappers/src/globals.d.ts new file mode 100644 index 000000000..94e63a32d --- /dev/null +++ b/packages/contract-wrappers/src/globals.d.ts @@ -0,0 +1,6 @@ +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts new file mode 100644 index 000000000..45af8a707 --- /dev/null +++ b/packages/contract-wrappers/src/index.ts @@ -0,0 +1,68 @@ +export { ContractWrappers } from './contract_wrappers'; +export { ExchangeWrapper } from './contract_wrappers/exchange_wrapper'; +export { TokenWrapper } from './contract_wrappers/token_wrapper'; +export { TokenRegistryWrapper } from './contract_wrappers/token_registry_wrapper'; +export { EtherTokenWrapper } from './contract_wrappers/ether_token_wrapper'; +export { TokenTransferProxyWrapper } from './contract_wrappers/token_transfer_proxy_wrapper'; + +export { + ContractWrappersError, + EventCallback, + ContractEvent, + Token, + IndexedFilterValues, + BlockRange, + OrderCancellationRequest, + OrderFillRequest, + ContractEventArgs, + ContractWrappersConfig, + MethodOpts, + OrderTransactionOpts, + TransactionOpts, + LogEvent, + DecodedLogEvent, + OnOrderStateChangeCallback, +} from './types'; + +export { + BlockParamLiteral, + FilterObject, + BlockParam, + ContractEventArg, + ExchangeContractErrs, + LogWithDecodedArgs, + Order, + Provider, + SignedOrder, + ECSignature, + OrderStateValid, + OrderStateInvalid, + OrderState, + TransactionReceipt, + TransactionReceiptWithDecodedLogs, +} from '@0xproject/types'; + +export { + EtherTokenContractEventArgs, + WithdrawalContractEventArgs, + DepositContractEventArgs, + EtherTokenEvents, +} from './contract_wrappers/generated/ether_token'; + +export { + TransferContractEventArgs, + ApprovalContractEventArgs, + TokenContractEventArgs, + TokenEvents, +} from './contract_wrappers/generated/token'; + +export { + LogErrorContractEventArgs, + LogCancelContractEventArgs, + LogFillContractEventArgs, + ExchangeContractEventArgs, + ExchangeEvents, +} from './contract_wrappers/generated/exchange'; + +export { BalanceAndProxyAllowanceLazyStore } from './stores/balance_proxy_allowance_lazy_store'; +export { OrderFilledCancelledLazyStore } from './stores/order_filled_cancelled_lazy_store'; diff --git a/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts b/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts new file mode 100644 index 000000000..dcb99d0f7 --- /dev/null +++ b/packages/contract-wrappers/src/monorepo_scripts/postpublish.ts @@ -0,0 +1,8 @@ +import { postpublishUtils } from '@0xproject/monorepo-scripts'; + +import * as packageJSON from '../package.json'; +import * as tsConfigJSON from '../tsconfig.json'; + +const cwd = `${__dirname}/..`; +// tslint:disable-next-line:no-floating-promises +postpublishUtils.runAsync(packageJSON, tsConfigJSON, cwd); diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts new file mode 100644 index 000000000..ac248b2d4 --- /dev/null +++ b/packages/contract-wrappers/src/schemas/contract_wrappers_config_schema.ts @@ -0,0 +1,5 @@ +export const ContractWrappersConfigSchema = { + id: '/ContractWrappersConfig', + oneOf: [{ $ref: '/ZeroExContractPrivateNetworkConfig' }, { $ref: '/ZeroExContractPublicNetworkConfig' }], + type: 'object', +}; diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts new file mode 100644 index 000000000..759078feb --- /dev/null +++ b/packages/contract-wrappers/src/schemas/contract_wrappers_private_network_config_schema.ts @@ -0,0 +1,35 @@ +export const contractWrappersPrivateNetworkConfigSchema = { + id: '/ZeroExContractPrivateNetworkConfig', + properties: { + networkId: { + type: 'number', + minimum: 1, + }, + gasPrice: { $ref: '/Number' }, + zrxContractAddress: { $ref: '/Address' }, + exchangeContractAddress: { $ref: '/Address' }, + tokenRegistryContractAddress: { $ref: '/Address' }, + tokenTransferProxyContractAddress: { $ref: '/Address' }, + orderWatcherConfig: { + type: 'object', + properties: { + pollingIntervalMs: { + type: 'number', + minimum: 0, + }, + numConfirmations: { + type: 'number', + minimum: 0, + }, + }, + }, + }, + type: 'object', + required: [ + 'networkId', + 'zrxContractAddress', + 'exchangeContractAddress', + 'tokenRegistryContractAddress', + 'tokenTransferProxyContractAddress', + ], +}; diff --git a/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts new file mode 100644 index 000000000..bf1000afa --- /dev/null +++ b/packages/contract-wrappers/src/schemas/contract_wrappers_public_network_config_schema.ts @@ -0,0 +1,29 @@ +export const contractWrappersPublicNetworkConfigSchema = { + id: '/ZeroExContractPublicNetworkConfig', + properties: { + networkId: { + type: 'number', + enum: [1, 3, 4, 42, 50], + }, + gasPrice: { $ref: '/Number' }, + zrxContractAddress: { $ref: '/Address' }, + exchangeContractAddress: { $ref: '/Address' }, + tokenRegistryContractAddress: { $ref: '/Address' }, + tokenTransferProxyContractAddress: { $ref: '/Address' }, + orderWatcherConfig: { + type: 'object', + properties: { + pollingIntervalMs: { + type: 'number', + minimum: 0, + }, + numConfirmations: { + type: 'number', + minimum: 0, + }, + }, + }, + }, + type: 'object', + required: ['networkId'], +}; diff --git a/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts b/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts new file mode 100644 index 000000000..614195157 --- /dev/null +++ b/packages/contract-wrappers/src/stores/balance_proxy_allowance_lazy_store.ts @@ -0,0 +1,91 @@ +import { AbstractBalanceAndProxyAllowanceFetcher } from '@0xproject/order-utils'; +import { BlockParamLiteral } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { TokenWrapper } from '../contract_wrappers/token_wrapper'; + +/** + * Copy on read store for balances/proxyAllowances of tokens/accounts + */ +export class BalanceAndProxyAllowanceLazyStore implements AbstractBalanceAndProxyAllowanceFetcher { + private _tokenWrapper: TokenWrapper; + private _defaultBlock: BlockParamLiteral; + private _balance: { + [tokenAddress: string]: { + [userAddress: string]: BigNumber; + }; + }; + private _proxyAllowance: { + [tokenAddress: string]: { + [userAddress: string]: BigNumber; + }; + }; + constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) { + this._tokenWrapper = token; + this._defaultBlock = defaultBlock; + this._balance = {}; + this._proxyAllowance = {}; + } + public async getBalanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> { + if (_.isUndefined(this._balance[tokenAddress]) || _.isUndefined(this._balance[tokenAddress][userAddress])) { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const balance = await this._tokenWrapper.getBalanceAsync(tokenAddress, userAddress, methodOpts); + this.setBalance(tokenAddress, userAddress, balance); + } + const cachedBalance = this._balance[tokenAddress][userAddress]; + return cachedBalance; + } + public setBalance(tokenAddress: string, userAddress: string, balance: BigNumber): void { + if (_.isUndefined(this._balance[tokenAddress])) { + this._balance[tokenAddress] = {}; + } + this._balance[tokenAddress][userAddress] = balance; + } + public deleteBalance(tokenAddress: string, userAddress: string): void { + if (!_.isUndefined(this._balance[tokenAddress])) { + delete this._balance[tokenAddress][userAddress]; + if (_.isEmpty(this._balance[tokenAddress])) { + delete this._balance[tokenAddress]; + } + } + } + public async getProxyAllowanceAsync(tokenAddress: string, userAddress: string): Promise<BigNumber> { + if ( + _.isUndefined(this._proxyAllowance[tokenAddress]) || + _.isUndefined(this._proxyAllowance[tokenAddress][userAddress]) + ) { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const proxyAllowance = await this._tokenWrapper.getProxyAllowanceAsync( + tokenAddress, + userAddress, + methodOpts, + ); + this.setProxyAllowance(tokenAddress, userAddress, proxyAllowance); + } + const cachedProxyAllowance = this._proxyAllowance[tokenAddress][userAddress]; + return cachedProxyAllowance; + } + public setProxyAllowance(tokenAddress: string, userAddress: string, proxyAllowance: BigNumber): void { + if (_.isUndefined(this._proxyAllowance[tokenAddress])) { + this._proxyAllowance[tokenAddress] = {}; + } + this._proxyAllowance[tokenAddress][userAddress] = proxyAllowance; + } + public deleteProxyAllowance(tokenAddress: string, userAddress: string): void { + if (!_.isUndefined(this._proxyAllowance[tokenAddress])) { + delete this._proxyAllowance[tokenAddress][userAddress]; + if (_.isEmpty(this._proxyAllowance[tokenAddress])) { + delete this._proxyAllowance[tokenAddress]; + } + } + } + public deleteAll(): void { + this._balance = {}; + this._proxyAllowance = {}; + } +} diff --git a/packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts b/packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts new file mode 100644 index 000000000..3d96a95f3 --- /dev/null +++ b/packages/contract-wrappers/src/stores/order_filled_cancelled_lazy_store.ts @@ -0,0 +1,75 @@ +import { AbstractOrderFilledCancelledFetcher } from '@0xproject/order-utils'; +import { BlockParamLiteral } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper'; + +/** + * Copy on read store for filled/cancelled taker amounts + */ +export class OrderFilledCancelledLazyStore implements AbstractOrderFilledCancelledFetcher { + private _exchangeWrapper: ExchangeWrapper; + private _defaultBlock: BlockParamLiteral; + private _filledTakerAmount: { + [orderHash: string]: BigNumber; + }; + private _cancelledTakerAmount: { + [orderHash: string]: BigNumber; + }; + constructor(exchange: ExchangeWrapper, defaultBlock: BlockParamLiteral) { + this._exchangeWrapper = exchange; + this._defaultBlock = defaultBlock; + this._filledTakerAmount = {}; + this._cancelledTakerAmount = {}; + } + public async getFilledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + if (_.isUndefined(this._filledTakerAmount[orderHash])) { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const filledTakerAmount = await this._exchangeWrapper.getFilledTakerAmountAsync(orderHash, methodOpts); + this.setFilledTakerAmount(orderHash, filledTakerAmount); + } + const cachedFilled = this._filledTakerAmount[orderHash]; + return cachedFilled; + } + public setFilledTakerAmount(orderHash: string, filledTakerAmount: BigNumber): void { + this._filledTakerAmount[orderHash] = filledTakerAmount; + } + public deleteFilledTakerAmount(orderHash: string): void { + delete this._filledTakerAmount[orderHash]; + } + public async getCancelledTakerAmountAsync(orderHash: string): Promise<BigNumber> { + if (_.isUndefined(this._cancelledTakerAmount[orderHash])) { + const methodOpts = { + defaultBlock: this._defaultBlock, + }; + const cancelledTakerAmount = await this._exchangeWrapper.getCancelledTakerAmountAsync( + orderHash, + methodOpts, + ); + this.setCancelledTakerAmount(orderHash, cancelledTakerAmount); + } + const cachedCancelled = this._cancelledTakerAmount[orderHash]; + return cachedCancelled; + } + public setCancelledTakerAmount(orderHash: string, cancelledTakerAmount: BigNumber): void { + this._cancelledTakerAmount[orderHash] = cancelledTakerAmount; + } + public deleteCancelledTakerAmount(orderHash: string): void { + delete this._cancelledTakerAmount[orderHash]; + } + public deleteAll(): void { + this._filledTakerAmount = {}; + this._cancelledTakerAmount = {}; + } + public async getUnavailableTakerAmountAsync(orderHash: string): Promise<BigNumber> { + const unavailableTakerAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + return unavailableTakerAmount; + } + public getZRXTokenAddress(): string { + const zrxToken = this._exchangeWrapper.getZRXTokenAddress(); + return zrxToken; + } +} diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts new file mode 100644 index 000000000..9a463d635 --- /dev/null +++ b/packages/contract-wrappers/src/types.ts @@ -0,0 +1,186 @@ +import { BigNumber } from '@0xproject/utils'; + +import { + BlockParam, + BlockParamLiteral, + ContractAbi, + ContractEventArg, + ExchangeContractErrs, + FilterObject, + LogEntryEvent, + LogWithDecodedArgs, + Order, + OrderState, + SignedOrder, +} from '@0xproject/types'; + +import { EtherTokenContractEventArgs, EtherTokenEvents } from './contract_wrappers/generated/ether_token'; +import { ExchangeContractEventArgs, ExchangeEvents } from './contract_wrappers/generated/exchange'; +import { TokenContractEventArgs, TokenEvents } from './contract_wrappers/generated/token'; + +export enum ContractWrappersError { + ExchangeContractDoesNotExist = 'EXCHANGE_CONTRACT_DOES_NOT_EXIST', + ZRXContractDoesNotExist = 'ZRX_CONTRACT_DOES_NOT_EXIST', + EtherTokenContractDoesNotExist = 'ETHER_TOKEN_CONTRACT_DOES_NOT_EXIST', + TokenTransferProxyContractDoesNotExist = 'TOKEN_TRANSFER_PROXY_CONTRACT_DOES_NOT_EXIST', + TokenRegistryContractDoesNotExist = 'TOKEN_REGISTRY_CONTRACT_DOES_NOT_EXIST', + TokenContractDoesNotExist = 'TOKEN_CONTRACT_DOES_NOT_EXIST', + ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', + InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', + InsufficientBalanceForTransfer = 'INSUFFICIENT_BALANCE_FOR_TRANSFER', + InsufficientEthBalanceForDeposit = 'INSUFFICIENT_ETH_BALANCE_FOR_DEPOSIT', + InsufficientWEthBalanceForWithdrawal = 'INSUFFICIENT_WETH_BALANCE_FOR_WITHDRAWAL', + InvalidJump = 'INVALID_JUMP', + OutOfGas = 'OUT_OF_GAS', + SubscriptionNotFound = 'SUBSCRIPTION_NOT_FOUND', + SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', +} + +export enum InternalContractWrappersError { + NoAbiDecoder = 'NO_ABI_DECODER', + ZrxNotInTokenRegistry = 'ZRX_NOT_IN_TOKEN_REGISTRY', + WethNotInTokenRegistry = 'WETH_NOT_IN_TOKEN_REGISTRY', +} + +export type LogEvent = LogEntryEvent; +export interface DecodedLogEvent<ArgsType> { + isRemoved: boolean; + log: LogWithDecodedArgs<ArgsType>; +} + +export type EventCallback<ArgsType> = (err: null | Error, log?: DecodedLogEvent<ArgsType>) => void; + +export enum ExchangeContractErrCodes { + ERROR_FILL_EXPIRED, // Order has already expired + ERROR_FILL_NO_VALUE, // Order has already been fully filled or cancelled + ERROR_FILL_TRUNCATION, // Rounding error too large + ERROR_FILL_BALANCE_ALLOWANCE, // Insufficient balance or allowance for token transfer + ERROR_CANCEL_EXPIRED, // Order has already expired + ERROR_CANCEL_NO_VALUE, // Order has already been fully filled or cancelled +} + +export interface ContractEvent { + logIndex: number; + transactionIndex: number; + transactionHash: string; + blockHash: string; + blockNumber: number; + address: string; + type: string; + event: string; + args: ContractEventArgs; +} + +export type ContractEventArgs = ExchangeContractEventArgs | TokenContractEventArgs | EtherTokenContractEventArgs; + +// [address, name, symbol, decimals, ipfsHash, swarmHash] +export type TokenMetadata = [string, string, string, number, string, string]; + +export interface Token { + name: string; + address: string; + symbol: string; + decimals: number; +} + +export interface TxOpts { + from: string; + gas?: number; + value?: BigNumber; + gasPrice?: BigNumber; +} + +export interface TokenAddressBySymbol { + [symbol: string]: string; +} + +export type ContractEvents = TokenEvents | ExchangeEvents | EtherTokenEvents; + +export interface IndexedFilterValues { + [index: string]: ContractEventArg; +} + +export interface BlockRange { + fromBlock: BlockParam; + toBlock: BlockParam; +} + +export interface OrderCancellationRequest { + order: Order | SignedOrder; + takerTokenCancelAmount: BigNumber; +} + +export interface OrderFillRequest { + signedOrder: SignedOrder; + takerTokenFillAmount: BigNumber; +} + +export type AsyncMethod = (...args: any[]) => Promise<any>; +export type SyncMethod = (...args: any[]) => any; + +/** + * networkId: The id of the underlying ethereum network your provider is connected to. (1-mainnet, 3-ropsten, 4-rinkeby, 42-kovan, 50-testrpc) + * gasPrice: Gas price to use with every transaction + * exchangeContractAddress: The address of an exchange contract to use + * zrxContractAddress: The address of the ZRX contract to use + * tokenRegistryContractAddress: The address of a token registry contract to use + * tokenTransferProxyContractAddress: The address of the token transfer proxy contract to use + * orderWatcherConfig: All the configs related to the orderWatcher + */ +export interface ContractWrappersConfig { + networkId: number; + gasPrice?: BigNumber; + exchangeContractAddress?: string; + zrxContractAddress?: string; + tokenRegistryContractAddress?: string; + tokenTransferProxyContractAddress?: string; +} + +/** + * expectedFillTakerTokenAmount: If specified, the validation method will ensure that the + * supplied order maker has a sufficient allowance/balance to fill this amount of the order's + * takerTokenAmount. If not specified, the validation method ensures that the maker has a sufficient + * allowance/balance to fill the entire remaining order amount. + */ +export interface ValidateOrderFillableOpts { + expectedFillTakerTokenAmount?: BigNumber; +} + +/** + * defaultBlock: The block up to which to query the blockchain state. Setting this to a historical block number + * let's the user query the blockchain's state at an arbitrary point in time. In order for this to work, the + * backing Ethereum node must keep the entire historical state of the chain (e.g setting `--pruning=archive` + * flag when running Parity). + */ +export interface MethodOpts { + defaultBlock?: BlockParam; +} + +/** + * gasPrice: Gas price in Wei to use for a transaction + * gasLimit: The amount of gas to send with a transaction + */ +export interface TransactionOpts { + gasPrice?: BigNumber; + gasLimit?: number; +} + +/** + * shouldValidate: Flag indicating whether the library should make attempts to validate a transaction before + * broadcasting it. For example, order has a valid signature, maker has sufficient funds, etc. Default=true. + */ +export interface OrderTransactionOpts extends TransactionOpts { + shouldValidate?: boolean; +} + +export enum TradeSide { + Maker = 'maker', + Taker = 'taker', +} + +export enum TransferType { + Trade = 'trade', + Fee = 'fee', +} + +export type OnOrderStateChangeCallback = (err: Error | null, orderState?: OrderState) => void; diff --git a/packages/contract-wrappers/src/utils/assert.ts b/packages/contract-wrappers/src/utils/assert.ts new file mode 100644 index 000000000..2588a4d09 --- /dev/null +++ b/packages/contract-wrappers/src/utils/assert.ts @@ -0,0 +1,31 @@ +import { assert as sharedAssert } from '@0xproject/assert'; +// We need those two unused imports because they're actually used by sharedAssert which gets injected here +// tslint:disable-next-line:no-unused-variable +import { Schema } from '@0xproject/json-schemas'; +// tslint:disable-next-line:no-unused-variable +import { ECSignature } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as _ from 'lodash'; + +import { isValidSignature } from '@0xproject/order-utils'; + +export const assert = { + ...sharedAssert, + isValidSignature(orderHash: string, ecSignature: ECSignature, signerAddress: string) { + const isValid = isValidSignature(orderHash, ecSignature, signerAddress); + this.assert(isValid, `Expected order with hash '${orderHash}' to have a valid signature`); + }, + async isSenderAddressAsync( + variableName: string, + senderAddressHex: string, + web3Wrapper: Web3Wrapper, + ): Promise<void> { + sharedAssert.isETHAddressHex(variableName, senderAddressHex); + const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex); + sharedAssert.assert( + isSenderAddressAvailable, + `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, + ); + }, +}; diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts new file mode 100644 index 000000000..07da6745d --- /dev/null +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -0,0 +1,11 @@ +import { BigNumber } from '@0xproject/utils'; + +export const constants = { + NULL_ADDRESS: '0x0000000000000000000000000000000000000000', + TESTRPC_NETWORK_ID: 50, + INVALID_JUMP_PATTERN: 'invalid JUMP at', + OUT_OF_GAS_PATTERN: 'out of gas', + INVALID_TAKER_FORMAT: 'instance.taker is not of a type(s) string', + UNLIMITED_ALLOWANCE_IN_BASE_UNITS: new BigNumber(2).pow(256).minus(1), + DEFAULT_BLOCK_POLLING_INTERVAL: 1000, +}; diff --git a/packages/contract-wrappers/src/utils/decorators.ts b/packages/contract-wrappers/src/utils/decorators.ts new file mode 100644 index 000000000..64123143c --- /dev/null +++ b/packages/contract-wrappers/src/utils/decorators.ts @@ -0,0 +1,91 @@ +import * as _ from 'lodash'; + +import { AsyncMethod, ContractWrappersError, SyncMethod } from '../types'; + +import { constants } from './constants'; + +type ErrorTransformer = (err: Error) => Error; + +const contractCallErrorTransformer = (error: Error) => { + if (_.includes(error.message, constants.INVALID_JUMP_PATTERN)) { + return new Error(ContractWrappersError.InvalidJump); + } + if (_.includes(error.message, constants.OUT_OF_GAS_PATTERN)) { + return new Error(ContractWrappersError.OutOfGas); + } + return error; +}; + +const schemaErrorTransformer = (error: Error) => { + if (_.includes(error.message, constants.INVALID_TAKER_FORMAT)) { + const errMsg = + 'Order taker must be of type string. If you want anyone to be able to fill an order - pass ZeroEx.NULL_ADDRESS'; + return new Error(errMsg); + } + return error; +}; + +/** + * Source: https://stackoverflow.com/a/29837695/3546986 + */ +const asyncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => { + const asyncErrorHandlingDecorator = ( + target: object, + key: string | symbol, + descriptor: TypedPropertyDescriptor<AsyncMethod>, + ) => { + const originalMethod = descriptor.value as AsyncMethod; + + // Do not use arrow syntax here. Use a function expression in + // order to use the correct value of `this` in this method + // tslint:disable-next-line:only-arrow-functions + descriptor.value = async function(...args: any[]) { + try { + const result = await originalMethod.apply(this, args); + return result; + } catch (error) { + const transformedError = errorTransformer(error); + throw transformedError; + } + }; + + return descriptor; + }; + + return asyncErrorHandlingDecorator; +}; + +const syncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => { + const syncErrorHandlingDecorator = ( + target: object, + key: string | symbol, + descriptor: TypedPropertyDescriptor<SyncMethod>, + ) => { + const originalMethod = descriptor.value as SyncMethod; + + // Do not use arrow syntax here. Use a function expression in + // order to use the correct value of `this` in this method + // tslint:disable-next-line:only-arrow-functions + descriptor.value = function(...args: any[]) { + try { + const result = originalMethod.apply(this, args); + return result; + } catch (error) { + const transformedError = errorTransformer(error); + throw transformedError; + } + }; + + return descriptor; + }; + + return syncErrorHandlingDecorator; +}; + +// _.flow(f, g) = f ∘ g +const zeroExErrorTransformer = _.flow(schemaErrorTransformer, contractCallErrorTransformer); + +export const decorators = { + asyncZeroExErrorHandler: asyncErrorHandlerFactory(zeroExErrorTransformer), + syncZeroExErrorHandler: syncErrorHandlerFactory(zeroExErrorTransformer), +}; diff --git a/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts new file mode 100644 index 000000000..b575285a1 --- /dev/null +++ b/packages/contract-wrappers/src/utils/exchange_transfer_simulator.ts @@ -0,0 +1,115 @@ +import { BlockParamLiteral, ExchangeContractErrs } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { TokenWrapper } from '../contract_wrappers/token_wrapper'; +import { BalanceAndProxyAllowanceLazyStore } from '../stores/balance_proxy_allowance_lazy_store'; +import { TradeSide, TransferType } from '../types'; +import { constants } from '../utils/constants'; + +enum FailureReason { + Balance = 'balance', + ProxyAllowance = 'proxyAllowance', +} + +const ERR_MSG_MAPPING = { + [FailureReason.Balance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeBalance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerBalance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeBalance, + }, + }, + [FailureReason.ProxyAllowance]: { + [TradeSide.Maker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientMakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientMakerFeeAllowance, + }, + [TradeSide.Taker]: { + [TransferType.Trade]: ExchangeContractErrs.InsufficientTakerAllowance, + [TransferType.Fee]: ExchangeContractErrs.InsufficientTakerFeeAllowance, + }, + }, +}; + +export class ExchangeTransferSimulator { + private _store: BalanceAndProxyAllowanceLazyStore; + private _UNLIMITED_ALLOWANCE_IN_BASE_UNITS: BigNumber; + private static _throwValidationError( + failureReason: FailureReason, + tradeSide: TradeSide, + transferType: TransferType, + ): never { + const errMsg = ERR_MSG_MAPPING[failureReason][tradeSide][transferType]; + throw new Error(errMsg); + } + constructor(token: TokenWrapper, defaultBlock: BlockParamLiteral) { + this._store = new BalanceAndProxyAllowanceLazyStore(token, defaultBlock); + this._UNLIMITED_ALLOWANCE_IN_BASE_UNITS = token.UNLIMITED_ALLOWANCE_IN_BASE_UNITS; + } + /** + * Simulates transferFrom call performed by a proxy + * @param tokenAddress Address of the token to be transferred + * @param from Owner of the transferred tokens + * @param to Recipient of the transferred tokens + * @param amountInBaseUnits The amount of tokens being transferred + * @param tradeSide Is Maker/Taker transferring + * @param transferType Is it a fee payment or a value transfer + */ + public async transferFromAsync( + tokenAddress: string, + from: string, + to: string, + amountInBaseUnits: BigNumber, + tradeSide: TradeSide, + transferType: TransferType, + ): Promise<void> { + // HACK: When simulating an open order (e.g taker is NULL_ADDRESS), we don't want to adjust balances/ + // allowances for the taker. We do however, want to increase the balance of the maker since the maker + // might be relying on those funds to fill subsequent orders or pay the order's fees. + if (from === constants.NULL_ADDRESS && tradeSide === TradeSide.Taker) { + await this._increaseBalanceAsync(tokenAddress, to, amountInBaseUnits); + return; + } + const balance = await this._store.getBalanceAsync(tokenAddress, from); + const proxyAllowance = await this._store.getProxyAllowanceAsync(tokenAddress, from); + if (proxyAllowance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.ProxyAllowance, tradeSide, transferType); + } + if (balance.lessThan(amountInBaseUnits)) { + ExchangeTransferSimulator._throwValidationError(FailureReason.Balance, tradeSide, transferType); + } + await this._decreaseProxyAllowanceAsync(tokenAddress, from, amountInBaseUnits); + await this._decreaseBalanceAsync(tokenAddress, from, amountInBaseUnits); + await this._increaseBalanceAsync(tokenAddress, to, amountInBaseUnits); + } + private async _decreaseProxyAllowanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const proxyAllowance = await this._store.getProxyAllowanceAsync(tokenAddress, userAddress); + if (!proxyAllowance.eq(this._UNLIMITED_ALLOWANCE_IN_BASE_UNITS)) { + this._store.setProxyAllowance(tokenAddress, userAddress, proxyAllowance.minus(amountInBaseUnits)); + } + } + private async _increaseBalanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(tokenAddress, userAddress); + this._store.setBalance(tokenAddress, userAddress, balance.plus(amountInBaseUnits)); + } + private async _decreaseBalanceAsync( + tokenAddress: string, + userAddress: string, + amountInBaseUnits: BigNumber, + ): Promise<void> { + const balance = await this._store.getBalanceAsync(tokenAddress, userAddress); + this._store.setBalance(tokenAddress, userAddress, balance.minus(amountInBaseUnits)); + } +} diff --git a/packages/contract-wrappers/src/utils/filter_utils.ts b/packages/contract-wrappers/src/utils/filter_utils.ts new file mode 100644 index 000000000..c5df7321e --- /dev/null +++ b/packages/contract-wrappers/src/utils/filter_utils.ts @@ -0,0 +1,95 @@ +import { + ConstructorAbi, + ContractAbi, + EventAbi, + FallbackAbi, + FilterObject, + LogEntry, + MethodAbi, +} from '@0xproject/types'; +import * as ethUtil from 'ethereumjs-util'; +import * as jsSHA3 from 'js-sha3'; +import * as _ from 'lodash'; +import * as uuid from 'uuid/v4'; + +import { BlockRange, ContractEvents, IndexedFilterValues } from '../types'; + +const TOPIC_LENGTH = 32; + +export const filterUtils = { + generateUUID(): string { + return uuid(); + }, + getFilter( + address: string, + eventName: ContractEvents, + indexFilterValues: IndexedFilterValues, + abi: ContractAbi, + blockRange?: BlockRange, + ): FilterObject { + const eventAbi = _.find(abi, { name: eventName }) as EventAbi; + const eventSignature = filterUtils.getEventSignatureFromAbiByName(eventAbi, eventName); + const topicForEventSignature = ethUtil.addHexPrefix(jsSHA3.keccak256(eventSignature)); + const topicsForIndexedArgs = filterUtils.getTopicsForIndexedArgs(eventAbi, indexFilterValues); + const topics = [topicForEventSignature, ...topicsForIndexedArgs]; + let filter: FilterObject = { + address, + topics, + }; + if (!_.isUndefined(blockRange)) { + filter = { + ...blockRange, + ...filter, + }; + } + return filter; + }, + getEventSignatureFromAbiByName(eventAbi: EventAbi, eventName: ContractEvents): string { + const types = _.map(eventAbi.inputs, 'type'); + const signature = `${eventAbi.name}(${types.join(',')})`; + return signature; + }, + getTopicsForIndexedArgs(abi: EventAbi, indexFilterValues: IndexedFilterValues): Array<string | null> { + const topics: Array<string | null> = []; + for (const eventInput of abi.inputs) { + if (!eventInput.indexed) { + continue; + } + if (_.isUndefined(indexFilterValues[eventInput.name])) { + // Null is a wildcard topic in a JSON-RPC call + topics.push(null); + } else { + const value = indexFilterValues[eventInput.name] as string; + const buffer = ethUtil.toBuffer(value); + const paddedBuffer = ethUtil.setLengthLeft(buffer, TOPIC_LENGTH); + const topic = ethUtil.bufferToHex(paddedBuffer); + topics.push(topic); + } + } + return topics; + }, + matchesFilter(log: LogEntry, filter: FilterObject): boolean { + if (!_.isUndefined(filter.address) && log.address !== filter.address) { + return false; + } + if (!_.isUndefined(filter.topics)) { + return filterUtils.matchesTopics(log.topics, filter.topics); + } + return true; + }, + matchesTopics(logTopics: string[], filterTopics: Array<string[] | string | null>): boolean { + const matchesTopic = _.zipWith(logTopics, filterTopics, filterUtils.matchesTopic.bind(filterUtils)); + const matchesTopics = _.every(matchesTopic); + return matchesTopics; + }, + matchesTopic(logTopic: string, filterTopic: string[] | string | null): boolean { + if (_.isArray(filterTopic)) { + return _.includes(filterTopic, logTopic); + } + if (_.isString(filterTopic)) { + return filterTopic === logTopic; + } + // null topic is a wildcard + return true; + }, +}; diff --git a/packages/contract-wrappers/src/utils/order_validation_utils.ts b/packages/contract-wrappers/src/utils/order_validation_utils.ts new file mode 100644 index 000000000..36dfbd800 --- /dev/null +++ b/packages/contract-wrappers/src/utils/order_validation_utils.ts @@ -0,0 +1,198 @@ +import { getOrderHashHex, isValidSignature, OrderError } from '@0xproject/order-utils'; +import { ExchangeContractErrs, Order, SignedOrder } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { ExchangeWrapper } from '../contract_wrappers/exchange_wrapper'; +import { ContractWrappersError, TradeSide, TransferType } from '../types'; +import { constants } from '../utils/constants'; +import { utils } from '../utils/utils'; + +import { ExchangeTransferSimulator } from './exchange_transfer_simulator'; + +export class OrderValidationUtils { + private _exchangeWrapper: ExchangeWrapper; + public static validateCancelOrderThrowIfInvalid( + order: Order, + cancelTakerTokenAmount: BigNumber, + unavailableTakerTokenAmount: BigNumber, + ): void { + if (cancelTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderCancelAmountZero); + } + if (order.takerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderAlreadyCancelledOrFilled); + } + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (order.expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderCancelExpired); + } + } + public static async validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + senderAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const fillMakerTokenAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.makerTokenAmount, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.makerTokenAddress, + signedOrder.maker, + senderAddress, + fillMakerTokenAmount, + TradeSide.Maker, + TransferType.Trade, + ); + await exchangeTradeEmulator.transferFromAsync( + signedOrder.takerTokenAddress, + senderAddress, + signedOrder.maker, + fillTakerTokenAmount, + TradeSide.Taker, + TransferType.Trade, + ); + const makerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.makerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + signedOrder.maker, + signedOrder.feeRecipient, + makerFeeAmount, + TradeSide.Maker, + TransferType.Fee, + ); + const takerFeeAmount = OrderValidationUtils._getPartialAmount( + fillTakerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.takerFee, + ); + await exchangeTradeEmulator.transferFromAsync( + zrxTokenAddress, + senderAddress, + signedOrder.feeRecipient, + takerFeeAmount, + TradeSide.Taker, + TransferType.Fee, + ); + } + private static _validateRemainingFillAmountNotZeroOrThrow( + takerTokenAmount: BigNumber, + unavailableTakerTokenAmount: BigNumber, + ) { + if (takerTokenAmount.eq(unavailableTakerTokenAmount)) { + throw new Error(ExchangeContractErrs.OrderRemainingFillAmountZero); + } + } + private static _validateOrderNotExpiredOrThrow(expirationUnixTimestampSec: BigNumber) { + const currentUnixTimestampSec = utils.getCurrentUnixTimestampSec(); + if (expirationUnixTimestampSec.lessThan(currentUnixTimestampSec)) { + throw new Error(ExchangeContractErrs.OrderFillExpired); + } + } + private static _getPartialAmount(numerator: BigNumber, denominator: BigNumber, target: BigNumber): BigNumber { + const fillMakerTokenAmount = numerator + .mul(target) + .div(denominator) + .round(0); + return fillMakerTokenAmount; + } + constructor(exchangeWrapper: ExchangeWrapper) { + this._exchangeWrapper = exchangeWrapper; + } + public async validateOrderFillableOrThrowAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + zrxTokenAddress: string, + expectedFillTakerTokenAmount?: BigNumber, + ): Promise<void> { + const orderHash = getOrderHashHex(signedOrder); + const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, + unavailableTakerTokenAmount, + ); + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); + let fillTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + if (!_.isUndefined(expectedFillTakerTokenAmount)) { + fillTakerTokenAmount = expectedFillTakerTokenAmount; + } + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + signedOrder.taker, + zrxTokenAddress, + ); + } + public async validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<BigNumber> { + if (fillTakerTokenAmount.eq(0)) { + throw new Error(ExchangeContractErrs.OrderFillAmountZero); + } + const orderHash = getOrderHashHex(signedOrder); + if (!isValidSignature(orderHash, signedOrder.ecSignature, signedOrder.maker)) { + throw new Error(OrderError.InvalidSignature); + } + const unavailableTakerTokenAmount = await this._exchangeWrapper.getUnavailableTakerAmountAsync(orderHash); + OrderValidationUtils._validateRemainingFillAmountNotZeroOrThrow( + signedOrder.takerTokenAmount, + unavailableTakerTokenAmount, + ); + if (signedOrder.taker !== constants.NULL_ADDRESS && signedOrder.taker !== takerAddress) { + throw new Error(ExchangeContractErrs.TransactionSenderIsNotFillOrderTaker); + } + OrderValidationUtils._validateOrderNotExpiredOrThrow(signedOrder.expirationUnixTimestampSec); + const remainingTakerTokenAmount = signedOrder.takerTokenAmount.minus(unavailableTakerTokenAmount); + const filledTakerTokenAmount = remainingTakerTokenAmount.lessThan(fillTakerTokenAmount) + ? remainingTakerTokenAmount + : fillTakerTokenAmount; + await OrderValidationUtils.validateFillOrderBalancesAllowancesThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + filledTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + + const wouldRoundingErrorOccur = await this._exchangeWrapper.isRoundingErrorAsync( + filledTakerTokenAmount, + signedOrder.takerTokenAmount, + signedOrder.makerTokenAmount, + ); + if (wouldRoundingErrorOccur) { + throw new Error(ExchangeContractErrs.OrderFillRoundingError); + } + return filledTakerTokenAmount; + } + public async validateFillOrKillOrderThrowIfInvalidAsync( + exchangeTradeEmulator: ExchangeTransferSimulator, + signedOrder: SignedOrder, + fillTakerTokenAmount: BigNumber, + takerAddress: string, + zrxTokenAddress: string, + ): Promise<void> { + const filledTakerTokenAmount = await this.validateFillOrderThrowIfInvalidAsync( + exchangeTradeEmulator, + signedOrder, + fillTakerTokenAmount, + takerAddress, + zrxTokenAddress, + ); + if (filledTakerTokenAmount !== fillTakerTokenAmount) { + throw new Error(ExchangeContractErrs.InsufficientRemainingFillAmount); + } + } +} diff --git a/packages/contract-wrappers/src/utils/utils.ts b/packages/contract-wrappers/src/utils/utils.ts new file mode 100644 index 000000000..af1125632 --- /dev/null +++ b/packages/contract-wrappers/src/utils/utils.ts @@ -0,0 +1,13 @@ +import { BigNumber } from '@0xproject/utils'; + +export const utils = { + spawnSwitchErr(name: string, value: any): Error { + return new Error(`Unexpected switch value: ${value} encountered for ${name}`); + }, + getCurrentUnixTimestampSec(): BigNumber { + return new BigNumber(Date.now() / 1000).round(); + }, + getCurrentUnixTimestampMs(): BigNumber { + return new BigNumber(Date.now()); + }, +}; |