From 6bfdbfce90952eab085f428dff2ee7febeb457ef Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 6 Nov 2024 14:29:05 +0100 Subject: [PATCH 01/17] chore: add jsx support in snap --- package.json | 1 + packages/starknet-snap/.eslintrc.js | 2 +- packages/starknet-snap/package.json | 6 +++--- packages/starknet-snap/snap.config.ts | 2 +- .../starknet-snap/src/{index.test.ts => index.test.tsx} | 0 packages/starknet-snap/src/{index.ts => index.tsx} | 0 packages/starknet-snap/tsconfig.json | 4 +++- 7 files changed, 9 insertions(+), 6 deletions(-) rename packages/starknet-snap/src/{index.test.ts => index.test.tsx} (100%) rename packages/starknet-snap/src/{index.ts => index.tsx} (100%) diff --git a/package.json b/package.json index e2e186c1..2088be4a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@commitlint/config-conventional": "^17.0.3", "@lavamoat/allow-scripts": "^3.0.0", "@lavamoat/preinstall-always-fail": "^2.0.0", + "@types/react": "18.0.15", "husky": "^8.0.0" }, "packageManager": "yarn@3.2.1", diff --git a/packages/starknet-snap/.eslintrc.js b/packages/starknet-snap/.eslintrc.js index 064abf0d..3742471f 100644 --- a/packages/starknet-snap/.eslintrc.js +++ b/packages/starknet-snap/.eslintrc.js @@ -22,7 +22,7 @@ module.exports = { }, }, { - files: ['*.test.ts'], + files: ['*.test.ts','*.test.tsx'], extends: ['@metamask/eslint-config-jest'], rules: { '@typescript-eslint/no-shadow': [ diff --git a/packages/starknet-snap/package.json b/packages/starknet-snap/package.json index c134d97a..c4744a52 100644 --- a/packages/starknet-snap/package.json +++ b/packages/starknet-snap/package.json @@ -22,9 +22,9 @@ "cover:report": "nyc report --reporter=lcov --reporter=text", "jest": "jest --passWithNoTests", "lint": "yarn lint:eslint && yarn lint:misc --check", - "lint:eslint": "eslint . --cache --ext js,ts", + "lint:eslint": "eslint . --cache --ext js,ts,tsx", "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", - "lint:misc": "prettier '**/*.ts' '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path .gitignore", + "lint:misc": "prettier '**/*.ts' '**/*.tsx' '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path .gitignore", "serve": "mm-snap serve", "start": "mm-snap watch", "test": "yarn run test:unit && yarn run cover:report && yarn run jest", @@ -39,7 +39,7 @@ }, "dependencies": { "@metamask/key-tree": "9.0.0", - "@metamask/snaps-sdk": "^4.0.0", + "@metamask/snaps-sdk": "^6.1.1", "@metamask/utils": "^9.1.0", "async-mutex": "^0.3.2", "ethereum-unit-converter": "^0.0.17", diff --git a/packages/starknet-snap/snap.config.ts b/packages/starknet-snap/snap.config.ts index 056840ca..7d8bb771 100644 --- a/packages/starknet-snap/snap.config.ts +++ b/packages/starknet-snap/snap.config.ts @@ -6,7 +6,7 @@ require('dotenv').config(); const config: SnapConfig = { bundler: 'webpack', - input: resolve(__dirname, 'src/index.ts'), + input: resolve(__dirname, 'src/index.tsx'), server: { port: 8081, }, diff --git a/packages/starknet-snap/src/index.test.ts b/packages/starknet-snap/src/index.test.tsx similarity index 100% rename from packages/starknet-snap/src/index.test.ts rename to packages/starknet-snap/src/index.test.tsx diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.tsx similarity index 100% rename from packages/starknet-snap/src/index.ts rename to packages/starknet-snap/src/index.tsx diff --git a/packages/starknet-snap/tsconfig.json b/packages/starknet-snap/tsconfig.json index 0fbd5d41..fc70d5dd 100644 --- a/packages/starknet-snap/tsconfig.json +++ b/packages/starknet-snap/tsconfig.json @@ -2,11 +2,13 @@ "compilerOptions": { "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "module": "commonjs" /* Specify what module code is generated. */, + "jsx": "react-jsx", + "jsxImportSource": "@metamask/snaps-sdk", "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, "resolveJsonModule": true /* lets us import JSON modules from within TypeScript modules. */, "strictNullChecks": true /* Enable strict null checks. */ }, - "include": ["**/*.ts"] + "include": ["**/*.ts", "**/*.tsx"] } From 76ae1eb34ed6af57a89c0856f325f196f7e22c9d Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 6 Nov 2024 14:29:05 +0100 Subject: [PATCH 02/17] chore: add jsx support in snap --- package.json | 1 + packages/starknet-snap/.eslintrc.js | 2 +- packages/starknet-snap/package.json | 6 +- packages/starknet-snap/snap.config.ts | 2 +- .../src/{index.test.ts => index.test.tsx} | 0 .../starknet-snap/src/{index.ts => index.tsx} | 0 packages/starknet-snap/tsconfig.json | 4 +- yarn.lock | 160 ++++++++++++++++-- 8 files changed, 154 insertions(+), 21 deletions(-) rename packages/starknet-snap/src/{index.test.ts => index.test.tsx} (100%) rename packages/starknet-snap/src/{index.ts => index.tsx} (100%) diff --git a/package.json b/package.json index e2e186c1..2088be4a 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@commitlint/config-conventional": "^17.0.3", "@lavamoat/allow-scripts": "^3.0.0", "@lavamoat/preinstall-always-fail": "^2.0.0", + "@types/react": "18.0.15", "husky": "^8.0.0" }, "packageManager": "yarn@3.2.1", diff --git a/packages/starknet-snap/.eslintrc.js b/packages/starknet-snap/.eslintrc.js index 064abf0d..bcb85612 100644 --- a/packages/starknet-snap/.eslintrc.js +++ b/packages/starknet-snap/.eslintrc.js @@ -22,7 +22,7 @@ module.exports = { }, }, { - files: ['*.test.ts'], + files: ['*.test.ts', '*.test.tsx'], extends: ['@metamask/eslint-config-jest'], rules: { '@typescript-eslint/no-shadow': [ diff --git a/packages/starknet-snap/package.json b/packages/starknet-snap/package.json index c134d97a..c4744a52 100644 --- a/packages/starknet-snap/package.json +++ b/packages/starknet-snap/package.json @@ -22,9 +22,9 @@ "cover:report": "nyc report --reporter=lcov --reporter=text", "jest": "jest --passWithNoTests", "lint": "yarn lint:eslint && yarn lint:misc --check", - "lint:eslint": "eslint . --cache --ext js,ts", + "lint:eslint": "eslint . --cache --ext js,ts,tsx", "lint:fix": "yarn lint:eslint --fix && yarn lint:misc --write", - "lint:misc": "prettier '**/*.ts' '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path .gitignore", + "lint:misc": "prettier '**/*.ts' '**/*.tsx' '**/*.json' '**/*.md' '!CHANGELOG.md' --ignore-path .gitignore", "serve": "mm-snap serve", "start": "mm-snap watch", "test": "yarn run test:unit && yarn run cover:report && yarn run jest", @@ -39,7 +39,7 @@ }, "dependencies": { "@metamask/key-tree": "9.0.0", - "@metamask/snaps-sdk": "^4.0.0", + "@metamask/snaps-sdk": "^6.1.1", "@metamask/utils": "^9.1.0", "async-mutex": "^0.3.2", "ethereum-unit-converter": "^0.0.17", diff --git a/packages/starknet-snap/snap.config.ts b/packages/starknet-snap/snap.config.ts index 056840ca..7d8bb771 100644 --- a/packages/starknet-snap/snap.config.ts +++ b/packages/starknet-snap/snap.config.ts @@ -6,7 +6,7 @@ require('dotenv').config(); const config: SnapConfig = { bundler: 'webpack', - input: resolve(__dirname, 'src/index.ts'), + input: resolve(__dirname, 'src/index.tsx'), server: { port: 8081, }, diff --git a/packages/starknet-snap/src/index.test.ts b/packages/starknet-snap/src/index.test.tsx similarity index 100% rename from packages/starknet-snap/src/index.test.ts rename to packages/starknet-snap/src/index.test.tsx diff --git a/packages/starknet-snap/src/index.ts b/packages/starknet-snap/src/index.tsx similarity index 100% rename from packages/starknet-snap/src/index.ts rename to packages/starknet-snap/src/index.tsx diff --git a/packages/starknet-snap/tsconfig.json b/packages/starknet-snap/tsconfig.json index 0fbd5d41..fc70d5dd 100644 --- a/packages/starknet-snap/tsconfig.json +++ b/packages/starknet-snap/tsconfig.json @@ -2,11 +2,13 @@ "compilerOptions": { "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "module": "commonjs" /* Specify what module code is generated. */, + "jsx": "react-jsx", + "jsxImportSource": "@metamask/snaps-sdk", "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */, "resolveJsonModule": true /* lets us import JSON modules from within TypeScript modules. */, "strictNullChecks": true /* Enable strict null checks. */ }, - "include": ["**/*.ts"] + "include": ["**/*.ts", "**/*.tsx"] } diff --git a/yarn.lock b/yarn.lock index 1b021ddb..95a4185a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2229,7 +2229,7 @@ __metadata: "@metamask/key-tree": 9.0.0 "@metamask/snaps-cli": ^6.2.1 "@metamask/snaps-jest": ^8.2.0 - "@metamask/snaps-sdk": ^4.0.0 + "@metamask/snaps-sdk": ^6.1.1 "@metamask/utils": ^9.1.0 "@types/chai": ^4.3.1 "@types/chai-as-promised": ^7.1.5 @@ -4269,6 +4269,17 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-engine@npm:^10.0.1": + version: 10.0.1 + resolution: "@metamask/json-rpc-engine@npm:10.0.1" + dependencies: + "@metamask/rpc-errors": ^7.0.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^10.0.0 + checksum: 277c68cf0036d62c9a1528e9d7e55e000233d02a55fb652edcc16b6149631346d34fe3fefaab13bc55377405e79293afdde5b6e3b61d49a2ce125ca50d7eafe1 + languageName: node + linkType: hard + "@metamask/json-rpc-engine@npm:^8.0.1, @metamask/json-rpc-engine@npm:^8.0.2": version: 8.0.2 resolution: "@metamask/json-rpc-engine@npm:8.0.2" @@ -4315,6 +4326,18 @@ __metadata: languageName: node linkType: hard +"@metamask/json-rpc-middleware-stream@npm:^8.0.5": + version: 8.0.5 + resolution: "@metamask/json-rpc-middleware-stream@npm:8.0.5" + dependencies: + "@metamask/json-rpc-engine": ^10.0.1 + "@metamask/safe-event-emitter": ^3.0.0 + "@metamask/utils": ^10.0.0 + readable-stream: ^3.6.2 + checksum: 4ac3d537bad1ab039bb1b42fb35113fe9a98bd89339155a0f759a086b957e5717ea1e75bdd340defd2b25f5886e07ab130235a63a1b8e627f8cb32a3020622c9 + languageName: node + linkType: hard + "@metamask/key-tree@npm:9.0.0": version: 9.0.0 resolution: "@metamask/key-tree@npm:9.0.0" @@ -4342,6 +4365,19 @@ __metadata: languageName: node linkType: hard +"@metamask/key-tree@npm:^9.1.2": + version: 9.1.2 + resolution: "@metamask/key-tree@npm:9.1.2" + dependencies: + "@metamask/scure-bip39": ^2.1.1 + "@metamask/utils": ^9.0.0 + "@noble/curves": ^1.2.0 + "@noble/hashes": ^1.3.2 + "@scure/base": ^1.0.0 + checksum: eb60bdbfa1806c2f248bf2602cd242e21b0fbe8bbb00ec97c3891739956a81e26c0dae125282a6207dbbe0643e727ff3574067b48210a0b01f12aae7b3159b77 + languageName: node + linkType: hard + "@metamask/number-to-bn@npm:^1.7.1": version: 1.7.1 resolution: "@metamask/number-to-bn@npm:1.7.1" @@ -4445,6 +4481,27 @@ __metadata: languageName: node linkType: hard +"@metamask/providers@npm:^18.1.1": + version: 18.1.1 + resolution: "@metamask/providers@npm:18.1.1" + dependencies: + "@metamask/json-rpc-engine": ^10.0.1 + "@metamask/json-rpc-middleware-stream": ^8.0.5 + "@metamask/object-multiplex": ^2.0.0 + "@metamask/rpc-errors": ^7.0.1 + "@metamask/safe-event-emitter": ^3.1.1 + "@metamask/utils": ^10.0.0 + detect-browser: ^5.2.0 + extension-port-stream: ^4.1.0 + fast-deep-equal: ^3.1.3 + is-stream: ^2.0.0 + readable-stream: ^3.6.2 + peerDependencies: + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: ca28bab03d7b67fd1e4fccf28045bd465a961c946b1f3e2464d6201730ec8c50970eb4a48d373bd3a7ac0bda471da604b71aaf5f22eae3c342a82e5b07134e91 + languageName: node + linkType: hard + "@metamask/rpc-errors@npm:^6.0.0, @metamask/rpc-errors@npm:^6.2.1": version: 6.3.0 resolution: "@metamask/rpc-errors@npm:6.3.0" @@ -4455,6 +4512,16 @@ __metadata: languageName: node linkType: hard +"@metamask/rpc-errors@npm:^7.0.1": + version: 7.0.1 + resolution: "@metamask/rpc-errors@npm:7.0.1" + dependencies: + "@metamask/utils": ^10.0.0 + fast-safe-stringify: ^2.0.6 + checksum: 20b300d26550c667a635eb5f97784c80d86c0b765433a32a9bced5b4c2a05a783cf2cd3a2bfe2aca6382181f53458bd2e7dc1bbb02e28005d3b4d0f3a46ca3ac + languageName: node + linkType: hard + "@metamask/safe-event-emitter@npm:^3.0.0, @metamask/safe-event-emitter@npm:^3.1.1": version: 3.1.1 resolution: "@metamask/safe-event-emitter@npm:3.1.1" @@ -4668,20 +4735,6 @@ __metadata: languageName: node linkType: hard -"@metamask/snaps-sdk@npm:^4.0.0": - version: 4.4.2 - resolution: "@metamask/snaps-sdk@npm:4.4.2" - dependencies: - "@metamask/key-tree": ^9.1.1 - "@metamask/providers": ^17.0.0 - "@metamask/rpc-errors": ^6.2.1 - "@metamask/utils": ^8.3.0 - fast-xml-parser: ^4.3.4 - superstruct: ^1.0.3 - checksum: 2ff3949cee3b6c5a580304a02191f3ec7fb049460c2ff89b1731f24b215baf5f9c08834a0b2b703ff43e3b74ede387386e22a96810b50be106bb029b180c44ce - languageName: node - linkType: hard - "@metamask/snaps-sdk@npm:^6.0.0": version: 6.0.0 resolution: "@metamask/snaps-sdk@npm:6.0.0" @@ -4695,6 +4748,19 @@ __metadata: languageName: node linkType: hard +"@metamask/snaps-sdk@npm:^6.1.1": + version: 6.10.0 + resolution: "@metamask/snaps-sdk@npm:6.10.0" + dependencies: + "@metamask/key-tree": ^9.1.2 + "@metamask/providers": ^18.1.1 + "@metamask/rpc-errors": ^7.0.1 + "@metamask/superstruct": ^3.1.0 + "@metamask/utils": ^10.0.0 + checksum: b389fe350e85d8ce0974ee10c0789ff1daa843efeacec234726de227a02a3937e13cf81d181855c8b00563dc42e519467ac9b5401af40bf601b91c8648302855 + languageName: node + linkType: hard + "@metamask/snaps-utils@npm:^7.0.1, @metamask/snaps-utils@npm:^7.7.0": version: 7.7.0 resolution: "@metamask/snaps-utils@npm:7.7.0" @@ -4752,6 +4818,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^10.0.0": + version: 10.0.1 + resolution: "@metamask/utils@npm:10.0.1" + dependencies: + "@ethereumjs/tx": ^4.2.0 + "@metamask/superstruct": ^3.1.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.3 + "@types/debug": ^4.1.7 + debug: ^4.3.4 + pony-cause: ^2.1.10 + semver: ^7.5.4 + uuid: ^9.0.1 + checksum: 4c350c7a1c881c6af446319942392e6eb62411bff9c512166d816d39702c7b4926a982ebfd56ada317f9332a5416b3211c09e022674cee8272228658977ba851 + languageName: node + linkType: hard + "@metamask/utils@npm:^6.0.1": version: 6.2.0 resolution: "@metamask/utils@npm:6.2.0" @@ -4783,6 +4866,23 @@ __metadata: languageName: node linkType: hard +"@metamask/utils@npm:^9.0.0": + version: 9.3.0 + resolution: "@metamask/utils@npm:9.3.0" + dependencies: + "@ethereumjs/tx": ^4.2.0 + "@metamask/superstruct": ^3.1.0 + "@noble/hashes": ^1.3.1 + "@scure/base": ^1.1.3 + "@types/debug": ^4.1.7 + debug: ^4.3.4 + pony-cause: ^2.1.10 + semver: ^7.5.4 + uuid: ^9.0.1 + checksum: f720b0f7bdd46054aa88d15a9702e1de6d7200a1ca1d4f6bc48761b039f1bbffb46ac88bc87fe79e66128c196d424f3b9ef071b3cb4b40139223786d56da35e0 + languageName: node + linkType: hard + "@metamask/utils@npm:^9.1.0": version: 9.1.0 resolution: "@metamask/utils@npm:9.1.0" @@ -7877,6 +7977,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:18.0.15": + version: 18.0.15 + resolution: "@types/react@npm:18.0.15" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: e22cc388d1c145aa184787e44dc28db4789976c704cd5db475c170bb76a560eb81def5f346cfe750949bb3d43ad88822b8cbb9f19b1286e3795892a8263e7715 + languageName: node + linkType: hard + "@types/resolve@npm:1.17.1": version: 1.17.1 resolution: "@types/resolve@npm:1.17.1" @@ -7900,6 +8011,13 @@ __metadata: languageName: node linkType: hard +"@types/scheduler@npm:*": + version: 0.23.0 + resolution: "@types/scheduler@npm:0.23.0" + checksum: 874d753aa65c17760dfc460a91e6df24009bde37bfd427a031577b30262f7770c1b8f71a21366c7dbc76111967384cf4090a31d65315155180ef14bd7acccb32 + languageName: node + linkType: hard + "@types/semver@npm:^7.3.10, @types/semver@npm:^7.3.12": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" @@ -14556,6 +14674,17 @@ __metadata: languageName: node linkType: hard +"extension-port-stream@npm:^4.1.0": + version: 4.2.0 + resolution: "extension-port-stream@npm:4.2.0" + dependencies: + readable-stream: ^3.6.2 || ^4.4.2 + peerDependencies: + webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 + checksum: 85559c82e3f3aa21462e234b30b7d53872708893664cd03f2f848af556cf0730cf2243b089efc9d40bbe9a4f73bd8fd19684db5a985329b0c4402b4f2fe26358 + languageName: node + linkType: hard + "extglob@npm:^2.0.4": version: 2.0.4 resolution: "extglob@npm:2.0.4" @@ -25096,6 +25225,7 @@ __metadata: "@commitlint/config-conventional": ^17.0.3 "@lavamoat/allow-scripts": ^3.0.0 "@lavamoat/preinstall-always-fail": ^2.0.0 + "@types/react": 18.0.15 husky: ^8.0.0 languageName: unknown linkType: soft From 6cd5cd2506c86c36d754d084dd9a3017d50719ee Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 6 Nov 2024 22:35:02 +0100 Subject: [PATCH 03/17] feat: jsx support management --- packages/starknet-snap/.eslintrc.js | 7 ++ packages/starknet-snap/src/index.tsx | 73 +++++++++++-------- packages/starknet-snap/src/on-home-page.ts | 4 +- packages/starknet-snap/src/types/snapState.ts | 1 + packages/starknet-snap/src/utils/snap.ts | 51 ++++++++++++- .../MinVersionModal/MinVersionModal.view.tsx | 34 ++++++--- 6 files changed, 128 insertions(+), 42 deletions(-) diff --git a/packages/starknet-snap/.eslintrc.js b/packages/starknet-snap/.eslintrc.js index bcb85612..6388e179 100644 --- a/packages/starknet-snap/.eslintrc.js +++ b/packages/starknet-snap/.eslintrc.js @@ -19,6 +19,13 @@ module.exports = { 'jsdoc/require-returns': 'off', 'jsdoc/require-param-description': 'off', 'jsdoc/match-description': 'off', + // This allows importing the `Text` JSX component. + '@typescript-eslint/no-shadow': [ + 'error', + { + allow: ['Text'], + }, + ], }, }, { diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index a33e66c9..aed9b3d4 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -4,7 +4,8 @@ import type { OnInstallHandler, OnUpdateHandler, } from '@metamask/snaps-sdk'; -import { panel, text, MethodNotFoundError } from '@metamask/snaps-sdk'; +import { SnapError, MethodNotFoundError } from '@metamask/snaps-sdk'; +import { Box, Link, Text } from '@metamask/snaps-sdk/jsx'; import { addNetwork } from './addNetwork'; import { Config } from './config'; @@ -59,7 +60,14 @@ import type { } from './types/snapApi'; import type { SnapState } from './types/snapState'; import { upgradeAccContract } from './upgradeAccContract'; -import { getDappUrl, isSnapRpcError } from './utils'; +import { + ensureJsxSupport, + getDappUrl, + getStateData, + isSnapRpcError, + setStateData, + updateRequiredMetaMaskComponent, +} from './utils'; import { CAIRO_VERSION_LEGACY, PRELOADED_TOKENS, @@ -93,12 +101,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { } // TODO: this will causing racing condition, need to be fixed - let state: SnapState = await snap.request({ - method: 'snap_manageState', - params: { - operation: 'get', - }, - }); + let state: SnapState = await getStateData(); if (!state) { state = { accContracts: [], @@ -107,13 +110,11 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { transactions: [], }; // initialize state if empty and set default data - await snap.request({ - method: 'snap_manageState', - params: { - operation: 'update', - newState: state, - }, - }); + await setStateData(state); + } + + if (state.requireMMUpgrade === undefined) { + throw SnapError; } // TODO: this can be remove, after state manager is implemented @@ -307,12 +308,15 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }; export const onInstall: OnInstallHandler = async () => { - const component = panel([ - text('Your MetaMask wallet is now compatible with Starknet!'), - text( - `To manage your Starknet account and send and receive funds, visit the [companion dapp for Starknet](${getDappUrl()}).`, - ), - ]); + const component = await ensureJsxSupport( + + Your MetaMask wallet is now compatible with Starknet! + + To manage your Starknet account and send and receive funds, visit the{' '} + companion dapp for Starknet. + + , + ); await snap.request({ method: 'snap_dialog', @@ -324,14 +328,15 @@ export const onInstall: OnInstallHandler = async () => { }; export const onUpdate: OnUpdateHandler = async () => { - const component = panel([ - text('Features released with this update:'), - text( - 'Support STRK token for the gas fee in sending transaction and estimating fee.', - ), - text('Default network changed to mainnet.'), - text('Support for multiple consecutive transactions.'), - ]); + const component = await ensureJsxSupport( + + Your Starknet Snap is now up-to-date ! + + As usual, to manage your Starknet account and send and receive funds, + visit the companion dapp for Starknet. + + , + ); await snap.request({ method: 'snap_dialog', @@ -343,5 +348,13 @@ export const onUpdate: OnUpdateHandler = async () => { }; export const onHomePage: OnHomePageHandler = async () => { - return await homePageController.execute(); + const state = await getStateData(); + state.requireMMUpgrade = false; + if (state.requireMMUpgrade !== undefined) { + return await homePageController.execute(); + } + + return { + content: updateRequiredMetaMaskComponent(), + }; }; diff --git a/packages/starknet-snap/src/on-home-page.ts b/packages/starknet-snap/src/on-home-page.ts index d84fa7ee..55c37c2f 100644 --- a/packages/starknet-snap/src/on-home-page.ts +++ b/packages/starknet-snap/src/on-home-page.ts @@ -50,7 +50,7 @@ export class HomePageController { const balance = await this.getBalance(network, address); - return this.buildComponenets(address, network, balance); + return this.buildComponents(address, network, balance); } catch (error) { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions logger.error('Failed to execute onHomePage', toJson(error)); @@ -97,7 +97,7 @@ export class HomePageController { ); } - protected buildComponenets( + protected buildComponents( address: string, network: Network, balance: string, diff --git a/packages/starknet-snap/src/types/snapState.ts b/packages/starknet-snap/src/types/snapState.ts index a1ae56e7..25359318 100644 --- a/packages/starknet-snap/src/types/snapState.ts +++ b/packages/starknet-snap/src/types/snapState.ts @@ -7,6 +7,7 @@ export type SnapState = { networks: Network[]; transactions: Transaction[]; currentNetwork?: Network; + requireMMUpgrade?: boolean; }; export type AccContract = { diff --git a/packages/starknet-snap/src/utils/snap.ts b/packages/starknet-snap/src/utils/snap.ts index 49856f57..b5e21506 100644 --- a/packages/starknet-snap/src/utils/snap.ts +++ b/packages/starknet-snap/src/utils/snap.ts @@ -1,7 +1,14 @@ import type { BIP44AddressKeyDeriver } from '@metamask/key-tree'; import { getBIP44AddressKeyDeriver } from '@metamask/key-tree'; import type { Component, DialogResult, Json } from '@metamask/snaps-sdk'; -import { DialogType, panel, type SnapsProvider } from '@metamask/snaps-sdk'; +import { + DialogType, + panel, + text, + type SnapsProvider, +} from '@metamask/snaps-sdk'; + +import type { SnapState } from '../types/snapState'; declare const snap: SnapsProvider; @@ -93,3 +100,45 @@ export async function setStateData(data: State) { }, }); } + +export const updateRequiredMetaMaskComponent = () => { + return panel([ + text( + 'You need to update your MetaMask to latest version to use this snap.', + ), + ]); +}; + +/** + * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. + * If MetaMask does not support JSX, an alert message is shown prompting the user to update MetaMask. + * + * @param component - The JSX component to display in the snap dialog. + * + * The function performs the following steps: + * 1. Tries to render the provided component using a `snap_dialog` method. + * 2. On success, it updates the `requireMMUpgrade` flag in the snap's state to `false`, indicating that JSX is supported. + * 3. If an error occurs (likely due to outdated MetaMask), it displays an alert dialog prompting the user to update MetaMask. + */ +export const ensureJsxSupport = async (component: Component) => { + try { + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: component, + }, + }); + const state = await getStateData(); + state.requireMMUpgrade = false; + await setStateData(state); + } catch { + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: updateRequiredMetaMaskComponent(), + }, + }); + } +}; diff --git a/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx b/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx index 6ed188a6..264b1a54 100644 --- a/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx +++ b/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx @@ -11,21 +11,37 @@ export const MinVersionModalView = () => { A new version of the Starknet Snap is available - To use this dapp, please install the latest version by following those - steps: + To use this dApp, please:
  • - Delete the current version in MetaMask by going in Settings {'>'}{' '} - Snaps {'>'} @consensys/starknet-snap {'>'} See details {'>'} Remove - Snap + Ensure you have the latest version of{' '} + MetaMask installed (v12.5 or + higher is required).
  • -
  • Refresh the page
  • - Click on connect, the new version will be proposed for installation. + Install the latest version of the Starknet Snap by following these + steps: +
      +
    • + Remove the current version in MetaMask by navigating to{' '} + + Settings {'>'} Snaps {'>'} @consensys/starknet-snap {'>'} See + Details {'>'} Remove Snap + + . +
    • +
    • Refresh the page.
    • +
    • + Click Connect, the new version will be proposed + for installation. +
    • +
- Note: Your account will be automatically recovered. Future upgrades will - be managed automatically +

+ Note: Your account will be automatically recovered, + and future upgrades will be managed automatically. +

); From db7eafb3b3477e85fbdd0e151068a419d4fede35 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 7 Nov 2024 00:35:48 +0100 Subject: [PATCH 04/17] chore: fix test and lint --- packages/starknet-snap/package.json | 2 ++ packages/starknet-snap/src/index.test.tsx | 33 +++++++++++++++++++++++ packages/starknet-snap/src/index.tsx | 13 ++++----- yarn.lock | 22 +++++++++++++++ 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/starknet-snap/package.json b/packages/starknet-snap/package.json index c4744a52..807678fd 100644 --- a/packages/starknet-snap/package.json +++ b/packages/starknet-snap/package.json @@ -59,6 +59,8 @@ "@metamask/snaps-jest": "^8.2.0", "@types/chai": "^4.3.1", "@types/chai-as-promised": "^7.1.5", + "@types/react": "18.2.4", + "@types/react-dom": "18.2.4", "@types/sinon": "^10.0.11", "@types/sinon-chai": "^3.2.8", "@typescript-eslint/eslint-plugin": "^5.42.1", diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index 2426ddf5..af00f8ee 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -4,14 +4,19 @@ import { onHomePage, onRpcRequest } from '.'; import * as createAccountApi from './createAccount'; import { HomePageController } from './on-home-page'; import * as keyPairUtils from './utils/keyPair'; +import * as snapHelper from './utils/snap'; jest.mock('./utils/logger'); +jest.mock('./utils/snap'); describe('onRpcRequest', () => { const createMockSpy = () => { + const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); + getStateDataSpy.mockResolvedValue({ requireMMUpgrade: false }); const createAccountSpy = jest.spyOn(createAccountApi, 'createAccount'); const keyPairSpy = jest.spyOn(keyPairUtils, 'getAddressKeyDeriver'); return { + getStateDataSpy, createAccountSpy, keyPairSpy, }; @@ -42,6 +47,7 @@ describe('onRpcRequest', () => { }); it('throws `MethodNotFoundError` if the request method not found', async () => { + createMockSpy(); await expect( onRpcRequest({ ...createMockRequest(), @@ -53,6 +59,31 @@ describe('onRpcRequest', () => { ).rejects.toThrow(MethodNotFoundError); }); + it('throws `SnapError` on request if MetaMask needs update', async () => { + await expect( + onRpcRequest({ + ...createMockRequest(), + request: { + ...createMockRequest().request, + method: 'ping', + }, + }), + ).rejects.toThrow(SnapError); + }); + + it('requests gets executed if MetaMask does not needs update', async () => { + createMockSpy(); + expect( + await onRpcRequest({ + ...createMockRequest(), + request: { + ...createMockRequest().request, + method: 'ping', + }, + }), + ).toBe('pong'); + }); + it('throws `SnapError` if the error is an instance of SnapError', async () => { const { createAccountSpy } = createMockSpy(); createAccountSpy.mockRejectedValue(new SnapError('error')); @@ -70,6 +101,8 @@ describe('onRpcRequest', () => { describe('onHomePage', () => { it('executes homePageController', async () => { + const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); + getStateDataSpy.mockResolvedValue({ requireMMUpgrade: true }); const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); executeSpy.mockResolvedValue({ content: text('test') }); diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index aed9b3d4..dc0d5687 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -95,11 +95,6 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); try { - if (request.method === 'ping') { - logger.log('pong'); - return 'pong'; - } - // TODO: this will causing racing condition, need to be fixed let state: SnapState = await getStateData(); if (!state) { @@ -114,9 +109,15 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { } if (state.requireMMUpgrade === undefined) { + console.log('hello'); throw SnapError; } + if (request.method === 'ping') { + logger.log('pong'); + return 'pong'; + } + // TODO: this can be remove, after state manager is implemented const saveMutex = acquireLock(); @@ -349,7 +350,7 @@ export const onUpdate: OnUpdateHandler = async () => { export const onHomePage: OnHomePageHandler = async () => { const state = await getStateData(); - state.requireMMUpgrade = false; + console.log(state); if (state.requireMMUpgrade !== undefined) { return await homePageController.execute(); } diff --git a/yarn.lock b/yarn.lock index 95a4185a..49f738f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2233,6 +2233,8 @@ __metadata: "@metamask/utils": ^9.1.0 "@types/chai": ^4.3.1 "@types/chai-as-promised": ^7.1.5 + "@types/react": 18.2.4 + "@types/react-dom": 18.2.4 "@types/sinon": ^10.0.11 "@types/sinon-chai": ^3.2.8 "@typescript-eslint/eslint-plugin": ^5.42.1 @@ -7949,6 +7951,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:18.2.4": + version: 18.2.4 + resolution: "@types/react-dom@npm:18.2.4" + dependencies: + "@types/react": "*" + checksum: 8301f35cf1cbfec8c723e9477aecf87774e3c168bd457d353b23c45064737213d3e8008b067c6767b7b08e4f2b3823ee239242a6c225fc91e7f8725ef8734124 + languageName: node + linkType: hard + "@types/react-dom@npm:^18.0.0, @types/react-dom@npm:^18.0.5": version: 18.3.0 resolution: "@types/react-dom@npm:18.3.0" @@ -7988,6 +7999,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:18.2.4": + version: 18.2.4 + resolution: "@types/react@npm:18.2.4" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: d920fc93832fe50d5e8175a0ba233086c97a9e238ff7327c8319b8dec57409618f491d6f71be2374c3132f40a8fc428b3e406c1e2a5f1dc32ccd6d47051786d2 + languageName: node + linkType: hard + "@types/resolve@npm:1.17.1": version: 1.17.1 resolution: "@types/resolve@npm:1.17.1" From ce79daac2ecaaeb539829b993bcfda1dde45ccc0 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 11 Nov 2024 15:44:47 +0100 Subject: [PATCH 05/17] fix: add mutex in jsx support detection mechanism --- packages/starknet-snap/src/index.tsx | 57 +++++++++--------------- packages/starknet-snap/src/utils/snap.ts | 12 +++-- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index dc0d5687..e354fdad 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -95,32 +95,33 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); try { + if (request.method === 'ping') { + logger.log('pong'); + return 'pong'; + } + // TODO: this can be remove, after state manager is implemented + const saveMutex = acquireLock(); + // TODO: this will causing racing condition, need to be fixed let state: SnapState = await getStateData(); - if (!state) { - state = { - accContracts: [], - erc20Tokens: [], - networks: [], - transactions: [], - }; - // initialize state if empty and set default data - await setStateData(state); - } + await saveMutex.runExclusive(async () => { + if (!state) { + state = { + accContracts: [], + erc20Tokens: [], + networks: [], + transactions: [], + }; + // initialize state if empty and set default data + await setStateData(state); + } + }); if (state.requireMMUpgrade === undefined) { console.log('hello'); throw SnapError; } - if (request.method === 'ping') { - logger.log('pong'); - return 'pong'; - } - - // TODO: this can be remove, after state manager is implemented - const saveMutex = acquireLock(); - // pre-inserted the default networks and tokens await upsertNetwork(STARKNET_MAINNET_NETWORK, snap, saveMutex, state); await upsertNetwork( @@ -309,7 +310,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }; export const onInstall: OnInstallHandler = async () => { - const component = await ensureJsxSupport( + await ensureJsxSupport( Your MetaMask wallet is now compatible with Starknet! @@ -318,18 +319,10 @@ export const onInstall: OnInstallHandler = async () => { , ); - - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: component, - }, - }); }; export const onUpdate: OnUpdateHandler = async () => { - const component = await ensureJsxSupport( + await ensureJsxSupport( Your Starknet Snap is now up-to-date ! @@ -338,14 +331,6 @@ export const onUpdate: OnUpdateHandler = async () => { , ); - - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: component, - }, - }); }; export const onHomePage: OnHomePageHandler = async () => { diff --git a/packages/starknet-snap/src/utils/snap.ts b/packages/starknet-snap/src/utils/snap.ts index b5e21506..73e3391a 100644 --- a/packages/starknet-snap/src/utils/snap.ts +++ b/packages/starknet-snap/src/utils/snap.ts @@ -9,6 +9,7 @@ import { } from '@metamask/snaps-sdk'; import type { SnapState } from '../types/snapState'; +import { acquireLock } from './lock'; declare const snap: SnapsProvider; @@ -122,6 +123,8 @@ export const updateRequiredMetaMaskComponent = () => { */ export const ensureJsxSupport = async (component: Component) => { try { + const saveMutex = acquireLock(); + await snap.request({ method: 'snap_dialog', params: { @@ -129,9 +132,12 @@ export const ensureJsxSupport = async (component: Component) => { content: component, }, }); - const state = await getStateData(); - state.requireMMUpgrade = false; - await setStateData(state); + + const state: SnapState = await getStateData(); + await saveMutex.runExclusive(async () => { + state.requireMMUpgrade = false; + await setStateData(state); + }); } catch { await snap.request({ method: 'snap_dialog', From 550bdb58e7897951b4254762f91b5b52af08910a Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 11 Nov 2024 15:58:01 +0100 Subject: [PATCH 06/17] chore: ensure test pass --- packages/starknet-snap/src/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index e354fdad..52d23778 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -95,10 +95,6 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); try { - if (request.method === 'ping') { - logger.log('pong'); - return 'pong'; - } // TODO: this can be remove, after state manager is implemented const saveMutex = acquireLock(); @@ -118,10 +114,14 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }); if (state.requireMMUpgrade === undefined) { - console.log('hello'); throw SnapError; } + if (request.method === 'ping') { + logger.log('pong'); + return 'pong'; + } + // pre-inserted the default networks and tokens await upsertNetwork(STARKNET_MAINNET_NETWORK, snap, saveMutex, state); await upsertNetwork( From a8a2a23c097a18daeabc0acf5a0133849ab9ff01 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 14 Nov 2024 11:55:04 +0100 Subject: [PATCH 07/17] feat: new init state manager class to manage state init and support check --- packages/starknet-snap/src/index.test.tsx | 45 ++++++++-- packages/starknet-snap/src/index.tsx | 67 +++++++-------- .../src/state/init-snap-state-manager.ts | 82 +++++++++++++++++++ .../starknet-snap/src/state/state-manager.ts | 1 + packages/starknet-snap/src/utils/snap.ts | 42 ---------- 5 files changed, 148 insertions(+), 89 deletions(-) create mode 100644 packages/starknet-snap/src/state/init-snap-state-manager.ts diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index af00f8ee..4c63adb6 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -3,20 +3,29 @@ import { text, MethodNotFoundError, SnapError } from '@metamask/snaps-sdk'; import { onHomePage, onRpcRequest } from '.'; import * as createAccountApi from './createAccount'; import { HomePageController } from './on-home-page'; +import { InitSnapStateManager } from './state/init-snap-state-manager'; +import { updateRequiredMetaMaskComponent } from './utils'; import * as keyPairUtils from './utils/keyPair'; -import * as snapHelper from './utils/snap'; jest.mock('./utils/logger'); jest.mock('./utils/snap'); +jest.mock('./utils', () => ({ + ...jest.requireActual('./utils'), + updateRequiredMetaMaskComponent: jest.fn(), +})); + describe('onRpcRequest', () => { const createMockSpy = () => { - const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); - getStateDataSpy.mockResolvedValue({ requireMMUpgrade: false }); + const getJsxSupportRequirementSpy = jest.spyOn( + InitSnapStateManager.prototype, + 'getJsxSupportRequirement', + ); + getJsxSupportRequirementSpy.mockResolvedValue(false); const createAccountSpy = jest.spyOn(createAccountApi, 'createAccount'); const keyPairSpy = jest.spyOn(keyPairUtils, 'getAddressKeyDeriver'); return { - getStateDataSpy, + getJsxSupportRequirementSpy, createAccountSpy, keyPairSpy, }; @@ -60,12 +69,14 @@ describe('onRpcRequest', () => { }); it('throws `SnapError` on request if MetaMask needs update', async () => { + const { getJsxSupportRequirementSpy } = createMockSpy(); + getJsxSupportRequirementSpy.mockResolvedValue(true); await expect( onRpcRequest({ ...createMockRequest(), request: { ...createMockRequest().request, - method: 'ping', + method: 'executeTxn', }, }), ).rejects.toThrow(SnapError); @@ -100,9 +111,12 @@ describe('onRpcRequest', () => { }); describe('onHomePage', () => { - it('executes homePageController', async () => { - const getStateDataSpy = jest.spyOn(snapHelper, 'getStateData'); - getStateDataSpy.mockResolvedValue({ requireMMUpgrade: true }); + it('executes homePageController normally if jsxSupport is not required', async () => { + const getJsxSupportRequirementSpy = jest.spyOn( + InitSnapStateManager.prototype, + 'getJsxSupportRequirement', + ); + getJsxSupportRequirementSpy.mockResolvedValue(false); const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); executeSpy.mockResolvedValue({ content: text('test') }); @@ -116,4 +130,19 @@ describe('onHomePage', () => { }, }); }); + it('renders updateRequiredMetaMaskComponent when jsx support is lacking', async () => { + const getJsxSupportRequirementSpy = jest.spyOn( + InitSnapStateManager.prototype, + 'getJsxSupportRequirement', + ); + getJsxSupportRequirementSpy.mockResolvedValue(true); + const updateRequiredMetaMaskComponentSpy = + updateRequiredMetaMaskComponent as jest.Mock; + const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); + + await onHomePage(); + + expect(executeSpy).toHaveBeenCalledTimes(0); + expect(updateRequiredMetaMaskComponentSpy).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index 52d23778..931945ef 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -4,7 +4,7 @@ import type { OnInstallHandler, OnUpdateHandler, } from '@metamask/snaps-sdk'; -import { SnapError, MethodNotFoundError } from '@metamask/snaps-sdk'; +import { MethodNotFoundError } from '@metamask/snaps-sdk'; import { Box, Link, Text } from '@metamask/snaps-sdk/jsx'; import { addNetwork } from './addNetwork'; @@ -53,19 +53,16 @@ import { } from './rpcs'; import { sendTransaction } from './sendTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; +import { InitSnapStateManager } from './state/init-snap-state-manager'; import type { ApiParams, ApiParamsWithKeyDeriver, ApiRequestParams, } from './types/snapApi'; -import type { SnapState } from './types/snapState'; import { upgradeAccContract } from './upgradeAccContract'; import { - ensureJsxSupport, getDappUrl, - getStateData, isSnapRpcError, - setStateData, updateRequiredMetaMaskComponent, } from './utils'; import { @@ -94,33 +91,23 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); - try { - // TODO: this can be remove, after state manager is implemented - const saveMutex = acquireLock(); + if (request.method === 'ping') { + logger.log('pong'); + return 'pong'; + } - // TODO: this will causing racing condition, need to be fixed - let state: SnapState = await getStateData(); - await saveMutex.runExclusive(async () => { - if (!state) { - state = { - accContracts: [], - erc20Tokens: [], - networks: [], - transactions: [], - }; - // initialize state if empty and set default data - await setStateData(state); - } - }); - - if (state.requireMMUpgrade === undefined) { - throw SnapError; + try { + const initSnapStateManager = new InitSnapStateManager(); + const requireMMUpgrade = + await initSnapStateManager.getJsxSupportRequirement(); + if (requireMMUpgrade) { + throw new Error('MetaMask upgrade required for compatibility'); } - if (request.method === 'ping') { - logger.log('pong'); - return 'pong'; - } + const state = await initSnapStateManager.get(); + + // TODO: this can be remove, after state manager is implemented + const saveMutex = acquireLock(); // pre-inserted the default networks and tokens await upsertNetwork(STARKNET_MAINNET_NETWORK, snap, saveMutex, state); @@ -310,7 +297,8 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }; export const onInstall: OnInstallHandler = async () => { - await ensureJsxSupport( + const initSnapStateManager = new InitSnapStateManager(); + await initSnapStateManager.ensureJsxSupport( Your MetaMask wallet is now compatible with Starknet! @@ -322,7 +310,8 @@ export const onInstall: OnInstallHandler = async () => { }; export const onUpdate: OnUpdateHandler = async () => { - await ensureJsxSupport( + const initSnapStateManager = new InitSnapStateManager(); + await initSnapStateManager.ensureJsxSupport( Your Starknet Snap is now up-to-date ! @@ -334,13 +323,13 @@ export const onUpdate: OnUpdateHandler = async () => { }; export const onHomePage: OnHomePageHandler = async () => { - const state = await getStateData(); - console.log(state); - if (state.requireMMUpgrade !== undefined) { - return await homePageController.execute(); + const initSnapStateManager = new InitSnapStateManager(); + const requireMMUpgrade = + await initSnapStateManager.getJsxSupportRequirement(); + if (requireMMUpgrade) { + return { + content: updateRequiredMetaMaskComponent(), + }; } - - return { - content: updateRequiredMetaMaskComponent(), - }; + return await homePageController.execute(); }; diff --git a/packages/starknet-snap/src/state/init-snap-state-manager.ts b/packages/starknet-snap/src/state/init-snap-state-manager.ts new file mode 100644 index 00000000..d60aaf16 --- /dev/null +++ b/packages/starknet-snap/src/state/init-snap-state-manager.ts @@ -0,0 +1,82 @@ +import type { Component } from '@metamask/snaps-sdk'; + +import type { SnapState } from '../types/snapState'; +import { updateRequiredMetaMaskComponent } from '../utils'; +import { StateManager, StateManagerError } from './state-manager'; + +export class InitSnapStateManager extends StateManager { + protected getCollection(_state: SnapState) { + return undefined; + } + + protected updateEntity(dataInState: SnapState, data: SnapState): void { + dataInState.requireMMUpgrade = data.requireMMUpgrade; + } + + /** + * Ensures that the SnapState has all required default values. + * If any property is missing, it initializes it with a default value. + * @returns The fully initialized SnapState. + */ + public override async get(): Promise { + return await super.get(); + } + + // Updates the `requireMMUpgrade` flag based on JSX support + async setJsxSupport(isSupported: boolean): Promise { + try { + await this.update(async (state: SnapState) => { + this.updateEntity(state, { ...state, requireMMUpgrade: !isSupported }); + }); + } catch (error) { + throw new StateManagerError( + 'Error updating MetaMask compatibility status', + ); + } + } + + // Retrieves the current `requireMMUpgrade` flag value + async getJsxSupportRequirement(): Promise { + const state = await this.get(); + if (state.requireMMUpgrade === undefined) { + throw new StateManagerError( + 'MetaMask compatibility state not initialized', + ); + } + return state.requireMMUpgrade; + } + + /** + * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. + * If MetaMask does not support JSX, an alert message is shown prompting the user to update MetaMask. + * + * @param component - The JSX component to display in the snap dialog. + * + * The function performs the following steps: + * 1. Tries to render the provided component using a `snap_dialog` method. + * 2. On success, it updates the `requireMMUpgrade` flag in the snap's state to `false`, indicating that JSX is supported. + * 3. If an error occurs (likely due to outdated MetaMask), it displays an alert dialog prompting the user to update MetaMask. + */ + async ensureJsxSupport(component: Component): Promise { + await this.get(); + try { + // Try rendering the JSX component to test compatibility + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: component, + }, + }); + } catch { + await this.setJsxSupport(false); + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: updateRequiredMetaMaskComponent(), + }, + }); + } + } +} diff --git a/packages/starknet-snap/src/state/state-manager.ts b/packages/starknet-snap/src/state/state-manager.ts index 7bee3e56..1bafa0a6 100644 --- a/packages/starknet-snap/src/state/state-manager.ts +++ b/packages/starknet-snap/src/state/state-manager.ts @@ -14,6 +14,7 @@ export abstract class StateManager extends SnapStateManager { erc20Tokens: [], networks: [], transactions: [], + requireMMUpgrade: undefined, }; } diff --git a/packages/starknet-snap/src/utils/snap.ts b/packages/starknet-snap/src/utils/snap.ts index 73e3391a..807817de 100644 --- a/packages/starknet-snap/src/utils/snap.ts +++ b/packages/starknet-snap/src/utils/snap.ts @@ -8,9 +8,6 @@ import { type SnapsProvider, } from '@metamask/snaps-sdk'; -import type { SnapState } from '../types/snapState'; -import { acquireLock } from './lock'; - declare const snap: SnapsProvider; /** @@ -109,42 +106,3 @@ export const updateRequiredMetaMaskComponent = () => { ), ]); }; - -/** - * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. - * If MetaMask does not support JSX, an alert message is shown prompting the user to update MetaMask. - * - * @param component - The JSX component to display in the snap dialog. - * - * The function performs the following steps: - * 1. Tries to render the provided component using a `snap_dialog` method. - * 2. On success, it updates the `requireMMUpgrade` flag in the snap's state to `false`, indicating that JSX is supported. - * 3. If an error occurs (likely due to outdated MetaMask), it displays an alert dialog prompting the user to update MetaMask. - */ -export const ensureJsxSupport = async (component: Component) => { - try { - const saveMutex = acquireLock(); - - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: component, - }, - }); - - const state: SnapState = await getStateData(); - await saveMutex.runExclusive(async () => { - state.requireMMUpgrade = false; - await setStateData(state); - }); - } catch { - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: updateRequiredMetaMaskComponent(), - }, - }); - } -}; From 396901bd93893b6440ff5244d85eacc65d695861 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 14 Nov 2024 15:43:27 +0100 Subject: [PATCH 08/17] fix: wait for hooks in request handler --- packages/starknet-snap/src/index.test.tsx | 24 ++++++++--------- packages/starknet-snap/src/index.tsx | 4 +-- .../src/state/init-snap-state-manager.ts | 27 ++++++++++++++----- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index 4c63adb6..9192b8ca 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -17,15 +17,15 @@ jest.mock('./utils', () => ({ describe('onRpcRequest', () => { const createMockSpy = () => { - const getJsxSupportRequirementSpy = jest.spyOn( + const requireMetaMaskUpgradeSpy = jest.spyOn( InitSnapStateManager.prototype, - 'getJsxSupportRequirement', + 'requireMetaMaskUpgrade', ); - getJsxSupportRequirementSpy.mockResolvedValue(false); + requireMetaMaskUpgradeSpy.mockResolvedValue(false); const createAccountSpy = jest.spyOn(createAccountApi, 'createAccount'); const keyPairSpy = jest.spyOn(keyPairUtils, 'getAddressKeyDeriver'); return { - getJsxSupportRequirementSpy, + requireMetaMaskUpgradeSpy, createAccountSpy, keyPairSpy, }; @@ -69,8 +69,8 @@ describe('onRpcRequest', () => { }); it('throws `SnapError` on request if MetaMask needs update', async () => { - const { getJsxSupportRequirementSpy } = createMockSpy(); - getJsxSupportRequirementSpy.mockResolvedValue(true); + const { requireMetaMaskUpgradeSpy } = createMockSpy(); + requireMetaMaskUpgradeSpy.mockResolvedValue(true); await expect( onRpcRequest({ ...createMockRequest(), @@ -112,11 +112,11 @@ describe('onRpcRequest', () => { describe('onHomePage', () => { it('executes homePageController normally if jsxSupport is not required', async () => { - const getJsxSupportRequirementSpy = jest.spyOn( + const requireMetaMaskUpgradeSpy = jest.spyOn( InitSnapStateManager.prototype, - 'getJsxSupportRequirement', + 'requireMetaMaskUpgrade', ); - getJsxSupportRequirementSpy.mockResolvedValue(false); + requireMetaMaskUpgradeSpy.mockResolvedValue(false); const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); executeSpy.mockResolvedValue({ content: text('test') }); @@ -131,11 +131,11 @@ describe('onHomePage', () => { }); }); it('renders updateRequiredMetaMaskComponent when jsx support is lacking', async () => { - const getJsxSupportRequirementSpy = jest.spyOn( + const requireMetaMaskUpgradeSpy = jest.spyOn( InitSnapStateManager.prototype, - 'getJsxSupportRequirement', + 'requireMetaMaskUpgrade', ); - getJsxSupportRequirementSpy.mockResolvedValue(true); + requireMetaMaskUpgradeSpy.mockResolvedValue(true); const updateRequiredMetaMaskComponentSpy = updateRequiredMetaMaskComponent as jest.Mock; const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index 931945ef..315b4dba 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -99,7 +99,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { try { const initSnapStateManager = new InitSnapStateManager(); const requireMMUpgrade = - await initSnapStateManager.getJsxSupportRequirement(); + await initSnapStateManager.requireMetaMaskUpgrade(); if (requireMMUpgrade) { throw new Error('MetaMask upgrade required for compatibility'); } @@ -325,7 +325,7 @@ export const onUpdate: OnUpdateHandler = async () => { export const onHomePage: OnHomePageHandler = async () => { const initSnapStateManager = new InitSnapStateManager(); const requireMMUpgrade = - await initSnapStateManager.getJsxSupportRequirement(); + await initSnapStateManager.requireMetaMaskUpgrade(); if (requireMMUpgrade) { return { content: updateRequiredMetaMaskComponent(), diff --git a/packages/starknet-snap/src/state/init-snap-state-manager.ts b/packages/starknet-snap/src/state/init-snap-state-manager.ts index d60aaf16..4fa7be09 100644 --- a/packages/starknet-snap/src/state/init-snap-state-manager.ts +++ b/packages/starknet-snap/src/state/init-snap-state-manager.ts @@ -35,16 +35,28 @@ export class InitSnapStateManager extends StateManager { } } - // Retrieves the current `requireMMUpgrade` flag value - async getJsxSupportRequirement(): Promise { - const state = await this.get(); + async requireMetaMaskUpgrade(): Promise { + let state = await this.get(); + const maxRetries = 10; // Define a maximum number of retries to avoid infinite loop + let retries = 0; + const retryInterval = 200; // Set an interval in milliseconds between retries + + while (state.requireMMUpgrade === undefined && retries < maxRetries) { + // The state can be undefined only if onInstall or onUpdate hook did not complete. + // Waiting for the state to become defined, as there is no other way to proceed. + await new Promise(resolve => setTimeout(resolve, retryInterval)); // Wait before retrying + state = await this.get(); + retries++; + } + if (state.requireMMUpgrade === undefined) { - throw new StateManagerError( - 'MetaMask compatibility state not initialized', - ); + throw new StateManagerError( + 'MetaMask compatibility state not initialized', + ); } + return state.requireMMUpgrade; - } +} /** * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. @@ -68,6 +80,7 @@ export class InitSnapStateManager extends StateManager { content: component, }, }); + await this.setJsxSupport(true); } catch { await this.setJsxSupport(false); await snap.request({ From 949f22f25762867d8e13cc32fd488aed50c079dd Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Thu, 14 Nov 2024 16:01:16 +0100 Subject: [PATCH 09/17] chore: lint --- packages/starknet-snap/src/index.tsx | 3 +-- .../src/state/init-snap-state-manager.ts | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index 315b4dba..e0ce2cbc 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -324,8 +324,7 @@ export const onUpdate: OnUpdateHandler = async () => { export const onHomePage: OnHomePageHandler = async () => { const initSnapStateManager = new InitSnapStateManager(); - const requireMMUpgrade = - await initSnapStateManager.requireMetaMaskUpgrade(); + const requireMMUpgrade = await initSnapStateManager.requireMetaMaskUpgrade(); if (requireMMUpgrade) { return { content: updateRequiredMetaMaskComponent(), diff --git a/packages/starknet-snap/src/state/init-snap-state-manager.ts b/packages/starknet-snap/src/state/init-snap-state-manager.ts index 4fa7be09..2bbe8436 100644 --- a/packages/starknet-snap/src/state/init-snap-state-manager.ts +++ b/packages/starknet-snap/src/state/init-snap-state-manager.ts @@ -42,21 +42,21 @@ export class InitSnapStateManager extends StateManager { const retryInterval = 200; // Set an interval in milliseconds between retries while (state.requireMMUpgrade === undefined && retries < maxRetries) { - // The state can be undefined only if onInstall or onUpdate hook did not complete. - // Waiting for the state to become defined, as there is no other way to proceed. - await new Promise(resolve => setTimeout(resolve, retryInterval)); // Wait before retrying - state = await this.get(); - retries++; + // The state can be undefined only if onInstall or onUpdate hook did not complete. + // Waiting for the state to become defined, as there is no other way to proceed. + await new Promise((resolve) => setTimeout(resolve, retryInterval)); // Wait before retrying + state = await this.get(); + retries += 1; } if (state.requireMMUpgrade === undefined) { - throw new StateManagerError( - 'MetaMask compatibility state not initialized', - ); + throw new StateManagerError( + 'MetaMask compatibility state not initialized', + ); } return state.requireMMUpgrade; -} + } /** * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. From f633cbb45aaa01da23d90abbaa5132055abac252 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 18 Nov 2024 00:30:22 +0100 Subject: [PATCH 10/17] fix: set jsx support to true before showing dialog --- packages/starknet-snap/src/state/init-snap-state-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/starknet-snap/src/state/init-snap-state-manager.ts b/packages/starknet-snap/src/state/init-snap-state-manager.ts index 2bbe8436..47c62826 100644 --- a/packages/starknet-snap/src/state/init-snap-state-manager.ts +++ b/packages/starknet-snap/src/state/init-snap-state-manager.ts @@ -72,6 +72,7 @@ export class InitSnapStateManager extends StateManager { async ensureJsxSupport(component: Component): Promise { await this.get(); try { + await this.setJsxSupport(true); // Try rendering the JSX component to test compatibility await snap.request({ method: 'snap_dialog', @@ -80,7 +81,6 @@ export class InitSnapStateManager extends StateManager { content: component, }, }); - await this.setJsxSupport(true); } catch { await this.setJsxSupport(false); await snap.request({ From 3dc8123a28915594396d95e2438520da13b9fbed Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Mon, 18 Nov 2024 10:00:29 +0100 Subject: [PATCH 11/17] chore: fix comment --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 2088be4a..e2e186c1 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "@commitlint/config-conventional": "^17.0.3", "@lavamoat/allow-scripts": "^3.0.0", "@lavamoat/preinstall-always-fail": "^2.0.0", - "@types/react": "18.0.15", "husky": "^8.0.0" }, "packageManager": "yarn@3.2.1", From 097514bb466b38670f3e2db9ba7985b10ef9675d Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 19 Nov 2024 08:24:16 +0100 Subject: [PATCH 12/17] fix: moved ping pong --- packages/starknet-snap/src/index.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index e0ce2cbc..cff51491 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -90,14 +90,14 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { const requestParams = request?.params as unknown as ApiRequestParams; logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); - - if (request.method === 'ping') { - logger.log('pong'); - return 'pong'; - } - try { const initSnapStateManager = new InitSnapStateManager(); + + if (request.method === 'ping') { + logger.log('pong'); + return 'pong'; + } + const requireMMUpgrade = await initSnapStateManager.requireMetaMaskUpgrade(); if (requireMMUpgrade) { From d8b06e07e6de3473ff78b793fe12eb630f85a89f Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 19 Nov 2024 08:24:55 +0100 Subject: [PATCH 13/17] chore: lint --- yarn.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5aee76b0..31e64ef0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2230,14 +2230,11 @@ __metadata: "@metamask/snaps-cli": ^6.2.1 "@metamask/snaps-jest": ^8.2.0 "@metamask/snaps-sdk": ^6.1.1 - "@metamask/snaps-sdk": ^6.1.1 "@metamask/utils": ^9.1.0 "@types/chai": ^4.3.1 "@types/chai-as-promised": ^7.1.5 "@types/react": 18.2.4 "@types/react-dom": 18.2.4 - "@types/react": 18.2.4 - "@types/react-dom": 18.2.4 "@types/sinon": ^10.0.11 "@types/sinon-chai": ^3.2.8 "@typescript-eslint/eslint-plugin": ^5.42.1 @@ -25239,7 +25236,6 @@ __metadata: "@commitlint/config-conventional": ^17.0.3 "@lavamoat/allow-scripts": ^3.0.0 "@lavamoat/preinstall-always-fail": ^2.0.0 - "@types/react": 18.0.15 husky: ^8.0.0 languageName: unknown linkType: soft From 47c81057ec3c8809b00d4725f7bdaae3e9e90978 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 19 Nov 2024 20:18:33 +0100 Subject: [PATCH 14/17] chore: rollback state --- packages/starknet-snap/src/index.test.tsx | 15 ------- packages/starknet-snap/src/index.tsx | 39 +++++++++---------- packages/starknet-snap/src/utils/snap-ui.ts | 43 ++++++++++++++++++++- packages/starknet-snap/src/utils/snap.ts | 15 +------ 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index 9192b8ca..c408725e 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -130,19 +130,4 @@ describe('onHomePage', () => { }, }); }); - it('renders updateRequiredMetaMaskComponent when jsx support is lacking', async () => { - const requireMetaMaskUpgradeSpy = jest.spyOn( - InitSnapStateManager.prototype, - 'requireMetaMaskUpgrade', - ); - requireMetaMaskUpgradeSpy.mockResolvedValue(true); - const updateRequiredMetaMaskComponentSpy = - updateRequiredMetaMaskComponent as jest.Mock; - const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); - - await onHomePage(); - - expect(executeSpy).toHaveBeenCalledTimes(0); - expect(updateRequiredMetaMaskComponentSpy).toHaveBeenCalledTimes(1); - }); }); diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index cff51491..26596246 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -53,17 +53,19 @@ import { } from './rpcs'; import { sendTransaction } from './sendTransaction'; import { signDeployAccountTransaction } from './signDeployAccountTransaction'; -import { InitSnapStateManager } from './state/init-snap-state-manager'; import type { ApiParams, ApiParamsWithKeyDeriver, ApiRequestParams, } from './types/snapApi'; +import type { SnapState } from './types/snapState'; import { upgradeAccContract } from './upgradeAccContract'; import { + ensureJsxSupport, getDappUrl, + getStateData, isSnapRpcError, - updateRequiredMetaMaskComponent, + setStateData, } from './utils'; import { CAIRO_VERSION_LEGACY, @@ -90,22 +92,26 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { const requestParams = request?.params as unknown as ApiRequestParams; logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); - try { - const initSnapStateManager = new InitSnapStateManager(); + try { if (request.method === 'ping') { logger.log('pong'); return 'pong'; } - const requireMMUpgrade = - await initSnapStateManager.requireMetaMaskUpgrade(); - if (requireMMUpgrade) { - throw new Error('MetaMask upgrade required for compatibility'); + // TODO: this will causing racing condition, need to be fixed + let state: SnapState = await getStateData(); + if (!state) { + state = { + accContracts: [], + erc20Tokens: [], + networks: [], + transactions: [], + }; + // initialize state if empty and set default data + await setStateData(state); } - const state = await initSnapStateManager.get(); - // TODO: this can be remove, after state manager is implemented const saveMutex = acquireLock(); @@ -297,8 +303,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { }; export const onInstall: OnInstallHandler = async () => { - const initSnapStateManager = new InitSnapStateManager(); - await initSnapStateManager.ensureJsxSupport( + await ensureJsxSupport( Your MetaMask wallet is now compatible with Starknet! @@ -310,8 +315,7 @@ export const onInstall: OnInstallHandler = async () => { }; export const onUpdate: OnUpdateHandler = async () => { - const initSnapStateManager = new InitSnapStateManager(); - await initSnapStateManager.ensureJsxSupport( + await ensureJsxSupport( Your Starknet Snap is now up-to-date ! @@ -323,12 +327,5 @@ export const onUpdate: OnUpdateHandler = async () => { }; export const onHomePage: OnHomePageHandler = async () => { - const initSnapStateManager = new InitSnapStateManager(); - const requireMMUpgrade = await initSnapStateManager.requireMetaMaskUpgrade(); - if (requireMMUpgrade) { - return { - content: updateRequiredMetaMaskComponent(), - }; - } return await homePageController.execute(); }; diff --git a/packages/starknet-snap/src/utils/snap-ui.ts b/packages/starknet-snap/src/utils/snap-ui.ts index 0aaafacb..eb655ac0 100644 --- a/packages/starknet-snap/src/utils/snap-ui.ts +++ b/packages/starknet-snap/src/utils/snap-ui.ts @@ -1,9 +1,50 @@ -import { divider, heading, row, text } from '@metamask/snaps-sdk'; +import type { Component } from '@metamask/snaps-sdk'; +import { divider, heading, panel, row, text } from '@metamask/snaps-sdk'; import { getExplorerUrl } from './explorer'; import { toJson } from './serializer'; import { shortenAddress } from './string'; +export const updateRequiredMetaMaskComponent = () => { + return panel([ + text( + 'You need to update your MetaMask to latest version to use this snap.', + ), + ]); +}; + +/** + * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. + * If MetaMask does not support JSX, an alert message is shown prompting the user to update MetaMask. + * + * @param component - The JSX component to display in the snap dialog. + * + * The function performs the following steps: + * 1. Tries to render the provided component using a `snap_dialog` method. + * 2. On success, it updates the `requireMMUpgrade` flag in the snap's state to `false`, indicating that JSX is supported. + * 3. If an error occurs (likely due to outdated MetaMask), it displays an alert dialog prompting the user to update MetaMask. + */ +export const ensureJsxSupport = async (component: Component): Promise => { + try { + // Try rendering the JSX component to test compatibility + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: component, + }, + }); + } catch { + await snap.request({ + method: 'snap_dialog', + params: { + type: 'alert', + content: updateRequiredMetaMaskComponent(), + }, + }); + } +}; + /** * Build a row component. * diff --git a/packages/starknet-snap/src/utils/snap.ts b/packages/starknet-snap/src/utils/snap.ts index 807817de..49856f57 100644 --- a/packages/starknet-snap/src/utils/snap.ts +++ b/packages/starknet-snap/src/utils/snap.ts @@ -1,12 +1,7 @@ import type { BIP44AddressKeyDeriver } from '@metamask/key-tree'; import { getBIP44AddressKeyDeriver } from '@metamask/key-tree'; import type { Component, DialogResult, Json } from '@metamask/snaps-sdk'; -import { - DialogType, - panel, - text, - type SnapsProvider, -} from '@metamask/snaps-sdk'; +import { DialogType, panel, type SnapsProvider } from '@metamask/snaps-sdk'; declare const snap: SnapsProvider; @@ -98,11 +93,3 @@ export async function setStateData(data: State) { }, }); } - -export const updateRequiredMetaMaskComponent = () => { - return panel([ - text( - 'You need to update your MetaMask to latest version to use this snap.', - ), - ]); -}; From d3c13f2feedd4695c7da7d4120cedfb0eed25918 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Tue, 19 Nov 2024 20:25:52 +0100 Subject: [PATCH 15/17] chore: lint --- packages/starknet-snap/src/index.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index c408725e..e905ac12 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -4,7 +4,6 @@ import { onHomePage, onRpcRequest } from '.'; import * as createAccountApi from './createAccount'; import { HomePageController } from './on-home-page'; import { InitSnapStateManager } from './state/init-snap-state-manager'; -import { updateRequiredMetaMaskComponent } from './utils'; import * as keyPairUtils from './utils/keyPair'; jest.mock('./utils/logger'); From b189a5403bb91d9416f3fcfe79a9ad5876d60c28 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 20 Nov 2024 08:47:05 +0100 Subject: [PATCH 16/17] chore: fix comments --- .../src/state/init-snap-state-manager.ts | 95 ------------------- .../MinVersionModal/MinVersionModal.view.tsx | 34 ++----- 2 files changed, 9 insertions(+), 120 deletions(-) delete mode 100644 packages/starknet-snap/src/state/init-snap-state-manager.ts diff --git a/packages/starknet-snap/src/state/init-snap-state-manager.ts b/packages/starknet-snap/src/state/init-snap-state-manager.ts deleted file mode 100644 index 47c62826..00000000 --- a/packages/starknet-snap/src/state/init-snap-state-manager.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Component } from '@metamask/snaps-sdk'; - -import type { SnapState } from '../types/snapState'; -import { updateRequiredMetaMaskComponent } from '../utils'; -import { StateManager, StateManagerError } from './state-manager'; - -export class InitSnapStateManager extends StateManager { - protected getCollection(_state: SnapState) { - return undefined; - } - - protected updateEntity(dataInState: SnapState, data: SnapState): void { - dataInState.requireMMUpgrade = data.requireMMUpgrade; - } - - /** - * Ensures that the SnapState has all required default values. - * If any property is missing, it initializes it with a default value. - * @returns The fully initialized SnapState. - */ - public override async get(): Promise { - return await super.get(); - } - - // Updates the `requireMMUpgrade` flag based on JSX support - async setJsxSupport(isSupported: boolean): Promise { - try { - await this.update(async (state: SnapState) => { - this.updateEntity(state, { ...state, requireMMUpgrade: !isSupported }); - }); - } catch (error) { - throw new StateManagerError( - 'Error updating MetaMask compatibility status', - ); - } - } - - async requireMetaMaskUpgrade(): Promise { - let state = await this.get(); - const maxRetries = 10; // Define a maximum number of retries to avoid infinite loop - let retries = 0; - const retryInterval = 200; // Set an interval in milliseconds between retries - - while (state.requireMMUpgrade === undefined && retries < maxRetries) { - // The state can be undefined only if onInstall or onUpdate hook did not complete. - // Waiting for the state to become defined, as there is no other way to proceed. - await new Promise((resolve) => setTimeout(resolve, retryInterval)); // Wait before retrying - state = await this.get(); - retries += 1; - } - - if (state.requireMMUpgrade === undefined) { - throw new StateManagerError( - 'MetaMask compatibility state not initialized', - ); - } - - return state.requireMMUpgrade; - } - - /** - * Ensures that JSX support is available in the MetaMask environment by attempting to render a component within a snap dialog. - * If MetaMask does not support JSX, an alert message is shown prompting the user to update MetaMask. - * - * @param component - The JSX component to display in the snap dialog. - * - * The function performs the following steps: - * 1. Tries to render the provided component using a `snap_dialog` method. - * 2. On success, it updates the `requireMMUpgrade` flag in the snap's state to `false`, indicating that JSX is supported. - * 3. If an error occurs (likely due to outdated MetaMask), it displays an alert dialog prompting the user to update MetaMask. - */ - async ensureJsxSupport(component: Component): Promise { - await this.get(); - try { - await this.setJsxSupport(true); - // Try rendering the JSX component to test compatibility - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: component, - }, - }); - } catch { - await this.setJsxSupport(false); - await snap.request({ - method: 'snap_dialog', - params: { - type: 'alert', - content: updateRequiredMetaMaskComponent(), - }, - }); - } - } -} diff --git a/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx b/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx index 264b1a54..6ed188a6 100644 --- a/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx +++ b/packages/wallet-ui/src/components/ui/organism/MinVersionModal/MinVersionModal.view.tsx @@ -11,37 +11,21 @@ export const MinVersionModalView = () => { A new version of the Starknet Snap is available - To use this dApp, please: + To use this dapp, please install the latest version by following those + steps:
  • - Ensure you have the latest version of{' '} - MetaMask installed (v12.5 or - higher is required). + Delete the current version in MetaMask by going in Settings {'>'}{' '} + Snaps {'>'} @consensys/starknet-snap {'>'} See details {'>'} Remove + Snap
  • +
  • Refresh the page
  • - Install the latest version of the Starknet Snap by following these - steps: -
      -
    • - Remove the current version in MetaMask by navigating to{' '} - - Settings {'>'} Snaps {'>'} @consensys/starknet-snap {'>'} See - Details {'>'} Remove Snap - - . -
    • -
    • Refresh the page.
    • -
    • - Click Connect, the new version will be proposed - for installation. -
    • -
    + Click on connect, the new version will be proposed for installation.
-

- Note: Your account will be automatically recovered, - and future upgrades will be managed automatically. -

+ Note: Your account will be automatically recovered. Future upgrades will + be managed automatically
); From c66d1aceeecb5b19b2ee5b72ab28bfc8bca52821 Mon Sep 17 00:00:00 2001 From: Florin Dzeladini Date: Wed, 20 Nov 2024 09:28:03 +0100 Subject: [PATCH 17/17] fix: test suits --- packages/starknet-snap/src/index.test.tsx | 26 ----------------------- 1 file changed, 26 deletions(-) diff --git a/packages/starknet-snap/src/index.test.tsx b/packages/starknet-snap/src/index.test.tsx index e905ac12..236eab98 100644 --- a/packages/starknet-snap/src/index.test.tsx +++ b/packages/starknet-snap/src/index.test.tsx @@ -3,7 +3,6 @@ import { text, MethodNotFoundError, SnapError } from '@metamask/snaps-sdk'; import { onHomePage, onRpcRequest } from '.'; import * as createAccountApi from './createAccount'; import { HomePageController } from './on-home-page'; -import { InitSnapStateManager } from './state/init-snap-state-manager'; import * as keyPairUtils from './utils/keyPair'; jest.mock('./utils/logger'); @@ -16,15 +15,9 @@ jest.mock('./utils', () => ({ describe('onRpcRequest', () => { const createMockSpy = () => { - const requireMetaMaskUpgradeSpy = jest.spyOn( - InitSnapStateManager.prototype, - 'requireMetaMaskUpgrade', - ); - requireMetaMaskUpgradeSpy.mockResolvedValue(false); const createAccountSpy = jest.spyOn(createAccountApi, 'createAccount'); const keyPairSpy = jest.spyOn(keyPairUtils, 'getAddressKeyDeriver'); return { - requireMetaMaskUpgradeSpy, createAccountSpy, keyPairSpy, }; @@ -67,20 +60,6 @@ describe('onRpcRequest', () => { ).rejects.toThrow(MethodNotFoundError); }); - it('throws `SnapError` on request if MetaMask needs update', async () => { - const { requireMetaMaskUpgradeSpy } = createMockSpy(); - requireMetaMaskUpgradeSpy.mockResolvedValue(true); - await expect( - onRpcRequest({ - ...createMockRequest(), - request: { - ...createMockRequest().request, - method: 'executeTxn', - }, - }), - ).rejects.toThrow(SnapError); - }); - it('requests gets executed if MetaMask does not needs update', async () => { createMockSpy(); expect( @@ -111,11 +90,6 @@ describe('onRpcRequest', () => { describe('onHomePage', () => { it('executes homePageController normally if jsxSupport is not required', async () => { - const requireMetaMaskUpgradeSpy = jest.spyOn( - InitSnapStateManager.prototype, - 'requireMetaMaskUpgrade', - ); - requireMetaMaskUpgradeSpy.mockResolvedValue(false); const executeSpy = jest.spyOn(HomePageController.prototype, 'execute'); executeSpy.mockResolvedValue({ content: text('test') });