Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/assets/AssetsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,37 @@ describe('AssetsController', () => {
});
});

it('should add tokens', async () => {
await assetsController.addTokens([
{ address: 'addressA', symbol: 'barA', decimals: 2 },
{ address: 'addressB', symbol: 'barB', decimals: 2 },
]);
expect(assetsController.state.tokens[0]).toEqual({
address: '0xAdDRessA',
decimals: 2,
symbol: 'barA',
});
expect(assetsController.state.tokens[1]).toEqual({
address: '0xAddReSSB',
decimals: 2,
symbol: 'barB',
});
await assetsController.addTokens([
{ address: 'addressA', symbol: 'bazA', decimals: 2 },
{ address: 'addressB', symbol: 'bazB', decimals: 2 },
]);
expect(assetsController.state.tokens[0]).toEqual({
address: '0xAdDRessA',
decimals: 2,
symbol: 'bazA',
});
expect(assetsController.state.tokens[1]).toEqual({
address: '0xAddReSSB',
decimals: 2,
symbol: 'bazB',
});
});

it('should add token by selected address', async () => {
const firstAddress = '0x123';
const secondAddress = '0x321';
Expand Down
37 changes: 37 additions & 0 deletions src/assets/AssetsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,43 @@ export class AssetsController extends BaseController<AssetsConfig, AssetsState>
}
}

/**
* Adds a batch of tokens to the stored token list
*
* @param tokens - Array of Tokens to be added or updated
* @returns - Current token list
*/
async addTokens(tokensToAdd: Token[]): Promise<Token[]> {
const releaseLock = await this.mutex.acquire();
const { allTokens, tokens } = this.state;
const { networkType, selectedAddress } = this.config;

try {
tokensToAdd.forEach((tokenToAdd) => {
const { address, symbol, decimals, image } = tokenToAdd;
const checksumAddress = toChecksumAddress(address);

const newEntry: Token = { address: checksumAddress, symbol, decimals, image };
const previousEntry = tokens.find((token) => token.address === checksumAddress);
if (previousEntry) {
const previousIndex = tokens.indexOf(previousEntry);
tokens[previousIndex] = newEntry;
} else {
tokens.push(newEntry);
}
});

const addressTokens = allTokens[selectedAddress];
const newAddressTokens = { ...addressTokens, ...{ [networkType]: tokens } };
const newAllTokens = { ...allTokens, ...{ [selectedAddress]: newAddressTokens } };
const newTokens = [...tokens];
this.update({ allTokens: newAllTokens, tokens: newTokens });
return newTokens;
} finally {
releaseLock();
}
}

/**
* Adds a new suggestedAsset to state. Parameters will be validated according to
* asset type being watched. A `<suggestedAssetMeta.id>:pending` hub event will be emitted once added.
Expand Down
14 changes: 9 additions & 5 deletions src/assets/AssetsDetectionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,20 +173,24 @@ export class AssetsDetectionController extends BaseController<AssetsDetectionCon
const balances = await assetsContractController.getBalancesInSingleCall(selectedAddress, tokensToDetect);
const assetsController = this.context.AssetsController as AssetsController;
const { ignoredTokens } = assetsController.state;
const tokensToAdd = [];
for (const tokenAddress in balances) {
let ignored;
/* istanbul ignore else */
if (ignoredTokens.length) {
ignored = ignoredTokens.find((token) => token.address === toChecksumAddress(tokenAddress));
}
if (!ignored) {
await assetsController.addToken(
tokenAddress,
contractMap[tokenAddress].symbol,
contractMap[tokenAddress].decimals,
);
tokensToAdd.push({
address: tokenAddress,
decimals: contractMap[tokenAddress].decimals,
symbol: contractMap[tokenAddress].symbol,
});
}
}
if (tokensToAdd.length) {
await assetsController.addTokens(tokensToAdd);
}
});
}

Expand Down
49 changes: 49 additions & 0 deletions src/transaction/TransactionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ describe('TransactionController', () => {
});
expect(controller.state.transactions[0].transaction.from).toBe(from);
expect(controller.state.transactions[0].networkID).toBe(MOCK_NETWORK.state.network);
expect(controller.state.transactions[0].chainId).toBe(MOCK_NETWORK.state.provider.chainId);
expect(controller.state.transactions[0].status).toBe('unapproved');
});

Expand Down Expand Up @@ -604,6 +605,25 @@ describe('TransactionController', () => {
expect(controller.state.transactions).toHaveLength(0);
});

// This tests the fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
it('should wipe transactions using networkID when there is no chainId', async () => {
const controller = new TransactionController({ provider: PROVIDER });
controller.wipeTransactions();
controller.context = {
NetworkController: MOCK_NETWORK,
} as any;
controller.onComposed();
controller.state.transactions.push({
from: MOCK_PRFERENCES.state.selectedAddress,
id: 'foo',
networkID: '3',
status: 'submitted',
transactionHash: '1337',
} as any);
controller.wipeTransactions();
expect(controller.state.transactions).toHaveLength(0);
});

it('should fail to approve an invalid transaction', () => {
return new Promise(async (resolve) => {
const controller = new TransactionController({
Expand Down Expand Up @@ -730,6 +750,35 @@ describe('TransactionController', () => {
});

it('should query transaction statuses', () => {
return new Promise((resolve) => {
const controller = new TransactionController({
provider: PROVIDER,
sign: async (transaction: any) => transaction,
});
controller.context = {
NetworkController: MOCK_NETWORK,
} as any;
controller.onComposed();
controller.state.transactions.push({
from: MOCK_PRFERENCES.state.selectedAddress,
id: 'foo',
networkID: '3',
chainId: '3',
status: 'submitted',
transactionHash: '1337',
} as any);
controller.state.transactions.push({} as any);

controller.hub.once(`${controller.state.transactions[0].id}:confirmed`, () => {
expect(controller.state.transactions[0].status).toBe('confirmed');
resolve('');
});
controller.queryTransactionStatuses();
});
});

// This tests the fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
it('should query transaction statuses with networkID only when there is no chainId', () => {
return new Promise((resolve) => {
const controller = new TransactionController({
provider: PROVIDER,
Expand Down
42 changes: 30 additions & 12 deletions src/transaction/TransactionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export interface TransactionMeta {
};
id: string;
networkID?: string;
chainId?: string;
origin?: string;
rawTransaction?: string;
status: string;
Expand Down Expand Up @@ -237,16 +238,18 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
*
* @param txMeta - Object containing the transaction information
* @param currentNetworkID - string representing the current network id

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The additional parameter should be documented here:

Suggested change
* @param currentNetworkID - string representing the current network id
* @param currentNetworkID - string representing the current network id
* @param currentChainId - The current chain ID

@andrepimenta andrepimenta Feb 19, 2021 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what to think of this "backwards compatibility" strategy 🤔 I think it will ensure things are no worse than they are today, but it'll still result in some interactions with the wrong transaction (e.g. if you have two networks with the same network ID but different chain IDs).

What's worse, this flaw will apply not just to old transactions, but to new ones as well.

I can approve this anyway (after tests have been added to pass the coverage requirements on CI), since it's less broken than before. But this remains somewhat broken. Maybe this could be improved in the future by removing the "networkID" property for new transactions, and only considering that for older transactions.

@Gudahtt I am 100% with you on all your comments. The end goal is to remove networkID completely. We are doing this incrementally because we still use it in some parts of the app so I thought it would be safer to keep it but always prefer chainId over it.

I have changed some part of the code to be sure that chainId is always preferred, we only fallback to networkID in case chainId is not there. For all new transactions, chainId will always be added, so at least we know that for new transactions, networkID will not be used.

We had a change some time ago where we basically did
const currentNetworkID = network.state.provider.chainId;
which could cause problems like you mentioned: could result in the wrong set of transactions being returned whenever the chainId was different from the networkId.
I now differentiate them:

const currentChainId= network.state.provider.chainId;
const currentNetworkID= network.state.network;

In this way, chainId and networkID at least can now have different values resolving that problem for now, but yes the objective is still to remove networkID completely in the near future.

Let me know what are your thoughts on this and if it's ok to merge!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good to me! Better to make these changes incrementally if possible

* @param currentChainId - string representing the current chain id
* @returns - TransactionMeta
*/
private normalizeTx(txMeta: EtherscanTransactionMeta, currentNetworkID: string): TransactionMeta {
private normalizeTx(txMeta: EtherscanTransactionMeta, currentNetworkID: string, currentChainId: string): TransactionMeta {
const time = parseInt(txMeta.timeStamp, 10) * 1000;
/* istanbul ignore next */
const status = txMeta.isError === '0' ? 'confirmed' : 'failed';
return {
blockNumber: txMeta.blockNumber,
id: random({ msecs: time }),
networkID: currentNetworkID,
chainId: currentChainId,
status,
time,
transaction: {
Expand All @@ -262,7 +265,7 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
};
}

private normalizeTokenTx = (txMeta: EtherscanTransactionMeta, currentNetworkID: string): TransactionMeta => {
private normalizeTokenTx = (txMeta: EtherscanTransactionMeta, currentNetworkID: string, currentChainId: string): TransactionMeta => {
const time = parseInt(txMeta.timeStamp, 10) * 1000;
const {
to, from, gas, gasPrice, hash, contractAddress, tokenDecimal, tokenSymbol, value,
Expand All @@ -271,6 +274,7 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
id: random({ msecs: time }),
isTransfer: true,
networkID: currentNetworkID,
chainId: currentChainId,
status: 'confirmed',
time,
transaction: {
Expand Down Expand Up @@ -381,12 +385,17 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
transaction = normalizeTransaction(transaction);
validateTransaction(transaction);

/* istanbul ignore next */
const networkID = network?.state?.provider?.chainId;
const {
state: {
network: networkID,
provider: { chainId },
},
} = network;

const transactionMeta = {
id: random(),
networkID,
chainId,
origin,
status: 'unapproved',
time: Date.now(),
Expand Down Expand Up @@ -649,12 +658,14 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
async queryTransactionStatuses() {
const { transactions } = this.state;
const network = this.context.NetworkController;
const currentNetworkID = network.state.provider.chainId;
const currentChainId = network.state.provider.chainId;
const currentNetworkID = network.state.network;
let gotUpdates = false;
await safelyExecute(() =>
Promise.all(
transactions.map(async (meta, index) => {
if (meta.status === 'submitted' && meta.networkID === currentNetworkID) {
// Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
if (meta.status === 'submitted' && (meta.chainId === currentChainId || (!meta.chainId && meta.networkID === currentNetworkID))) {
const txObj = await query(this.ethQuery, 'getTransactionByHash', [meta.transactionHash]);
/* istanbul ignore else */
if (txObj && txObj.blockNumber) {
Expand Down Expand Up @@ -701,8 +712,14 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
if (!network) {
return;
}
const currentNetworkID = network.state.provider.chainId;
const newTransactions = this.state.transactions.filter(({ networkID }) => networkID !== currentNetworkID);
const currentChainId = network.state.provider.chainId;
const currentNetworkID = network.state.network;
const newTransactions = this.state.transactions.filter(({ networkID, chainId }) => {
// Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
const isCurrentNetwork = (chainId === currentChainId || (!chainId && networkID === currentNetworkID));
return !isCurrentNetwork;
});

this.update({ transactions: newTransactions });
}

Expand All @@ -719,7 +736,7 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
const {
state: {
network: currentNetworkID,
provider: { type: networkType },
provider: { type: networkType, chainId: currentChainId },
},
} = network;

Expand All @@ -736,14 +753,14 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
etherscanTxResponse.result.forEach((tx: EtherscanTransactionMeta) => {
/* istanbul ignore next */
if (!remoteTxList[tx.hash]) {
const cleanTx = this.normalizeTx(tx, currentNetworkID);
const cleanTx = this.normalizeTx(tx, currentNetworkID, currentChainId);
remoteTxs.push(cleanTx);
remoteTxList[tx.hash] = 1;
}
});

etherscanTokenResponse.result.forEach((tx: EtherscanTransactionMeta) => {
const cleanTx = this.normalizeTokenTx(tx, currentNetworkID);
const cleanTx = this.normalizeTokenTx(tx, currentNetworkID, currentChainId);
remoteTxs.push(cleanTx);
/* istanbul ignore next */
remoteTxList[cleanTx.transactionHash || ''] = 1;
Expand All @@ -761,7 +778,8 @@ export class TransactionController extends BaseController<TransactionConfig, Tra
allTxs.forEach(async (tx) => {
/* istanbul ignore next */
if (
tx.networkID === currentNetworkID &&
// Using fallback to networkID only when there is no chainId present. Should be removed when networkID is completely removed.
(tx.chainId === currentChainId || (!tx.chainId && tx.networkID === currentNetworkID)) &&
tx.transaction.to &&
tx.transaction.to.toLowerCase() === address.toLowerCase()
) {
Expand Down