diff options
author | Steve Klebanoff <steve.klebanoff@gmail.com> | 2018-10-24 08:15:23 +0800 |
---|---|---|
committer | Steve Klebanoff <steve.klebanoff@gmail.com> | 2018-10-24 08:15:23 +0800 |
commit | 3cd9f40e63d82c983ce10963c62432c87808f993 (patch) | |
tree | 471b1aa39da8ee36f27e6d21978a8cd1c52cf048 | |
parent | a766d78706cb45336263388360584390fe0e62f2 (diff) | |
parent | 4a72dc6c6f0825232d61e470f5b7975aec2e5c0e (diff) | |
download | dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar.gz dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar.bz2 dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar.lz dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar.xz dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.tar.zst dexon-sol-tools-3cd9f40e63d82c983ce10963c62432c87808f993.zip |
Merge branch 'development' into feature/instant/failure-state
64 files changed, 978 insertions, 134 deletions
diff --git a/CODEOWNERS b/CODEOWNERS index 278234aff..ba40af31a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,3 +26,4 @@ packages/subproviders/ @fabioberger @dekz packages/connect/ @fragosti packages/monorepo-scripts/ @fabioberger packages/order-utils/ @fabioberger @LogvinovLeon +python-packages/ @feuGeneA
\ No newline at end of file diff --git a/packages/asset-buyer/CHANGELOG.json b/packages/asset-buyer/CHANGELOG.json index 5ebe2aa24..7ebcd8c2f 100644 --- a/packages/asset-buyer/CHANGELOG.json +++ b/packages/asset-buyer/CHANGELOG.json @@ -25,6 +25,10 @@ { "note": "Add missing types to public interface", "pr": 1139 + }, + { + "note": "Throw `SignatureRequestDenied` and `TransactionValueTooLow` errors when executing buy", + "pr": 1147 } ], "timestamp": 1539871071 diff --git a/packages/asset-buyer/src/asset_buyer.ts b/packages/asset-buyer/src/asset_buyer.ts index 2fe8f6deb..2f9a3108e 100644 --- a/packages/asset-buyer/src/asset_buyer.ts +++ b/packages/asset-buyer/src/asset_buyer.ts @@ -1,4 +1,4 @@ -import { ContractWrappers } from '@0x/contract-wrappers'; +import { ContractWrappers, ContractWrappersError, ForwarderWrapperError } from '@0x/contract-wrappers'; import { schemas } from '@0x/json-schemas'; import { SignedOrder } from '@0x/order-utils'; import { ObjectMap } from '@0x/types'; @@ -210,21 +210,32 @@ export class AssetBuyer { throw new Error(AssetBuyerError.NoAddressAvailable); } } - // if no ethAmount is provided, default to the worst ethAmount from buyQuote - const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( - orders, - assetBuyAmount, - finalTakerAddress, - ethAmount || worstCaseQuoteInfo.totalEthAmount, - feeOrders, - feePercentage, - feeRecipient, - { - gasLimit, - gasPrice, - }, - ); - return txHash; + try { + // if no ethAmount is provided, default to the worst ethAmount from buyQuote + const txHash = await this._contractWrappers.forwarder.marketBuyOrdersWithEthAsync( + orders, + assetBuyAmount, + finalTakerAddress, + ethAmount || worstCaseQuoteInfo.totalEthAmount, + feeOrders, + feePercentage, + feeRecipient, + { + gasLimit, + gasPrice, + shouldValidate: true, + }, + ); + return txHash; + } catch (err) { + if (_.includes(err.message, ContractWrappersError.SignatureRequestDenied)) { + throw new Error(AssetBuyerError.SignatureRequestDenied); + } else if (_.includes(err.message, ForwarderWrapperError.CompleteFillFailed)) { + throw new Error(AssetBuyerError.TransactionValueTooLow); + } else { + throw err; + } + } } /** * Grab orders from the map, if there is a miss or it is time to refresh, fetch and process the orders diff --git a/packages/asset-buyer/src/types.ts b/packages/asset-buyer/src/types.ts index 0d8e732d7..f15e7e934 100644 --- a/packages/asset-buyer/src/types.ts +++ b/packages/asset-buyer/src/types.ts @@ -112,6 +112,8 @@ export enum AssetBuyerError { NoAddressAvailable = 'NO_ADDRESS_AVAILABLE', InvalidOrderProviderResponse = 'INVALID_ORDER_PROVIDER_RESPONSE', AssetUnavailable = 'ASSET_UNAVAILABLE', + SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED', + TransactionValueTooLow = 'TRANSACTION_VALUE_TOO_LOW', } export interface OrdersAndFillableAmounts { diff --git a/packages/contract-wrappers/CHANGELOG.json b/packages/contract-wrappers/CHANGELOG.json index f66be3f0a..c3d986b4a 100644 --- a/packages/contract-wrappers/CHANGELOG.json +++ b/packages/contract-wrappers/CHANGELOG.json @@ -37,6 +37,14 @@ "note": "Removed ContractNotFound errors. Checking for this error was somewhat ineffecient. Relevant methods/functions now return the default error from web3-wrapper, which we feel provides enough information.", "pr": 1105 + }, + { + "note": "Add `ForwarderWrapperError` to public interface", + "pr": 1147 + }, + { + "note": "Add `ContractWrapperError.SignatureRequestDenied` to public interface", + "pr": 1147 } ], "timestamp": 1539871071 diff --git a/packages/contract-wrappers/src/index.ts b/packages/contract-wrappers/src/index.ts index a38bd122b..d66ff5c9c 100644 --- a/packages/contract-wrappers/src/index.ts +++ b/packages/contract-wrappers/src/index.ts @@ -39,6 +39,7 @@ export { TransactionEncoder } from './utils/transaction_encoder'; export { ContractWrappersError, + ForwarderWrapperError, IndexedFilterValues, BlockRange, ContractWrappersConfig, diff --git a/packages/contract-wrappers/src/types.ts b/packages/contract-wrappers/src/types.ts index 60750179f..5a5bdd530 100644 --- a/packages/contract-wrappers/src/types.ts +++ b/packages/contract-wrappers/src/types.ts @@ -18,6 +18,10 @@ export enum ExchangeWrapperError { AssetDataMismatch = 'ASSET_DATA_MISMATCH', } +export enum ForwarderWrapperError { + CompleteFillFailed = 'COMPLETE_FILL_FAILED', +} + export enum ContractWrappersError { ContractNotDeployedOnNetwork = 'CONTRACT_NOT_DEPLOYED_ON_NETWORK', InsufficientAllowanceForTransfer = 'INSUFFICIENT_ALLOWANCE_FOR_TRANSFER', @@ -30,6 +34,7 @@ export enum ContractWrappersError { SubscriptionAlreadyPresent = 'SUBSCRIPTION_ALREADY_PRESENT', ERC721OwnerNotFound = 'ERC_721_OWNER_NOT_FOUND', ERC721NoApproval = 'ERC_721_NO_APPROVAL', + SignatureRequestDenied = 'SIGNATURE_REQUEST_DENIED', } export enum InternalContractWrappersError { diff --git a/packages/contract-wrappers/src/utils/constants.ts b/packages/contract-wrappers/src/utils/constants.ts index 9ff61f62a..c587ba526 100644 --- a/packages/contract-wrappers/src/utils/constants.ts +++ b/packages/contract-wrappers/src/utils/constants.ts @@ -14,4 +14,5 @@ export const constants = { ZERO_AMOUNT: new BigNumber(0), ONE_AMOUNT: new BigNumber(1), ETHER_TOKEN_DECIMALS: 18, + USER_DENIED_SIGNATURE_PATTERN: 'User denied transaction signature', }; diff --git a/packages/contract-wrappers/src/utils/decorators.ts b/packages/contract-wrappers/src/utils/decorators.ts index e17246015..a4207ae4c 100644 --- a/packages/contract-wrappers/src/utils/decorators.ts +++ b/packages/contract-wrappers/src/utils/decorators.ts @@ -29,6 +29,14 @@ const schemaErrorTransformer = (error: Error) => { return error; }; +const signatureRequestErrorTransformer = (error: Error) => { + if (_.includes(error.message, constants.USER_DENIED_SIGNATURE_PATTERN)) { + const errMsg = ContractWrappersError.SignatureRequestDenied; + return new Error(errMsg); + } + return error; +}; + /** * Source: https://stackoverflow.com/a/29837695/3546986 */ @@ -87,7 +95,11 @@ const syncErrorHandlerFactory = (errorTransformer: ErrorTransformer) => { }; // _.flow(f, g) = f ∘ g -const zeroExErrorTransformer = _.flow(schemaErrorTransformer, contractCallErrorTransformer); +const zeroExErrorTransformer = _.flow( + schemaErrorTransformer, + contractCallErrorTransformer, + signatureRequestErrorTransformer, +); export const decorators = { asyncZeroExErrorHandler: asyncErrorHandlerFactory(zeroExErrorTransformer), diff --git a/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol b/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol index fea9a53c2..4991c0ea5 100644 --- a/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol +++ b/packages/contracts/contracts/extensions/Forwarder/MixinExchangeWrapper.sol @@ -155,8 +155,10 @@ contract MixinExchangeWrapper is uint256 remainingMakerAssetFillAmount = safeSub(makerAssetFillAmount, totalFillResults.makerAssetFilledAmount); // Convert the remaining amount of makerAsset to buy into remaining amount - // of takerAsset to sell, assuming entire amount can be sold in the current order - uint256 remainingTakerAssetFillAmount = getPartialAmountFloor( + // of takerAsset to sell, assuming entire amount can be sold in the current order. + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingTakerAssetFillAmount = getPartialAmountCeil( orders[i].takerAssetAmount, orders[i].makerAssetAmount, remainingMakerAssetFillAmount @@ -224,7 +226,9 @@ contract MixinExchangeWrapper is // Convert the remaining amount of ZRX to buy into remaining amount // of WETH to sell, assuming entire amount can be sold in the current order. - uint256 remainingWethSellAmount = getPartialAmountFloor( + // We round up because the exchange rate computed by fillOrder rounds in favor + // of the Maker. In this case we want to overestimate the amount of takerAsset. + uint256 remainingWethSellAmount = getPartialAmountCeil( orders[i].takerAssetAmount, safeSub(orders[i].makerAssetAmount, orders[i].takerFee), // our exchange rate after fees remainingZrxBuyAmount @@ -233,7 +237,7 @@ contract MixinExchangeWrapper is // Attempt to sell the remaining amount of WETH. FillResults memory singleFillResult = fillOrderNoThrow( orders[i], - safeAdd(remainingWethSellAmount, 1), // we add 1 wei to the fill amount to make up for rounding errors + remainingWethSellAmount, signatures[i] ); diff --git a/packages/contracts/test/extensions/forwarder.ts b/packages/contracts/test/extensions/forwarder.ts index b76624fa9..c006be0fe 100644 --- a/packages/contracts/test/extensions/forwarder.ts +++ b/packages/contracts/test/extensions/forwarder.ts @@ -45,6 +45,7 @@ describe(ContractName.Forwarder, () => { let weth: DummyERC20TokenContract; let zrxToken: DummyERC20TokenContract; + let erc20TokenA: DummyERC20TokenContract; let erc721Token: DummyERC721TokenContract; let forwarderContract: ForwarderContract; let wethContract: WETH9Contract; @@ -77,7 +78,6 @@ describe(ContractName.Forwarder, () => { erc20Wrapper = new ERC20Wrapper(provider, usedAddresses, owner); const numDummyErc20ToDeploy = 3; - let erc20TokenA; [erc20TokenA, zrxToken] = await erc20Wrapper.deployDummyTokensAsync( numDummyErc20ToDeploy, constants.DUMMY_TOKEN_DECIMALS, @@ -902,6 +902,269 @@ describe(ContractName.Forwarder, () => { ); expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded', async () => { + // The 0x Protocol contracts round the exchange rate in favor of the Maker. + // In this case, the taker must round up how much they're going to spend, which + // in turn increases the amount of MakerAsset being purchased. + // Example: + // The taker wants to buy 5 units of the MakerAsset at a rate of 3M/2T. + // For every 2 units of TakerAsset, the taker will receive 3 units of MakerAsset. + // To purchase 5 units, the taker must spend 10/3 = 3.33 units of TakerAssset. + // However, the Taker can only spend whole units. + // Spending floor(10/3) = 3 units will yield a profit of Floor(3*3/2) = Floor(4.5) = 4 units of MakerAsset. + // Spending ceil(10/3) = 4 units will yield a profit of Floor(4*3/2) = 6 units of MakerAsset. + // + // The forwarding contract will opt for the second option, which overbuys, to ensure the taker + // receives at least the amount of MakerAsset they requested. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX', async () => { + // See the test case above for a detailed description of this case. + // The difference here is that the MakerAsset is ZRX. We expect the same result as above, + // but this tests a different code path. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('20'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const desiredMakerAssetFillAmount = new BigNumber('5'); + const makerAssetFillAmount = new BigNumber('6'); + const ethValue = new BigNumber('4'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: assetDataUtils.encodeERC20AssetData(erc20TokenA.address), + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[makerAddress][defaultMakerAssetAddress].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][defaultMakerAssetAddress]).to.be.bignumber.equal( + erc20Balances[takerAddress][defaultMakerAssetAddress].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(newBalances[forwarderContract.address][defaultMakerAssetAddress]).to.be.bignumber.equal( + constants.ZERO_AMOUNT, + ); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy slightly greater MakerAsset when exchange rate is rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // Order taken from a transaction on mainnet that failed due to a rounding error. + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('268166666666666666666'), + takerAssetAmount: new BigNumber('219090625878836371'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + // The taker will receive more than the desired amount of makerAsset due to rounding + const desiredMakerAssetFillAmount = new BigNumber('5000000000000000000'); + const ethValue = new BigNumber('4084971271824171'); + const makerAssetFillAmount = ethValue + .times(orderWithoutFee.makerAssetAmount) + .dividedToIntegerBy(orderWithoutFee.takerAssetAmount); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync( + ordersWithoutFee, + feeOrders, + desiredMakerAssetFillAmount, + { + value: ethValue, + from: takerAddress, + }, + ); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(makerAssetFillAmount).to.be.bignumber.greaterThan(desiredMakerAssetFillAmount); + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('Should buy correct MakerAsset when exchange rate is NOT rounded, and MakerAsset is ZRX (Regression Test)', async () => { + // An extra unit of TakerAsset was sent to the exchange contract to account for rounding errors, in Forwarder v1. + // Specifically, the takerFillAmount was calculated using Floor(desiredMakerAmount * exchangeRate) + 1 + // We have since changed this to be Ceil(desiredMakerAmount * exchangeRate) + // These calculations produce different results when `desiredMakerAmount * exchangeRate` is an integer. + // + // This test verifies that `ceil` is sufficient: + // Let TakerAssetAmount = MakerAssetAmount * 2 + // -> exchangeRate = TakerAssetAmount / MakerAssetAmount = (2*MakerAssetAmount)/MakerAssetAmount = 2 + // .: desiredMakerAmount * exchangeRate is an integer. + // + // Construct test case using values from example above + orderWithoutFee = await orderFactory.newSignedOrderAsync({ + makerAssetAmount: new BigNumber('30'), + takerAssetAmount: new BigNumber('60'), + makerAssetData: zrxAssetData, + takerAssetData: assetDataUtils.encodeERC20AssetData(weth.address), + makerFee: new BigNumber(0), + takerFee: new BigNumber(0), + }); + const ordersWithoutFee = [orderWithoutFee]; + const feeOrders: SignedOrder[] = []; + const makerAssetFillAmount = new BigNumber('5'); + const ethValue = new BigNumber('10'); + // Execute test case + tx = await forwarderWrapper.marketBuyOrdersWithEthAsync(ordersWithoutFee, feeOrders, makerAssetFillAmount, { + value: ethValue, + from: takerAddress, + }); + // Fetch end balances and construct expected outputs + const takerEthBalanceAfter = await web3Wrapper.getBalanceInWeiAsync(takerAddress); + const forwarderEthBalance = await web3Wrapper.getBalanceInWeiAsync(forwarderContract.address); + const newBalances = await erc20Wrapper.getBalancesAsync(); + const primaryTakerAssetFillAmount = ethValue; + const totalEthSpent = primaryTakerAssetFillAmount.plus(gasPrice.times(tx.gasUsed)); + // Validate test case + expect(takerEthBalanceAfter).to.be.bignumber.equal(takerEthBalanceBefore.minus(totalEthSpent)); + expect(newBalances[makerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][zrxToken.address].minus(makerAssetFillAmount), + ); + expect(newBalances[takerAddress][zrxToken.address]).to.be.bignumber.equal( + erc20Balances[takerAddress][zrxToken.address].plus(makerAssetFillAmount), + ); + expect(newBalances[makerAddress][weth.address]).to.be.bignumber.equal( + erc20Balances[makerAddress][weth.address].plus(primaryTakerAssetFillAmount), + ); + expect(newBalances[forwarderContract.address][weth.address]).to.be.bignumber.equal(constants.ZERO_AMOUNT); + expect(forwarderEthBalance).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); }); describe('marketBuyOrdersWithEth with extra fees', () => { it('should buy an asset and send fee to feeRecipient', async () => { diff --git a/packages/contracts/test/utils/forwarder_wrapper.ts b/packages/contracts/test/utils/forwarder_wrapper.ts index f1a64d47d..a0bfcfe1d 100644 --- a/packages/contracts/test/utils/forwarder_wrapper.ts +++ b/packages/contracts/test/utils/forwarder_wrapper.ts @@ -26,9 +26,12 @@ export class ForwarderWrapper { _.forEach(feeOrders, feeOrder => { const feeAvailable = feeOrder.makerAssetAmount.minus(feeOrder.takerFee); if (!remainingFeeAmount.isZero() && feeAvailable.gt(remainingFeeAmount)) { - wethAmount = wethAmount - .plus(feeOrder.takerAssetAmount.times(remainingFeeAmount).dividedToIntegerBy(feeAvailable)) - .plus(1); + wethAmount = wethAmount.plus( + feeOrder.takerAssetAmount + .times(remainingFeeAmount) + .dividedBy(feeAvailable) + .ceil(), + ); remainingFeeAmount = new BigNumber(0); } else if (!remainingFeeAmount.isZero()) { wethAmount = wethAmount.plus(feeOrder.takerAssetAmount); diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 2def34fd7..f999d59a8 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -23,7 +23,7 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote); + const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer); return ( <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 142e74dae..ffa5a8250 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -31,7 +31,7 @@ export interface ZeroExInstantOptionalProps { } export class ZeroExInstant extends React.Component<ZeroExInstantProps> { - public store: Store; + private readonly _store: Store; private static _mergeInitialStateWithProps(props: ZeroExInstantProps, state: State = INITIAL_STATE): State { // Create merged object such that properties in props override default settings const optionalPropsWithDefaults: ZeroExInstantOptionalProps = { @@ -58,14 +58,14 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> { } constructor(props: ZeroExInstantProps) { super(props); - this.store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); + this._store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAndDispatchToStore(this.store); + asyncData.fetchAndDispatchToStore(this._store); } public render(): React.ReactNode { return ( - <Provider store={this.store}> + <Provider store={this._store}> <SelectedAssetThemeProvider> <ZeroExInstantContainer /> </SelectedAssetThemeProvider> diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 7f4c5a058..31491c80a 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,3 +1,4 @@ import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ethDecimals = 18; +export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index fe78ce401..f648b37f2 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; import { ZeroExInstant, ZeroExInstantProps } from './index'; -export const render = (props: ZeroExInstantProps, selector: string = '#zeroExInstantContainer') => { +export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); }; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 7323123c3..c340623ad 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -41,6 +41,7 @@ export interface ERC721Asset { assetData: string; metaData: ERC721AssetMetaData; } + export interface Asset { assetData: string; metaData: AssetMetaData; diff --git a/packages/instant/test/util/asset.test.ts b/packages/instant/test/util/asset.test.ts new file mode 100644 index 000000000..c7db7eba7 --- /dev/null +++ b/packages/instant/test/util/asset.test.ts @@ -0,0 +1,47 @@ +import { AssetProxyId, ObjectMap } from '@0x/types'; + +import { Asset, AssetMetaData, ERC20AssetMetaData, Network, ZeroExInstantError } from '../../src/types'; +import { assetUtils } from '../../src/util/asset'; + +const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; +const ZRX_ASSET_DATA_KOVAN = '0xf47261b00000000000000000000000002002d3812f58e35f0ea1ffbf80a75a38c32175fa'; +const ZRX_META_DATA: ERC20AssetMetaData = { + assetProxyId: AssetProxyId.ERC20, + symbol: 'zrx', + decimals: 18, +}; +const ZRX_ASSET: Asset = { + assetData: ZRX_ASSET_DATA, + metaData: ZRX_META_DATA, +}; +const META_DATA_MAP: ObjectMap<AssetMetaData> = { + [ZRX_ASSET_DATA]: ZRX_META_DATA, +}; + +describe('assetDataUtil', () => { + describe('bestNameForAsset', () => { + it('should return default string if assetData is undefined', () => { + expect(assetUtils.bestNameForAsset(undefined, 'xyz')).toEqual('xyz'); + }); + it('should return ZRX for ZRX assetData', () => { + expect(assetUtils.bestNameForAsset(ZRX_ASSET, 'mah default')).toEqual('ZRX'); + }); + }); + describe('getMetaDataOrThrow', () => { + it('should return the metaData for the supplied mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA, META_DATA_MAP, Network.Mainnet)).toEqual( + ZRX_META_DATA, + ); + }); + it('should return the metaData for the supplied non-mainnet asset data', () => { + expect(assetUtils.getMetaDataOrThrow(ZRX_ASSET_DATA_KOVAN, META_DATA_MAP, Network.Kovan)).toEqual( + ZRX_META_DATA, + ); + }); + it('should throw if the metaData for the asset is not available', () => { + expect(() => + assetUtils.getMetaDataOrThrow('asset data we dont have', META_DATA_MAP, Network.Mainnet), + ).toThrowError(ZeroExInstantError.AssetMetaDataNotAvailable); + }); + }); +}); diff --git a/packages/json-schemas/CHANGELOG.json b/packages/json-schemas/CHANGELOG.json index 8a88df6e2..3bd6552e3 100644 --- a/packages/json-schemas/CHANGELOG.json +++ b/packages/json-schemas/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "version": "2.0.1", + "changes": [ + { + "note": + "Improve schemas by enforcing that amounts that must be whole numbers (e.g Order asset amounts) no longer allow decimal amounts", + "pr": 1173 + } + ] + }, + { "version": "2.0.0", "changes": [ { diff --git a/packages/json-schemas/schemas/order_cancel_schema.json b/packages/json-schemas/schemas/order_cancel_schema.json index 09d4068c6..8d0999941 100644 --- a/packages/json-schemas/schemas/order_cancel_schema.json +++ b/packages/json-schemas/schemas/order_cancel_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "order": { "$ref": "/orderSchema" }, - "takerTokenCancelAmount": { "$ref": "/numberSchema" } + "takerTokenCancelAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["order", "takerTokenCancelAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json index c9549c72e..73bbf20bb 100644 --- a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json +++ b/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "signedOrder": { "$ref": "/signedOrderSchema" }, - "fillTakerAmount": { "$ref": "/numberSchema" } + "fillTakerAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["signedOrder", "fillTakerAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_fill_requests_schema.json b/packages/json-schemas/schemas/order_fill_requests_schema.json index 98325653b..d06fb19a2 100644 --- a/packages/json-schemas/schemas/order_fill_requests_schema.json +++ b/packages/json-schemas/schemas/order_fill_requests_schema.json @@ -4,7 +4,7 @@ "items": { "properties": { "signedOrder": { "$ref": "/signedOrderSchema" }, - "takerTokenFillAmount": { "$ref": "/numberSchema" } + "takerTokenFillAmount": { "$ref": "/wholeNumberSchema" } }, "required": ["signedOrder", "takerTokenFillAmount"], "type": "object" diff --git a/packages/json-schemas/schemas/order_schema.json b/packages/json-schemas/schemas/order_schema.json index de2c8c7c2..c70b9e2dd 100644 --- a/packages/json-schemas/schemas/order_schema.json +++ b/packages/json-schemas/schemas/order_schema.json @@ -3,17 +3,17 @@ "properties": { "makerAddress": { "$ref": "/addressSchema" }, "takerAddress": { "$ref": "/addressSchema" }, - "makerFee": { "$ref": "/numberSchema" }, - "takerFee": { "$ref": "/numberSchema" }, + "makerFee": { "$ref": "/wholeNumberSchema" }, + "takerFee": { "$ref": "/wholeNumberSchema" }, "senderAddress": { "$ref": "/addressSchema" }, - "makerAssetAmount": { "$ref": "/numberSchema" }, - "takerAssetAmount": { "$ref": "/numberSchema" }, + "makerAssetAmount": { "$ref": "/wholeNumberSchema" }, + "takerAssetAmount": { "$ref": "/wholeNumberSchema" }, "makerAssetData": { "$ref": "/hexSchema" }, "takerAssetData": { "$ref": "/hexSchema" }, - "salt": { "$ref": "/numberSchema" }, + "salt": { "$ref": "/wholeNumberSchema" }, "exchangeAddress": { "$ref": "/addressSchema" }, "feeRecipientAddress": { "$ref": "/addressSchema" }, - "expirationTimeSeconds": { "$ref": "/numberSchema" } + "expirationTimeSeconds": { "$ref": "/wholeNumberSchema" } }, "required": [ "makerAddress", diff --git a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json b/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json index 0ab9b444f..e0f274c5d 100644 --- a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json +++ b/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json @@ -3,8 +3,8 @@ "type": "object", "properties": { "assetData": { "$ref": "/hexSchema" }, - "minAmount": { "$ref": "/numberSchema" }, - "maxAmount": { "$ref": "/numberSchema" }, + "minAmount": { "$ref": "/wholeNumberSchema" }, + "maxAmount": { "$ref": "/wholeNumberSchema" }, "precision": { "type": "number" } }, "required": ["assetData"] diff --git a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json b/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json index bc95b61ef..f4583fc62 100644 --- a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json +++ b/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json @@ -4,12 +4,12 @@ "properties": { "makerAddress": { "$ref": "/addressSchema" }, "takerAddress": { "$ref": "/addressSchema" }, - "makerAssetAmount": { "$ref": "/numberSchema" }, - "takerAssetAmount": { "$ref": "/numberSchema" }, + "makerAssetAmount": { "$ref": "/wholeNumberSchema" }, + "takerAssetAmount": { "$ref": "/wholeNumberSchema" }, "makerAssetData": { "$ref": "/hexSchema" }, "takerAssetData": { "$ref": "/hexSchema" }, "exchangeAddress": { "$ref": "/addressSchema" }, - "expirationTimeSeconds": { "$ref": "/numberSchema" } + "expirationTimeSeconds": { "$ref": "/wholeNumberSchema" } }, "required": [ "makerAddress", diff --git a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json b/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json index 0742febdf..8193861e1 100644 --- a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json +++ b/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json @@ -2,8 +2,8 @@ "id": "/relayerApiOrderConfigResponseSchema", "type": "object", "properties": { - "makerFee": { "$ref": "/numberSchema" }, - "takerFee": { "$ref": "/numberSchema" }, + "makerFee": { "$ref": "/wholeNumberSchema" }, + "takerFee": { "$ref": "/wholeNumberSchema" }, "feeRecipientAddress": { "$ref": "/addressSchema" }, "senderAddress": { "$ref": "/addressSchema" } }, diff --git a/packages/json-schemas/schemas/whole_number_schema.json b/packages/json-schemas/schemas/whole_number_schema.json new file mode 100644 index 000000000..944ce820e --- /dev/null +++ b/packages/json-schemas/schemas/whole_number_schema.json @@ -0,0 +1,5 @@ +{ + "id": "/wholeNumberSchema", + "type": "string", + "pattern": "^\\d+$" +} diff --git a/packages/json-schemas/schemas/zero_ex_transaction_schema.json b/packages/json-schemas/schemas/zero_ex_transaction_schema.json index ad3b11583..0c714f14d 100644 --- a/packages/json-schemas/schemas/zero_ex_transaction_schema.json +++ b/packages/json-schemas/schemas/zero_ex_transaction_schema.json @@ -3,7 +3,7 @@ "properties": { "data": { "$ref": "/hexSchema" }, "signerAddress": { "$ref": "/addressSchema" }, - "salt": { "$ref": "/numberSchema" } + "salt": { "$ref": "/wholeNumberSchema" } }, "required": ["data", "salt", "signerAddress"], "type": "object" diff --git a/packages/json-schemas/src/schemas.ts b/packages/json-schemas/src/schemas.ts index 8318df617..8ece5de75 100644 --- a/packages/json-schemas/src/schemas.ts +++ b/packages/json-schemas/src/schemas.ts @@ -34,6 +34,7 @@ import * as signedOrderSchema from '../schemas/signed_order_schema.json'; import * as signedOrdersSchema from '../schemas/signed_orders_schema.json'; import * as tokenSchema from '../schemas/token_schema.json'; import * as txDataSchema from '../schemas/tx_data_schema.json'; +import * as wholeNumberSchema from '../schemas/whole_number_schema.json'; import * as zeroExTransactionSchema from '../schemas/zero_ex_transaction_schema.json'; export const schemas = { @@ -74,4 +75,5 @@ export const schemas = { relayerApiOrdersResponseSchema, relayerApiAssetDataPairsSchema, zeroExTransactionSchema, + wholeNumberSchema, }; diff --git a/packages/json-schemas/test/schema_test.ts b/packages/json-schemas/test/schema_test.ts index 795261ef2..bfa2c5668 100644 --- a/packages/json-schemas/test/schema_test.ts +++ b/packages/json-schemas/test/schema_test.ts @@ -36,6 +36,7 @@ const { relayerApiOrdersChannelUpdateSchema, relayerApiOrdersResponseSchema, relayerApiOrderSchema, + wholeNumberSchema, } = schemas; describe('Schema', () => { @@ -73,6 +74,17 @@ describe('Schema', () => { validateAgainstSchema(testCases, numberSchema, shouldFail); }); }); + describe('#wholeNumberSchema', () => { + it('should validate valid numbers', () => { + const testCases = ['5', '42', '0']; + validateAgainstSchema(testCases, wholeNumberSchema); + }); + it('should fail for invalid numbers', () => { + const testCases = ['1.3', '0.2', '00.00', '.3', '1.', 'abacaba', 'и', '1..0']; + const shouldFail = true; + validateAgainstSchema(testCases, wholeNumberSchema, shouldFail); + }); + }); describe('#addressSchema', () => { it('should validate valid addresses', () => { const testCases = ['0x8b0292b11a196601ed2ce54b665cafeca0347d42', NULL_ADDRESS]; diff --git a/packages/json-schemas/tsconfig.json b/packages/json-schemas/tsconfig.json index 425dfcfe1..7b14166c0 100644 --- a/packages/json-schemas/tsconfig.json +++ b/packages/json-schemas/tsconfig.json @@ -43,6 +43,7 @@ "./schemas/js_number.json", "./schemas/zero_ex_transaction_schema.json", "./schemas/tx_data_schema.json", - "./schemas/index_filter_values_schema.json" + "./schemas/index_filter_values_schema.json", + "./schemas/whole_number_schema.json" ] } diff --git a/packages/monorepo-scripts/CHANGELOG.json b/packages/monorepo-scripts/CHANGELOG.json index 4797fd929..170a97a33 100644 --- a/packages/monorepo-scripts/CHANGELOG.json +++ b/packages/monorepo-scripts/CHANGELOG.json @@ -9,6 +9,10 @@ { "note": "Add AssetBuyerError to the IGNORED_EXCESSIVE_TYPES array", "pr": 1139 + }, + { + "note": "Add ForwarderError to the IGNORED_EXCESSIVE_TYPES array", + "pr": 1147 } ] }, diff --git a/packages/monorepo-scripts/src/doc_gen_configs.ts b/packages/monorepo-scripts/src/doc_gen_configs.ts index 0aaf5a6a5..7a14f8664 100644 --- a/packages/monorepo-scripts/src/doc_gen_configs.ts +++ b/packages/monorepo-scripts/src/doc_gen_configs.ts @@ -56,6 +56,7 @@ export const docGenConfigs: DocGenConfigs = { 'ContractWrappersError', 'OrderError', 'AssetBuyerError', + 'ForwarderWrapperError', ], // Some libraries only export types. In those cases, we cannot check if the exported types are part of the // "exported public interface". Thus we add them here and skip those checks. diff --git a/packages/order-utils/test/asset_data_utils_test.ts b/packages/order-utils/test/asset_data_utils_test.ts new file mode 100644 index 000000000..f8b850604 --- /dev/null +++ b/packages/order-utils/test/asset_data_utils_test.ts @@ -0,0 +1,31 @@ +import * as chai from 'chai'; + +import { ERC20AssetData } from '@0x/types'; + +import { assetDataUtils } from '../src/asset_data_utils'; + +import { chaiSetup } from './utils/chai_setup'; + +chaiSetup.configure(); +const expect = chai.expect; + +const KNOWN_ENCODINGS = [ + { + address: '0x1dc4c1cefef38a777b15aa20260a54e584b16c48', + assetData: '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48', + }, +]; + +const ERC20_ASSET_PROXY_ID = '0xf47261b0'; + +describe('assetDataUtils', () => { + it('should encode', () => { + const assetData = assetDataUtils.encodeERC20AssetData(KNOWN_ENCODINGS[0].address); + expect(assetData).to.equal(KNOWN_ENCODINGS[0].assetData); + }); + it('should decode', () => { + const assetData: ERC20AssetData = assetDataUtils.decodeERC20AssetData(KNOWN_ENCODINGS[0].assetData); + expect(assetData.tokenAddress).to.equal(KNOWN_ENCODINGS[0].address); + expect(assetData.assetProxyId).to.equal(ERC20_ASSET_PROXY_ID); + }); +}); diff --git a/packages/web3-wrapper/CHANGELOG.json b/packages/web3-wrapper/CHANGELOG.json index b8d06eac1..6b554110f 100644 --- a/packages/web3-wrapper/CHANGELOG.json +++ b/packages/web3-wrapper/CHANGELOG.json @@ -1,5 +1,15 @@ [ { + "version": "3.1.1", + "changes": [ + { + "note": + "Fix bug in `getTransactionByHashAsync` which was causing the return value to have the wrong type (raw fields instead of unmarshalled fields).", + "pr": 1177 + } + ] + }, + { "version": "3.1.0", "changes": [ { diff --git a/packages/web3-wrapper/src/web3_wrapper.ts b/packages/web3-wrapper/src/web3_wrapper.ts index 3ba153680..56877fef3 100644 --- a/packages/web3-wrapper/src/web3_wrapper.ts +++ b/packages/web3-wrapper/src/web3_wrapper.ts @@ -23,7 +23,13 @@ import { import * as _ from 'lodash'; import { marshaller } from './marshaller'; -import { BlockWithoutTransactionDataRPC, BlockWithTransactionDataRPC, NodeType, Web3WrapperErrors } from './types'; +import { + BlockWithoutTransactionDataRPC, + BlockWithTransactionDataRPC, + NodeType, + TransactionRPC, + Web3WrapperErrors, +} from './types'; import { utils } from './utils'; const BASE_TEN = 10; @@ -228,10 +234,11 @@ export class Web3Wrapper { */ public async getTransactionByHashAsync(txHash: string): Promise<Transaction> { assert.isHexString('txHash', txHash); - const transaction = await this.sendRawPayloadAsync<Transaction>({ + const transactionRpc = await this.sendRawPayloadAsync<TransactionRPC>({ method: 'eth_getTransactionByHash', params: [txHash], }); + const transaction = marshaller.unmarshalTransaction(transactionRpc); return transaction; } /** diff --git a/packages/website/md/docs/asset_buyer/installation.md b/packages/website/md/docs/asset_buyer/installation.md index 76affaa09..3c0c95068 100644 --- a/packages/website/md/docs/asset_buyer/installation.md +++ b/packages/website/md/docs/asset_buyer/installation.md @@ -1,10 +1,10 @@ -**Install** +#### Install ```bash yarn add @0x/asset-buyer ``` -**Import** +#### Import ```javascript import { AssetBuyer } from '@0x/asset-buyer'; diff --git a/packages/website/md/docs/asset_buyer/usage.md b/packages/website/md/docs/asset_buyer/usage.md index 6462d938e..209c15062 100644 --- a/packages/website/md/docs/asset_buyer/usage.md +++ b/packages/website/md/docs/asset_buyer/usage.md @@ -1,4 +1,4 @@ -**Construction** +#### Construction Connecting to a standard relayer API compliant url: @@ -16,7 +16,7 @@ const orders = []; // get these from your own API, a relayer, a friend, from any const assetBuyer = AssetBuyer.getAssetBuyerForProvidedOrders(provider, orders); ``` -**Get a quote** +#### Get a quote A [BuyQuote](#types-BuyQuote) object contains enough information to display buy information to an end user @@ -30,7 +30,7 @@ console.log(quoteInfo.feeAmount); // a portion of the total ethAmount above that console.log(quoteInfo.ethPerAssetPrice); // the rate that this quote provides (e.g. 0.0035ETH / REP) ``` -**Perform a buy** +#### Perform a buy Pass the [BuyQuote](#types-BuyQuote) object from above back to the assetBuyer in order to initiate the buy transaction diff --git a/packages/website/md/docs/json_schemas/2/schemas.md b/packages/website/md/docs/json_schemas/2/schemas.md index ec7cf6f15..8ea6c8e85 100644 --- a/packages/website/md/docs/json_schemas/2/schemas.md +++ b/packages/website/md/docs/json_schemas/2/schemas.md @@ -1,40 +1,40 @@ Basic Schemas -* [Address type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/address.json) -* [Number type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/number.json) -* [Hex type](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/hex.json) -* [JS number](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/js_number.json) +* [Address type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/address_schema.json) +* [Number type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/number_schema.json) +* [Hex type](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/hex_schema.json) +* [JS number](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/js_number.json) 0x Protocol Schemas -* [Order](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_schema.json) -* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/signed_order_schema.json) -* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_hash_schema.json) -* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/ec_signature_schema.json) +* [Order](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_schema.json) +* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/signed_order_schema.json) +* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_hash_schema.json) +* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/ec_signature_schema.json) 0x.js Schemas -* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/block_param_schema.json) -* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/block_range_schema.json) -* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/index_filter_values_schema.json) -* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_fill_requests_schema.json) -* [OrderCancellationRequests](https://github.com/0xProjet/0x-monorepo/blob/development/packages/json-schemas/schemas/order_cancel_schema.json) -* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) -* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/signed_orders_schema.json) -* [Token](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/token_schema.json) -* [TxData](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/tx_data_schema.json) +* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/block_param_schema.json) +* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/block_range_schema.json) +* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/index_filter_values_schema.json) +* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_fill_requests_schema.json) +* [OrderCancellationRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_cancel_schema.json) +* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) +* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/signed_orders_schema.json) +* [Token](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/token_schema.json) +* [TxData](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/tx_data_schema.json) Standard Relayer API Schemas -* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/paginated_collection_schema.json) -* [Error response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_error_response_schema.json) -* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) -* [Order config response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) -* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) -* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) -* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) -* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) -* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_pairs_schema.json) -* [Trade info](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_trade_info_schema.json) -* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_asset_pairs_response_schema.json) -* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/development/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) +* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/paginated_collection_schema.json) +* [Error response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_error_response_schema.json) +* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) +* [Order config response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) +* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) +* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) +* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) +* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) +* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_pairs_schema.json) +* [Trade info](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json) +* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_asset_data_pairs_response_schema.json) +* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/5ec4b27200297708298deca97603849a37b2f66a/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) diff --git a/packages/website/md/docs/json_schemas/3/schemas.md b/packages/website/md/docs/json_schemas/3/schemas.md new file mode 100644 index 000000000..99aff7b5a --- /dev/null +++ b/packages/website/md/docs/json_schemas/3/schemas.md @@ -0,0 +1,41 @@ +Basic Schemas + +* [Address type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/address_schema.json) +* [Number type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/number_schema.json) +* [Whole number type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/whole_number_schema.json) +* [Hex type](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/hex_schema.json) +* [JS number](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/js_number.json) + +0x Protocol Schemas + +* [Order](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_schema.json) +* [SignedOrder](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/signed_order_schema.json) +* [OrderHash](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_hash_schema.json) +* [ECSignature](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/ec_signature_schema.json) + +0x.js Schemas + +* [BlockParam](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/block_param_schema.json) +* [BlockRange](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/block_range_schema.json) +* [IndexFilter Values](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/index_filter_values_schema.json) +* [OrderFillRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_fill_requests_schema.json) +* [OrderCancellationRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_cancel_schema.json) +* [OrderFillOrKillRequests](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/order_fill_or_kill_requests_schema.json) +* [SignedOrders](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/signed_orders_schema.json) +* [Token](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/token_schema.json) +* [TxData](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/tx_data_schema.json) + +Standard Relayer API Schemas + +* [Paginated collection](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/paginated_collection_schema.json) +* [Error response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_error_response_schema.json) +* [Order config payload](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_order_config_payload_schema.json) +* [Order config response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_order_config_response_schema.json) +* [Orders channel subscribe payload](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_payload_schema.json) +* [Orders channel subscribe](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_subscribe_schema.json) +* [Orders channel update](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orders_channel_update_response_schema.json) +* [Orderbook response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_orderbook_response_schema.json) +* [Asset pairs](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_pairs_schema.json) +* [Trade info](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_trade_info_schema.json) +* [Asset pairs response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_asset_data_pairs_response_schema.json) +* [Fee recipients response](https://github.com/0xProject/0x-monorepo/blob/528ae4376e5e605dac9666f2a5917803e942a1f9/packages/json-schemas/schemas/relayer_api_fee_recipients_response_schema.json) diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index efb5aef23..62e16dd1d 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -163,7 +163,7 @@ export class Blockchain { const providerName = this._getNameGivenProvider(injectedWeb3.currentProvider); // Wrap Metamask in a compatability wrapper MetamaskSubprovider (to handle inconsistencies) const signerSubprovider = - providerName === Providers.Metamask + providerName === constants.PROVIDER_NAME_METAMASK ? new MetamaskSubprovider(injectedWeb3.currentProvider) : new SignerSubprovider(injectedWeb3.currentProvider); provider.addProvider(signerSubprovider); diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 2299881c4..94ab63ac6 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -210,12 +210,16 @@ export class Portal extends React.Component<PortalProps, PortalState> { isLoaded: false, }; } - this.setState({ - trackedTokenStateByAddress, - }); - // Fetch the actual balance/allowance. - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + this.setState( + { + trackedTokenStateByAddress, + }, + () => { + // Fetch the actual balance/allowance. + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); + }, + ); } } public render(): React.ReactNode { @@ -644,6 +648,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { } private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]): Promise<void> { + if (_.isEmpty(tokenAddresses)) { + return; + } const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress; const balancesAndAllowances = await Promise.all( diff --git a/packages/website/ts/containers/json_schemas_documentation.ts b/packages/website/ts/containers/json_schemas_documentation.ts index cb5918784..9c4bb8e26 100644 --- a/packages/website/ts/containers/json_schemas_documentation.ts +++ b/packages/website/ts/containers/json_schemas_documentation.ts @@ -15,8 +15,9 @@ const InstallationMarkdown1 = require('md/docs/json_schemas/1/installation'); const InstallationMarkdown3 = require('md/docs/json_schemas/3/installation'); const usageMarkdown1 = require('md/docs/json_schemas/1/usage'); const usageMarkdown3 = require('md/docs/json_schemas/3/usage'); -const SchemasMarkdownV1 = require('md/docs/json_schemas/1/schemas'); -const SchemasMarkdownV2 = require('md/docs/json_schemas/2/schemas'); +const SchemasMarkdown1 = require('md/docs/json_schemas/1/schemas'); +const SchemasMarkdown2 = require('md/docs/json_schemas/2/schemas'); +const SchemasMarkdown3 = require('md/docs/json_schemas/3/schemas'); /* tslint:enable:no-var-requires */ const markdownSections = { @@ -40,19 +41,25 @@ const docsInfoConfig: DocsInfoConfig = { '0.0.1': { [markdownSections.introduction]: IntroMarkdown1, [markdownSections.installation]: InstallationMarkdown1, - [markdownSections.schemas]: SchemasMarkdownV1, + [markdownSections.schemas]: SchemasMarkdown1, [markdownSections.usage]: usageMarkdown1, }, '1.0.0': { [markdownSections.introduction]: IntroMarkdown1, [markdownSections.installation]: InstallationMarkdown1, - [markdownSections.schemas]: SchemasMarkdownV2, + [markdownSections.schemas]: SchemasMarkdown2, [markdownSections.usage]: usageMarkdown1, }, '2.0.0': { [markdownSections.introduction]: IntroMarkdown3, [markdownSections.installation]: InstallationMarkdown3, - [markdownSections.schemas]: SchemasMarkdownV2, + [markdownSections.schemas]: SchemasMarkdown2, + [markdownSections.usage]: usageMarkdown3, + }, + '2.0.1': { + [markdownSections.introduction]: IntroMarkdown3, + [markdownSections.installation]: InstallationMarkdown3, + [markdownSections.schemas]: SchemasMarkdown3, [markdownSections.usage]: usageMarkdown3, }, }, diff --git a/python-packages/order_utils/setup.py b/python-packages/order_utils/setup.py index a76d724aa..1a094cfe1 100644..100755 --- a/python-packages/order_utils/setup.py +++ b/python-packages/order_utils/setup.py @@ -1,13 +1,16 @@ +#!/usr/bin/env python + """setuptools module for order_utils package.""" import subprocess # nosec from shutil import rmtree -from os import path, remove, walk +from os import environ, path, remove, walk +from sys import argv -from distutils.command.clean import clean # type: ignore -from setuptools import setup # type: ignore -import setuptools.command.build_py # type: ignore -from setuptools.command.test import test as TestCommand # type: ignore +from distutils.command.clean import clean +import distutils.command.build_py +from setuptools import setup +from setuptools.command.test import test as TestCommand class TestCommandExtension(TestCommand): @@ -15,13 +18,13 @@ class TestCommandExtension(TestCommand): def run_tests(self): """Invoke pytest.""" - import pytest # type: ignore + import pytest pytest.main() # pylint: disable=too-many-ancestors -class LintCommand(setuptools.command.build_py.build_py): +class LintCommand(distutils.command.build_py.build_py): """Custom setuptools command class for running linters.""" def run(self): @@ -34,7 +37,7 @@ class LintCommand(setuptools.command.build_py.build_py): # docstring style checker: "pydocstyle src test setup.py".split(), # static type checker: - "mypy src setup.py".split(), + "mypy src test setup.py".split(), # security issue checker: "bandit -r src ./setup.py".split(), # general linter: @@ -42,6 +45,21 @@ class LintCommand(setuptools.command.build_py.build_py): # pylint takes relatively long to run, so it runs last, to enable # fast failures. ] + + # tell mypy where to find interface stubs for 3rd party libs + environ["MYPYPATH"] = path.join( + path.dirname(path.realpath(argv[0])), "stubs" + ) + + # HACK(gene): until eth_abi releases + # https://github.com/ethereum/eth-abi/pull/107 , we need to simply + # create an empty file `py.typed` in the eth_abi package directory. + import eth_abi + + eth_abi_dir = path.dirname(path.realpath(eth_abi.__file__)) + with open(path.join(eth_abi_dir, "py.typed"), "a"): + pass + for lint_command in lint_commands: print( "Running lint command `", " ".join(lint_command).strip(), "`" @@ -79,7 +97,7 @@ setup( "test": TestCommandExtension, }, include_package_data=True, - install_requires=["web3"], + install_requires=["eth-abi", "web3"], extras_require={ "dev": [ "bandit", @@ -87,6 +105,7 @@ setup( "coverage", "coveralls", "mypy", + "mypy_extensions", "pycodestyle", "pydocstyle", "pylint", @@ -118,7 +137,7 @@ setup( "Topic :: Software Development :: Libraries", "Topic :: Utilities", ], - zip_safe=False, + zip_safe=False, # required per mypy command_options={ "build_sphinx": { "source_dir": ("setup.py", "src"), diff --git a/python-packages/order_utils/src/conf.py b/python-packages/order_utils/src/conf.py index f3f15967c..e74a29d00 100644 --- a/python-packages/order_utils/src/conf.py +++ b/python-packages/order_utils/src/conf.py @@ -2,6 +2,9 @@ # Reference: http://www.sphinx-doc.org/en/master/config +from typing import List + + # pylint: disable=invalid-name # because these variables are not named in upper case, as globals should be. @@ -29,7 +32,7 @@ master_doc = "index" # The master toctree document. language = None -exclude_patterns = [] # type: ignore +exclude_patterns: List[str] = [] # The name of the Pygments (syntax highlighting) style to use. pygments_style = None diff --git a/python-packages/order_utils/src/index.rst b/python-packages/order_utils/src/index.rst index cbc4c8409..e09abcca2 100644 --- a/python-packages/order_utils/src/index.rst +++ b/python-packages/order_utils/src/index.rst @@ -10,9 +10,12 @@ order_utils.py .. automodule:: zero_ex.order_utils :members: -.. automodule:: zero_ex.order_utils.signature_utils +.. automodule:: zero_ex.order_utils.asset_data_utils :members: +.. autoclass:: zero_ex.order_utils.asset_data_utils.ERC20AssetData + +See source for properties. Sphinx does not easily generate class property docs; pull requests welcome. Indices and tables ================== diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py new file mode 100644 index 000000000..b6a224d2c --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/__init__.py @@ -0,0 +1 @@ +"""Dev utils to be shared across 0x projects and packages.""" diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py new file mode 100644 index 000000000..71b6128ca --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/abi_utils.py @@ -0,0 +1,102 @@ +"""Ethereum ABI utilities. + +Builds on the eth-abi package, adding some convenience methods like those found +in npmjs.com/package/ethereumjs-abi. Ideally, all of this code should be +pushed upstream into eth-abi. +""" + +import re +from typing import Any, List + +from mypy_extensions import TypedDict + +from eth_abi import encode_abi +from web3 import Web3 + +from .type_assertions import assert_is_string, assert_is_list + + +class MethodSignature(TypedDict, total=False): + """Object interface to an ABI method signature.""" + + method: str + args: List[str] + + +def parse_signature(signature: str) -> MethodSignature: + """Parse a method signature into its constituent parts. + + >>> parse_signature("ERC20Token(address)") + {'method': 'ERC20Token', 'args': ['address']} + """ + assert_is_string(signature, "signature") + + matches = re.match(r"^(\w+)\((.+)\)$", signature) + if matches is None: + raise ValueError(f"Invalid method signature {signature}") + return {"method": matches[1], "args": matches[2].split(",")} + + +def elementary_name(name: str) -> str: + """Convert from short to canonical names; barely implemented. + + Modeled after ethereumjs-abi's ABI.elementaryName(), but only implemented + to support our particular use case and a few other simple ones. + + >>> elementary_name("address") + 'address' + >>> elementary_name("uint") + 'uint256' + """ + assert_is_string(name, "name") + + return { + "int": "int256", + "uint": "uint256", + "fixed": "fixed128x128", + "ufixed": "ufixed128x128", + }.get(name, name) + + +def event_id(name: str, types: List[str]) -> str: + """Return the Keccak-256 hash of the given method. + + >>> event_id("ERC20Token", ["address"]) + '0xf47261b06eedbfce68afd46d0f3c27c60b03faad319eaf33103611cf8f6456ad' + """ + assert_is_string(name, "name") + assert_is_list(types, "types") + + signature = f"{name}({','.join(list(map(elementary_name, types)))})" + return Web3.sha3(text=signature).hex() + + +def method_id(name: str, types: List[str]) -> str: + """Return the 4-byte method identifier. + + >>> method_id("ERC20Token", ["address"]) + '0xf47261b0' + """ + assert_is_string(name, "name") + assert_is_list(types, "types") + + return event_id(name, types)[0:10] + + +def simple_encode(method: str, *args: Any) -> bytes: + # docstring considered all one line by pylint: disable=line-too-long + r"""Encode a method ABI. + + >>> simple_encode("ERC20Token(address)", "0x1dc4c1cefef38a777b15aa20260a54e584b16c48") + b'\xf4ra\xb0\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\xc4\xc1\xce\xfe\xf3\x8aw{\x15\xaa &\nT\xe5\x84\xb1lH' + """ # noqa: E501 (line too long) + assert_is_string(method, "method") + + signature: MethodSignature = parse_signature(method) + + return bytes.fromhex( + ( + method_id(signature["method"], signature["args"]) + + encode_abi(signature["args"], args).hex() + )[2:] + ) diff --git a/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py new file mode 100644 index 000000000..745d014e6 --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/dev_utils/type_assertions.py @@ -0,0 +1,33 @@ +"""Assertions for runtime type checking of function arguments.""" + +from typing import Any + + +def assert_is_string(value: Any, name: str) -> None: + """If :param value: isn't of type str, raise a TypeError. + + >>> try: assert_is_string(123, 'var') + ... except TypeError as type_error: print(str(type_error)) + ... + expected variable 'var', with value 123, to have type 'str', not 'int' + """ + if not isinstance(value, str): + raise TypeError( + f"expected variable '{name}', with value {str(value)}, to have" + + f" type 'str', not '{type(value).__name__}'" + ) + + +def assert_is_list(value: Any, name: str) -> None: + """If :param value: isn't of type list, raise a TypeError. + + >>> try: assert_is_list(123, 'var') + ... except TypeError as type_error: print(str(type_error)) + ... + expected variable 'var', with value 123, to have type 'list', not 'int' + """ + if not isinstance(value, list): + raise TypeError( + f"expected variable '{name}', with value {str(value)}, to have" + + f" type 'list', not '{type(value).__name__}'" + ) diff --git a/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py new file mode 100644 index 000000000..451de39af --- /dev/null +++ b/python-packages/order_utils/src/zero_ex/order_utils/asset_data_utils.py @@ -0,0 +1,72 @@ +"""Asset data encoding and decoding utilities.""" + +from mypy_extensions import TypedDict + +import eth_abi + +from zero_ex.dev_utils import abi_utils +from zero_ex.dev_utils.type_assertions import assert_is_string + + +ERC20_ASSET_DATA_BYTE_LENGTH = 36 +SELECTOR_LENGTH = 10 + + +class ERC20AssetData(TypedDict): + """Object interface to ERC20 asset data.""" + + asset_proxy_id: str + token_address: str + + +def encode_erc20_asset_data(token_address: str) -> str: + """Encode an ERC20 token address into an asset data string. + + :param token_address: the ERC20 token's contract address. + :rtype: hex encoded asset data string, usable in the makerAssetData or + takerAssetData fields in a 0x order. + + >>> encode_erc20_asset_data('0x1dc4c1cefef38a777b15aa20260a54e584b16c48') + '0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48' + """ + assert_is_string(token_address, "token_address") + + return ( + "0x" + + abi_utils.simple_encode("ERC20Token(address)", token_address).hex() + ) + + +def decode_erc20_asset_data(asset_data: str) -> ERC20AssetData: + # docstring considered all one line by pylint: disable=line-too-long + """Decode an ERC20 assetData hex string. + + :param asset_data: String produced by prior call to encode_erc20_asset_data() + + >>> decode_erc20_asset_data("0xf47261b00000000000000000000000001dc4c1cefef38a777b15aa20260a54e584b16c48") + {'asset_proxy_id': '0xf47261b0', 'token_address': '0x1dc4c1cefef38a777b15aa20260a54e584b16c48'} + """ # noqa: E501 (line too long) + assert_is_string(asset_data, "asset_data") + + if len(asset_data) < ERC20_ASSET_DATA_BYTE_LENGTH: + raise ValueError( + "Could not decode ERC20 Proxy Data. Expected length of encoded" + + f" data to be at least {str(ERC20_ASSET_DATA_BYTE_LENGTH)}." + + f" Got {str(len(asset_data))}." + ) + + asset_proxy_id: str = asset_data[0:10] + if asset_proxy_id != abi_utils.method_id("ERC20Token", ["address"]): + raise ValueError( + "Could not decode ERC20 Proxy Data. Expected Asset Proxy Id to be" + + f" ERC20 ({abi_utils.method_id('ERC20Token', ['address'])})" + + f" but got {asset_proxy_id}." + ) + + # workaround for https://github.com/PyCQA/pylint/issues/1498 + # pylint: disable=unsubscriptable-object + token_address = eth_abi.decode_abi( + ["address"], bytes.fromhex(asset_data[SELECTOR_LENGTH:]) + )[0] + + return {"asset_proxy_id": asset_proxy_id, "token_address": token_address} diff --git a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py b/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py deleted file mode 100644 index 7f4697106..000000000 --- a/python-packages/order_utils/src/zero_ex/order_utils/signature_utils.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Signature utilities.""" - - -def ec_sign_order_hash(): - """Signs an orderHash. - - Returns its elliptic curve signature and signature type. This method - currently supports TestRPC, Geth, and Parity above and below v1.6.6. - - >>> ec_sign_order_hash() - 'stub return value' - """ - return "stub return value" diff --git a/python-packages/order_utils/stubs/distutils/__init__.pyi b/python-packages/order_utils/stubs/distutils/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/__init__.pyi diff --git a/python-packages/order_utils/stubs/distutils/command/__init__.pyi b/python-packages/order_utils/stubs/distutils/command/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/command/__init__.pyi diff --git a/python-packages/order_utils/stubs/distutils/command/clean.pyi b/python-packages/order_utils/stubs/distutils/command/clean.pyi new file mode 100644 index 000000000..46a42ddb1 --- /dev/null +++ b/python-packages/order_utils/stubs/distutils/command/clean.pyi @@ -0,0 +1,7 @@ +from distutils.core import Command + +class clean(Command): + def initialize_options(self: clean) -> None: ... + def finalize_options(self: clean) -> None: ... + def run(self: clean) -> None: ... + ... diff --git a/python-packages/order_utils/stubs/pytest/__init__.pyi b/python-packages/order_utils/stubs/pytest/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/pytest/__init__.pyi diff --git a/python-packages/order_utils/stubs/pytest/raises.pyi b/python-packages/order_utils/stubs/pytest/raises.pyi new file mode 100644 index 000000000..2e3b29f3d --- /dev/null +++ b/python-packages/order_utils/stubs/pytest/raises.pyi @@ -0,0 +1 @@ +def raises(exception: Exception) -> ExceptionInfo: ... diff --git a/python-packages/order_utils/stubs/setuptools/__init__.pyi b/python-packages/order_utils/stubs/setuptools/__init__.pyi new file mode 100644 index 000000000..baa349d70 --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/__init__.pyi @@ -0,0 +1,6 @@ +from distutils.dist import Distribution +from typing import Any + +def setup(**attrs: Any) -> Distribution: ... + +class Command: ... diff --git a/python-packages/order_utils/stubs/setuptools/command/__init__.pyi b/python-packages/order_utils/stubs/setuptools/command/__init__.pyi new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/command/__init__.pyi diff --git a/python-packages/order_utils/stubs/setuptools/command/test.pyi b/python-packages/order_utils/stubs/setuptools/command/test.pyi new file mode 100644 index 000000000..c5ec770ad --- /dev/null +++ b/python-packages/order_utils/stubs/setuptools/command/test.pyi @@ -0,0 +1,3 @@ +from setuptools import Command + +class test(Command): ... diff --git a/python-packages/order_utils/stubs/web3/__init__.pyi b/python-packages/order_utils/stubs/web3/__init__.pyi new file mode 100644 index 000000000..c6f357009 --- /dev/null +++ b/python-packages/order_utils/stubs/web3/__init__.pyi @@ -0,0 +1,10 @@ +from typing import Optional, Union + +class Web3: + @staticmethod + def sha3( + primitive: Optional[Union[bytes, int, None]] = None, + text: Optional[str] = None, + hexstr: Optional[str] = None + ) -> bytes: ... + ... diff --git a/python-packages/order_utils/test/test_abi_utils.py b/python-packages/order_utils/test/test_abi_utils.py new file mode 100644 index 000000000..49a2a4f20 --- /dev/null +++ b/python-packages/order_utils/test/test_abi_utils.py @@ -0,0 +1,53 @@ +"""Tests of 0x.abi_utils.""" + +import pytest + +from zero_ex.dev_utils.abi_utils import ( + elementary_name, + event_id, + method_id, + parse_signature, + simple_encode, +) + + +def test_parse_signature_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + parse_signature(123) + + +def test_parse_signature_bad_input(): + """Test that passing a non-signature string raises a ValueError.""" + with pytest.raises(ValueError): + parse_signature("a string that's not even close to a signature") + + +def test_elementary_name_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + elementary_name(123) + + +def test_event_id_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + event_id(123, []) + + with pytest.raises(TypeError): + event_id("valid string", 123) + + +def test_method_id_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + method_id(123, []) + + with pytest.raises(TypeError): + method_id("ERC20Token", 123) + + +def test_simple_encode_type_error(): + """Test that passing in wrong types raises TypeError.""" + with pytest.raises(TypeError): + simple_encode(123) diff --git a/python-packages/order_utils/test/test_asset_data_utils.py b/python-packages/order_utils/test/test_asset_data_utils.py new file mode 100644 index 000000000..eeada5873 --- /dev/null +++ b/python-packages/order_utils/test/test_asset_data_utils.py @@ -0,0 +1,35 @@ +"""Tests of 0x.order_utils.asset_data_utils.""" + +import pytest + +from zero_ex.order_utils.asset_data_utils import ( + encode_erc20_asset_data, + decode_erc20_asset_data, + ERC20_ASSET_DATA_BYTE_LENGTH, +) + + +def test_encode_erc20_asset_data_type_error(): + """Test that passing in a non-string raises a TypeError.""" + with pytest.raises(TypeError): + encode_erc20_asset_data(123) + + +def test_decode_erc20_asset_data_type_error(): + """Test that passing in a non-string raises a TypeError.""" + with pytest.raises(TypeError): + decode_erc20_asset_data(123) + + +def test_decode_erc20_asset_data_too_short(): + """Test that passing an insufficiently long string raises a ValueError.""" + with pytest.raises(ValueError): + decode_erc20_asset_data(" " * (ERC20_ASSET_DATA_BYTE_LENGTH - 1)) + + +def test_decode_erc20_asset_data_invalid_proxy_id(): + """Test that passing data with an invalid proxy ID raises a ValueError.""" + with pytest.raises(ValueError): + decode_erc20_asset_data( + "0xffffffff" + (" " * ERC20_ASSET_DATA_BYTE_LENGTH) + ) diff --git a/python-packages/order_utils/test/test_doctest.py b/python-packages/order_utils/test/test_doctest.py index a0e61f84a..ba5da5418 100644 --- a/python-packages/order_utils/test/test_doctest.py +++ b/python-packages/order_utils/test/test_doctest.py @@ -1,10 +1,24 @@ """Exercise doctests for order_utils module.""" from doctest import testmod -from zero_ex.order_utils import signature_utils +from zero_ex.dev_utils import abi_utils, type_assertions +from zero_ex.order_utils import asset_data_utils -def test_doctest(): - """Invoke doctest on the module.""" - (failure_count, _) = testmod(signature_utils) + +def test_doctest_asset_data_utils(): + """Invoke doctest on the asset_data_utils module.""" + (failure_count, _) = testmod(asset_data_utils) + assert failure_count == 0 + + +def test_doctest_abi_utils(): + """Invoke doctest on the abi_utils module.""" + (failure_count, _) = testmod(abi_utils) + assert failure_count == 0 + + +def test_doctest_type_assertions(): + """Invoke doctest on the type_assertions module.""" + (failure_count, _) = testmod(type_assertions) assert failure_count == 0 diff --git a/python-packages/order_utils/test/test_signature_utils.py b/python-packages/order_utils/test/test_signature_utils.py deleted file mode 100644 index 7e830f9f8..000000000 --- a/python-packages/order_utils/test/test_signature_utils.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Tests of 0x.order_utils.signature_utils.*.""" - -from zero_ex.order_utils.signature_utils import ec_sign_order_hash - - -def test_ec_sign_order_hash(): - """Test the signing of order hashes.""" - assert ec_sign_order_hash() == "stub return value" |