Skip to content

Commit

Permalink
Merge in main. Fix tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
Cal-L committed Jul 29, 2021
2 parents 6ef6fbe + 7486785 commit 5d7c648
Show file tree
Hide file tree
Showing 12 changed files with 338 additions and 56 deletions.
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [14.0.2]
### Changed
- Fix `resetPolling` functionality ([#546](https://github.com/MetaMask/controllers/pull/546))
- TokenService improvements ([#541](https://github.com/MetaMask/controllers/pull/541))

## [14.0.1]
### Changed
- Ensure gas estimate fetching in gasFeeController correctly handles responses with invalid number of decimals ([#544](https://github.com/MetaMask/controllers/pull/544))
- Bump @metamask/contract-metadata from 1.27.0 to 1.28.0 ([#540](https://github.com/MetaMask/controllers/pull/540))

## [14.0.0]
### Added
- **BREAKING** Add EIP1559 support including `speedUpTransaction` and `stopTransaction` ([#521](https://github.com/MetaMask/controllers/pull/521))
Expand Down Expand Up @@ -310,7 +320,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- Remove shapeshift controller (#209)

[Unreleased]: https://github.com/MetaMask/controllers/compare/v14.0.0...HEAD
[Unreleased]: https://github.com/MetaMask/controllers/compare/v14.0.2...HEAD
[14.0.2]: https://github.com/MetaMask/controllers/compare/v14.0.1...v14.0.2
[14.0.1]: https://github.com/MetaMask/controllers/compare/v14.0.0...v14.0.1
[14.0.0]: https://github.com/MetaMask/controllers/compare/v13.2.0...v14.0.0
[13.2.0]: https://github.com/MetaMask/controllers/compare/v13.1.0...v13.2.0
[13.1.0]: https://github.com/MetaMask/controllers/compare/v13.0.0...v13.1.0
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@metamask/controllers",
"version": "14.0.0",
"version": "14.0.2",
"description": "Collection of platform-agnostic modules for creating secure data models for cryptocurrency wallets",
"keywords": [
"MetaMask",
Expand Down
2 changes: 1 addition & 1 deletion src/apis/token-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('FetchtokenList', () => {
it('should call the api to return the token metadata for eth address provided', async () => {
nock(TOKEN_END_POINT_API)
.get(
`/tokens/${NetworksChainId.mainnet}?address=0x514910771af9ca656af840dff83e8264ecf986ca`,
`/token/${NetworksChainId.mainnet}?address=0x514910771af9ca656af840dff83e8264ecf986ca`,
)
.reply(200, sampleToken)
.persist();
Expand Down
75 changes: 39 additions & 36 deletions src/apis/token-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function getTokensURL(chainId: string) {
return `${END_POINT}/tokens/${chainId}`;
}
function getTokenMetadataURL(chainId: string, tokenAddress: string) {
return `${END_POINT}/tokens/${chainId}?address=${tokenAddress}`;
return `${END_POINT}/token/${chainId}?address=${tokenAddress}`;
}

// Token list averages 1.6 MB in size
Expand All @@ -19,20 +19,26 @@ const timeout = 10000;
/**
* Fetches the list of token metadata for a given network chainId
*
* @returns - Promise resolving token List
* @returns - Promise resolving token List
*/
export async function fetchTokenList(chainId: string): Promise<Response> {
export async function fetchTokenList(chainId: string): Promise<unknown> {
const tokenURL = getTokensURL(chainId);
const fetchOptions: RequestInit = {
referrer: tokenURL,
referrerPolicy: 'no-referrer-when-downgrade',
method: 'GET',
mode: 'cors',
};
fetchOptions.headers = new window.Headers();
fetchOptions.headers.set('Content-Type', 'application/json');
const tokenResponse = await timeoutFetch(tokenURL, fetchOptions, timeout);
return await tokenResponse.json();
const response = await queryApi(tokenURL);
return parseJsonResponse(response);
}

/**
* Fetch metadata for the token address provided for a given network chainId
*
* @return Promise resolving token metadata for the tokenAddress provided
*/
export async function fetchTokenMetadata(
chainId: string,
tokenAddress: string,
): Promise<unknown> {
const tokenMetadataURL = getTokenMetadataURL(chainId, tokenAddress);
const response = await queryApi(tokenMetadataURL);
return parseJsonResponse(response);
}

/**
Expand All @@ -42,39 +48,36 @@ export async function fetchTokenList(chainId: string): Promise<Response> {
*/
export async function syncTokens(chainId: string): Promise<void> {
const syncURL = syncTokensURL(chainId);
const fetchOptions: RequestInit = {
referrer: syncURL,
referrerPolicy: 'no-referrer-when-downgrade',
method: 'GET',
mode: 'cors',
};
fetchOptions.headers = new window.Headers();
fetchOptions.headers.set('Content-Type', 'application/json');
await timeoutFetch(syncURL, fetchOptions, timeout);
queryApi(syncURL);
}

/**
* Fetch metadata for the token address provided for a given network chainId
* Perform fetch request against the api
*
* @return Promise resolving token metadata for the tokenAddress provided
* @return Promise resolving request response
*/
export async function fetchTokenMetadata(
chainId: string,
tokenAddress: string,
): Promise<Response> {
const tokenMetadataURL = getTokenMetadataURL(chainId, tokenAddress);
async function queryApi(apiURL: string): Promise<Response> {
const fetchOptions: RequestInit = {
referrer: tokenMetadataURL,
referrer: apiURL,
referrerPolicy: 'no-referrer-when-downgrade',
method: 'GET',
mode: 'cors',
};
fetchOptions.headers = new window.Headers();
fetchOptions.headers.set('Content-Type', 'application/json');
const tokenResponse = await timeoutFetch(
tokenMetadataURL,
fetchOptions,
timeout,
);
return await tokenResponse.json();
return await timeoutFetch(apiURL, fetchOptions, timeout);
}

/**
* Parse response
*
* @return Promise resolving request response json value
*/
async function parseJsonResponse(apiResponse: Response): Promise<unknown> {
const responseObj = await apiResponse.json();
// api may return errors as json without setting an error http status code
if (responseObj?.error) {
throw new Error(`TokenService Error: ${responseObj.error}`);
}
return responseObj;
}
2 changes: 1 addition & 1 deletion src/assets/TokenListController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1127,7 +1127,7 @@ describe('TokenListController', () => {

it('should return the metadata for a tokenAddress provided', async () => {
nock(TOKEN_END_POINT_API)
.get(`/tokens/${NetworksChainId.mainnet}`)
.get(`/token/${NetworksChainId.mainnet}`)
.query({ address: '0x514910771af9ca656af840dff83e8264ecf986ca' })
.reply(200, sampleTokenMetaData)
.persist();
Expand Down
7 changes: 4 additions & 3 deletions src/assets/TokenListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,10 @@ export class TokenListController extends BaseController<
async fetchTokenMetadata(tokenAddress: string): Promise<DynamicToken> {
const releaseLock = await this.mutex.acquire();
try {
const token: DynamicToken = await safelyExecute(() =>
fetchTokenMetadata(this.chainId, tokenAddress),
);
const token = (await fetchTokenMetadata(
this.chainId,
tokenAddress,
)) as DynamicToken;
return token;
} finally {
releaseLock();
Expand Down
14 changes: 9 additions & 5 deletions src/gas/GasFeeController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {

private getChainId;

private currentChainId;

private ethQuery: any;

/**
Expand Down Expand Up @@ -283,23 +285,25 @@ export class GasFeeController extends BaseController<typeof name, GasFeeState> {
this.EIP1559APIEndpoint = EIP1559APIEndpoint;
this.legacyAPIEndpoint = legacyAPIEndpoint;
this.getChainId = getChainId;

this.currentChainId = this.getChainId();
const provider = getProvider();
this.ethQuery = new EthQuery(provider);
onNetworkStateChange(async () => {
const newProvider = getProvider();
const newChainId = this.getChainId();
this.ethQuery = new EthQuery(newProvider);
await this.resetPolling();
if (this.currentChainId !== newChainId) {
this.currentChainId = newChainId;
await this.resetPolling();
}
});
}

async resetPolling() {
if (this.pollTokens.size !== 0) {
// restart polling
const { getGasFeeEstimatesAndStartPolling } = this;
const tokens = Array.from(this.pollTokens);
this.stopPolling();
await getGasFeeEstimatesAndStartPolling(tokens[0]);
await this.getGasFeeEstimatesAndStartPolling(tokens[0]);
tokens.slice(1).forEach((token) => {
this.pollTokens.add(token);
});
Expand Down
166 changes: 165 additions & 1 deletion src/gas/gas-util.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,102 @@
import nock from 'nock';
import { fetchLegacyGasPriceEstimates } from './gas-util';
import {
fetchLegacyGasPriceEstimates,
normalizeGWEIDecimalNumbers,
fetchGasEstimates,
} from './gas-util';

const mockEIP1559ApiResponses = [
{
low: {
minWaitTimeEstimate: 120000,
maxWaitTimeEstimate: 300000,
suggestedMaxPriorityFeePerGas: '1',
suggestedMaxFeePerGas: '35',
},
medium: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 30000,
suggestedMaxPriorityFeePerGas: '2',
suggestedMaxFeePerGas: '40',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '60',
},
estimatedBaseFee: '30',
},
{
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 360000,
suggestedMaxPriorityFeePerGas: '1.0000000162',
suggestedMaxFeePerGas: '40',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '1.0000000160000028',
suggestedMaxFeePerGas: '45',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '1.000000016522',
},
estimatedBaseFee: '32.000000016522',
},
];

describe('gas utils', () => {
describe('fetchGasEstimates', () => {
it('should fetch external gasFeeEstimates when data is valid', async () => {
const scope = nock('https://not-a-real-url/')
.get(/.+/u)
.reply(200, mockEIP1559ApiResponses[0])
.persist();
const result = await fetchGasEstimates('https://not-a-real-url/');
expect(result).toMatchObject(mockEIP1559ApiResponses[0]);
scope.done();
nock.cleanAll();
});

it('should fetch and normalize external gasFeeEstimates when data is has an invalid number of decimals', async () => {
const expectedResult = {
low: {
minWaitTimeEstimate: 180000,
maxWaitTimeEstimate: 360000,
suggestedMaxPriorityFeePerGas: '1.000000016',
suggestedMaxFeePerGas: '40',
},
medium: {
minWaitTimeEstimate: 15000,
maxWaitTimeEstimate: 60000,
suggestedMaxPriorityFeePerGas: '1.000000016',
suggestedMaxFeePerGas: '45',
},
high: {
minWaitTimeEstimate: 0,
maxWaitTimeEstimate: 15000,
suggestedMaxPriorityFeePerGas: '3',
suggestedMaxFeePerGas: '1.000000017',
},
estimatedBaseFee: '32.000000017',
};

const scope = nock('https://not-a-real-url/')
.get(/.+/u)
.reply(200, mockEIP1559ApiResponses[1])
.persist();
const result = await fetchGasEstimates('https://not-a-real-url/');
expect(result).toMatchObject(expectedResult);
scope.done();
nock.cleanAll();
});
});

describe('fetchLegacyGasPriceEstimates', () => {
it('should fetch external gasPrices and return high/medium/low', async () => {
const scope = nock('https://not-a-real-url/')
Expand All @@ -24,4 +119,73 @@ describe('gas utils', () => {
nock.cleanAll();
});
});

describe('normalizeGWEIDecimalNumbers', () => {
it('should convert a whole number to WEI', () => {
expect(normalizeGWEIDecimalNumbers(1)).toBe('1');
expect(normalizeGWEIDecimalNumbers(123)).toBe('123');
expect(normalizeGWEIDecimalNumbers(101)).toBe('101');
expect(normalizeGWEIDecimalNumbers(1234)).toBe('1234');
expect(normalizeGWEIDecimalNumbers(1000)).toBe('1000');
});

it('should convert a number with a decimal part to WEI', () => {
expect(normalizeGWEIDecimalNumbers(1.1)).toBe('1.1');
expect(normalizeGWEIDecimalNumbers(123.01)).toBe('123.01');
expect(normalizeGWEIDecimalNumbers(101.001)).toBe('101.001');
expect(normalizeGWEIDecimalNumbers(100.001)).toBe('100.001');
expect(normalizeGWEIDecimalNumbers(1234.567)).toBe('1234.567');
});

it('should convert a number < 1 to WEI', () => {
expect(normalizeGWEIDecimalNumbers(0.1)).toBe('0.1');
expect(normalizeGWEIDecimalNumbers(0.01)).toBe('0.01');
expect(normalizeGWEIDecimalNumbers(0.001)).toBe('0.001');
expect(normalizeGWEIDecimalNumbers(0.567)).toBe('0.567');
});

it('should round to whole WEI numbers', () => {
expect(normalizeGWEIDecimalNumbers(0.1001)).toBe('0.1001');
expect(normalizeGWEIDecimalNumbers(0.0109)).toBe('0.0109');
expect(normalizeGWEIDecimalNumbers(0.0014)).toBe('0.0014');
expect(normalizeGWEIDecimalNumbers(0.5676)).toBe('0.5676');
});

it('should handle inputs with more than 9 decimal places', () => {
expect(normalizeGWEIDecimalNumbers(1.0000000162)).toBe('1.000000016');
expect(normalizeGWEIDecimalNumbers(1.0000000165)).toBe('1.000000017');
expect(normalizeGWEIDecimalNumbers(1.0000000199)).toBe('1.00000002');
expect(normalizeGWEIDecimalNumbers(1.9999999999)).toBe('2');
expect(normalizeGWEIDecimalNumbers(1.0000005998)).toBe('1.0000006');
expect(normalizeGWEIDecimalNumbers(123456.0000005998)).toBe(
'123456.0000006',
);
expect(normalizeGWEIDecimalNumbers(1.000000016025)).toBe('1.000000016');
expect(normalizeGWEIDecimalNumbers(1.0000000160000028)).toBe(
'1.000000016',
);
expect(normalizeGWEIDecimalNumbers(1.000000016522)).toBe('1.000000017');
expect(normalizeGWEIDecimalNumbers(1.000000016800022)).toBe(
'1.000000017',
);
});

it('should work if there are extraneous trailing decimal zeroes', () => {
expect(normalizeGWEIDecimalNumbers('0.5000')).toBe('0.5');
expect(normalizeGWEIDecimalNumbers('123.002300')).toBe('123.0023');
expect(normalizeGWEIDecimalNumbers('123.002300000000')).toBe('123.0023');
expect(normalizeGWEIDecimalNumbers('0.00000200000')).toBe('0.000002');
});

it('should work if there is no whole number specified', () => {
expect(normalizeGWEIDecimalNumbers('.1')).toBe('0.1');
expect(normalizeGWEIDecimalNumbers('.01')).toBe('0.01');
expect(normalizeGWEIDecimalNumbers('.001')).toBe('0.001');
expect(normalizeGWEIDecimalNumbers('.567')).toBe('0.567');
});

it('should handle NaN', () => {
expect(normalizeGWEIDecimalNumbers(NaN)).toBe('0');
});
});
});
Loading

0 comments on commit 5d7c648

Please sign in to comment.