diff --git a/src/constants.ts b/src/constants.ts index ec77a4dee..eb514e149 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,6 +18,7 @@ export const ONE_SECOND_MS = 1000; export const ONE_MINUTE_MS = ONE_SECOND_MS * 60; export const TEN_MINUTES_MS = ONE_MINUTE_MS * 10; export const DEFAULT_VALIDATION_GAS_LIMIT = 10e6; +export const HEX_BASE = 16; // The number of orders to post to Mesh at one time export const MESH_ORDERS_BATCH_SIZE = 200; diff --git a/src/utils/number_utils.ts b/src/utils/number_utils.ts new file mode 100644 index 000000000..1f17311ff --- /dev/null +++ b/src/utils/number_utils.ts @@ -0,0 +1,19 @@ +export const numberUtils = { + // from MDN https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + randomNumberInclusive: (minimumSpecified: number, maximumSpecified: number): number => { + const min = Math.ceil(minimumSpecified); + const max = Math.floor(maximumSpecified); + return Math.floor(Math.random() * (max - min + 1)) + min; // The maximum is inclusive and the minimum is inclusive + }, + // creates a random hex number of desired length by stringing together + // random integers from 1-15, guaranteeing the + // result is a hex number of the given length + randomHexNumberOfLength: (numberLength: number): string => { + let res = ''; + for (let i = 0; i < numberLength; i++) { + // tslint:disable-next-line:custom-no-magic-numbers + res = `${res}${numberUtils.randomNumberInclusive(1, 15).toString(16)}`; + } + return res; + }, +}; diff --git a/src/utils/service_utils.ts b/src/utils/service_utils.ts index db4fac0ab..c4e58420d 100644 --- a/src/utils/service_utils.ts +++ b/src/utils/service_utils.ts @@ -18,6 +18,7 @@ import { GAS_BURN_REFUND, GST_DIVISOR, GST_INTERACTION_COST, + HEX_BASE, ONE_SECOND_MS, PERCENTAGE_SIG_DIGITS, SSTORE_COST, @@ -29,6 +30,8 @@ import { GasTokenRefundInfo, GetSwapQuoteResponseLiquiditySource } from '../type import { orderUtils } from '../utils/order_utils'; import { findTokenDecimalsIfExists } from '../utils/token_metadata_utils'; +import { numberUtils } from './number_utils'; + export const serviceUtils = { attributeSwapQuoteOrders( swapQuote: MarketSellSwapQuote | MarketBuySwapQuote, @@ -69,8 +72,20 @@ export const serviceUtils = { stateMutability: 'view', type: 'function', }); - const timestamp = new BigNumber(Date.now() / ONE_SECOND_MS).integerValue(); - const encodedAffiliateData = affiliateCallDataEncoder.encode([affiliateAddressOrDefault, timestamp]); + + // Generate unique identiifer + const timestampInSeconds = new BigNumber(Date.now() / ONE_SECOND_MS).integerValue(); + const hexTimestamp = timestampInSeconds.toString(HEX_BASE); + const randomNumber = numberUtils.randomHexNumberOfLength(10); + + // Concatenate the hex identifier with the hex timestamp + // In the final encoded call data, this will leave us with a 5-byte ID followed by + // a 4-byte timestamp, and won't break parsers of the timestamp made prior to the + // addition of the ID + const uniqueIdentifier = new BigNumber(`${randomNumber}${hexTimestamp}`, HEX_BASE); + + // Encode additional call data and return + const encodedAffiliateData = affiliateCallDataEncoder.encode([affiliateAddressOrDefault, uniqueIdentifier]); const affiliatedData = `${data}${encodedAffiliateData.slice(2)}`; return affiliatedData; }, diff --git a/test/constants.ts b/test/constants.ts index 3a08ce367..f71caa057 100644 --- a/test/constants.ts +++ b/test/constants.ts @@ -18,3 +18,4 @@ export const SYMBOL_TO_ADDRESS: ObjectMap = { ZRX: ZRX_TOKEN_ADDRESS, WETH: WETH_TOKEN_ADDRESS, }; +export const AFFILIATE_DATA_SELECTOR = '869584cd'; diff --git a/test/service_utils_test.ts b/test/service_utils_test.ts index 174006fd4..c590947b5 100644 --- a/test/service_utils_test.ts +++ b/test/service_utils_test.ts @@ -6,7 +6,7 @@ import 'mocha'; import { ZERO } from '../src/constants'; import { serviceUtils } from '../src/utils/service_utils'; -import { MAX_INT } from './constants'; +import { AFFILIATE_DATA_SELECTOR, MAX_INT } from './constants'; const SUITE_NAME = 'serviceUtils test'; @@ -68,4 +68,26 @@ describe(SUITE_NAME, () => { expect(gasTokenGasCost.toNumber()).to.be.eq(0); }); }); + describe('attributeCallData', () => { + it('it returns a reasonable ID and timestamp', () => { + const fakeCallData = '0x0000000000000'; + const fakeAffiliate = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'; + const attributedCallData = serviceUtils.attributeCallData(fakeCallData, fakeAffiliate); + const currentTime = new Date(); + + // parse out items from call data to ensure they are reasonable values + const selectorPos = attributedCallData.indexOf(AFFILIATE_DATA_SELECTOR); + const affiliateAddress = '0x'.concat(attributedCallData.substring(selectorPos + 32, selectorPos + 72)); + const randomId = attributedCallData.substring(selectorPos + 118, selectorPos + 128); + const timestampFromCallDataHex = attributedCallData.substring(selectorPos + 128, selectorPos + 136); + const timestampFromCallData = parseInt(timestampFromCallDataHex, 16); + + expect(affiliateAddress).to.be.eq(fakeAffiliate); + // call data timestamp is within 3 seconds of timestamp created during test + expect(timestampFromCallData).to.be.greaterThan(currentTime.getTime() / 1000 - 3); + expect(timestampFromCallData).to.be.lessThan(currentTime.getTime() / 1000 + 3); + // ID is a 10-digit hex number + expect(randomId).to.match(/[0-9A-Fa-f]{10}/); + }); + }); });