diff --git a/CHANGELOG.md b/CHANGELOG.md index d7cb11fc..b3687759 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Expose `SmartTransactionsController` methods through its messenger ([#574](https://github.com/MetaMask/smart-transactions-controller/pull/574)) + - The following actions are now available: + - `SmartTransactionsController:checkPoll` + - `SmartTransactionsController:initializeSmartTransactionsForChainId` + - `SmartTransactionsController:poll` + - `SmartTransactionsController:stop` + - `SmartTransactionsController:setOptInState` + - `SmartTransactionsController:trackStxStatusChange` + - `SmartTransactionsController:isNewSmartTransaction` + - `SmartTransactionsController:updateSmartTransaction` + - `SmartTransactionsController:updateSmartTransactions` + - `SmartTransactionsController:fetchSmartTransactionsStatus` + - `SmartTransactionsController:clearFees` + - `SmartTransactionsController:getFees` + - `SmartTransactionsController:submitSignedTransactions` + - `SmartTransactionsController:cancelSmartTransaction` + - `SmartTransactionsController:fetchLiveness` + - `SmartTransactionsController:setStatusRefreshInterval` + - `SmartTransactionsController:getTransactions` + - `SmartTransactionsController:getSmartTransactionByMinedTxHash` + - `SmartTransactionsController:wipeSmartTransactions` + - Corresponding action types (e.g. `SmartTransactionsControllerCheckPollAction`) are available as well. + +### Changed + +- **BREAKING:** Upgrade TypeScript from `~4.8.4` to `~5.3.3` ([#574](https://github.com/MetaMask/smart-transactions-controller/pull/574)) + - Consumers on TypeScript 4.x may experience type errors and should upgrade to TypeScript 5.x. + ## [23.0.0] ### Changed diff --git a/package.json b/package.json index 8c9a456f..f43a8079 100644 --- a/package.json +++ b/package.json @@ -29,10 +29,11 @@ ], "scripts": { "build": "ts-bridge --project tsconfig.build.json --clean", - "lint": "yarn lint:eslint && yarn lint:misc --check && yarn lint:changelog", + "generate-method-action-types": "tsx ./scripts/generate-method-action-types.ts ./src", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn lint:changelog && yarn generate-method-action-types --check", "lint:changelog": "auto-changelog validate --prettier", "lint:eslint": "eslint . --cache --ext js,ts", - "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn lint:changelog", + "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write && yarn lint:changelog && yarn generate-method-action-types --fix", "lint:misc": "prettier '**/*.json' '**/*.md' '**/*.yml' '!.yarnrc.yml' --ignore-path .gitignore --no-error-on-unmatched-pattern", "prepack": "./scripts/prepack.sh", "test": "jest && attw --pack", @@ -73,6 +74,7 @@ "@metamask/gas-fee-controller": "^26.0.0", "@metamask/json-rpc-engine": "^10.0.1", "@ts-bridge/cli": "^0.6.3", + "@types/eslint": "^9.6.1", "@types/jest": "^26.0.24", "@types/lodash": "^4.14.194", "@types/node": "^18.19.17", @@ -94,7 +96,9 @@ "prettier-plugin-packagejson": "^2.4.3", "sinon": "^9.2.4", "ts-jest": "^29.1.4", - "typescript": "~4.8.4" + "tsx": "^4.20.5", + "typescript": "~5.3.3", + "yargs": "^17.7.2" }, "peerDependenciesMeta": { "@metamask/accounts-controller": { @@ -125,7 +129,8 @@ "@metamask/controller-utils>ethereumjs-util>ethereum-cryptography>secp256k1": false, "@metamask/controller-utils>babel-runtime>core-js": false, "@metamask/transaction-controller>@metamask/core-backend>@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography>keccak": false, - "@metamask/transaction-controller>@metamask/core-backend>@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography>secp256k1": false + "@metamask/transaction-controller>@metamask/core-backend>@metamask/keyring-controller>ethereumjs-wallet>ethereum-cryptography>secp256k1": false, + "tsx>esbuild": false } } } diff --git a/scripts/generate-method-action-types.ts b/scripts/generate-method-action-types.ts new file mode 100755 index 00000000..93a999f0 --- /dev/null +++ b/scripts/generate-method-action-types.ts @@ -0,0 +1,755 @@ +#!yarn tsx +/* eslint-disable no-restricted-globals */ +/* eslint-disable @typescript-eslint/await-thenable */ +/* eslint-disable import/no-nodejs-modules */ + +import { assert, hasProperty, isObject } from '@metamask/utils'; +import { ESLint } from 'eslint'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; +import yargs from 'yargs'; + +type MethodInfo = { + name: string; + jsDoc: string; + signature: string; +}; + +type ControllerInfo = { + name: string; + filePath: string; + exposedMethods: string[]; + methods: MethodInfo[]; +}; + +/** + * The parsed command-line arguments. + */ +type CommandLineArguments = { + /** + * Whether to check if the action types files are up to date. + */ + check: boolean; + /** + * Whether to fix the action types files. + */ + fix: boolean; + /** + * Optional path to a specific controller to process. + */ + controllerPath: string; +}; + +/** + * Uses `yargs` to parse the arguments given to the script. + * + * @returns The command line arguments. + */ +async function parseCommandLineArguments(): Promise { + const { + check, + fix, + path: controllerPath, + } = await yargs(process.argv.slice(2)) + .command( + '$0 [path]', + 'Generate method action types for a controller messenger', + (yargsInstance) => { + yargsInstance.positional('path', { + type: 'string', + description: 'Path to the folder where controllers are located', + default: 'src', + }); + }, + ) + .option('check', { + type: 'boolean', + description: 'Check if generated action type files are up to date', + default: false, + }) + .option('fix', { + type: 'boolean', + description: 'Generate/update action type files', + default: false, + }) + .help() + .check((argv) => { + if (!argv.check && !argv.fix) { + throw new Error('Either --check or --fix must be provided.\n'); + } + return true; + }).argv; + + return { + check, + fix, + // TypeScript doesn't narrow the type of `controllerPath` even though we defined it as a string in yargs, so we need to cast it here. + controllerPath: controllerPath as string, + }; +} + +/** + * Checks if generated action types files are up to date. + * + * @param controllers - Array of controller information objects. + * @param eslint - The ESLint instance to use for formatting. + */ +async function checkActionTypesFiles( + controllers: ControllerInfo[], + eslint: ESLint, +): Promise { + let hasErrors = false; + + // Track files that exist and their corresponding temp files + const fileComparisonJobs: { + expectedTempFile: string; + actualFile: string; + baseFileName: string; + }[] = []; + + try { + // Check each controller and prepare comparison jobs + for (const controller of controllers) { + console.log(`\nšŸ”§ Checking ${controller.name}...`); + const outputDir = path.dirname(controller.filePath); + const baseFileName = path.basename(controller.filePath, '.ts'); + const actualFile = path.join( + outputDir, + `${baseFileName}-method-action-types.ts`, + ); + + const expectedContent = generateActionTypesContent(controller); + const expectedTempFile = actualFile.replace('.ts', '.tmp.ts'); + + try { + // Check if actual file exists first + await fs.promises.access(actualFile); + + // Write expected content to temp file + await fs.promises.writeFile(expectedTempFile, expectedContent, 'utf8'); + + // Add to comparison jobs + fileComparisonJobs.push({ + expectedTempFile, + actualFile, + baseFileName, + }); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + console.error( + `āŒ ${baseFileName}-method-action-types.ts does not exist`, + ); + } else { + console.error( + `āŒ Error reading ${baseFileName}-method-action-types.ts:`, + error, + ); + } + hasErrors = true; + } + } + + // Run ESLint on all files at once if we have comparisons to make + if (fileComparisonJobs.length > 0) { + console.log('\nšŸ“ Running ESLint to compare files...'); + + const results = await eslint.lintFiles( + fileComparisonJobs.map((job) => job.expectedTempFile), + ); + await ESLint.outputFixes(results); + + // Compare expected vs actual content + for (const job of fileComparisonJobs) { + const expectedContent = await fs.promises.readFile( + job.expectedTempFile, + 'utf8', + ); + const actualContent = await fs.promises.readFile( + job.actualFile, + 'utf8', + ); + + if (expectedContent === actualContent) { + console.log( + `āœ… ${job.baseFileName}-method-action-types.ts is up to date`, + ); + } else { + console.error( + `āŒ ${job.baseFileName}-method-action-types.ts is out of date`, + ); + hasErrors = true; + } + } + } + } finally { + // Clean up temp files + for (const job of fileComparisonJobs) { + try { + await fs.promises.unlink(job.expectedTempFile); + } catch { + // Ignore cleanup errors + } + } + } + + if (hasErrors) { + console.error('\nšŸ’„ Some action type files are out of date or missing.'); + console.error( + 'Run `yarn generate-method-action-types --fix` to update them.', + ); + process.exitCode = 1; + } else { + console.log('\nšŸŽ‰ All action type files are up to date!'); + } +} + +/** + * Main entry point for the script. + */ +async function main(): Promise { + const { fix, controllerPath } = await parseCommandLineArguments(); + + console.log('šŸ” Searching for controllers with MESSENGER_EXPOSED_METHODS...'); + + const controllers = await findControllersWithExposedMethods(controllerPath); + + if (controllers.length === 0) { + console.log('āš ļø No controllers found with MESSENGER_EXPOSED_METHODS'); + return; + } + + console.log( + `šŸ“¦ Found ${controllers.length} controller(s) with exposed methods`, + ); + + const eslint = new ESLint({ + fix: true, + errorOnUnmatchedPattern: false, + }); + + if (fix) { + await generateAllActionTypesFiles(controllers, eslint); + console.log('\nšŸŽ‰ All action types generated successfully!'); + } else { + // -check mode: check files + await checkActionTypesFiles(controllers, eslint); + } +} + +/** + * Check if a path is a directory. + * + * @param pathValue - The path to check. + * @returns True if the path is a directory, false otherwise. + * @throws If an error occurs other than the path not existing. + */ +async function isDirectory(pathValue: string): Promise { + try { + const stats = await fs.promises.stat(pathValue); + return stats.isDirectory(); + } catch (error) { + if ( + isObject(error) && + hasProperty(error, 'code') && + error.code === 'ENOENT' + ) { + return false; + } + + throw error; + } +} + +/** + * Finds all controller files that have MESSENGER_EXPOSED_METHODS constants. + * + * @param controllerPath - Path to the folder where controllers are located. + * @returns A list of controller information objects. + */ +async function findControllersWithExposedMethods( + controllerPath: string, +): Promise { + const srcPath = path.resolve(process.cwd(), controllerPath); + const controllers: ControllerInfo[] = []; + + if (!(await isDirectory(srcPath))) { + throw new Error(`The specified path is not a directory: ${srcPath}`); + } + + const srcFiles = await fs.promises.readdir(srcPath); + + for (const file of srcFiles) { + if (!file.endsWith('.ts') || file.endsWith('.test.ts')) { + continue; + } + + const filePath = path.join(srcPath, file); + const content = await fs.promises.readFile(filePath, 'utf8'); + + if (content.includes('MESSENGER_EXPOSED_METHODS')) { + const controllerInfo = await parseControllerFile(filePath); + if (controllerInfo) { + controllers.push(controllerInfo); + } + } + } + + return controllers; +} + +/** + * Context for AST visiting. + */ +type VisitorContext = { + exposedMethods: string[]; + className: string; + methods: MethodInfo[]; + sourceFile: ts.SourceFile; +}; + +/** + * Visits AST nodes to find exposed methods and controller class. + * + * @param context - The visitor context. + * @returns A function to visit nodes. + */ +function createASTVisitor(context: VisitorContext): (node: ts.Node) => void { + /** + * Visits AST nodes to find exposed methods and controller class. + * + * @param node - The AST node to visit. + */ + function visitNode(node: ts.Node): void { + if (ts.isVariableStatement(node)) { + const declaration = node.declarationList.declarations[0]; + if ( + ts.isIdentifier(declaration.name) && + declaration.name.text === 'MESSENGER_EXPOSED_METHODS' + ) { + if (declaration.initializer) { + let arrayExpression: ts.ArrayLiteralExpression | undefined; + + // Handle direct array literal + if (ts.isArrayLiteralExpression(declaration.initializer)) { + arrayExpression = declaration.initializer; + } + // Handle "as const" assertion: expression is wrapped in type assertion + else if ( + ts.isAsExpression(declaration.initializer) && + ts.isArrayLiteralExpression(declaration.initializer.expression) + ) { + arrayExpression = declaration.initializer.expression; + } + + if (arrayExpression) { + context.exposedMethods = arrayExpression.elements + .filter(ts.isStringLiteral) + .map((element) => element.text); + } + } + } + } + + // Find the controller or service class + if (ts.isClassDeclaration(node) && node.name) { + const classText = node.name.text; + if (classText.includes('Controller') || classText.includes('Service')) { + context.className = classText; + + // Extract method info for exposed methods + const seenMethods = new Set(); + for (const member of node.members) { + if ( + ts.isMethodDeclaration(member) && + member.name && + ts.isIdentifier(member.name) + ) { + const methodName = member.name.text; + if ( + context.exposedMethods.includes(methodName) && + !seenMethods.has(methodName) + ) { + seenMethods.add(methodName); + const jsDoc = extractJSDoc(member, context.sourceFile); + const signature = extractMethodSignature(member); + context.methods.push({ + name: methodName, + jsDoc, + signature, + }); + } + } + } + } + } + + ts.forEachChild(node, visitNode); + } + + return visitNode; +} + +/** + * Create a TypeScript program for the given file by locating the nearest + * tsconfig.json. + * + * @param filePath - Absolute path to the source file. + * @returns A TypeScript program, or null if no tsconfig was found. + */ +function createProgramForFile(filePath: string): ts.Program | null { + const configPath = ts.findConfigFile( + path.dirname(filePath), + ts.sys.fileExists.bind(ts.sys), + 'tsconfig.json', + ); + if (!configPath) { + return null; + } + + const { config, error } = ts.readConfigFile( + configPath, + ts.sys.readFile.bind(ts.sys), + ); + + if (error) { + return null; + } + + const parsedConfig = ts.parseJsonConfigFileContent( + config, + ts.sys, + path.dirname(configPath), + ); + + return ts.createProgram({ + rootNames: parsedConfig.fileNames, + options: parsedConfig.options, + }); +} + +/** + * Find a class declaration with the given name in a source file. + * + * @param sourceFile - The source file to search. + * @param className - The class name to look for. + * @returns The class declaration node, or null if not found. + */ +function findClassInSourceFile( + sourceFile: ts.SourceFile, + className: string, +): ts.ClassDeclaration | null { + return ( + sourceFile.statements.find( + (node): node is ts.ClassDeclaration => + ts.isClassDeclaration(node) && node.name?.text === className, + ) ?? null + ); +} + +/** + * Search through the class hierarchy of a TypeScript type to find the + * declaration of a method with the given name. + * + * @param classType - The class type to search. + * @param methodName - The method name to look for. + * @returns The method declaration node, or null if not found. + */ +function findMethodInHierarchy( + classType: ts.Type, + methodName: string, +): ts.MethodDeclaration | null { + const symbol = classType.getProperty(methodName); + if (!symbol) { + return null; + } + + const declarations = symbol.getDeclarations(); + if (!declarations) { + return null; + } + + for (const declaration of declarations) { + if (ts.isMethodDeclaration(declaration)) { + return declaration; + } + } + + return null; +} + +/** + * Parses a controller file to extract exposed methods and their metadata. + * + * @param filePath - Path to the controller file to parse. + * @returns Controller information or null if parsing fails. + */ +async function parseControllerFile( + filePath: string, +): Promise { + try { + const content = await fs.promises.readFile(filePath, 'utf8'); + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true, + ); + + const context: VisitorContext = { + exposedMethods: [], + className: '', + methods: [], + sourceFile, + }; + + createASTVisitor(context)(sourceFile); + + if (context.exposedMethods.length === 0 || !context.className) { + return null; + } + + // For exposed methods not found directly in the class body, attempt to + // locate them in the inheritance hierarchy using the type checker. + const foundMethodNames = new Set( + context.methods.map((method) => method.name), + ); + + const inheritedMethodNames = context.exposedMethods.filter( + (name) => !foundMethodNames.has(name), + ); + + if (inheritedMethodNames.length > 0) { + const program = createProgramForFile(filePath); + const checker = program?.getTypeChecker(); + const programSourceFile = program?.getSourceFile(filePath); + + assert( + checker, + `Type checker could not be created for "${filePath}". Ensure a valid tsconfig.json is present.`, + ); + + assert( + programSourceFile, + `Source file "${filePath}" not found in program.`, + ); + + const classNode = findClassInSourceFile( + programSourceFile, + context.className, + ); + + assert( + classNode, + `Class "${context.className}" not found in "${filePath}".`, + ); + + const classType = checker.getTypeAtLocation(classNode); + for (const methodName of inheritedMethodNames) { + const methodDeclaration = findMethodInHierarchy(classType, methodName); + + const jsDoc = methodDeclaration + ? extractJSDoc(methodDeclaration, methodDeclaration.getSourceFile()) + : ''; + context.methods.push({ name: methodName, jsDoc, signature: '' }); + } + } + + return { + name: context.className, + filePath, + exposedMethods: context.exposedMethods, + methods: context.methods, + }; + } catch (error) { + console.error(`Error parsing ${filePath}:`, error); + return null; + } +} + +/** + * Extracts JSDoc comment from a method declaration. + * + * @param node - The method declaration node. + * @param sourceFile - The source file. + * @returns The JSDoc comment. + */ +function extractJSDoc( + node: ts.MethodDeclaration, + sourceFile: ts.SourceFile, +): string { + const jsDocTags = ts.getJSDocCommentsAndTags(node); + if (jsDocTags.length === 0) { + return ''; + } + + const jsDoc = jsDocTags[0]; + if (ts.isJSDoc(jsDoc)) { + const fullText = sourceFile.getFullText(); + const start = jsDoc.getFullStart(); + const end = jsDoc.getEnd(); + const rawJsDoc = fullText.substring(start, end).trim(); + return formatJSDoc(rawJsDoc); + } + + return ''; +} + +/** + * Formats JSDoc comments to have consistent indentation for the generated file. + * + * @param rawJsDoc - The raw JSDoc comment from the source. + * @returns The formatted JSDoc comment. + */ +function formatJSDoc(rawJsDoc: string): string { + const lines = rawJsDoc.split('\n'); + const formattedLines: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (i === 0) { + // First line should be /** + formattedLines.push('/**'); + } else if (i === lines.length - 1) { + // Last line should be */ + formattedLines.push(' */'); + } else { + // Middle lines should start with ' * ' + const trimmed = line.trim(); + if (trimmed.startsWith('*')) { + // Remove existing * and normalize + const content = trimmed.substring(1).trim(); + formattedLines.push(content ? ` * ${content}` : ' *'); + } else { + // Handle lines that don't start with * + formattedLines.push(trimmed ? ` * ${trimmed}` : ' *'); + } + } + } + + return formattedLines.join('\n'); +} + +/** + * Extracts method signature as a string for the handler type. + * + * @param node - The method declaration node. + * @returns The method signature. + */ +function extractMethodSignature(node: ts.MethodDeclaration): string { + // Since we're just using the method reference in the handler type, + // we don't need the full signature - just return the method name + // The actual signature will be inferred from the controller class + return node.name ? (node.name as ts.Identifier).text : ''; +} + +/** + * Generates action types files for all controllers. + * + * @param controllers - Array of controller information objects. + * @param eslint - The ESLint instance to use for formatting. + */ +async function generateAllActionTypesFiles( + controllers: ControllerInfo[], + eslint: ESLint, +): Promise { + const outputFiles: string[] = []; + + // Write all files first + for (const controller of controllers) { + console.log(`\nšŸ”§ Processing ${controller.name}...`); + const outputDir = path.dirname(controller.filePath); + const baseFileName = path.basename(controller.filePath, '.ts'); + const outputFile = path.join( + outputDir, + `${baseFileName}-method-action-types.ts`, + ); + + const generatedContent = generateActionTypesContent(controller); + await fs.promises.writeFile(outputFile, generatedContent, 'utf8'); + outputFiles.push(outputFile); + console.log(`āœ… Generated action types for ${controller.name}`); + } + + // Run ESLint on all the actual files + if (outputFiles.length > 0) { + console.log('\nšŸ“ Running ESLint on generated files...'); + + const results = await eslint.lintFiles(outputFiles); + await ESLint.outputFixes(results); + const errors = ESLint.getErrorResults(results); + if (errors.length > 0) { + console.error('āŒ ESLint errors:', errors); + process.exitCode = 1; + } else { + console.log('āœ… ESLint formatting applied'); + } + } +} + +/** + * Generates the content for the action types file. + * + * @param controller - The controller information object. + * @returns The content for the action types file. + */ +function generateActionTypesContent(controller: ControllerInfo): string { + const baseFileName = path.basename(controller.filePath, '.ts'); + const controllerImportPath = `./${baseFileName}`; + + let content = `/** + * This file is auto generated by \`scripts/generate-method-action-types.ts\`. + * Do not edit manually. + */ + +import type { ${controller.name} } from '${controllerImportPath}'; + +`; + + const actionTypeNames: string[] = []; + + // Generate action types for each exposed method + for (const method of controller.methods) { + const actionTypeName = `${controller.name}${capitalize(method.name)}Action`; + const actionString = `${controller.name}:${method.name}`; + + actionTypeNames.push(actionTypeName); + + // Add the JSDoc if available + if (method.jsDoc) { + content += `${method.jsDoc}\n`; + } + + content += `export type ${actionTypeName} = { + type: \`${actionString}\`; + handler: ${controller.name}['${method.name}']; +};\n\n`; + } + + // Generate union type of all action types + if (actionTypeNames.length > 0) { + const unionTypeName = `${controller.name}MethodActions`; + content += `/** + * Union of all ${controller.name} action types. + */ +export type ${unionTypeName} = ${actionTypeNames.join(' | ')};\n`; + } + + return `${content.trimEnd()}\n`; +} + +/** + * Capitalizes the first letter of a string. + * + * @param str - The string to capitalize. + * @returns The capitalized string. + */ +function capitalize(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +// Error handling wrapper +main().catch((error) => { + console.error('āŒ Script failed:', error); + process.exitCode = 1; +}); diff --git a/src/SmartTransactionsController-method-action-types.ts b/src/SmartTransactionsController-method-action-types.ts new file mode 100644 index 00000000..47171e3f --- /dev/null +++ b/src/SmartTransactionsController-method-action-types.ts @@ -0,0 +1,135 @@ +/** + * This file is auto generated by `scripts/generate-method-action-types.ts`. + * Do not edit manually. + */ + +import type { SmartTransactionsController } from './SmartTransactionsController'; + +export type SmartTransactionsControllerCheckPollAction = { + type: `SmartTransactionsController:checkPoll`; + handler: SmartTransactionsController['checkPoll']; +}; + +export type SmartTransactionsControllerInitializeSmartTransactionsForChainIdAction = + { + type: `SmartTransactionsController:initializeSmartTransactionsForChainId`; + handler: SmartTransactionsController['initializeSmartTransactionsForChainId']; + }; + +export type SmartTransactionsControllerPollAction = { + type: `SmartTransactionsController:poll`; + handler: SmartTransactionsController['poll']; +}; + +export type SmartTransactionsControllerStopAction = { + type: `SmartTransactionsController:stop`; + handler: SmartTransactionsController['stop']; +}; + +export type SmartTransactionsControllerSetOptInStateAction = { + type: `SmartTransactionsController:setOptInState`; + handler: SmartTransactionsController['setOptInState']; +}; + +export type SmartTransactionsControllerTrackStxStatusChangeAction = { + type: `SmartTransactionsController:trackStxStatusChange`; + handler: SmartTransactionsController['trackStxStatusChange']; +}; + +export type SmartTransactionsControllerIsNewSmartTransactionAction = { + type: `SmartTransactionsController:isNewSmartTransaction`; + handler: SmartTransactionsController['isNewSmartTransaction']; +}; + +export type SmartTransactionsControllerUpdateSmartTransactionAction = { + type: `SmartTransactionsController:updateSmartTransaction`; + handler: SmartTransactionsController['updateSmartTransaction']; +}; + +export type SmartTransactionsControllerUpdateSmartTransactionsAction = { + type: `SmartTransactionsController:updateSmartTransactions`; + handler: SmartTransactionsController['updateSmartTransactions']; +}; + +export type SmartTransactionsControllerFetchSmartTransactionsStatusAction = { + type: `SmartTransactionsController:fetchSmartTransactionsStatus`; + handler: SmartTransactionsController['fetchSmartTransactionsStatus']; +}; + +export type SmartTransactionsControllerClearFeesAction = { + type: `SmartTransactionsController:clearFees`; + handler: SmartTransactionsController['clearFees']; +}; + +export type SmartTransactionsControllerGetFeesAction = { + type: `SmartTransactionsController:getFees`; + handler: SmartTransactionsController['getFees']; +}; + +export type SmartTransactionsControllerSubmitSignedTransactionsAction = { + type: `SmartTransactionsController:submitSignedTransactions`; + handler: SmartTransactionsController['submitSignedTransactions']; +}; + +export type SmartTransactionsControllerCancelSmartTransactionAction = { + type: `SmartTransactionsController:cancelSmartTransaction`; + handler: SmartTransactionsController['cancelSmartTransaction']; +}; + +/** + * Fetches the liveness status of Smart Transactions for a given chain. + * + * @param options - The options object. + * @param options.chainId - The chain ID to fetch liveness for. Preferred over networkClientId. + * @param options.networkClientId - The network client ID to derive chain ID from. + * @returns A promise that resolves to the liveness status. + */ +export type SmartTransactionsControllerFetchLivenessAction = { + type: `SmartTransactionsController:fetchLiveness`; + handler: SmartTransactionsController['fetchLiveness']; +}; + +export type SmartTransactionsControllerSetStatusRefreshIntervalAction = { + type: `SmartTransactionsController:setStatusRefreshInterval`; + handler: SmartTransactionsController['setStatusRefreshInterval']; +}; + +export type SmartTransactionsControllerGetTransactionsAction = { + type: `SmartTransactionsController:getTransactions`; + handler: SmartTransactionsController['getTransactions']; +}; + +export type SmartTransactionsControllerGetSmartTransactionByMinedTxHashAction = + { + type: `SmartTransactionsController:getSmartTransactionByMinedTxHash`; + handler: SmartTransactionsController['getSmartTransactionByMinedTxHash']; + }; + +export type SmartTransactionsControllerWipeSmartTransactionsAction = { + type: `SmartTransactionsController:wipeSmartTransactions`; + handler: SmartTransactionsController['wipeSmartTransactions']; +}; + +/** + * Union of all SmartTransactionsController action types. + */ +export type SmartTransactionsControllerMethodActions = + | SmartTransactionsControllerCheckPollAction + | SmartTransactionsControllerInitializeSmartTransactionsForChainIdAction + | SmartTransactionsControllerPollAction + | SmartTransactionsControllerStopAction + | SmartTransactionsControllerSetOptInStateAction + | SmartTransactionsControllerTrackStxStatusChangeAction + | SmartTransactionsControllerIsNewSmartTransactionAction + | SmartTransactionsControllerUpdateSmartTransactionAction + | SmartTransactionsControllerUpdateSmartTransactionsAction + | SmartTransactionsControllerFetchSmartTransactionsStatusAction + | SmartTransactionsControllerClearFeesAction + | SmartTransactionsControllerGetFeesAction + | SmartTransactionsControllerSubmitSignedTransactionsAction + | SmartTransactionsControllerCancelSmartTransactionAction + | SmartTransactionsControllerFetchLivenessAction + | SmartTransactionsControllerSetStatusRefreshIntervalAction + | SmartTransactionsControllerGetTransactionsAction + | SmartTransactionsControllerGetSmartTransactionByMinedTxHashAction + | SmartTransactionsControllerWipeSmartTransactionsAction; diff --git a/src/SmartTransactionsController.ts b/src/SmartTransactionsController.ts index a326b4d6..10f51ed2 100644 --- a/src/SmartTransactionsController.ts +++ b/src/SmartTransactionsController.ts @@ -48,6 +48,7 @@ import { getSmartTransactionsFeatureFlagsForChain, } from './featureFlags/feature-flags'; import { validateSmartTransactionsFeatureFlags } from './featureFlags/validators'; +import type { SmartTransactionsControllerMethodActions } from './SmartTransactionsController-method-action-types'; import type { Fees, IndividualTxFees, @@ -154,11 +155,36 @@ export type SmartTransactionsControllerGetStateAction = SmartTransactionsControllerState >; +// === MESSENGER === + +const MESSENGER_EXPOSED_METHODS = [ + 'cancelSmartTransaction', + 'checkPoll', + 'clearFees', + 'fetchLiveness', + 'fetchSmartTransactionsStatus', + 'getFees', + 'getSmartTransactionByMinedTxHash', + 'getTransactions', + 'initializeSmartTransactionsForChainId', + 'isNewSmartTransaction', + 'poll', + 'setOptInState', + 'setStatusRefreshInterval', + 'stop', + 'submitSignedTransactions', + 'trackStxStatusChange', + 'updateSmartTransaction', + 'updateSmartTransactions', + 'wipeSmartTransactions', +] as const; + /** * The actions that can be performed using the {@link SmartTransactionsController}. */ export type SmartTransactionsControllerActions = - SmartTransactionsControllerGetStateAction; + | SmartTransactionsControllerGetStateAction + | SmartTransactionsControllerMethodActions; type AllowedActions = | NetworkControllerGetNetworkClientByIdAction @@ -392,6 +418,11 @@ export class SmartTransactionsController extends StaticIntervalPollingController this.messenger.subscribe('RemoteFeatureFlagController:stateChange', () => { this.#validateAndReportFeatureFlags(); }); + + this.messenger.registerMethodActionHandlers( + this, + MESSENGER_EXPOSED_METHODS, + ); } async _executePoll({ diff --git a/src/index.ts b/src/index.ts index 332ff7da..eba2b414 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,27 @@ export type { SmartTransactionsControllerSmartTransactionConfirmationDoneEvent, SmartTransactionsControllerEvents, } from './SmartTransactionsController'; +export type { + SmartTransactionsControllerCheckPollAction, + SmartTransactionsControllerInitializeSmartTransactionsForChainIdAction, + SmartTransactionsControllerPollAction, + SmartTransactionsControllerStopAction, + SmartTransactionsControllerSetOptInStateAction, + SmartTransactionsControllerTrackStxStatusChangeAction, + SmartTransactionsControllerIsNewSmartTransactionAction, + SmartTransactionsControllerUpdateSmartTransactionAction, + SmartTransactionsControllerUpdateSmartTransactionsAction, + SmartTransactionsControllerFetchSmartTransactionsStatusAction, + SmartTransactionsControllerClearFeesAction, + SmartTransactionsControllerGetFeesAction, + SmartTransactionsControllerSubmitSignedTransactionsAction, + SmartTransactionsControllerCancelSmartTransactionAction, + SmartTransactionsControllerFetchLivenessAction, + SmartTransactionsControllerSetStatusRefreshIntervalAction, + SmartTransactionsControllerGetTransactionsAction, + SmartTransactionsControllerGetSmartTransactionByMinedTxHashAction, + SmartTransactionsControllerWipeSmartTransactionsAction, +} from './SmartTransactionsController-method-action-types'; export { type Fee, type Fees, diff --git a/yarn.lock b/yarn.lock index f991294d..6cde73bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -450,6 +450,188 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/aix-ppc64@npm:0.27.4" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/android-arm64@npm:0.27.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/android-arm@npm:0.27.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/android-x64@npm:0.27.4" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/darwin-arm64@npm:0.27.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/darwin-x64@npm:0.27.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/freebsd-arm64@npm:0.27.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/freebsd-x64@npm:0.27.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-arm64@npm:0.27.4" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-arm@npm:0.27.4" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-ia32@npm:0.27.4" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-loong64@npm:0.27.4" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-mips64el@npm:0.27.4" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-ppc64@npm:0.27.4" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-riscv64@npm:0.27.4" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-s390x@npm:0.27.4" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/linux-x64@npm:0.27.4" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/netbsd-arm64@npm:0.27.4" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/netbsd-x64@npm:0.27.4" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/openbsd-arm64@npm:0.27.4" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/openbsd-x64@npm:0.27.4" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/openharmony-arm64@npm:0.27.4" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/sunos-x64@npm:0.27.4" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/win32-arm64@npm:0.27.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/win32-ia32@npm:0.27.4" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.27.4": + version: 0.27.4 + resolution: "@esbuild/win32-x64@npm:0.27.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0": version: 4.4.0 resolution: "@eslint-community/eslint-utils@npm:4.4.0" @@ -2260,6 +2442,7 @@ __metadata: "@metamask/transaction-controller": ^63.0.0 "@metamask/utils": ^11.0.0 "@ts-bridge/cli": ^0.6.3 + "@types/eslint": ^9.6.1 "@types/jest": ^26.0.24 "@types/lodash": ^4.14.194 "@types/node": ^18.19.17 @@ -2285,7 +2468,9 @@ __metadata: reselect: ^5.1.1 sinon: ^9.2.4 ts-jest: ^29.1.4 - typescript: ~4.8.4 + tsx: ^4.20.5 + typescript: ~5.3.3 + yargs: ^17.7.2 peerDependenciesMeta: "@metamask/accounts-controller": optional: true @@ -3040,6 +3225,23 @@ __metadata: languageName: node linkType: hard +"@types/eslint@npm:^9.6.1": + version: 9.6.1 + resolution: "@types/eslint@npm:9.6.1" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: c286e79707ab604b577cf8ce51d9bbb9780e3d6a68b38a83febe13fa05b8012c92de17c28532fac2b03d3c460123f5055d603a579685325246ca1c86828223e0 + languageName: node + linkType: hard + +"@types/estree@npm:*": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: bd93e2e415b6f182ec4da1074e1f36c480f1d26add3e696d54fb30c09bc470897e41361c8fd957bf0985024f8fbf1e6e2aff977d79352ef7eb93a5c6dcff6c11 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -3084,6 +3286,13 @@ __metadata: languageName: node linkType: hard +"@types/json-schema@npm:*": + version: 7.0.15 + resolution: "@types/json-schema@npm:7.0.15" + checksum: 97ed0cb44d4070aecea772b7b2e2ed971e10c81ec87dd4ecc160322ffa55ff330dace1793489540e3e318d90942064bb697cc0f8989391797792d919737b3b98 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" @@ -4974,6 +5183,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:~0.27.0": + version: 0.27.4 + resolution: "esbuild@npm:0.27.4" + dependencies: + "@esbuild/aix-ppc64": 0.27.4 + "@esbuild/android-arm": 0.27.4 + "@esbuild/android-arm64": 0.27.4 + "@esbuild/android-x64": 0.27.4 + "@esbuild/darwin-arm64": 0.27.4 + "@esbuild/darwin-x64": 0.27.4 + "@esbuild/freebsd-arm64": 0.27.4 + "@esbuild/freebsd-x64": 0.27.4 + "@esbuild/linux-arm": 0.27.4 + "@esbuild/linux-arm64": 0.27.4 + "@esbuild/linux-ia32": 0.27.4 + "@esbuild/linux-loong64": 0.27.4 + "@esbuild/linux-mips64el": 0.27.4 + "@esbuild/linux-ppc64": 0.27.4 + "@esbuild/linux-riscv64": 0.27.4 + "@esbuild/linux-s390x": 0.27.4 + "@esbuild/linux-x64": 0.27.4 + "@esbuild/netbsd-arm64": 0.27.4 + "@esbuild/netbsd-x64": 0.27.4 + "@esbuild/openbsd-arm64": 0.27.4 + "@esbuild/openbsd-x64": 0.27.4 + "@esbuild/openharmony-arm64": 0.27.4 + "@esbuild/sunos-x64": 0.27.4 + "@esbuild/win32-arm64": 0.27.4 + "@esbuild/win32-ia32": 0.27.4 + "@esbuild/win32-x64": 0.27.4 + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: f560cdef05e3ac35e79ac5480bdc3801530e20f8333a78809604f001363d4bcaa6cce5641af18d5c7279165118561d12116a40e608f01327e2c522d67d79b3f0 + languageName: node + linkType: hard + "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -5746,7 +6044,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2": +"fsevents@npm:^2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -5756,7 +6054,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.3.2#~builtin": +"fsevents@patch:fsevents@^2.3.2#~builtin, fsevents@patch:fsevents@~2.3.3#~builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#~builtin::version=2.3.3&hash=18f3a7" dependencies: @@ -5891,6 +6189,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5": + version: 4.13.6 + resolution: "get-tsconfig@npm:4.13.6" + dependencies: + resolve-pkg-maps: ^1.0.0 + checksum: 946575897a75b3de39905a8e95fe761b1ae9a595c8608c3982fa3a5748ed0d6441c799197a99fee27876cd8c5a2158418ad6e88d8008ec1244639c0f5ee2a2d8 + languageName: node + linkType: hard + "git-hooks-list@npm:^3.0.0": version: 3.1.0 resolution: "git-hooks-list@npm:3.1.0" @@ -8845,6 +9152,13 @@ __metadata: languageName: node linkType: hard +"resolve-pkg-maps@npm:^1.0.0": + version: 1.0.0 + resolution: "resolve-pkg-maps@npm:1.0.0" + checksum: 1012afc566b3fdb190a6309cc37ef3b2dcc35dff5fa6683a9d00cd25c3247edfbc4691b91078c97adc82a29b77a2660c30d791d65dab4fc78bfc473f60289977 + languageName: node + linkType: hard + "resolve.exports@npm:^2.0.0": version: 2.0.2 resolution: "resolve.exports@npm:2.0.2" @@ -9778,6 +10092,22 @@ __metadata: languageName: node linkType: hard +"tsx@npm:^4.20.5": + version: 4.21.0 + resolution: "tsx@npm:4.21.0" + dependencies: + esbuild: ~0.27.0 + fsevents: ~2.3.3 + get-tsconfig: ^4.7.5 + dependenciesMeta: + fsevents: + optional: true + bin: + tsx: dist/cli.mjs + checksum: 50c98e4b6e66d1c30f72925c8e5e7be1a02377574de7cd367d7e7a6d4af43ca8ff659f91c654e7628b25a5498015e32f090529b92c679b0342811e1cf682e8cf + languageName: node + linkType: hard + "tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3" @@ -9885,13 +10215,13 @@ __metadata: languageName: node linkType: hard -"typescript@npm:~4.8.4": - version: 4.8.4 - resolution: "typescript@npm:4.8.4" +"typescript@npm:~5.3.3": + version: 5.3.3 + resolution: "typescript@npm:5.3.3" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 3e4f061658e0c8f36c820802fa809e0fd812b85687a9a2f5430bc3d0368e37d1c9605c3ce9b39df9a05af2ece67b1d844f9f6ea8ff42819f13bcb80f85629af0 + checksum: 2007ccb6e51bbbf6fde0a78099efe04dc1c3dfbdff04ca3b6a8bc717991862b39fd6126c0c3ebf2d2d98ac5e960bcaa873826bb2bb241f14277034148f41f6a2 languageName: node linkType: hard @@ -9905,13 +10235,13 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@~4.8.4#~builtin": - version: 4.8.4 - resolution: "typescript@patch:typescript@npm%3A4.8.4#~builtin::version=4.8.4&hash=7ad353" +"typescript@patch:typescript@~5.3.3#~builtin": + version: 5.3.3 + resolution: "typescript@patch:typescript@npm%3A5.3.3#~builtin::version=5.3.3&hash=7ad353" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 563a0ef47abae6df27a9a3ab38f75fc681f633ccf1a3502b1108e252e187787893de689220f4544aaf95a371a4eb3141e4a337deb9895de5ac3c1ca76430e5f0 + checksum: f61375590b3162599f0f0d5b8737877ac0a7bc52761dbb585d67e7b8753a3a4c42d9a554c4cc929f591ffcf3a2b0602f65ae3ce74714fd5652623a816862b610 languageName: node linkType: hard