Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
34 changes: 26 additions & 8 deletions src/SmartTransactionsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,7 @@
await withController(
{
options: {
supportedChainIds: [ChainId.mainnet],
getSupportedChainIds: () => [ChainId.mainnet],
},
},
({ controller, triggerNetworStateChange }) => {
Expand Down Expand Up @@ -446,7 +446,7 @@

controller.timeoutHandle = setTimeout(() => ({}));

controller.poll(1000);

Check warning on line 449 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 449 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

expect(updateSmartTransactionsSpy).toHaveBeenCalled();
});
Expand Down Expand Up @@ -1141,6 +1141,26 @@
});
});

it('fetches liveness using custom getSentinelUrl', async () => {
const customSentinelUrl = 'https://custom-sentinel.example.com';
await withController(
{
options: {
getSentinelUrl: (_chainId: Hex) => customSentinelUrl,
},
},
async ({ controller }) => {
nock(customSentinelUrl)
.get(`/network`)
.reply(200, createSuccessLivenessApiResponse());

const liveness = await controller.fetchLiveness();

expect(liveness).toBe(true);
},
);
});

it('fetches liveness and sets in feesByChainId state for the Smart Transactions API for the chainId of the networkClientId passed in', async () => {
await withController(async ({ controller }) => {
nock(SENTINEL_API_BASE_URL_MAP[sepoliaChainIdDec])
Expand Down Expand Up @@ -1869,7 +1889,7 @@
await withController(
{
options: {
// pending transactions in state are required to test polling
getSupportedChainIds: () => [ChainId.mainnet, ChainId.sepolia],
state: {
smartTransactionsState: {
...getDefaultSmartTransactionsControllerState()
Expand Down Expand Up @@ -1989,7 +2009,7 @@
await withController(
{
options: {
// pending transactions in state are required to test polling
getSupportedChainIds: () => [ChainId.mainnet],
state: {
smartTransactionsState: {
...getDefaultSmartTransactionsControllerState()
Expand Down Expand Up @@ -2166,12 +2186,10 @@
);
});

it('removes transactions from the current chainId (even if it is not in supportedChainIds) if ignoreNetwork is false', async () => {
it('removes transactions from the current chainId (even if it is not returned by getSupportedChainIds) if ignoreNetwork is false', async () => {
Copy link

Choose a reason for hiding this comment

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

Bug: Test Setup Mismatches Test Case

The test "removes transactions from the current chainId (even if it is not returned by getSupportedChainIds)" no longer tests its stated edge case. The test setup now implicitly includes the current chain (Mainnet) in the supported chains via the default getSupportedChainIds function, contradicting the test's name. To correctly test the scenario, getSupportedChainIds should be explicitly configured to exclude the current chain (e.g., () => [ChainId.sepolia]).

Fix in Cursor Fix in Web

await withController(
{
options: {
supportedChainIds: [ChainId.sepolia],
chainId: ChainId.mainnet,
state: {
smartTransactionsState: {
...getDefaultSmartTransactionsControllerState()
Expand Down Expand Up @@ -2218,11 +2236,11 @@
);
});

it('removes transactions from all chains (even if they are not in supportedChainIds) if ignoreNetwork is true', async () => {
it('removes transactions from all chains (even if they are not returned by getSupportedChainIds) if ignoreNetwork is true', async () => {
await withController(
{
options: {
supportedChainIds: [],
getSupportedChainIds: () => [],
state: {
smartTransactionsState: {
...getDefaultSmartTransactionsControllerState()
Expand Down Expand Up @@ -2703,7 +2721,7 @@
triggerNetworStateChange,
});
} finally {
controller.stop();

Check warning on line 2724 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 2724 in src/SmartTransactionsController.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
controller.stopAllPolling();
}
}
33 changes: 21 additions & 12 deletions src/SmartTransactionsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@
interval?: number;
clientId: ClientId;
chainId?: Hex;
supportedChainIds?: Hex[];
getNonceLock: TransactionController['getNonceLock'];
confirmExternalTransaction: TransactionController['confirmExternalTransaction'];
trackMetaMetricsEvent: (
Expand All @@ -212,6 +211,8 @@
getFeatureFlags: () => FeatureFlags;
updateTransaction: (transaction: TransactionMeta, note: string) => void;
trace?: TraceCallback;
getSentinelUrl?: (chainId: Hex) => string | undefined;
getSupportedChainIds?: () => Hex[];
};

export type SmartTransactionsControllerPollingInput = {
Expand All @@ -229,8 +230,6 @@

#chainId: Hex;

#supportedChainIds: Hex[];

timeoutHandle?: NodeJS.Timeout;

readonly #getNonceLock: SmartTransactionsControllerOptions['getNonceLock'];
Expand All @@ -253,6 +252,10 @@

#trace: TraceCallback;

#getSentinelUrl?: SmartTransactionsControllerOptions['getSentinelUrl'];

#getSupportedChainIds: () => Hex[];

/* istanbul ignore next */
async #fetch(request: string, options?: RequestInit) {
const fetchOptions = {
Expand All @@ -270,7 +273,6 @@
interval = DEFAULT_INTERVAL,
clientId,
chainId: InitialChainId = ChainId.mainnet,
supportedChainIds = [ChainId.mainnet, ChainId.sepolia],
getNonceLock,
confirmExternalTransaction,
trackMetaMetricsEvent,
Expand All @@ -281,6 +283,8 @@
getFeatureFlags,
updateTransaction,
trace,
getSentinelUrl,
getSupportedChainIds = () => [ChainId.mainnet, ChainId.sepolia],
}: SmartTransactionsControllerOptions) {
super({
name: controllerName,
Expand All @@ -294,7 +298,6 @@
this.#interval = interval;
this.#clientId = clientId;
this.#chainId = InitialChainId;
this.#supportedChainIds = supportedChainIds;
this.setIntervalLength(interval);
this.#getNonceLock = getNonceLock;
this.#ethQuery = undefined;
Expand All @@ -305,6 +308,8 @@
this.#getFeatureFlags = getFeatureFlags;
this.#updateTransaction = updateTransaction;
this.#trace = trace ?? (((_request, fn) => fn?.()) as TraceCallback);
this.#getSentinelUrl = getSentinelUrl;
this.#getSupportedChainIds = getSupportedChainIds;

this.initializeSmartTransactionsForChainId();

Expand Down Expand Up @@ -339,7 +344,7 @@
// wondering if we should add some kind of predicate to the polling controller to check whether
// we should poll or not
const filteredChainIds = (chainIds ?? []).filter((chainId) =>
this.#supportedChainIds.includes(chainId),
this.#getSupportedChainIds().includes(chainId),
);

if (filteredChainIds.length === 0) {
Expand All @@ -358,14 +363,14 @@
isSmartTransactionPending,
);
if (!this.timeoutHandle && pendingTransactions?.length > 0) {
this.poll();

Check warning on line 366 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 366 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
} else if (this.timeoutHandle && pendingTransactions?.length === 0) {
this.stop();

Check warning on line 368 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 368 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
}

initializeSmartTransactionsForChainId() {
if (this.#supportedChainIds.includes(this.#chainId)) {
if (this.#getSupportedChainIds().includes(this.#chainId)) {
this.update((state) => {
state.smartTransactionsState.smartTransactions[this.#chainId] =
state.smartTransactionsState.smartTransactions[this.#chainId] ?? [];
Expand All @@ -380,12 +385,12 @@

this.timeoutHandle && clearInterval(this.timeoutHandle);

if (!this.#supportedChainIds.includes(this.#chainId)) {
if (!this.#getSupportedChainIds().includes(this.#chainId)) {
return;
}

this.timeoutHandle = setInterval(() => {
safelyExecute(async () => this.updateSmartTransactions());

Check warning on line 393 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 393 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}, this.#interval);
await safelyExecute(async () => this.updateSmartTransactions());
}
Expand Down Expand Up @@ -452,7 +457,7 @@
ethQuery = new EthQuery(provider);
}

this.#createOrUpdateSmartTransaction(smartTransaction, {

Check warning on line 460 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 460 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
chainId,
ethQuery,
});
Expand Down Expand Up @@ -739,7 +744,7 @@
: originalTxMeta;

if (this.#doesTransactionNeedConfirmation(txHash)) {
this.#confirmExternalTransaction(

Check warning on line 747 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator

Check warning on line 747 in src/SmartTransactionsController.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
// TODO: Replace 'as' assertion with correct typing for `txMeta`
txMeta as TransactionMeta,
transactionReceipt,
Expand Down Expand Up @@ -1059,7 +1064,7 @@
);
return Object.keys(networkConfigurationsByChainId).filter(
(chainId): chainId is Hex =>
this.#supportedChainIds.includes(chainId as Hex),
this.#getSupportedChainIds().includes(chainId as Hex),
);
}

Expand Down Expand Up @@ -1122,14 +1127,18 @@
const chainId = this.#getChainId({ networkClientId });
let liveness = false;
try {
const url = getAPIRequestURL(
APIType.LIVENESS,
chainId,
this.#getSentinelUrl?.(chainId),
);
const response = await this.#trace(
{ name: SmartTransactionsTraceName.FetchLiveness },
async () =>
await this.#fetch(getAPIRequestURL(APIType.LIVENESS, chainId)),
async () => await this.#fetch(url),
);
liveness = Boolean(response.smartTransactions);
} catch (error) {
console.log('"fetchLiveness" API call failed');
console.error('"fetchLiveness" API call failed:', error);
}

this.update((state) => {
Expand Down
20 changes: 20 additions & 0 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,24 @@
expect(updateTransactionMock).not.toHaveBeenCalled();
});
});

describe('getSentinelBaseUrl', () => {
it('returns the correct base URL for Ethereum Mainnet', () => {
const chainId = ChainId.mainnet;
const result = utils.getSentinelBaseUrl(chainId);
expect(result).toBe(SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.mainnet, 16)]);

Check failure on line 629 in src/utils.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Replace `SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.mainnet,·16)]` with `⏎········SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.mainnet,·16)],⏎······`

Check failure on line 629 in src/utils.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Replace `SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.mainnet,·16)]` with `⏎········SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.mainnet,·16)],⏎······`
});

it('returns the correct base URL for Sepolia', () => {
const chainId = ChainId.sepolia;
const result = utils.getSentinelBaseUrl(chainId);
expect(result).toBe(SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.sepolia, 16)]);

Check failure on line 635 in src/utils.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Replace `SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.sepolia,·16)]` with `⏎········SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.sepolia,·16)],⏎······`

Check failure on line 635 in src/utils.test.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Replace `SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.sepolia,·16)]` with `⏎········SENTINEL_API_BASE_URL_MAP[parseInt(ChainId.sepolia,·16)],⏎······`
});

it('returns undefined for unsupported chainId', () => {
const unsupportedChainId = '0x999'; // Arbitrary unsupported chain
const result = utils.getSentinelBaseUrl(unsupportedChainId);
expect(result).toBeUndefined();
});
});
});
31 changes: 20 additions & 11 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,39 +36,48 @@
) => stxStatus === 'uuid_not_found';

// TODO use actual url once API is defined
export function getAPIRequestURL(apiType: APIType, chainId: string): string {
export function getAPIRequestURL(
apiType: APIType,
chainId: string,
sentinelUrl?: string,
): string {
const chainIdDec = parseInt(chainId, 16);
switch (apiType) {
case APIType.LIVENESS: {
const effectiveSentinelUrl: string | undefined =
sentinelUrl ?? getSentinelBaseUrl(chainId);

if (effectiveSentinelUrl === undefined) {
throw new Error(`No sentinel URL for chainId ${chainId}`);
}
return `${effectiveSentinelUrl}/network`;
}
case APIType.GET_FEES: {
return `${API_BASE_URL}/networks/${chainIdDec}/getFees`;
}

case APIType.ESTIMATE_GAS: {
return `${API_BASE_URL}/networks/${chainIdDec}/estimateGas`;
}

case APIType.SUBMIT_TRANSACTIONS: {
return `${API_BASE_URL}/networks/${chainIdDec}/submitTransactions?stxControllerVersion=${packageJson.version}`;
}

case APIType.CANCEL: {
return `${API_BASE_URL}/networks/${chainIdDec}/cancel`;
}

case APIType.BATCH_STATUS: {
return `${API_BASE_URL}/networks/${chainIdDec}/batchStatus`;
}

case APIType.LIVENESS: {
return `${SENTINEL_API_BASE_URL_MAP[chainIdDec]}/network`;
}

default: {
throw new Error(`Invalid APIType`); // It can never get here thanks to TypeScript.
throw new Error(`Invalid APIType`);
}
}
}

export function getSentinelBaseUrl(chainId: string): string | undefined {
const chainIdDec = parseInt(chainId, 16);
return SENTINEL_API_BASE_URL_MAP[chainIdDec];
}

export const calculateStatus = (stxStatus: SmartTransactionsStatus) => {
if (isSmartTransactionStatusResolved(stxStatus)) {
return SmartTransactionStatuses.RESOLVED;
Expand Down Expand Up @@ -343,8 +352,8 @@
status: TransactionStatus.failed,
error: {
name: 'SmartTransactionFailed',
message: `Smart transaction failed with status: ${status}`,

Check warning on line 355 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Invalid type "string | undefined" of template literal expression

Check warning on line 355 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Invalid type "string | undefined" of template literal expression
},
};
updateTransaction(updatedTransaction, `Smart transaction status: ${status}`);

Check warning on line 358 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

Invalid type "string | undefined" of template literal expression

Check warning on line 358 in src/utils.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

Invalid type "string | undefined" of template literal expression
};
Loading