Skip to content

Commit

Permalink
Add support for more transactions, emit events (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan437 authored Feb 5, 2024
1 parent a093681 commit d280879
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 15 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@metamask/network-controller": "^17.2.0",
"@metamask/polling-controller": "^5.0.0",
"bignumber.js": "^9.0.1",
"events": "^3.3.0",
"fast-json-patch": "^3.1.0",
"lodash": "^4.17.21"
},
Expand Down
48 changes: 46 additions & 2 deletions src/SmartTransactionsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ import SmartTransactionsController, {
DEFAULT_INTERVAL,
} from './SmartTransactionsController';
import { API_BASE_URL, CHAIN_IDS } from './constants';
import { SmartTransaction, SmartTransactionStatuses } from './types';
import {
SmartTransaction,
SmartTransactionStatuses,
UnsignedTransaction,
} from './types';
import * as utils from './utils';
import { flushPromises, advanceTime } from './test-helpers';

Expand Down Expand Up @@ -63,7 +67,7 @@ const createUnsignedTransaction = (chainId: number) => {
to: '0x0000000000000000000000000000000000000000',
value: 0,
data: '0x',
nonce: 0,
nonce: 1,
type: 2,
chainId,
};
Expand Down Expand Up @@ -507,6 +511,32 @@ describe('SmartTransactionsController', () => {
});
});

describe('clearFees', () => {
it('clears fees', async () => {
const tradeTx = createUnsignedTransaction(ethereumChainIdDec);
const approvalTx = createUnsignedTransaction(ethereumChainIdDec);
const getFeesApiResponse = createGetFeesApiResponse();
nock(API_BASE_URL)
.post(`/networks/${ethereumChainIdDec}/getFees`)
.reply(200, getFeesApiResponse);
const fees = await smartTransactionsController.getFees(
tradeTx,
approvalTx,
);
expect(fees).toMatchObject({
approvalTxFees: getFeesApiResponse.txs[0],
tradeTxFees: getFeesApiResponse.txs[1],
});
await smartTransactionsController.clearFees();
expect(
smartTransactionsController.state.smartTransactionsState.fees,
).toStrictEqual({
approvalTxFees: undefined,
tradeTxFees: undefined,
});
});
});

describe('getFees', () => {
it('gets unsigned transactions and estimates based on an unsigned transaction', async () => {
const tradeTx = createUnsignedTransaction(ethereumChainIdDec);
Expand All @@ -525,6 +555,20 @@ describe('SmartTransactionsController', () => {
});
});

it('gets estimates based on an unsigned transaction with an undefined nonce', async () => {
const tradeTx: UnsignedTransaction =
createUnsignedTransaction(ethereumChainIdDec);
tradeTx.nonce = undefined;
const getFeesApiResponse = createGetFeesApiResponse();
nock(API_BASE_URL)
.post(`/networks/${ethereumChainIdDec}/getFees`)
.reply(200, getFeesApiResponse);
const fees = await smartTransactionsController.getFees(tradeTx);
expect(fees).toMatchObject({
tradeTxFees: getFeesApiResponse.txs[0],
});
});

it('should add fee data to feesByChainId state using the networkClientId passed in to identify the appropriate chain', async () => {
const tradeTx = createUnsignedTransaction(goerliChainIdDec);
const approvalTx = createUnsignedTransaction(goerliChainIdDec);
Expand Down
55 changes: 42 additions & 13 deletions src/SmartTransactionsController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EventEmitter from 'events';
import { BaseConfig, BaseState } from '@metamask/base-controller';
import { safelyExecute, query } from '@metamask/controller-utils';
import {
Expand All @@ -10,6 +11,7 @@ import { StaticIntervalPollingControllerV1 } from '@metamask/polling-controller'
import { BigNumber } from 'bignumber.js';
import { hexlify } from '@ethersproject/bytes';
import cloneDeep from 'lodash/cloneDeep';

import {
APIType,
SmartTransaction,
Expand Down Expand Up @@ -77,6 +79,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo

private trackMetaMetricsEvent: any;

private eventEmitter: EventEmitter;

private getNetworkClientById: NetworkController['getNetworkClientById'];

/* istanbul ignore next */
Expand Down Expand Up @@ -169,6 +173,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
});

this.subscribe((currentState: any) => this.checkPoll(currentState));
this.eventEmitter = new EventEmitter();
}

_executePoll(networkClientId: string): Promise<void> {
Expand Down Expand Up @@ -322,7 +327,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
): void {
const { smartTransactionsState } = this.state;
const { smartTransactions } = smartTransactionsState;
const currentSmartTransactions = smartTransactions[chainId];
const currentSmartTransactions = smartTransactions[chainId] ?? [];
const currentIndex = currentSmartTransactions?.findIndex(
(stx) => stx.uuid === smartTransaction.uuid,
);
Expand Down Expand Up @@ -377,10 +382,12 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
...currentSmartTransaction,
...smartTransaction,
};
this.#confirmSmartTransaction(nextSmartTransaction, {
chainId,
ethQuery,
});
if (!smartTransaction.skipConfirm) {
this.#confirmSmartTransaction(nextSmartTransaction, {
chainId,
ethQuery,
});
}
}

this.update({
Expand Down Expand Up @@ -430,6 +437,9 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
ethQuery: EthQuery;
},
) {
if (smartTransaction.skipConfirm) {
return;
}
const txHash = smartTransaction.statusMetadata?.minedHash;
try {
const transactionReceipt: {
Expand Down Expand Up @@ -532,6 +542,12 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
>;

Object.entries(data).forEach(([uuid, stxStatus]) => {
const transactionHash = stxStatus?.minedHash;
this.eventEmitter.emit(`${uuid}:status`, stxStatus);
if (transactionHash) {
this.eventEmitter.emit(`${uuid}:transaction-hash`, transactionHash);
}

this.#updateSmartTransaction(
{
statusMetadata: stxStatus,
Expand Down Expand Up @@ -574,7 +590,7 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo

async getFees(
tradeTx: UnsignedTransaction,
approvalTx: UnsignedTransaction,
approvalTx?: UnsignedTransaction,
{ networkClientId }: { networkClientId?: NetworkClientId } = {},
): Promise<Fees> {
const chainId = this.#getChainId({ networkClientId });
Expand All @@ -589,6 +605,8 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
// If there is an approval tx, the trade tx's nonce is increased by 1.
nonce: incrementNonceInHex(unsignedApprovalTransactionWithNonce.nonce),
};
} else if (tradeTx.nonce) {
unsignedTradeTransactionWithNonce = tradeTx;
} else {
unsignedTradeTransactionWithNonce = await this.addNonceToTransaction(
tradeTx,
Expand Down Expand Up @@ -642,11 +660,13 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
signedTransactions,
signedCanceledTransactions,
networkClientId,
skipConfirm,
}: {
signedTransactions: SignedTransaction[];
signedCanceledTransactions: SignedCanceledTransaction[];
txParams?: any;
networkClientId?: NetworkClientId;
skipConfirm?: boolean;
}) {
const chainId = this.#getChainId({ networkClientId });
const ethQuery = this.#getEthQuery({ networkClientId });
Expand All @@ -670,14 +690,22 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
} catch (e) {
console.error('provider error', e);
}
const nonceLock = await this.getNonceLock(txParams?.from);
try {
const nonce = hexlify(nonceLock.nextNonce);
if (txParams && !txParams?.nonce) {
txParams.nonce = nonce;

const requiresNonce = !txParams.nonce;
let nonce;
let nonceLock;
let nonceDetails = {};

if (requiresNonce) {
nonceLock = await this.getNonceLock(txParams?.from);
nonce = hexlify(nonceLock.nextNonce);
nonceDetails = nonceLock.nonceDetails;
if (txParams) {
txParams.nonce ??= nonce;
}
const { nonceDetails } = nonceLock;
}

try {
this.#updateSmartTransaction(
{
chainId,
Expand All @@ -688,11 +716,12 @@ export default class SmartTransactionsController extends StaticIntervalPollingCo
txParams,
uuid: data.uuid,
cancellable: true,
skipConfirm: skipConfirm ?? false,
},
{ chainId, ethQuery },
);
} finally {
nonceLock.releaseLock();
nonceLock?.releaseLock();
}

return data;
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export type SmartTransaction = {
type?: string;
confirmed?: boolean;
cancellable?: boolean;
skipConfirm?: boolean;
};

export type Fee = {
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,7 @@ __metadata:
eslint-plugin-jsdoc: ^39.2.9
eslint-plugin-node: ^11.1.0
eslint-plugin-prettier: ^4.2.1
events: ^3.3.0
fast-json-patch: ^3.1.0
isomorphic-fetch: ^3.0.0
jest: ^26.4.2
Expand Down Expand Up @@ -3597,6 +3598,13 @@ __metadata:
languageName: node
linkType: hard

"events@npm:^3.3.0":
version: 3.3.0
resolution: "events@npm:3.3.0"
checksum: f6f487ad2198aa41d878fa31452f1a3c00958f46e9019286ff4787c84aac329332ab45c9cdc8c445928fc6d7ded294b9e005a7fce9426488518017831b272780
languageName: node
linkType: hard

"evp_bytestokey@npm:^1.0.3":
version: 1.0.3
resolution: "evp_bytestokey@npm:1.0.3"
Expand Down

0 comments on commit d280879

Please sign in to comment.