Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jsx support detection #416

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion packages/starknet-snap/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ 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'],
},
],
},
},
{
files: ['*.test.ts'],
files: ['*.test.ts', '*.test.tsx'],
extends: ['@metamask/eslint-config-jest'],
rules: {
'@typescript-eslint/no-shadow': [
Expand Down
8 changes: 5 additions & 3 deletions packages/starknet-snap/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/starknet-snap/snap.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -42,6 +47,7 @@ describe('onRpcRequest', () => {
});

it('throws `MethodNotFoundError` if the request method not found', async () => {
createMockSpy();
await expect(
onRpcRequest({
...createMockRequest(),
Expand All @@ -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'));
Expand All @@ -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') });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -87,18 +95,8 @@ 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 snap.request({
method: 'snap_manageState',
params: {
operation: 'get',
},
});
let state: SnapState = await getStateData<SnapState>();
if (!state) {
state = {
accContracts: [],
Expand All @@ -107,13 +105,17 @@ 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) {
console.log('hello');
throw SnapError;
}

if (request.method === 'ping') {
logger.log('pong');
return 'pong';
}

// TODO: this can be remove, after state manager is implemented
Expand Down Expand Up @@ -307,12 +309,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(
<Box>
<Text>Your MetaMask wallet is now compatible with Starknet!</Text>
<Text>
To manage your Starknet account and send and receive funds, visit the{' '}
<Link href={getDappUrl()}>companion dapp for Starknet</Link>.
</Text>
</Box>,
);

await snap.request({
method: 'snap_dialog',
Expand All @@ -324,14 +329,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(
<Box>
<Text>Your Starknet Snap is now up-to-date !</Text>
<Text>
As usual, to manage your Starknet account and send and receive funds,
visit the <Link href={getDappUrl()}>companion dapp for Starknet</Link>.
</Text>
</Box>,
);

await snap.request({
method: 'snap_dialog',
Expand All @@ -343,5 +349,13 @@ export const onUpdate: OnUpdateHandler = async () => {
};

export const onHomePage: OnHomePageHandler = async () => {
return await homePageController.execute();
const state = await getStateData<SnapState>();
console.log(state);
if (state.requireMMUpgrade !== undefined) {
return await homePageController.execute();
}

return {
content: updateRequiredMetaMaskComponent(),
};
};
4 changes: 2 additions & 2 deletions packages/starknet-snap/src/on-home-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -97,7 +97,7 @@ export class HomePageController {
);
}

protected buildComponenets(
protected buildComponents(
address: string,
network: Network,
balance: string,
Expand Down
1 change: 1 addition & 0 deletions packages/starknet-snap/src/types/snapState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type SnapState = {
networks: Network[];
transactions: Transaction[];
currentNetwork?: Network;
requireMMUpgrade?: boolean;
};

export type AccContract = {
Expand Down
51 changes: 50 additions & 1 deletion packages/starknet-snap/src/utils/snap.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -93,3 +100,45 @@ export async function setStateData<State>(data: State) {
},
});
}

export const updateRequiredMetaMaskComponent = () => {
khanti42 marked this conversation as resolved.
Show resolved Hide resolved
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<SnapState>();
khanti42 marked this conversation as resolved.
Show resolved Hide resolved
state.requireMMUpgrade = false;
await setStateData(state);
} catch {
await snap.request({
method: 'snap_dialog',
params: {
type: 'alert',
content: updateRequiredMetaMaskComponent(),
},
});
}
};
4 changes: 3 additions & 1 deletion packages/starknet-snap/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
Loading
Loading