Skip to content

Commit 342023d

Browse files
authored
fix: simulation should handle NFT mints (#4217)
1 parent 9349754 commit 342023d

File tree

2 files changed

+111
-23
lines changed

2 files changed

+111
-23
lines changed

packages/transaction-controller/src/utils/simulation.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,61 @@ describe('Simulation Utils', () => {
559559
});
560560
});
561561

562+
it('on NFT mint', async () => {
563+
mockParseLog({
564+
erc721: {
565+
...PARSED_ERC721_TRANSFER_EVENT_MOCK,
566+
args: [
567+
'0x0000000000000000000000000000000000000000',
568+
USER_ADDRESS_MOCK,
569+
TOKEN_ID_MOCK,
570+
],
571+
},
572+
});
573+
574+
simulateTransactionsMock
575+
.mockResolvedValueOnce(
576+
createEventResponseMock([createLogMock(CONTRACT_ADDRESS_MOCK)]),
577+
)
578+
.mockResolvedValueOnce(
579+
createBalanceOfResponse([], [USER_ADDRESS_MOCK]),
580+
);
581+
582+
const simulationData = await getSimulationData(REQUEST_MOCK);
583+
584+
expect(simulateTransactionsMock).toHaveBeenCalledTimes(2);
585+
// The second call should only simulate the minting of the NFT and
586+
// check the balance after, and not before.
587+
expect(simulateTransactionsMock).toHaveBeenNthCalledWith(
588+
2,
589+
REQUEST_MOCK.chainId,
590+
{
591+
transactions: [
592+
REQUEST_MOCK,
593+
{
594+
from: REQUEST_MOCK.from,
595+
to: CONTRACT_ADDRESS_MOCK,
596+
data: expect.any(String),
597+
},
598+
],
599+
},
600+
);
601+
expect(simulationData).toStrictEqual({
602+
nativeBalanceChange: undefined,
603+
tokenBalanceChanges: [
604+
{
605+
standard: SimulationTokenStandard.erc721,
606+
address: CONTRACT_ADDRESS_MOCK,
607+
id: TOKEN_ID_MOCK,
608+
previousBalance: '0x0',
609+
newBalance: '0x1',
610+
difference: '0x1',
611+
isDecrease: false,
612+
},
613+
],
614+
});
615+
});
616+
562617
it('as empty if events cannot be parsed', async () => {
563618
mockParseLog({});
564619

packages/transaction-controller/src/utils/simulation.ts

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ const SUPPORTED_TOKEN_ABIS = {
9191

9292
const REVERTED_ERRORS = ['execution reverted', 'insufficient funds for gas'];
9393

94+
type BalanceTransactionMap = Map<SimulationToken, SimulationRequestTransaction>;
95+
9496
/**
9597
* Generate simulation data for a transaction.
9698
* @param request - The transaction to simulate.
@@ -199,7 +201,7 @@ function getNativeBalanceChange(
199201
* @param response - The simulation response.
200202
* @returns The parsed events.
201203
*/
202-
function getEvents(response: SimulationResponse): ParsedEvent[] {
204+
export function getEvents(response: SimulationResponse): ParsedEvent[] {
203205
/* istanbul ignore next */
204206
const logs = extractLogs(
205207
response.transactions[0]?.callTrace ?? ({} as SimulationResponseCallTrace),
@@ -284,41 +286,45 @@ async function getTokenBalanceChanges(
284286
request: GetSimulationDataRequest,
285287
events: ParsedEvent[],
286288
): Promise<SimulationTokenBalanceChange[]> {
287-
const balanceTransactionsByToken = getTokenBalanceTransactions(
288-
request,
289-
events,
290-
);
289+
const balanceTxs = getTokenBalanceTransactions(request, events);
291290

292-
const balanceTransactions = [...balanceTransactionsByToken.values()];
291+
log('Generated balance transactions', [...balanceTxs.after.values()]);
293292

294-
log('Generated balance transactions', balanceTransactions);
293+
const transactions = [
294+
...balanceTxs.before.values(),
295+
request,
296+
...balanceTxs.after.values(),
297+
];
295298

296-
if (!balanceTransactions.length) {
299+
if (transactions.length === 1) {
297300
return [];
298301
}
299302

300303
const response = await simulateTransactions(request.chainId as Hex, {
301-
transactions: [...balanceTransactions, request, ...balanceTransactions],
304+
transactions,
302305
});
303306

304307
log('Balance simulation response', response);
305308

306-
if (response.transactions.length !== balanceTransactions.length * 2 + 1) {
309+
if (response.transactions.length !== transactions.length) {
307310
throw new SimulationInvalidResponseError();
308311
}
309312

310-
return [...balanceTransactionsByToken.keys()]
313+
return [...balanceTxs.after.keys()]
311314
.map((token, index) => {
312-
const previousBalance = getValueFromBalanceTransaction(
313-
request.from,
314-
token,
315-
response.transactions[index],
316-
);
315+
const previousBalanceCheckSkipped = !balanceTxs.before.get(token);
316+
const previousBalance = previousBalanceCheckSkipped
317+
? '0x0'
318+
: getValueFromBalanceTransaction(
319+
request.from,
320+
token,
321+
response.transactions[index],
322+
);
317323

318324
const newBalance = getValueFromBalanceTransaction(
319325
request.from,
320326
token,
321-
response.transactions[index + balanceTransactions.length + 1],
327+
response.transactions[index + balanceTxs.before.size + 1],
322328
);
323329

324330
const balanceChange = getSimulationBalanceChange(
@@ -347,8 +353,13 @@ async function getTokenBalanceChanges(
347353
function getTokenBalanceTransactions(
348354
request: GetSimulationDataRequest,
349355
events: ParsedEvent[],
350-
): Map<SimulationToken, SimulationRequestTransaction> {
356+
): {
357+
before: BalanceTransactionMap;
358+
after: BalanceTransactionMap;
359+
} {
351360
const tokenKeys = new Set();
361+
const before = new Map();
362+
const after = new Map();
352363

353364
const userEvents = events.filter(
354365
(event) =>
@@ -358,7 +369,7 @@ function getTokenBalanceTransactions(
358369

359370
log('Filtered user events', userEvents);
360371

361-
return userEvents.reduce((result, event) => {
372+
for (const event of userEvents) {
362373
const tokenIds = getEventTokenIds(event);
363374

364375
log('Extracted token ids', tokenIds);
@@ -388,15 +399,37 @@ function getTokenBalanceTransactions(
388399
tokenId,
389400
);
390401

391-
result.set(simulationToken, {
402+
const transaction: SimulationRequestTransaction = {
392403
from: request.from,
393404
to: event.contractAddress,
394405
data,
395-
});
406+
};
407+
408+
if (skipPriorBalanceCheck(event)) {
409+
after.set(simulationToken, transaction);
410+
} else {
411+
before.set(simulationToken, transaction);
412+
after.set(simulationToken, transaction);
413+
}
396414
}
415+
}
397416

398-
return result;
399-
}, new Map<SimulationToken, SimulationRequestTransaction>());
417+
return { before, after };
418+
}
419+
420+
/**
421+
* Check if an event needs to check the previous balance.
422+
* @param event - The parsed event.
423+
* @returns True if the prior balance check should be skipped.
424+
*/
425+
function skipPriorBalanceCheck(event: ParsedEvent): boolean {
426+
// In the case of an NFT mint, we cannot check the NFT owner before the mint
427+
// as the balance check transaction would revert.
428+
return (
429+
event.name === 'Transfer' &&
430+
event.tokenStandard === SimulationTokenStandard.erc721 &&
431+
parseInt(event.args.from as string, 16) === 0
432+
);
400433
}
401434

402435
/**

0 commit comments

Comments
 (0)