From de42c084e6e391cfe6a961e364a1e781fbf17786 Mon Sep 17 00:00:00 2001 From: khanti42 Date: Thu, 16 Jan 2025 14:19:01 +0100 Subject: [PATCH] feat: localization (#471) * feat: added locale utils * feat: localisation for jsx components * chore: lint * chore: tests fix for localisation and lint * feat: added french translation * chore: lint + prettier * chore: handle comments * chore: lint + prettier * chore: fix tests * chore: lint + prettier * chore: fix comment and added RULES.md * chore: fix comments --- packages/starknet-snap/locales/RULES.md | 24 ++ packages/starknet-snap/locales/en.json | 155 +++++++++ packages/starknet-snap/locales/fr.json | 155 +++++++++ packages/starknet-snap/snap.manifest.json | 6 +- packages/starknet-snap/src/index.tsx | 19 +- .../starknet-snap/src/on-home-page.test.ts | 2 + packages/starknet-snap/src/on-home-page.ts | 12 +- .../src/rpcs/declare-contract.test.ts | 2 + .../src/rpcs/declare-contract.ts | 10 +- .../src/rpcs/display-private-key.test.ts | 3 + .../src/signDeployAccountTransaction.ts | 4 +- .../src/ui/components/DisplayPrivateKeyUI.tsx | 15 +- .../src/ui/components/ExecuteTxnUI.tsx | 16 +- .../components/SignDeclareTransactionUI.tsx | 12 +- .../src/ui/components/SignMessageUI.tsx | 6 +- .../src/ui/components/SignTransactionUI.tsx | 12 +- .../src/ui/components/SwitchNetworkUI.tsx | 6 +- .../src/ui/components/WatchAssetUI.tsx | 16 +- .../starknet-snap/src/upgradeAccContract.ts | 5 +- .../starknet-snap/src/utils/locale.test.ts | 63 ++++ packages/starknet-snap/src/utils/locale.ts | 77 +++++ .../starknet-snap/src/utils/snapUtils.test.ts | 2 + packages/starknet-snap/src/utils/snapUtils.ts | 200 +++-------- .../starknet-snap/test/src/addNetwork.test.ts | 324 ------------------ .../test/src/createAccount.test.ts | 279 --------------- .../src/signDeployAccountTransaction.test.ts | 11 +- .../test/src/upgradeAccContract.test.ts | 49 +-- 27 files changed, 624 insertions(+), 861 deletions(-) create mode 100644 packages/starknet-snap/locales/RULES.md create mode 100644 packages/starknet-snap/locales/en.json create mode 100644 packages/starknet-snap/locales/fr.json create mode 100644 packages/starknet-snap/src/utils/locale.test.ts create mode 100644 packages/starknet-snap/src/utils/locale.ts delete mode 100644 packages/starknet-snap/test/src/addNetwork.test.ts delete mode 100644 packages/starknet-snap/test/src/createAccount.test.ts diff --git a/packages/starknet-snap/locales/RULES.md b/packages/starknet-snap/locales/RULES.md new file mode 100644 index 00000000..8ee13907 --- /dev/null +++ b/packages/starknet-snap/locales/RULES.md @@ -0,0 +1,24 @@ +### Rules for Adding New Fields to the Language File + +1. **Key Naming Convention:** + + - Use **English text** to derive the key. + - The key should be **camelized** (first word lowercase, subsequent words capitalized). + - Keep the key to **no more than 5 words**. + - If the text in English is **shorter than 5 words**, camelize the entire phrase. + - For **longer text**, use a **concise and meaningful key** (this is a soft constraint). + +2. **Dynamic Text Placeholders:** + + - Use `{1}`, `{2}`, etc., for dynamic placeholders in messages. + - Example: + - `"visitCompanionDappAndUpgrade": { "message": "Visit the [companion dapp for Starknet]({1}) and click “Upgrade”.\nThank you!" }` + - Usage: `translate("visitCompanionDappAndUpgrade", "https://website.com")` + +3. **Consistency:** + - Ensure key names are meaningful and reflect the message context. + - Avoid over-complicating keys; aim for clarity and simplicity. + +--- + +These guidelines will ensure consistency and clarity when adding new fields to the language file. diff --git a/packages/starknet-snap/locales/en.json b/packages/starknet-snap/locales/en.json new file mode 100644 index 00000000..f349154e --- /dev/null +++ b/packages/starknet-snap/locales/en.json @@ -0,0 +1,155 @@ +{ + "locale": "en", + "messages": { + "accountDeploymentMandatory": { + "message": "Account Deployment Mandatory!" + }, + "accountDeploymentNotice": { + "message": "The account will be deployed with this transaction." + }, + "accountManagementIntro": { + "message": "To manage your Starknet account and send and receive funds, visit the" + }, + "accountManagementReminder": { + "message": "As usual, to manage your Starknet account and send and receive funds, visit the" + }, + "accountUpgradeMandatory": { + "message": "Account Upgrade Mandatory!" + }, + "addTokenPrompt": { + "message": "Do you want to add this token?" + }, + "address": { + "message": "Address" + }, + "amount": { + "message": "Amount" + }, + "amountWithSymbol": { + "message": "Amount ({1})" + }, + "balance": { + "message": "Balance" + }, + "baseUrl": { + "message": "Base URL" + }, + "callData": { + "message": "Call Data" + }, + "call": { + "message": "Call" + }, + "chainId": { + "message": "Chain ID" + }, + "chainName": { + "message": "Chain Name" + }, + "classHash": { + "message": "Class Hash" + }, + "companionDapp": { + "message": "companion dapp for Starknet" + }, + "compiledClassHash": { + "message": "Compiled Class Hash" + }, + "confirmPrivateKeyToDisplay": { + "message": "Confirming this action will display your private key. Ensure you are in a secure environment." + }, + "contract": { + "message": "Contract" + }, + "decimals": { + "message": "Decimals" + }, + "declareTransactionDetails": { + "message": "Declare Transaction Details" + }, + "estimatedGasFee": { + "message": "Estimated Gas Fee" + }, + "explorerUrl": { + "message": "Explorer URL" + }, + "message": { + "message": "Message" + }, + "maxFeeETH": { + "message": "Max Fee (ETH)" + }, + "name": { + "message": "Name" + }, + "network": { + "message": "Network" + }, + "networkFee": { + "message": "network fee" + }, + "recipientAddress": { + "message": "Recipient Address" + }, + "recipient": { + "message": "Recipient" + }, + "rpcUrl": { + "message": "RPC URL" + }, + "senderAddress": { + "message": "Sender Address" + }, + "signMessagePrompt": { + "message": "Do you want to sign this message?" + }, + "signTransactionPrompt": { + "message": "Do you want to sign this transaction?" + }, + "signerAddress": { + "message": "Signer Address" + }, + "signer": { + "message": "Signer" + }, + "snapIsUpToDate": { + "message": "Your Starknet Snap is now up-to-date!" + }, + "starknetPrivateKeyConfidential": { + "message": "Below is your Starknet Account private key. Keep it confidential." + }, + "starknetPrivateKey": { + "message": "Starknet Account Private Key" + }, + "switchNetworkPrompt": { + "message": "Do you want to switch to this network?" + }, + "symbol": { + "message": "Symbol" + }, + "token": { + "message": "Token" + }, + "totalFor": { + "message": "Total for" + }, + "transaction": { + "message": "Transaction" + }, + "transactions": { + "message": "Transactions" + }, + "visitCompanionDappHomePage": { + "message": "Visit the [companion dapp for Starknet]({1}) to manage your account." + }, + "visitCompanionDappAndDeploy": { + "message": "Visit the [companion dapp for Starknet]({1}) to deploy your account.\nThank you!" + }, + "visitCompanionDappAndUpgrade": { + "message": "Visit the [companion dapp for Starknet]({1}) and click “Upgrade”.\nThank you!" + }, + "walletIsCompatible": { + "message": "Your MetaMask wallet is now compatible with Starknet!" + } + } +} diff --git a/packages/starknet-snap/locales/fr.json b/packages/starknet-snap/locales/fr.json new file mode 100644 index 00000000..d4b73e34 --- /dev/null +++ b/packages/starknet-snap/locales/fr.json @@ -0,0 +1,155 @@ +{ + "locale": "fr", + "messages": { + "accountDeploymentMandatory": { + "message": "Déploiement du compte obligatoire !" + }, + "accountDeploymentNotice": { + "message": "Le compte sera déployé avec cette transaction." + }, + "accountManagementIntro": { + "message": "Pour gérer votre compte Starknet et envoyer ou recevoir des fonds, accédez à cette page" + }, + "accountManagementReminder": { + "message": "Comme d'habitude, pour gérer votre compte Starknet et envoyer ou recevoir des fonds, accédez à cette page" + }, + "accountUpgradeMandatory": { + "message": "Mise à niveau du compte obligatoire !" + }, + "addTokenPrompt": { + "message": "Voulez-vous ajouter ce token ?" + }, + "address": { + "message": "Adresse" + }, + "amount": { + "message": "Montant" + }, + "amountWithSymbol": { + "message": "Montant ({1})" + }, + "balance": { + "message": "Solde" + }, + "baseUrl": { + "message": "URL de base" + }, + "callData": { + "message": "Données d'appel" + }, + "call": { + "message": "Appel" + }, + "chainId": { + "message": "ID de la chaîne" + }, + "chainName": { + "message": "Nom de la chaîne" + }, + "classHash": { + "message": "Hash de la classe" + }, + "companionDapp": { + "message": "dapp compagnon pour Starknet" + }, + "compiledClassHash": { + "message": "Hash de la classe compilée" + }, + "confirmPrivateKeyToDisplay": { + "message": "Confirmer cette action affichera votre clé privée. Assurez-vous d'être dans un environnement sécurisé." + }, + "contract": { + "message": "Contrat" + }, + "decimals": { + "message": "Décimales" + }, + "declareTransactionDetails": { + "message": "Détails de la transaction à déclarer" + }, + "estimatedGasFee": { + "message": "Frais de gaz estimés" + }, + "explorerUrl": { + "message": "URL de l'explorateur" + }, + "message": { + "message": "Message" + }, + "maxFeeETH": { + "message": "Frais max (ETH)" + }, + "name": { + "message": "Nom" + }, + "network": { + "message": "Réseau" + }, + "networkFee": { + "message": "frais de réseau" + }, + "recipientAddress": { + "message": "Adresse du destinataire" + }, + "recipient": { + "message": "Destinataire" + }, + "rpcUrl": { + "message": "URL RPC" + }, + "senderAddress": { + "message": "Adresse de l'expéditeur" + }, + "signMessagePrompt": { + "message": "Voulez-vous signer ce message ?" + }, + "signTransactionPrompt": { + "message": "Voulez-vous signer cette transaction ?" + }, + "signerAddress": { + "message": "Adresse du signataire" + }, + "signer": { + "message": "Signataire" + }, + "snapIsUpToDate": { + "message": "Votre Starknet Snap est maintenant à jour !" + }, + "starknetPrivateKeyConfidential": { + "message": "Voici la clé privée de votre compte Starknet. Gardez-la confidentielle." + }, + "starknetPrivateKey": { + "message": "Clé privée du compte Starknet" + }, + "switchNetworkPrompt": { + "message": "Voulez-vous changer pour ce réseau ?" + }, + "symbol": { + "message": "Symbole" + }, + "token": { + "message": "Token" + }, + "totalFor": { + "message": "Total pour" + }, + "transaction": { + "message": "Transaction" + }, + "transactions": { + "message": "Transactions" + }, + "visitCompanionDappHomePage": { + "message": "Visitez la [dapp compagnon pour Starknet]({1}) pour gérer votre compte." + }, + "visitCompanionDappAndDeploy": { + "message": "Visitez la [dapp compagnon pour Starknet]({1}) pour déployer votre compte.\nMerci !" + }, + "visitCompanionDappAndUpgrade": { + "message": "Visitez la [dapp compagnon pour Starknet]({1}) et cliquez sur “Upgrade”.\nMerci !" + }, + "walletIsCompatible": { + "message": "Votre portefeuille MetaMask est désormais compatible avec Starknet !" + } + } +} diff --git a/packages/starknet-snap/snap.manifest.json b/packages/starknet-snap/snap.manifest.json index d2a86dee..78904082 100644 --- a/packages/starknet-snap/snap.manifest.json +++ b/packages/starknet-snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/ConsenSys/starknet-snap.git" }, "source": { - "shasum": "uG3NeaD7Ky06SZo9Hnx1KWn5EjavmUJC5VBSY/sqctM=", + "shasum": "ioF+6KhOlxdYsfladG3HE9FxV/qNm7oggLavdt1V1jg=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -15,9 +15,11 @@ "packageName": "@consensys/starknet-snap", "registry": "https://registry.npmjs.org" } - } + }, + "locales": ["locales/en.json", "locales/fr.json"] }, "initialPermissions": { + "snap_getPreferences": {}, "snap_dialog": {}, "endowment:network-access": {}, "snap_getBip44Entropy": [ diff --git a/packages/starknet-snap/src/index.tsx b/packages/starknet-snap/src/index.tsx index b799bcb5..f0d9b9ed 100644 --- a/packages/starknet-snap/src/index.tsx +++ b/packages/starknet-snap/src/index.tsx @@ -79,6 +79,7 @@ import { } from './utils/constants'; import { UnknownError } from './utils/exceptions'; import { getAddressKeyDeriver } from './utils/keyPair'; +import { getTranslator, loadLocale } from './utils/locale'; import { acquireLock } from './utils/lock'; import { logger } from './utils/logger'; import { RpcMethod, validateOrigin } from './utils/permission'; @@ -96,6 +97,7 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ origin, request, }) => { + await loadLocale(); const requestParams = request?.params as unknown as ApiRequestParams; logger.log(`${request.method}:\nrequestParams: ${toJson(requestParams)}`); @@ -297,30 +299,35 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ }; export const onInstall: OnInstallHandler = async () => { + await loadLocale(); + const translate = getTranslator(); await ensureJsxSupport( - Your MetaMask wallet is now compatible with Starknet! + {translate('walletIsCompatible')} - To manage your Starknet account and send and receive funds, visit the{' '} - companion dapp for Starknet. + {translate('accountManagementIntro')}{' '} + {translate('companionDapp')}. , ); }; export const onUpdate: OnUpdateHandler = async () => { + await loadLocale(); + const translate = getTranslator(); await ensureJsxSupport( - Your Starknet Snap is now up-to-date ! + {translate('snapIsUpToDate')} - As usual, to manage your Starknet account and send and receive funds, - visit the companion dapp for Starknet. + {translate('accountManagementReminder')}{' '} + {translate('companionDapp')}. , ); }; export const onHomePage: OnHomePageHandler = async () => { + await loadLocale(); return await homePageController.execute(); }; diff --git a/packages/starknet-snap/src/on-home-page.test.ts b/packages/starknet-snap/src/on-home-page.test.ts index dfbc9388..2bbc5fab 100644 --- a/packages/starknet-snap/src/on-home-page.test.ts +++ b/packages/starknet-snap/src/on-home-page.test.ts @@ -9,6 +9,7 @@ import { ETHER_MAINNET, STARKNET_SEPOLIA_TESTNET_NETWORK, } from './utils/constants'; +import { loadLocale } from './utils/locale'; import * as snapHelper from './utils/snap'; import * as starknetUtils from './utils/starknetUtils'; @@ -65,6 +66,7 @@ describe('homepageController', () => { }; it('returns the correct homepage response', async () => { + await loadLocale(); const { currentNetwork } = state; await mockState(); const account = await mockAccount( diff --git a/packages/starknet-snap/src/on-home-page.ts b/packages/starknet-snap/src/on-home-page.ts index 55c37c2f..f0e9bbf0 100644 --- a/packages/starknet-snap/src/on-home-page.ts +++ b/packages/starknet-snap/src/on-home-page.ts @@ -19,6 +19,7 @@ import { toJson, } from './utils'; import { BlockIdentifierEnum, ETHER_MAINNET } from './utils/constants'; +import { getTranslator } from './utils/locale'; import { getBalance, getCorrectContractAddress, @@ -102,16 +103,15 @@ export class HomePageController { network: Network, balance: string, ): OnHomePageResponse { + const translate = getTranslator(); const panelItems: Component[] = []; - panelItems.push(text('Address')); + panelItems.push(text(translate('address'))); panelItems.push(copyable(`${address}`)); - panelItems.push(row('Network', text(`${network.name}`))); - panelItems.push(row('Balance', text(`${balance} ETH`))); + panelItems.push(row(translate('network'), text(`${network.name}`))); + panelItems.push(row(translate('balance'), text(`${balance} ETH`))); panelItems.push(divider()); panelItems.push( - text( - `Visit the [companion dapp for Starknet](${getDappUrl()}) to manage your account.`, - ), + text(translate('visitCompanionDappHomePage', getDappUrl())), ); return { content: panel(panelItems), diff --git a/packages/starknet-snap/src/rpcs/declare-contract.test.ts b/packages/starknet-snap/src/rpcs/declare-contract.test.ts index d745397c..b2ae56a5 100644 --- a/packages/starknet-snap/src/rpcs/declare-contract.test.ts +++ b/packages/starknet-snap/src/rpcs/declare-contract.test.ts @@ -10,6 +10,7 @@ import { InvalidRequestParamsError, UnknownError, } from '../utils/exceptions'; +import { loadLocale } from '../utils/locale'; import * as starknetUtils from '../utils/starknetUtils'; import { buildDividerComponent, @@ -60,6 +61,7 @@ const prepareMockDeclareContract = async ( payload: DeclareContractPayload, details: UniversalDetails, ) => { + await loadLocale(); const state = { accContracts: [], erc20Tokens: [], diff --git a/packages/starknet-snap/src/rpcs/declare-contract.ts b/packages/starknet-snap/src/rpcs/declare-contract.ts index c6a7b48d..94749fdc 100644 --- a/packages/starknet-snap/src/rpcs/declare-contract.ts +++ b/packages/starknet-snap/src/rpcs/declare-contract.ts @@ -17,6 +17,7 @@ import { headerUI, } from '../utils'; import { UserRejectedOpError } from '../utils/exceptions'; +import { getTranslator } from '../utils/locale'; import { declareContract as declareContractUtil } from '../utils/starknetUtils'; import { AccountRpcController } from './abstract/account-rpc-controller'; @@ -104,9 +105,10 @@ export class DeclareContractRpc extends AccountRpcController< } protected async getDeclareContractConsensus(params: DeclareContractParams) { + const translate = getTranslator(); const { payload, details, address } = params; const components: Component[] = []; - components.push(headerUI('Do you want to sign this transaction?')); + components.push(headerUI(translate('signTransactionPrompt'))); components.push( signerUI({ @@ -128,7 +130,7 @@ export class DeclareContractRpc extends AccountRpcController< components.push(dividerUI()); components.push( rowUI({ - label: 'Compiled Class Hash', + label: translate('compiledClassHash'), value: compiledClassHash, }), ); @@ -138,7 +140,7 @@ export class DeclareContractRpc extends AccountRpcController< components.push(dividerUI()); components.push( rowUI({ - label: 'Class Hash', + label: translate('classHash'), value: classHash, }), ); @@ -149,7 +151,7 @@ export class DeclareContractRpc extends AccountRpcController< components.push(dividerUI()); components.push( rowUI({ - label: 'Max Fee (ETH)', + label: translate('maxFeeETH'), value: maxFeeInEth, }), ); diff --git a/packages/starknet-snap/src/rpcs/display-private-key.test.ts b/packages/starknet-snap/src/rpcs/display-private-key.test.ts index aa0aac00..651a207d 100644 --- a/packages/starknet-snap/src/rpcs/display-private-key.test.ts +++ b/packages/starknet-snap/src/rpcs/display-private-key.test.ts @@ -6,6 +6,7 @@ import { UserRejectedOpError, InvalidRequestParamsError, } from '../utils/exceptions'; +import { loadLocale } from '../utils/locale'; import { mockAccount, prepareMockAccount, @@ -37,6 +38,7 @@ describe('displayPrivateKey', () => { }; it('displays private key correctly', async () => { + await loadLocale(); const chainId = constants.StarknetChainId.SN_SEPOLIA; const account = await mockAccount(chainId); prepareMockAccount(account, state); @@ -51,6 +53,7 @@ describe('displayPrivateKey', () => { }); it('renders confirmation dialog', async () => { + await loadLocale(); const chainId = constants.StarknetChainId.SN_SEPOLIA; const account = await mockAccount(chainId); prepareMockAccount(account, state); diff --git a/packages/starknet-snap/src/signDeployAccountTransaction.ts b/packages/starknet-snap/src/signDeployAccountTransaction.ts index 222a323f..90661bb1 100644 --- a/packages/starknet-snap/src/signDeployAccountTransaction.ts +++ b/packages/starknet-snap/src/signDeployAccountTransaction.ts @@ -5,6 +5,7 @@ import type { ApiParamsWithKeyDeriver, SignDeployAccountTransactionRequestParams, } from './types/snapApi'; +import { getTranslator } from './utils/locale'; import { logger } from './utils/logger'; import { toJson } from './utils/serializer'; import { @@ -53,12 +54,13 @@ export async function signDeployAccountTransaction( ); if (requestParamsObj.enableAuthorize) { + const translate = getTranslator(); const response = await wallet.request({ method: 'snap_dialog', params: { type: DialogType.Confirmation, content: panel([ - heading('Do you want to sign this transaction?'), + heading(translate('signTransactionPrompt')), ...snapComponents, ]), }, diff --git a/packages/starknet-snap/src/ui/components/DisplayPrivateKeyUI.tsx b/packages/starknet-snap/src/ui/components/DisplayPrivateKeyUI.tsx index ca895f08..fada218a 100644 --- a/packages/starknet-snap/src/ui/components/DisplayPrivateKeyUI.tsx +++ b/packages/starknet-snap/src/ui/components/DisplayPrivateKeyUI.tsx @@ -1,21 +1,21 @@ import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; import { Box, Icon, Text, Heading, Copyable } from '@metamask/snaps-sdk/jsx'; +import { getTranslator } from '../../utils/locale'; + /** * Builds a UI component to confirm the action of revealing the private key. * * @returns A JSX component prompting the user to confirm revealing their private key. */ export const DisplayPrivateKeyDialogUI: SnapComponent = () => { + const translate = getTranslator(); return ( Are you sure you want to reveal your private key? - - Confirming this action will display your private key. Ensure you are - in a secure environment. - + {translate('confirmPrivateKeyToDisplay')} ); @@ -35,12 +35,11 @@ export type DisplayPrivateKeyAlertUIProps = { export const DisplayPrivateKeyAlertUI: SnapComponent< DisplayPrivateKeyAlertUIProps > = ({ privateKey }) => { + const translate = getTranslator(); return ( - Starknet Account Private Key - - Below is your Starknet Account private key. Keep it confidential. - + {translate('starknetPrivateKey')} + {translate('starknetPrivateKeyConfidential')} ); diff --git a/packages/starknet-snap/src/ui/components/ExecuteTxnUI.tsx b/packages/starknet-snap/src/ui/components/ExecuteTxnUI.tsx index bbcb6c8b..fba4c303 100644 --- a/packages/starknet-snap/src/ui/components/ExecuteTxnUI.tsx +++ b/packages/starknet-snap/src/ui/components/ExecuteTxnUI.tsx @@ -11,6 +11,7 @@ import { import type { FeeToken } from '../../types/snapApi'; import type { FormattedCallData } from '../../types/snapState'; import { DEFAULT_DECIMAL_PLACES } from '../../utils/constants'; +import { getTranslator } from '../../utils/locale'; import { AddressUI, JsonDataUI, NetworkUI, SignerUI } from '../fragments'; import { Amount } from '../fragments/Amount'; import { FeeTokenSelector } from '../fragments/FeeTokenSelector'; @@ -63,6 +64,7 @@ export const ExecuteTxnUI: SnapComponent = ({ errors, }) => { // Calculate the totals using the helper + const translate = getTranslator(); const tokenTotals = accumulateTotals(calls, maxFee, selectedFeeToken); return ( @@ -77,26 +79,26 @@ export const ExecuteTxnUI: SnapComponent = ({ {calls.map((call) => (
{call.tokenTransferData ? (
) : ( - + )}
))} @@ -107,7 +109,7 @@ export const ExecuteTxnUI: SnapComponent = ({
= ({ {Object.entries(tokenTotals).map( ([tokenSymbol, { amount, decimals }]) => ( = ({ {includeDeploy ? ( - The account will be deployed with this transaction + {translate('accountDeploymentNotice')} ) : null}
diff --git a/packages/starknet-snap/src/ui/components/SignDeclareTransactionUI.tsx b/packages/starknet-snap/src/ui/components/SignDeclareTransactionUI.tsx index 4765b16c..be4b0ec2 100644 --- a/packages/starknet-snap/src/ui/components/SignDeclareTransactionUI.tsx +++ b/packages/starknet-snap/src/ui/components/SignDeclareTransactionUI.tsx @@ -3,6 +3,7 @@ import { Box, Heading, Section } from '@metamask/snaps-sdk/jsx'; import type { Infer } from 'superstruct'; import type { DeclareSignDetailsStruct } from '../../utils'; +import { getTranslator } from '../../utils/locale'; import { AddressUI, JsonDataUI, NetworkUI } from '../fragments'; export type SignDeclareTransactionUIProps = { @@ -25,14 +26,19 @@ export type SignDeclareTransactionUIProps = { export const SignDeclareTransactionUI: SnapComponent< SignDeclareTransactionUIProps > = ({ senderAddress, networkName, chainId, declareTransactions }) => { + const translate = getTranslator(); return ( - Do you want to sign this transaction? + {translate('signTransactionPrompt')}
- +
diff --git a/packages/starknet-snap/src/ui/components/SignMessageUI.tsx b/packages/starknet-snap/src/ui/components/SignMessageUI.tsx index cbde85cc..a0d6c9eb 100644 --- a/packages/starknet-snap/src/ui/components/SignMessageUI.tsx +++ b/packages/starknet-snap/src/ui/components/SignMessageUI.tsx @@ -3,6 +3,7 @@ import { Box, Heading, Section } from '@metamask/snaps-sdk/jsx'; import type { Infer } from 'superstruct'; import type { TypeDataStruct } from '../../utils'; +import { getTranslator } from '../../utils/locale'; import { JsonDataUI, SignerUI } from '../fragments'; export type SignMessageUIProps = { @@ -25,12 +26,13 @@ export const SignMessageUI: SnapComponent = ({ chainId, typedDataMessage, }) => { + const translate = getTranslator(); return ( - Do you want to sign this message? + {translate('signeMessagePrompt')}
- +
); diff --git a/packages/starknet-snap/src/ui/components/SignTransactionUI.tsx b/packages/starknet-snap/src/ui/components/SignTransactionUI.tsx index a2a3f8cc..bf7d2bf9 100644 --- a/packages/starknet-snap/src/ui/components/SignTransactionUI.tsx +++ b/packages/starknet-snap/src/ui/components/SignTransactionUI.tsx @@ -3,6 +3,7 @@ import { Box, Heading, Section } from '@metamask/snaps-sdk/jsx'; import type { Infer } from 'superstruct'; import type { CallDataStruct } from '../../utils'; +import { getTranslator } from '../../utils/locale'; import { JsonDataUI, AddressUI, NetworkUI } from '../fragments'; export type SignTransactionUIProps = { @@ -28,13 +29,18 @@ export const SignTransactionUI: SnapComponent = ({ chainId, transactions, }) => { + const translate = getTranslator(); return ( - Do you want to sign this transaction? + {translate('signTransactionPrompt')}
- + - +
); diff --git a/packages/starknet-snap/src/ui/components/SwitchNetworkUI.tsx b/packages/starknet-snap/src/ui/components/SwitchNetworkUI.tsx index 82cf6e4c..45a82a21 100644 --- a/packages/starknet-snap/src/ui/components/SwitchNetworkUI.tsx +++ b/packages/starknet-snap/src/ui/components/SwitchNetworkUI.tsx @@ -1,6 +1,7 @@ import type { SnapComponent } from '@metamask/snaps-sdk/jsx'; import { Box, Heading, Copyable, Bold } from '@metamask/snaps-sdk/jsx'; +import { getTranslator } from '../../utils/locale'; import { NetworkUI } from '../fragments'; export type SwitchNetworkUIProps = { @@ -20,12 +21,13 @@ export const SwitchNetworkUI: SnapComponent = ({ name, chainId, }) => { + const translate = getTranslator(); return ( - Do you want to switch to this network? + {translate('switchNetworkPrompt')} - Chain ID + {translate('chainId')} diff --git a/packages/starknet-snap/src/ui/components/WatchAssetUI.tsx b/packages/starknet-snap/src/ui/components/WatchAssetUI.tsx index fad88f7e..b8b7ff6e 100644 --- a/packages/starknet-snap/src/ui/components/WatchAssetUI.tsx +++ b/packages/starknet-snap/src/ui/components/WatchAssetUI.tsx @@ -9,6 +9,7 @@ import { } from '@metamask/snaps-sdk/jsx'; import type { Erc20Token } from '../../types/snapState'; +import { getTranslator } from '../../utils/locale'; import { AddressUI, NetworkUI } from '../fragments'; export type WatchAssetUIProps = { @@ -31,27 +32,32 @@ export const WatchAssetUI: SnapComponent = ({ chainId, token, }) => { + const translate = getTranslator(); const { name, symbol, address, decimals } = token; return ( - Do you want to add this token? + {translate('signTransactionPrompt')}
- +
{name ? ( - + {name} ) : null} {symbol ? ( - + {symbol} ) : null} {decimals !== null && ( - + {decimals.toString()} )} diff --git a/packages/starknet-snap/src/upgradeAccContract.ts b/packages/starknet-snap/src/upgradeAccContract.ts index 708739ec..08ea3009 100644 --- a/packages/starknet-snap/src/upgradeAccContract.ts +++ b/packages/starknet-snap/src/upgradeAccContract.ts @@ -7,6 +7,7 @@ import type { } from './types/snapApi'; import { ContractFuncName } from './types/snapState'; import { ACCOUNT_CLASS_HASH, CAIRO_VERSION_LEGACY } from './utils/constants'; +import { getTranslator } from './utils/locale'; import { logger } from './utils/logger'; import { toJson } from './utils/serializer'; import { @@ -105,13 +106,13 @@ export async function upgradeAccContract(params: ApiParamsWithKeyDeriver) { maxFee, network, ); - + const translate = getTranslator(); const response = await wallet.request({ method: 'snap_dialog', params: { type: DialogType.Confirmation, content: panel([ - heading('Do you want to sign this transaction ?'), + heading(translate('signTransactionPrompt')), ...dialogComponents, ]), }, diff --git a/packages/starknet-snap/src/utils/locale.test.ts b/packages/starknet-snap/src/utils/locale.test.ts new file mode 100644 index 00000000..78d39ab6 --- /dev/null +++ b/packages/starknet-snap/src/utils/locale.test.ts @@ -0,0 +1,63 @@ +/* eslint-disable no-restricted-globals */ +import { + loadLocale, + getUserLocale, + getTranslator, + getUserLocalePreference, +} from './locale'; + +const mockSnapRequest = jest.fn(); + +jest.mock('../../locales/en.json', () => ({ + messages: { greeting: { message: 'Hello' } }, +})); + +(global as any).snap = { + request: mockSnapRequest.mockResolvedValue({ locale: 'en' }), +}; + +describe('locale utils', () => { + describe('getUserLocale', () => { + it("returns the locale messages for the user's preferred locale", async () => { + const locale = await getUserLocalePreference(); + expect(locale).toStrictEqual({ greeting: { message: 'Hello' } }); + }); + + it("returns the default locale messages if the user's preferred locale is not available", async () => { + mockSnapRequest.mockRejectedValue(new Error('Locale not found')); + + const locale = await getUserLocalePreference(); + expect(locale).toStrictEqual({ greeting: { message: 'Hello' } }); + }); + }); + + describe('loadLocale', () => { + it('loads and set the user locale', async () => { + await loadLocale(); + const locale = getUserLocale(); + expect(locale).toStrictEqual({ greeting: { message: 'Hello' } }); + }); + + it('loads and set the default locale if user locale is not available', async () => { + mockSnapRequest.mockRejectedValue(new Error('Locale not found')); + + await loadLocale(); + const locale = getUserLocale(); + expect(locale).toStrictEqual({ greeting: { message: 'Hello' } }); + }); + }); + + describe('getTranslator', () => { + it('translates keys to user locale messages', async () => { + await loadLocale(); + const translate = getTranslator(); + expect(translate('greeting')).toBe('Hello'); + }); + + it('returns the key wrapped in curly braces if translation is not found', async () => { + await loadLocale(); + const translate = getTranslator(); + expect(translate('nonexistent')).toBe('{nonexistent}'); + }); + }); +}); diff --git a/packages/starknet-snap/src/utils/locale.ts b/packages/starknet-snap/src/utils/locale.ts new file mode 100644 index 00000000..d183acdd --- /dev/null +++ b/packages/starknet-snap/src/utils/locale.ts @@ -0,0 +1,77 @@ +import { acquireLock } from './lock'; + +const localeSetterLock = acquireLock(true); +let locale: Locale; + +/** + * Loads the user's locale preferences and sets the locale variable. + */ +export async function loadLocale() { + await localeSetterLock.runExclusive(async () => { + locale = await getUserLocalePreference(); + }); +} + +/** + * Retrieves the current user locale. + * + * @returns The current user locale. + */ +export function getUserLocale() { + return locale; +} + +export type Locale = Record< + string, + { + message: string; + } +>; + +/** + * Retrieves the user's locale preferences. + * + * @returns A promise that resolves to the user's locale messages. + */ +export async function getUserLocalePreference(): Promise { + try { + const { locale: userLocale } = await snap.request({ + method: 'snap_getPreferences', + }); + return (await import(`../../locales/${userLocale}.json`)).messages; + } catch { + return (await import(`../../locales/en.json`)).messages; + } +} + +export type Translator = (string, ...args: (string | undefined)[]) => string; + +/** + * Returns a translator function that translates keys to user locale messages. + * + * @returns A function that translates keys to user locale messages. + */ +export function getTranslator(): Translator { + const userLocale = getUserLocale(); + + return (key: string, ...args: string[]): string => { + const template = userLocale[key]?.message ?? `{${key}}`; + + // Replace placeholders like $1, $2, etc., with corresponding arguments + return template.replace(/\{(\d+)\}/gu, (_, index: string) => { + const argIndex = parseInt(index, 10) - 1; // {1} corresponds to args[0], {2} to args[1], etc. + return args[argIndex] ?? `{${index}}`; // Fallback to placeholder if argument is missing + }); + }; +} + +/** + * Translates keys to user local message + * + * @param key + * @returns A function that translates keys to user locale messages. + */ +export function translate(key: string): string { + const userLocale = getUserLocale(); + return userLocale[key]?.message ?? `{${key}}`; +} diff --git a/packages/starknet-snap/src/utils/snapUtils.test.ts b/packages/starknet-snap/src/utils/snapUtils.test.ts index 9392758f..771cb497 100644 --- a/packages/starknet-snap/src/utils/snapUtils.test.ts +++ b/packages/starknet-snap/src/utils/snapUtils.test.ts @@ -3,6 +3,7 @@ import { constants } from 'starknet'; import { generateAccounts } from '../__tests__/helper'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from './constants'; import { DeployRequiredError, UpgradeRequiredError } from './exceptions'; +import { loadLocale } from './locale'; import * as snapHelper from './snap'; import { verifyIfAccountNeedUpgradeOrDeploy } from './snapUtils'; import * as starknetUtils from './starknetUtils'; @@ -111,6 +112,7 @@ describe('verifyIfAccountNeedUpgradeOrDeploy', () => { ])( 'throws error and renders alert dialog if the account required $action and `showAlert` is true', async (testData: { error: Error }) => { + await loadLocale(); const account = await mockAcccount(); const { verifyIfAccountNeedUpgradeOrDeploySpy, alertDialogSpy } = prepareMock(); diff --git a/packages/starknet-snap/src/utils/snapUtils.ts b/packages/starknet-snap/src/utils/snapUtils.ts index 3666d29d..792fee2b 100644 --- a/packages/starknet-snap/src/utils/snapUtils.ts +++ b/packages/starknet-snap/src/utils/snapUtils.ts @@ -4,18 +4,13 @@ import type { Mutex } from 'async-mutex'; import convert from 'ethereum-unit-converter'; import { num as numUtils, constants } from 'starknet'; import type { - InvocationsDetails, - DeclareContractPayload, - Abi, DeclareSignerDetails, Call, DeployAccountSignerDetails, - Invocations, - UniversalDetails, } from 'starknet'; import { Config } from '../config'; -import { FeeToken, type AddNetworkRequestParams } from '../types/snapApi'; +import { type AddNetworkRequestParams } from '../types/snapApi'; import { ContractFuncName, TransactionStatus } from '../types/snapState'; import type { Network, @@ -31,6 +26,7 @@ import { STARKNET_SEPOLIA_TESTNET_NETWORK, } from './constants'; import { DeployRequiredError, UpgradeRequiredError } from './exceptions'; +import { getTranslator } from './locale'; import { logger } from './logger'; import { toJson } from './serializer'; import { alertDialog } from './snap'; @@ -218,64 +214,18 @@ export function addDialogTxt( * @param network */ export function getNetworkTxt(network: Network) { + const translate = getTranslator(); const components = []; - addDialogTxt(components, 'Chain Name', network.name); - addDialogTxt(components, 'Chain ID', network.chainId); + addDialogTxt(components, translate('chainName'), network.name); + addDialogTxt(components, translate('chainId'), network.chainId); if (network.baseUrl) { - addDialogTxt(components, 'Base URL', network.baseUrl); + addDialogTxt(components, translate('baseUrl'), network.baseUrl); } if (network.nodeUrl) { - addDialogTxt(components, 'RPC URL', network.nodeUrl); + addDialogTxt(components, translate('rpcUrl'), network.nodeUrl); } if (network.voyagerUrl) { - addDialogTxt(components, 'Explorer URL', network.voyagerUrl); - } - return components; -} - -/** - * - * @param senderAddress - * @param network - * @param invocations - * @param abis - * @param details - */ -export function getTxnSnapTxt( - senderAddress: string, - network: Network, - invocations: Invocations | Call | Call[], - abis?: Abi[], - details?: UniversalDetails, -) { - const components = []; - addDialogTxt(components, 'Network', network.name); - addDialogTxt(components, 'Signer Address', senderAddress); - addDialogTxt( - components, - 'Transaction Invocation', - JSON.stringify(invocations, null, 2), - ); - if (abis && abis.length > 0) { - addDialogTxt(components, 'Abis', JSON.stringify(abis, null, 2)); - } - - if (details?.maxFee) { - const feeToken: FeeToken = - details?.version === constants.TRANSACTION_VERSION.V3 - ? FeeToken.STRK - : FeeToken.ETH; - addDialogTxt( - components, - `Max Fee(${feeToken})`, - convert(details.maxFee, 'wei', 'ether'), - ); - } - if (details?.nonce) { - addDialogTxt(components, 'Nonce', details.nonce.toString()); - } - if (details?.version) { - addDialogTxt(components, 'Version', details.version.toString()); + addDialogTxt(components, translate('explorerUrl'), network.voyagerUrl); } return components; } @@ -300,17 +250,22 @@ export function getSendTxnText( network: Network, ): Component[] { // Retrieve the ERC-20 token from snap state for confirmation display purpose + const translate = getTranslator(); const token = getErc20Token(state, contractAddress, network.chainId); const components = []; - addDialogTxt(components, 'Signer Address', senderAddress); - addDialogTxt(components, 'Contract', contractAddress); - addDialogTxt(components, 'Call Data', `[${contractCallData.join(', ')}]`); + addDialogTxt(components, translate('signerAddress'), senderAddress); + addDialogTxt(components, translate('contract'), contractAddress); + addDialogTxt( + components, + translate('callData'), + `[${contractCallData.join(', ')}]`, + ); addDialogTxt( components, - 'Estimated Gas Fee(ETH)', + `${translate('estimatedGasFee')}(ETH)`, convert(maxFee, 'wei', 'ether'), ); - addDialogTxt(components, 'Network', network.name); + addDialogTxt(components, translate('network'), network.name); if (token && contractFuncName === ContractFuncName.Transfer) { try { @@ -322,9 +277,17 @@ export function getSendTxnText( Number(contractCallData[1]) * Math.pow(10, -1 * token.decimals) ).toFixed(token.decimals); } - addDialogTxt(components, 'Sender Address', senderAddress); - addDialogTxt(components, 'Recipient Address', contractCallData[0]); - addDialogTxt(components, `Amount(${token.symbol})`, amount); + addDialogTxt(components, translate('senderAddress'), senderAddress); + addDialogTxt( + components, + translate('recipientAddress'), + contractCallData[0], + ); + addDialogTxt( + components, + translate('amountWithSymbol', token.symbol), + amount, + ); } catch (error) { logger.error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions @@ -350,99 +313,18 @@ export function getSignTxnTxt( network: Network, txnInvocation: Call[] | DeclareSignerDetails | DeployAccountSignerDetails, ) { + const translate = getTranslator(); const components = []; - addDialogTxt(components, 'Network', network.name); - addDialogTxt(components, 'Signer Address', signerAddress); + addDialogTxt(components, translate('network'), network.name); + addDialogTxt(components, translate('signerAddress'), signerAddress); addDialogTxt( components, - 'Transaction', + translate('transaction'), JSON.stringify(txnInvocation, null, 2), ); return components; } -/** - * - * @param senderAddress - * @param network - * @param contractPayload - * @param invocationsDetails - */ -export function getDeclareSnapTxt( - senderAddress: string, - network: Network, - contractPayload: DeclareContractPayload, - invocationsDetails?: InvocationsDetails, -) { - const components = []; - addDialogTxt(components, 'Network', network.name); - addDialogTxt(components, 'Signer Address', senderAddress); - - if (contractPayload.contract) { - const _contractPayload = - typeof contractPayload.contract === 'string' || - contractPayload.contract instanceof String - ? contractPayload.contract.toString() - : JSON.stringify(contractPayload.contract, null, 2); - addDialogTxt(components, 'Contract', _contractPayload); - } - if (contractPayload.compiledClassHash) { - addDialogTxt( - components, - 'Complied Class Hash', - contractPayload.compiledClassHash, - ); - } - if (contractPayload.classHash) { - addDialogTxt(components, 'Class Hash', contractPayload.classHash); - } - if (contractPayload.casm) { - addDialogTxt( - components, - 'Casm', - JSON.stringify(contractPayload.casm, null, 2), - ); - } - if (invocationsDetails?.maxFee !== undefined) { - addDialogTxt( - components, - 'Max Fee(ETH)', - convert(invocationsDetails.maxFee, 'wei', 'ether'), - ); - } - if (invocationsDetails?.nonce !== undefined) { - addDialogTxt(components, 'Nonce', invocationsDetails.nonce.toString()); - } - if (invocationsDetails?.version !== undefined) { - addDialogTxt(components, 'Version', invocationsDetails.version.toString()); - } - return components; -} - -/** - * - * @param tokenAddress - * @param tokenName - * @param tokenSymbol - * @param tokenDecimals - * @param network - */ -export function getAddTokenText( - tokenAddress: string, - tokenName: string, - tokenSymbol: string, - tokenDecimals: number, - network: Network, -) { - const components = []; - addDialogTxt(components, 'Network', network.name); - addDialogTxt(components, 'Token Address', tokenAddress); - addDialogTxt(components, 'Token Name', tokenName); - addDialogTxt(components, 'Token Symbol', tokenSymbol); - addDialogTxt(components, 'Token Decimals', tokenDecimals.toString()); - return components; -} - /** * * @param state @@ -727,7 +609,7 @@ export function getNetworkFromChainId( const network = getNetwork(state, chainId); if (network === undefined) { throw new Error( - `can't find the network in snap state with chainId: ${chainId}`, + `can'translate find the network in snap state with chainId: ${chainId}`, ); } logger.log( @@ -1018,11 +900,10 @@ export function toMap( * Displays a modal to the user requesting them to upgrade their account. */ export async function showUpgradeRequestModal() { + const translate = getTranslator(); await alertDialog([ - heading('Account Upgrade Mandatory!'), - text( - `Visit the [companion dapp for Starknet](${getDappUrl()}) and click “Upgrade”.\nThank you!`, - ), + heading(translate('accountUpgradeMandatory')), + text(translate('visitCompanionDappAndUpgrade', getDappUrl())), ]); } @@ -1030,11 +911,10 @@ export async function showUpgradeRequestModal() { * Displays a modal to the user requesting them to deploy their account. */ export async function showDeployRequestModal() { + const translate = getTranslator(); await alertDialog([ - heading('Account Deployment Mandatory!'), - text( - `Visit the [companion dapp for Starknet](${getDappUrl()}) to deploy your account.\nThank you!`, - ), + heading(translate('accountDeploymentMandatory')), + text(translate('visitCompanionDappAndDeploy', getDappUrl())), ]); } diff --git a/packages/starknet-snap/test/src/addNetwork.test.ts b/packages/starknet-snap/test/src/addNetwork.test.ts deleted file mode 100644 index 70b180d0..00000000 --- a/packages/starknet-snap/test/src/addNetwork.test.ts +++ /dev/null @@ -1,324 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import { SnapState } from '../../src/types/snapState'; -import * as snapUtils from '../../src/utils/snapUtils'; -import { - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import { addNetwork } from '../../src/addNetwork'; -import { Mutex } from 'async-mutex'; -import { AddNetworkRequestParams, ApiParams } from '../../src/types/snapApi'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: addNetwork', function () { - const walletStub = new WalletMock(); - const state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - const apiParams: ApiParams = { - state, - requestParams: {}, - wallet: walletStub, - saveMutex: new Mutex(), - }; - let stateStub: sinon.SinonStub; - let dialogStub: sinon.SinonStub; - beforeEach(function () { - stateStub = walletStub.rpcStubs.snap_manageState; - dialogStub = walletStub.rpcStubs.snap_dialog; - stateStub.resolves(state); - dialogStub.resolves(true); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - }); - - it('should add the network correctly', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA.starknet.io', - networkNodeUrl: 'https://alpha-unit-SN_SEPOLIA.starknet.io', - }; - apiParams.requestParams = requestObject; - const result = await addNetwork(apiParams); - expect(result).to.be.eql(true); - expect(stateStub).to.be.calledOnce; - expect(state.networks.length).to.be.eql(3); - }); - - it('should update the network correctly', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA 2', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: 'https://alpha-unit-SN_SEPOLIA.starknet.io', - }; - apiParams.requestParams = requestObject; - const result = await addNetwork(apiParams); - expect(result).to.be.eql(true); - expect(stateStub).to.be.calledOnce; - expect(state.networks.length).to.be.eql(3); - }); - - it('should not update snap state with the duplicated network', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA 2', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: 'https://alpha-unit-SN_SEPOLIA.starknet.io', - }; - apiParams.requestParams = requestObject; - const result = await addNetwork(apiParams); - expect(result).to.be.eql(true); - expect(stateStub).to.be.callCount(0); - expect(state.networks.length).to.be.eql(3); - }); - - it('should throw an error if upsertNetwork failed', async function () { - sandbox.stub(snapUtils, 'upsertNetwork').throws(new Error()); - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA 2', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: 'https://alpha-unit-SN_SEPOLIA.starknet.io', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network name is undefined', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: undefined as unknown as string, - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network chain id is undefined', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA 2', - networkChainId: undefined as unknown as string, - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if both the network base url and node url are empty string', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA 2', - networkChainId: '0x534e5f474f777', - networkBaseUrl: '', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network name is not in ASCII chars', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'аррӏе SN_SEPOLIA', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network name is longer than 64 chars', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: - 'Starknet Unit SN_SEPOLIA xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network name is in all spaces', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: ' ', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network chainId is not in hex string', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '534e5f474f777', - networkBaseUrl: 'https://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network base URL is not valid', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '0x534e5f474f777', - networkBaseUrl: 'wss://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network node URL is not valid', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '0x534e5f474f777', - networkBaseUrl: '', - networkNodeUrl: 'wss://alpha-unit-SN_SEPOLIA-2.starknet.io', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network Voyager URL is not valid', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '0x534e5f474f777', - networkBaseUrl: '', - networkNodeUrl: 'http://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkVoyagerUrl: 'wss://test.com', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network chainId is one of the preloaded network chainId', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: 'Starknet Unit SN_SEPOLIA', - networkChainId: '0x534e5f5345504f4c4941', - networkBaseUrl: 'http://alpha-unit-SN_SEPOLIA-2.starknet.io', - networkNodeUrl: '', - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); - - it('should throw an error if the network name is one of the preloaded network name', async function () { - const requestObject: AddNetworkRequestParams = { - networkName: STARKNET_SEPOLIA_TESTNET_NETWORK.name, - networkChainId: STARKNET_SEPOLIA_TESTNET_NETWORK.chainId, - networkBaseUrl: STARKNET_SEPOLIA_TESTNET_NETWORK.baseUrl, - networkNodeUrl: STARKNET_SEPOLIA_TESTNET_NETWORK.nodeUrl, - }; - apiParams.requestParams = requestObject; - let result; - try { - result = await addNetwork(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); -}); diff --git a/packages/starknet-snap/test/src/createAccount.test.ts b/packages/starknet-snap/test/src/createAccount.test.ts deleted file mode 100644 index 64cb3027..00000000 --- a/packages/starknet-snap/test/src/createAccount.test.ts +++ /dev/null @@ -1,279 +0,0 @@ -import chai, { expect } from 'chai'; -import sinon from 'sinon'; -import sinonChai from 'sinon-chai'; -import { WalletMock } from '../wallet.mock.test'; -import * as utils from '../../src/utils/starknetUtils'; -import * as snapUtils from '../../src/utils/snapUtils'; -import { createAccount } from '../../src/createAccount'; -import { SnapState } from '../../src/types/snapState'; -import { - STARKNET_MAINNET_NETWORK, - STARKNET_SEPOLIA_TESTNET_NETWORK, -} from '../../src/utils/constants'; -import { - createAccountProxyTxn, - createAccountProxyResp, - createAccountProxyMainnetResp, - createAccountFailedProxyResp, - createAccountProxyMainnetResp2, - getBip44EntropyStub, - estimateDeployFeeResp, - getBalanceResp, - account1, -} from '../constants.test'; -import { getAddressKeyDeriver } from '../../src/utils/keyPair'; -import { Mutex } from 'async-mutex'; -import { - ApiParamsWithKeyDeriver, - CreateAccountRequestParams, -} from '../../src/types/snapApi'; -import { GetTransactionReceiptResponse } from 'starknet'; -import { BIP44AddressKeyDeriver } from '@metamask/key-tree'; - -chai.use(sinonChai); -const sandbox = sinon.createSandbox(); - -describe('Test function: createAccount', function () { - this.timeout(10000); - const walletStub = new WalletMock(); - let waitForTransactionStub; - let state: SnapState = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - let apiParams: ApiParamsWithKeyDeriver; - - beforeEach(async function () { - walletStub.rpcStubs.snap_getBip44Entropy.callsFake(getBip44EntropyStub); - apiParams = { - state, - requestParams: {}, - wallet: walletStub, - saveMutex: new Mutex(), - keyDeriver: await getAddressKeyDeriver(walletStub), - }; - sandbox.useFakeTimers(createAccountProxyTxn.timestamp); - walletStub.rpcStubs.snap_dialog.resolves(true); - walletStub.rpcStubs.snap_manageState.resolves(state); - waitForTransactionStub = sandbox.stub(utils, 'waitForTransaction'); - waitForTransactionStub.resolves( - {} as unknown as GetTransactionReceiptResponse, - ); - sandbox.stub(utils, 'estimateAccountDeployFee').callsFake(async () => { - return estimateDeployFeeResp; - }); - }); - - afterEach(function () { - walletStub.reset(); - sandbox.restore(); - state = { - accContracts: [], - erc20Tokens: [], - networks: [STARKNET_MAINNET_NETWORK, STARKNET_SEPOLIA_TESTNET_NETWORK], - transactions: [], - }; - }); - - it('should only return derived address without sending deploy txn correctly in mainnet if deploy is false', async function () { - const requestObject: CreateAccountRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - }; - apiParams.requestParams = requestObject; - const result = await createAccount(apiParams); - const { publicKey } = await utils.getKeysFromAddressIndex( - apiParams.keyDeriver as unknown as BIP44AddressKeyDeriver, - STARKNET_MAINNET_NETWORK.chainId, - state, - -1, - ); - const { address: contractAddress } = - utils.getAccContractAddressAndCallData(publicKey); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(0); - expect(result.address).to.be.eq(contractAddress); - expect(state.accContracts.length).to.be.eq(0); - expect(state.transactions.length).to.be.eq(0); - }); - - it('waits for tansaction after an account has deployed', async function () { - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyMainnetResp; - }); - sandbox.stub(utils, 'getSigner').throws(new Error()); - sandbox.stub(utils, 'callContract').callsFake(async () => { - return getBalanceResp; - }); - - const requestObject: CreateAccountRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - deploy: true, - }; - apiParams.requestParams = requestObject; - await createAccount(apiParams, false, true); - - expect(waitForTransactionStub).to.have.been.callCount(1); - }); - - it('should create and store an user account with proxy in state correctly in mainnet', async function () { - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyMainnetResp; - }); - - const requestObject: CreateAccountRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - deploy: true, - }; - apiParams.requestParams = requestObject; - const result = await createAccount(apiParams); - const { publicKey: expectedPublicKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - createAccountProxyMainnetResp.contract_address, - ); - expect(result.address).to.be.eq( - createAccountProxyMainnetResp.contract_address, - ); - expect(result.transaction_hash).to.be.eq( - createAccountProxyMainnetResp.transaction_hash, - ); - expect(state.accContracts.length).to.be.eq(1); - expect(state.accContracts[0].address).to.be.eq( - createAccountProxyMainnetResp.contract_address, - ); - expect(state.accContracts[0].deployTxnHash).to.be.eq( - createAccountProxyMainnetResp.transaction_hash, - ); - expect(state.accContracts[0].publicKey).to.be.eq(expectedPublicKey); - expect(state.accContracts[0].addressSalt).to.be.eq(expectedPublicKey); - expect(state.transactions.length).to.be.eq(1); - }); - - it('should create and store an user account of specific address index with proxy in state correctly in mainnet', async function () { - state.accContracts.push(account1); - state.transactions.push(createAccountProxyTxn); - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyMainnetResp2; - }); - - const requestObject: CreateAccountRequestParams = { - chainId: STARKNET_MAINNET_NETWORK.chainId, - addressIndex: 1, - deploy: true, - }; - apiParams.requestParams = requestObject; - const result = await createAccount(apiParams); - const { publicKey: expectedPublicKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_MAINNET_NETWORK, - state, - createAccountProxyMainnetResp2.contract_address, - ); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); - expect(result.address).to.be.eq( - createAccountProxyMainnetResp2.contract_address, - ); - expect(result.transaction_hash).to.be.eq( - createAccountProxyMainnetResp2.transaction_hash, - ); - expect(state.accContracts.length).to.be.eq(2); - expect(state.accContracts[1].address).to.be.eq( - createAccountProxyMainnetResp2.contract_address, - ); - expect(state.accContracts[1].deployTxnHash).to.be.eq( - createAccountProxyMainnetResp2.transaction_hash, - ); - expect(state.accContracts[1].publicKey).to.be.eq(expectedPublicKey); - expect(state.accContracts[1].addressSalt).to.be.eq(expectedPublicKey); - expect(state.transactions.length).to.be.eq(2); - }); - - it('should create and store an user account with proxy in state correctly in SN_SEPOLIA in silent mode', async function () { - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyResp; - }); - - const requestObject: CreateAccountRequestParams = { deploy: true }; - apiParams.requestParams = requestObject; - const result = await createAccount(apiParams, true); - const { publicKey: expectedPublicKey } = await utils.getKeysFromAddress( - apiParams.keyDeriver, - STARKNET_SEPOLIA_TESTNET_NETWORK, - state, - createAccountProxyResp.contract_address, - ); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(4); - expect(result.address).to.be.eq(createAccountProxyResp.contract_address); - expect(result.transaction_hash).to.be.eq( - createAccountProxyResp.transaction_hash, - ); - expect(state.accContracts.length).to.be.eq(1); - expect(state.accContracts[0].address).to.be.eq( - createAccountProxyResp.contract_address, - ); - expect(state.accContracts[0].deployTxnHash).to.be.eq( - createAccountProxyResp.transaction_hash, - ); - expect(state.accContracts[0].publicKey).to.be.eq(expectedPublicKey); - expect(state.accContracts[0].addressSalt).to.be.eq(expectedPublicKey); - expect(state.transactions.length).to.be.eq(1); - }); - - it('should not create any user account with proxy in state in SN_SEPOLIA if not in silentMode and user rejected', async function () { - sandbox.stub(utils, 'getAccContractAddressAndCallData').callsFake(() => { - return { - address: account1.address, - callData: [], - }; - }); - walletStub.rpcStubs.snap_dialog.resolves(false); - const requestObject: CreateAccountRequestParams = { deploy: true }; - apiParams.requestParams = requestObject; - - const result = await createAccount(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(0); - expect(result.address).to.be.eq(account1.address); - expect(state.accContracts.length).to.be.eq(0); - expect(state.transactions.length).to.be.eq(0); - }); - - it('should skip upsert account and transaction if deployTxn response code has no transaction_hash in SN_SEPOLIA', async function () { - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountFailedProxyResp; - }); - - const requestObject: CreateAccountRequestParams = { deploy: true }; - apiParams.requestParams = requestObject; - const result = await createAccount(apiParams); - expect(walletStub.rpcStubs.snap_manageState).to.have.been.callCount(0); - expect(result.address).to.be.eq( - createAccountFailedProxyResp.contract_address, - ); - expect(result.transaction_hash).to.be.eq( - createAccountFailedProxyResp.transaction_hash, - ); - expect(state.accContracts.length).to.be.eq(0); - expect(state.transactions.length).to.be.eq(0); - }); - - it('should throw error if upsertAccount failed', async function () { - sandbox.stub(snapUtils, 'upsertAccount').throws(new Error()); - sandbox.stub(utils, 'deployAccount').callsFake(async () => { - return createAccountProxyResp; - }); - - const requestObject: CreateAccountRequestParams = { deploy: true }; - apiParams.requestParams = requestObject; - - let result; - try { - await createAccount(apiParams); - } catch (err) { - result = err; - } finally { - expect(result).to.be.an('Error'); - } - }); -}); diff --git a/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts b/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts index 853413c4..bf538a98 100644 --- a/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts +++ b/packages/starknet-snap/test/src/signDeployAccountTransaction.test.ts @@ -22,11 +22,7 @@ import { } from '../../src/types/snapApi'; import { DeployAccountSignerDetails, constants } from 'starknet'; import * as utils from '../../src/utils/starknetUtils'; -import * as snapsUtil from '../../src/utils/snapUtils'; -import { - DeployRequiredError, - UpgradeRequiredError, -} from '../../src/utils/exceptions'; +import { loadLocale } from '../../src/utils/locale'; chai.use(sinonChai); const sandbox = sinon.createSandbox(); @@ -83,6 +79,7 @@ describe('Test function: signDeployAccountTransaction', function () { }); it('should sign a transaction from an user account correctly', async function () { + await loadLocale(); sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); sandbox.stub(utils, 'signDeployAccountTransaction').resolves(signature3); const result = await signDeployAccountTransaction(apiParams); @@ -91,6 +88,7 @@ describe('Test function: signDeployAccountTransaction', function () { }); it('should throw error if signDeployAccountTransaction fail', async function () { + await loadLocale(); sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); sandbox.stub(utils, 'signDeployAccountTransaction').throws(new Error()); let result; @@ -105,6 +103,7 @@ describe('Test function: signDeployAccountTransaction', function () { }); it('should return false if user deny to sign the transaction', async function () { + await loadLocale(); sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); const stub = sandbox.stub(utils, 'signDeployAccountTransaction'); walletStub.rpcStubs.snap_dialog.resolves(false); @@ -116,6 +115,7 @@ describe('Test function: signDeployAccountTransaction', function () { }); it('should skip dialog if enableAuthorize is false', async function () { + await loadLocale(); sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); sandbox.stub(utils, 'signDeployAccountTransaction').resolves(signature3); const paramsObject = @@ -128,6 +128,7 @@ describe('Test function: signDeployAccountTransaction', function () { }); it('should skip dialog if enableAuthorize is omit', async function () { + await loadLocale(); sandbox.stub(utils, 'validateAccountRequireUpgradeOrDeploy').resolvesThis(); sandbox.stub(utils, 'signDeployAccountTransaction').resolves(signature3); const paramsObject = diff --git a/packages/starknet-snap/test/src/upgradeAccContract.test.ts b/packages/starknet-snap/test/src/upgradeAccContract.test.ts index 026b5fd4..80102911 100644 --- a/packages/starknet-snap/test/src/upgradeAccContract.test.ts +++ b/packages/starknet-snap/test/src/upgradeAccContract.test.ts @@ -5,11 +5,7 @@ import sinonChai from 'sinon-chai'; import { WalletMock } from '../wallet.mock.test'; import * as utils from '../../src/utils/starknetUtils'; import * as snapUtils from '../../src/utils/snapUtils'; -import { - SnapState, - VoyagerTransactionType, - TransactionStatus, -} from '../../src/types/snapState'; +import { SnapState } from '../../src/types/snapState'; import { upgradeAccContract } from '../../src/upgradeAccContract'; import { STARKNET_SEPOLIA_TESTNET_NETWORK } from '../../src/utils/constants'; import { @@ -29,6 +25,7 @@ import { ACCOUNT_CLASS_HASH, } from '../../src/utils/constants'; import { CallData, num } from 'starknet'; +import { loadLocale } from '../../src/utils/locale'; chai.use(sinonChai); chai.use(chaiAsPromised); @@ -156,44 +153,8 @@ describe('Test function: upgradeAccContract', function () { estimateFeeStub = sandbox.stub(utils, 'estimateFee'); }); - it('should use provided max fee to execute txn when max fee provided', async function () { - (apiParams.requestParams as UpgradeTransactionRequestParams).maxFee = - '10000'; - walletStub.rpcStubs.snap_dialog.resolves(true); - executeTxnStub.resolves(sendTransactionResp); - - const address = ( - apiParams.requestParams as UpgradeTransactionRequestParams - ).contractAddress; - const calldata = CallData.compile({ - implementation: ACCOUNT_CLASS_HASH, - calldata: [0], - }); - - const txnInvocation = { - contractAddress: address, - entrypoint: 'upgrade', - calldata, - }; - - const result = await upgradeAccContract(apiParams); - - expect(executeTxnStub).to.calledOnce; - expect(executeTxnStub).to.calledWith( - STARKNET_SEPOLIA_TESTNET_NETWORK, - address, - 'pk', - txnInvocation, - undefined, - { - maxFee: num.toBigInt(10000), - }, - CAIRO_VERSION_LEGACY, - ); - expect(result).to.be.equal(sendTransactionResp); - }); - it('should use calculated max fee to execute txn when max fee not provided', async function () { + await loadLocale(); walletStub.rpcStubs.snap_dialog.resolves(true); executeTxnStub.resolves(sendTransactionResp); estimateFeeStub.resolves(estimateFeeResp); @@ -230,6 +191,7 @@ describe('Test function: upgradeAccContract', function () { }); it('should return executed txn result when user accept to sign the transaction', async function () { + await loadLocale(); executeTxnStub.resolves(sendTransactionResp); estimateFeeStub.resolves(estimateFeeResp); walletStub.rpcStubs.snap_dialog.resolves(true); @@ -242,6 +204,7 @@ describe('Test function: upgradeAccContract', function () { }); it('should return false when user rejected to sign the transaction', async function () { + await loadLocale(); executeTxnStub.resolves(sendTransactionResp); estimateFeeStub.resolves(estimateFeeResp); walletStub.rpcStubs.snap_dialog.resolves(false); @@ -254,6 +217,7 @@ describe('Test function: upgradeAccContract', function () { }); it('should return executed txn result when execute transaction success', async function () { + await loadLocale(); executeTxnStub.resolves(sendTransactionResp); estimateFeeStub.resolves(estimateFeeResp); walletStub.rpcStubs.snap_dialog.resolves(true); @@ -266,6 +230,7 @@ describe('Test function: upgradeAccContract', function () { }); it('should throw exception when execute transaction result null', async function () { + await loadLocale(); executeTxnStub.resolves(null); estimateFeeStub.resolves(estimateFeeResp); walletStub.rpcStubs.snap_dialog.resolves(true);