Skip to content

Commit

Permalink
feat: add support for returning a txHash asap (#467)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan437 authored Nov 27, 2024
1 parent 42e161c commit e1605d9
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 14 deletions.
171 changes: 169 additions & 2 deletions src/SmartTransactionsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import type {
SmartTransactionsControllerEvents,
} from './SmartTransactionsController';
import type { SmartTransaction, UnsignedTransaction, Hex } from './types';
import { SmartTransactionStatuses } from './types';
import { SmartTransactionStatuses, ClientId } from './types';
import * as utils from './utils';

jest.mock('@ethersproject/bytes', () => ({
Expand Down Expand Up @@ -1214,6 +1214,170 @@ describe('SmartTransactionsController', () => {
},
);
});

it('calls updateTransaction when smart transaction is cancelled and returnTxHashAsap is true', async () => {
const mockUpdateTransaction = jest.fn();
const defaultState = getDefaultSmartTransactionsControllerState();
const pendingStx = createStateAfterPending();
await withController(
{
options: {
updateTransaction: mockUpdateTransaction,
getFeatureFlags: () => ({
smartTransactions: {
mobileReturnTxHashAsap: true,
},
}),
getTransactions: () => [
{
id: 'test-tx-id',
status: TransactionStatus.submitted,
chainId: '0x1',
time: 123,
txParams: {
from: '0x123',
},
},
],
state: {
smartTransactionsState: {
...defaultState.smartTransactionsState,
smartTransactions: {
[ChainId.mainnet]: pendingStx as SmartTransaction[],
},
},
},
},
},
async ({ controller }) => {
const smartTransaction = {
uuid: 'uuid1',
status: SmartTransactionStatuses.CANCELLED,
transactionId: 'test-tx-id',
};

controller.updateSmartTransaction(smartTransaction);

expect(mockUpdateTransaction).toHaveBeenCalledWith(
{
id: 'test-tx-id',
status: TransactionStatus.failed,
chainId: '0x1',
time: 123,
txParams: {
from: '0x123',
},
},
'Smart transaction cancelled',
);
},
);
});

it('does not call updateTransaction when smart transaction is cancelled but returnTxHashAsap is false', async () => {
const mockUpdateTransaction = jest.fn();
await withController(
{
options: {
updateTransaction: mockUpdateTransaction,
getFeatureFlags: () => ({
smartTransactions: {
mobileReturnTxHashAsap: false,
},
}),
getTransactions: () => [
{
id: 'test-tx-id',
status: TransactionStatus.submitted,
chainId: '0x1',
time: 123,
txParams: {
from: '0x123',
},
},
],
},
},
async ({ controller }) => {
const smartTransaction = {
uuid: 'test-uuid',
status: SmartTransactionStatuses.CANCELLED,
transactionId: 'test-tx-id',
};

controller.updateSmartTransaction(smartTransaction);

expect(mockUpdateTransaction).not.toHaveBeenCalled();
},
);
});

it('does not call updateTransaction when transaction is not found in regular transactions', async () => {
const mockUpdateTransaction = jest.fn();

await withController(
{
options: {
updateTransaction: mockUpdateTransaction,
getFeatureFlags: () => ({
smartTransactions: {
mobileReturnTxHashAsap: true,
},
}),
getTransactions: () => [],
},
},
async ({ controller }) => {
const smartTransaction = {
uuid: 'test-uuid',
status: SmartTransactionStatuses.CANCELLED,
transactionId: 'test-tx-id',
};

controller.updateSmartTransaction(smartTransaction);

expect(mockUpdateTransaction).not.toHaveBeenCalled();
},
);
});

it('does not call updateTransaction for non-cancelled transactions', async () => {
const mockUpdateTransaction = jest.fn();
await withController(
{
options: {
updateTransaction: mockUpdateTransaction,
getFeatureFlags: () => ({
smartTransactions: {
mobileReturnTxHashAsap: true,
},
}),
getTransactions: () => [
{
id: 'test-tx-id',
status: TransactionStatus.submitted,
chainId: '0x1',
time: 123,
txParams: {
from: '0x123',
},
},
],
},
},
async ({ controller }) => {
const smartTransaction = {
uuid: 'test-uuid',
status: SmartTransactionStatuses.PENDING,
transactionId: 'test-tx-id',
};

controller.updateSmartTransaction(smartTransaction);

expect(mockUpdateTransaction).not.toHaveBeenCalled();
},
);
});
});

describe('cancelSmartTransaction', () => {
Expand Down Expand Up @@ -1438,7 +1602,7 @@ describe('SmartTransactionsController', () => {
const fetchHeaders = {
headers: {
'Content-Type': 'application/json',
'X-Client-Id': 'default',
'X-Client-Id': ClientId.Mobile,
},
};

Expand Down Expand Up @@ -1813,6 +1977,7 @@ async function withController<ReturnValue>(

const controller = new SmartTransactionsController({
messenger,
clientId: ClientId.Mobile,
getNonceLock: jest.fn().mockResolvedValue({
nextNonce: 'nextNonce',
releaseLock: jest.fn(),
Expand All @@ -1827,6 +1992,8 @@ async function withController<ReturnValue>(
deviceModel: 'ledger',
});
}),
getFeatureFlags: jest.fn(),
updateTransaction: jest.fn(),
...options,
});

Expand Down
58 changes: 47 additions & 11 deletions src/SmartTransactionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import type {
UnsignedTransaction,
GetTransactionsOptions,
MetaMetricsProps,
FeatureFlags,
ClientId,
} from './types';
import { APIType, SmartTransactionStatuses } from './types';
import {
Expand All @@ -53,11 +55,11 @@ import {
getTxHash,
getSmartTransactionMetricsProperties,
getSmartTransactionMetricsSensitiveProperties,
getReturnTxHashAsap,
} from './utils';

const SECOND = 1000;
export const DEFAULT_INTERVAL = SECOND * 5;
const DEFAULT_CLIENT_ID = 'default';
const ETH_QUERY_ERROR_MSG =
'`ethQuery` is not defined on SmartTransactionsController';

Expand Down Expand Up @@ -178,7 +180,7 @@ export type SmartTransactionsControllerMessenger =

type SmartTransactionsControllerOptions = {
interval?: number;
clientId?: string;
clientId: ClientId;
chainId?: Hex;
supportedChainIds?: Hex[];
getNonceLock: TransactionController['getNonceLock'];
Expand All @@ -198,6 +200,8 @@ type SmartTransactionsControllerOptions = {
messenger: SmartTransactionsControllerMessenger;
getTransactions: (options?: GetTransactionsOptions) => TransactionMeta[];
getMetaMetricsProps: () => Promise<MetaMetricsProps>;
getFeatureFlags: () => FeatureFlags;
updateTransaction: (transaction: TransactionMeta, note: string) => void;
};

export type SmartTransactionsControllerPollingInput = {
Expand All @@ -211,7 +215,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
> {
#interval: number;

#clientId: string;
#clientId: ClientId;

#chainId: Hex;

Expand All @@ -233,6 +237,10 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo

readonly #getMetaMetricsProps: () => Promise<MetaMetricsProps>;

#getFeatureFlags: SmartTransactionsControllerOptions['getFeatureFlags'];

#updateTransaction: SmartTransactionsControllerOptions['updateTransaction'];

/* istanbul ignore next */
async #fetch(request: string, options?: RequestInit) {
const fetchOptions = {
Expand All @@ -248,7 +256,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo

constructor({
interval = DEFAULT_INTERVAL,
clientId = DEFAULT_CLIENT_ID,
clientId,
chainId: InitialChainId = ChainId.mainnet,
supportedChainIds = [ChainId.mainnet, ChainId.sepolia],
getNonceLock,
Expand All @@ -258,6 +266,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
messenger,
getTransactions,
getMetaMetricsProps,
getFeatureFlags,
updateTransaction,
}: SmartTransactionsControllerOptions) {
super({
name: controllerName,
Expand All @@ -279,6 +289,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
this.#getRegularTransactions = getTransactions;
this.#trackMetaMetricsEvent = trackMetaMetricsEvent;
this.#getMetaMetricsProps = getMetaMetricsProps;
this.#getFeatureFlags = getFeatureFlags;
this.#updateTransaction = updateTransaction;

this.initializeSmartTransactionsForChainId();

Expand Down Expand Up @@ -530,24 +542,47 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
return;
}

const currentSmartTransaction = currentSmartTransactions[currentIndex];
const nextSmartTransaction = {
...currentSmartTransaction,
...smartTransaction,
};

// We have to emit this event here, because then a txHash is returned to the TransactionController once it's available
// and the #doesTransactionNeedConfirmation function will work properly, since it will find the txHash in the regular transactions list.
this.messagingSystem.publish(
`SmartTransactionsController:smartTransaction`,
smartTransaction,
nextSmartTransaction,
);

if (nextSmartTransaction.status === SmartTransactionStatuses.CANCELLED) {
const returnTxHashAsap = getReturnTxHashAsap(
this.#clientId,
this.#getFeatureFlags()?.smartTransactions,
);
if (returnTxHashAsap && nextSmartTransaction.transactionId) {
const foundTransaction = this.#getRegularTransactions().find(
(transaction) =>
transaction.id === nextSmartTransaction.transactionId,
);
if (foundTransaction) {
const updatedTransaction = {
...foundTransaction,
status: TransactionStatus.failed,
};
this.#updateTransaction(
updatedTransaction as TransactionMeta,
'Smart transaction cancelled',
);
}
}
}

if (
(smartTransaction.status === SmartTransactionStatuses.SUCCESS ||
smartTransaction.status === SmartTransactionStatuses.REVERTED) &&
!smartTransaction.confirmed
) {
// confirm smart transaction
const currentSmartTransaction = currentSmartTransactions[currentIndex];
const nextSmartTransaction = {
...currentSmartTransaction,
...smartTransaction,
};
await this.#confirmSmartTransaction(nextSmartTransaction, {
chainId,
ethQuery,
Expand Down Expand Up @@ -892,6 +927,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
txHash: submitTransactionResponse.txHash,
cancellable: true,
type: transactionMeta?.type ?? 'swap',
transactionId: transactionMeta?.id,
},
{ chainId, ethQuery },
);
Expand Down
4 changes: 4 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SmartTransactionsController, {
type AllowedActions,
type AllowedEvents,
} from './SmartTransactionsController';
import { ClientId } from './types';

describe('default export', () => {
it('exports SmartTransactionsController', () => {
Expand All @@ -30,6 +31,9 @@ describe('default export', () => {
getMetaMetricsProps: jest.fn(async () => {
return Promise.resolve({});
}),
getFeatureFlags: jest.fn(),
updateTransaction: jest.fn(),
clientId: ClientId.Extension,
});
expect(controller).toBeInstanceOf(SmartTransactionsController);
jest.clearAllTimers();
Expand Down
Loading

0 comments on commit e1605d9

Please sign in to comment.