Skip to content

Commit a2edc02

Browse files
authored
fix: Improve error handling and monitoring (#508)
1 parent 38929d8 commit a2edc02

File tree

2 files changed

+328
-12
lines changed

2 files changed

+328
-12
lines changed

src/SmartTransactionsController.test.ts

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,6 +723,183 @@ describe('SmartTransactionsController', () => {
723723
});
724724
});
725725

726+
it('should acquire nonce for Swap transactions only', async () => {
727+
// Create a mock for getNonceLock
728+
const mockGetNonceLock = jest.fn().mockResolvedValue({
729+
nextNonce: 'nextNonce',
730+
nonceDetails: { test: 'details' },
731+
releaseLock: jest.fn(),
732+
});
733+
734+
await withController(
735+
{
736+
options: {
737+
getNonceLock: mockGetNonceLock,
738+
},
739+
},
740+
async ({ controller }) => {
741+
const signedTransaction = createSignedTransaction();
742+
const submitTransactionsApiResponse =
743+
createSubmitTransactionsApiResponse();
744+
745+
// First API mock for the case without nonce
746+
nock(API_BASE_URL)
747+
.post(
748+
`/networks/${ethereumChainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`,
749+
)
750+
.reply(200, submitTransactionsApiResponse);
751+
752+
// Second API mock for the case with nonce
753+
nock(API_BASE_URL)
754+
.post(
755+
`/networks/${ethereumChainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`,
756+
)
757+
.reply(200, submitTransactionsApiResponse);
758+
759+
// Case 1: Swap transaction without nonce (should call getNonceLock)
760+
const txParamsWithoutNonce = {
761+
...createTxParams(),
762+
nonce: undefined, // Explicitly undefined nonce
763+
};
764+
765+
await controller.submitSignedTransactions({
766+
signedTransactions: [signedTransaction],
767+
txParams: txParamsWithoutNonce,
768+
// No transactionMeta means type defaults to 'swap'
769+
});
770+
771+
// Verify getNonceLock was called for the Swap
772+
expect(mockGetNonceLock).toHaveBeenCalledTimes(1);
773+
expect(mockGetNonceLock).toHaveBeenCalledWith(
774+
txParamsWithoutNonce.from,
775+
NetworkType.mainnet,
776+
);
777+
778+
// Reset the mock
779+
mockGetNonceLock.mockClear();
780+
781+
// Case 2: Transaction with nonce already set (should NOT call getNonceLock)
782+
const txParamsWithNonce = createTxParams(); // This has nonce: '0'
783+
784+
await controller.submitSignedTransactions({
785+
signedTransactions: [signedTransaction],
786+
txParams: txParamsWithNonce,
787+
});
788+
789+
// Verify getNonceLock was NOT called for transaction with nonce
790+
expect(mockGetNonceLock).not.toHaveBeenCalled();
791+
},
792+
);
793+
});
794+
795+
it('should properly set nonce on txParams and mark transaction as swap type', async () => {
796+
// Mock with a specific nextNonce value we can verify
797+
const mockGetNonceLock = jest.fn().mockResolvedValue({
798+
nextNonce: 42,
799+
nonceDetails: { test: 'nonce details' },
800+
releaseLock: jest.fn(),
801+
});
802+
803+
await withController(
804+
{
805+
options: {
806+
getNonceLock: mockGetNonceLock,
807+
},
808+
},
809+
async ({ controller }) => {
810+
const signedTransaction = createSignedTransaction();
811+
const submitTransactionsApiResponse =
812+
createSubmitTransactionsApiResponse();
813+
nock(API_BASE_URL)
814+
.post(
815+
`/networks/${ethereumChainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`,
816+
)
817+
.reply(200, submitTransactionsApiResponse);
818+
819+
// Create txParams without nonce
820+
const txParamsWithoutNonce = {
821+
...createTxParams(),
822+
nonce: undefined,
823+
from: addressFrom,
824+
};
825+
826+
await controller.submitSignedTransactions({
827+
signedTransactions: [signedTransaction],
828+
txParams: txParamsWithoutNonce,
829+
// No transactionMeta provided, should default to 'swap' type
830+
});
831+
832+
// Get the created smart transaction
833+
const createdSmartTransaction =
834+
controller.state.smartTransactionsState.smartTransactions[
835+
ChainId.mainnet
836+
][0];
837+
838+
// Verify nonce was set correctly on the txParams in the created transaction
839+
expect(createdSmartTransaction.txParams.nonce).toBe('0x42'); // 42 as a hex string
840+
841+
// Verify transaction type is set to 'swap' by default
842+
expect(createdSmartTransaction.type).toBe('swap');
843+
844+
// Verify nonceDetails were passed correctly
845+
expect(createdSmartTransaction.nonceDetails).toStrictEqual({
846+
test: 'nonce details',
847+
});
848+
},
849+
);
850+
});
851+
852+
it('should handle errors when acquiring nonce lock', async () => {
853+
// Mock getNonceLock to reject with an error
854+
const mockError = new Error('Failed to acquire nonce');
855+
const mockGetNonceLock = jest.fn().mockRejectedValue(mockError);
856+
857+
// Spy on console.error to verify it's called
858+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
859+
860+
await withController(
861+
{
862+
options: {
863+
getNonceLock: mockGetNonceLock,
864+
},
865+
},
866+
async ({ controller }) => {
867+
const signedTransaction = createSignedTransaction();
868+
const submitTransactionsApiResponse =
869+
createSubmitTransactionsApiResponse();
870+
nock(API_BASE_URL)
871+
.post(
872+
`/networks/${ethereumChainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`,
873+
)
874+
.reply(200, submitTransactionsApiResponse);
875+
876+
// Create txParams without nonce
877+
const txParamsWithoutNonce = {
878+
...createTxParams(),
879+
nonce: undefined,
880+
from: addressFrom,
881+
};
882+
883+
// Attempt to submit a transaction that will fail when acquiring nonce
884+
await expect(
885+
controller.submitSignedTransactions({
886+
signedTransactions: [signedTransaction],
887+
txParams: txParamsWithoutNonce,
888+
}),
889+
).rejects.toThrow('Failed to acquire nonce');
890+
891+
// Verify error was logged
892+
expect(consoleErrorSpy).toHaveBeenCalledWith(
893+
'Failed to acquire nonce lock:',
894+
mockError,
895+
);
896+
897+
// Cleanup spy
898+
consoleErrorSpy.mockRestore();
899+
},
900+
);
901+
});
902+
726903
it('submits a batch of signed transactions', async () => {
727904
await withController(async ({ controller }) => {
728905
const signedTransaction1 = createSignedTransaction();
@@ -2090,6 +2267,126 @@ describe('SmartTransactionsController', () => {
20902267
);
20912268
});
20922269
});
2270+
2271+
describe('createOrUpdateSmartTransaction', () => {
2272+
beforeEach(() => {
2273+
jest
2274+
.spyOn(SmartTransactionsController.prototype, 'checkPoll')
2275+
.mockImplementation(() => ({}));
2276+
});
2277+
2278+
it('adds metaMetricsProps to new smart transactions', async () => {
2279+
const { smartTransactionsState } =
2280+
getDefaultSmartTransactionsControllerState();
2281+
const newSmartTransaction = {
2282+
uuid: 'new-uuid-test',
2283+
status: SmartTransactionStatuses.PENDING,
2284+
txParams: {
2285+
from: addressFrom,
2286+
},
2287+
};
2288+
2289+
await withController(
2290+
{
2291+
options: {
2292+
state: {
2293+
smartTransactionsState: {
2294+
...smartTransactionsState,
2295+
smartTransactions: {
2296+
[ChainId.mainnet]: [],
2297+
},
2298+
},
2299+
},
2300+
getMetaMetricsProps: jest.fn().mockResolvedValue({
2301+
accountHardwareType: 'Test Hardware',
2302+
accountType: 'test-account',
2303+
deviceModel: 'test-model',
2304+
}),
2305+
},
2306+
},
2307+
async ({ controller }) => {
2308+
controller.updateSmartTransaction(
2309+
newSmartTransaction as SmartTransaction,
2310+
);
2311+
2312+
// Allow async operations to complete
2313+
await flushPromises();
2314+
2315+
// Verify MetaMetricsProps were added
2316+
const updatedTransaction =
2317+
controller.state.smartTransactionsState.smartTransactions[
2318+
ChainId.mainnet
2319+
][0];
2320+
expect(updatedTransaction.accountHardwareType).toBe('Test Hardware');
2321+
expect(updatedTransaction.accountType).toBe('test-account');
2322+
expect(updatedTransaction.deviceModel).toBe('test-model');
2323+
},
2324+
);
2325+
});
2326+
2327+
it('continues without metaMetricsProps if adding them fails', async () => {
2328+
const { smartTransactionsState } =
2329+
getDefaultSmartTransactionsControllerState();
2330+
const newSmartTransaction = {
2331+
uuid: 'new-uuid-test',
2332+
status: SmartTransactionStatuses.PENDING,
2333+
txParams: {
2334+
from: addressFrom,
2335+
},
2336+
};
2337+
2338+
// Mock console.error to verify it's called
2339+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
2340+
2341+
await withController(
2342+
{
2343+
options: {
2344+
state: {
2345+
smartTransactionsState: {
2346+
...smartTransactionsState,
2347+
smartTransactions: {
2348+
[ChainId.mainnet]: [],
2349+
},
2350+
},
2351+
},
2352+
// Mock getting MetaMetricsProps to fail
2353+
getMetaMetricsProps: jest
2354+
.fn()
2355+
.mockRejectedValue(new Error('Test metrics error')),
2356+
},
2357+
},
2358+
async ({ controller }) => {
2359+
controller.updateSmartTransaction(
2360+
newSmartTransaction as SmartTransaction,
2361+
);
2362+
2363+
// Allow async operations to complete
2364+
await flushPromises();
2365+
2366+
// Verify transaction was still added even without metrics props
2367+
const updatedTransaction =
2368+
controller.state.smartTransactionsState.smartTransactions[
2369+
ChainId.mainnet
2370+
][0];
2371+
expect(updatedTransaction.uuid).toBe('new-uuid-test');
2372+
2373+
// These should be undefined since getting metrics props failed
2374+
expect(updatedTransaction.accountHardwareType).toBeUndefined();
2375+
expect(updatedTransaction.accountType).toBeUndefined();
2376+
expect(updatedTransaction.deviceModel).toBeUndefined();
2377+
2378+
// Verify the error was logged
2379+
expect(consoleErrorSpy).toHaveBeenCalledWith(
2380+
'Failed to add metrics props to smart transaction:',
2381+
expect.any(Error),
2382+
);
2383+
2384+
// Clean up the spy
2385+
consoleErrorSpy.mockRestore();
2386+
},
2387+
);
2388+
});
2389+
});
20932390
});
20942391

20952392
type WithControllerCallback<ReturnValue> = ({

src/SmartTransactionsController.ts

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -513,12 +513,20 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
513513
smartTransaction.uuid,
514514
chainId,
515515
);
516-
if (this.#ethQuery === undefined) {
516+
if (ethQuery === undefined) {
517517
throw new Error(ETH_QUERY_ERROR_MSG);
518518
}
519519

520520
if (isNewSmartTransaction) {
521-
await this.#addMetaMetricsPropsToNewSmartTransaction(smartTransaction);
521+
try {
522+
await this.#addMetaMetricsPropsToNewSmartTransaction(smartTransaction);
523+
} catch (error) {
524+
console.error(
525+
'Failed to add metrics props to smart transaction:',
526+
error,
527+
);
528+
// Continue without metrics props
529+
}
522530
}
523531

524532
this.trackStxStatusChange(
@@ -955,28 +963,34 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
955963
preTxBalance = new BigNumber(preTxBalanceBN).toString(16);
956964
}
957965
} catch (error) {
958-
console.error('provider error', error);
966+
console.error('ethQuery.getBalance error:', error);
959967
}
960968

961969
const requiresNonce = txParams && !txParams.nonce;
962970
let nonce;
963971
let nonceLock;
964972
let nonceDetails = {};
965973

974+
// This should only happen for Swaps. Non-swaps transactions should already have a nonce
966975
if (requiresNonce) {
967-
nonceLock = await this.#getNonceLock(
968-
txParams.from,
969-
selectedNetworkClientId,
970-
);
971-
nonce = hexlify(nonceLock.nextNonce);
972-
nonceDetails = nonceLock.nonceDetails;
973-
txParams.nonce ??= nonce;
976+
try {
977+
nonceLock = await this.#getNonceLock(
978+
txParams.from,
979+
selectedNetworkClientId,
980+
);
981+
nonce = hexlify(nonceLock.nextNonce);
982+
nonceDetails = nonceLock.nonceDetails;
983+
txParams.nonce ??= nonce;
984+
} catch (error) {
985+
console.error('Failed to acquire nonce lock:', error);
986+
throw error;
987+
}
974988
}
975989

976990
const txHashes = signedTransactions.map((tx) => getTxHash(tx));
977991
const submitTransactionResponse = {
978992
...data,
979-
txHash: txHashes[0], // For backward compatibility
993+
txHash: txHashes[txHashes.length - 1], // For backward compatibility - use the last tx hash
980994
txHashes,
981995
};
982996

@@ -999,8 +1013,13 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
9991013
},
10001014
{ chainId, ethQuery },
10011015
);
1016+
} catch (error) {
1017+
console.error('Failed to create a smart transaction:', error);
1018+
throw error;
10021019
} finally {
1003-
nonceLock?.releaseLock();
1020+
if (nonceLock) {
1021+
nonceLock.releaseLock();
1022+
}
10041023
}
10051024

10061025
return submitTransactionResponse;

0 commit comments

Comments
 (0)