diff --git a/.gitignore b/.gitignore index 03cef6b4..a8d44ca4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ dist.7z /playwright/.cache/ dependency-usage.md extension-dependencies.md +emulator-account.pkey +flow.json diff --git a/_raw/_locales/en/messages.json b/_raw/_locales/en/messages.json index 87b3436d..525d0873 100644 --- a/_raw/_locales/en/messages.json +++ b/_raw/_locales/en/messages.json @@ -196,6 +196,9 @@ "Testnet": { "message": "Testnet" }, + "Emulator": { + "message": "Emulator" + }, "Crescendo": { "message": "Crescendo" }, diff --git a/_raw/_locales/ja/messages.json b/_raw/_locales/ja/messages.json index 4e8add82..fde3c98b 100644 --- a/_raw/_locales/ja/messages.json +++ b/_raw/_locales/ja/messages.json @@ -190,6 +190,9 @@ "Testnet": { "message": "Testnet" }, + "Emulator": { + "message": "Emulator" + }, "Crescendo": { "message": "Crescendo" }, diff --git a/_raw/_locales/ru/messages.json b/_raw/_locales/ru/messages.json index 4f2505c6..1344649d 100644 --- a/_raw/_locales/ru/messages.json +++ b/_raw/_locales/ru/messages.json @@ -190,6 +190,9 @@ "Testnet": { "message": "Testnet" }, + "Emulator": { + "message": "Emulator" + }, "Crescendo": { "message": "Crescendo" }, diff --git a/_raw/_locales/zh_CN/messages.json b/_raw/_locales/zh_CN/messages.json index 3afa0cd5..324ab7bc 100644 --- a/_raw/_locales/zh_CN/messages.json +++ b/_raw/_locales/zh_CN/messages.json @@ -190,6 +190,9 @@ "Testnet": { "message": "测试网" }, + "Emulator": { + "message": "沙盒网" + }, "Crescendo": { "message": "沙盒网" }, diff --git a/src/background/controller/wallet.ts b/src/background/controller/wallet.ts index af399b3c..314a937c 100644 --- a/src/background/controller/wallet.ts +++ b/src/background/controller/wallet.ts @@ -51,14 +51,19 @@ import { import i18n from 'background/service/i18n'; import { type DisplayedKeryring, KEYRING_CLASS } from 'background/service/keyring'; import type { CacheState } from 'background/service/pageStateCache'; -import { getScripts } from 'background/utils'; +import { + getScripts, + checkEmulatorStatus, + checkEmulatorAccount, + createEmulatorAccount, +} from 'background/utils'; import emoji from 'background/utils/emoji.json'; import fetchConfig from 'background/utils/remoteConfig'; import { notification, storage } from 'background/webapi'; import { openIndexPage } from 'background/webapi/tab'; import { INTERNAL_REQUEST_ORIGIN, EVENTS, KEYRING_TYPE } from 'consts'; -import { fclTestnetConfig, fclMainnetConfig } from '../fclConfig'; +import { fclTestnetConfig, fclMainnetConfig, fclEmulatorConfig } from '../fclConfig'; import placeholder from '../images/placeholder.png'; import { type CoinItem } from '../service/coinList'; import DisplayKeyring from '../service/keyring/display'; @@ -3306,12 +3311,24 @@ export class WalletController extends BaseController { } switchNetwork = async (network: string) => { + if (network === 'emulator') { + const isEmulatorRunning = await checkEmulatorStatus(); + console.log('isEmulatorRunning ', isEmulatorRunning); + if (!isEmulatorRunning) { + throw new Error( + 'Flow emulator is not running. Please start it with `flow emulator start` and try again.' + ); + } + } + await userWalletService.setNetwork(network); eventBus.emit('switchNetwork', network); if (network === 'testnet') { await fclTestnetConfig(); } else if (network === 'mainnet') { await fclMainnetConfig(); + } else if (network === 'emulator') { + await fclEmulatorConfig(); } this.refreshAll(); @@ -3406,6 +3423,9 @@ export class WalletController extends BaseController { case 'crescendo': baseURL = 'https://flow-view-source.vercel.app/crescendo'; break; + case 'emulator': + baseURL = 'http://localhost:8888/flow/events'; // Flow emulator explorer endpoint + break; } } @@ -3461,98 +3481,7 @@ export class WalletController extends BaseController { title = chrome.i18n.getMessage('Transaction__Sealed'), body = '', icon = chrome.runtime.getURL('./images/icon-64.png') - ) => { - if (!txId || !txId.match(/^0?x?[0-9a-fA-F]{64}/)) { - return; - } - const address = (await this.getCurrentAddress()) || '0x'; - const network = await this.getNetwork(); - - try { - chrome.storage.session.set({ - transactionPending: { txId, network, date: new Date() }, - }); - eventBus.emit('transactionPending'); - chrome.runtime.sendMessage({ - msg: 'transactionPending', - network: network, - }); - transactionService.setPending(txId, address, network, icon, title); - - // Listen to the transaction until it's sealed. - // This will throw an error if there is an error with the transaction - await fcl.tx(txId).onceSealed(); - - // Track the transaction result - mixpanelTrack.track('transaction_result', { - tx_id: txId, - is_successful: true, - }); - - try { - // Send a notification to the user only on success - if (sendNotification) { - const baseURL = this.getFlowscanUrl(); - notification.create(`${baseURL}/transaction/${txId}`, title, body, icon); - } - } catch (err: unknown) { - // We don't want to throw an error if the notification fails - console.error('listenTransaction notification error ', err); - } - } catch (err: unknown) { - // An error has occurred while listening to the transaction - let errorMessage = 'unknown error'; - let errorCode: number | undefined = undefined; - - if (err instanceof TransactionError) { - errorCode = err.code; - errorMessage = err.message; - } else { - if (err instanceof Error) { - errorMessage = err.message; - } else if (typeof err === 'string') { - errorMessage = err; - } - // From fcl-core transaction-error.ts - const ERROR_CODE_REGEX = /\[Error Code: (\d+)\]/; - const match = errorMessage.match(ERROR_CODE_REGEX); - errorCode = match ? parseInt(match[1], 10) : undefined; - } - - console.warn({ - msg: 'transactionError', - errorMessage, - errorCode, - }); - - // Track the transaction error - mixpanelTrack.track('transaction_result', { - tx_id: txId, - is_successful: false, - error_message: errorMessage, - }); - - // Tell the UI that there was an error - chrome.runtime.sendMessage({ - msg: 'transactionError', - errorMessage, - errorCode, - }); - } finally { - // Remove the pending transaction from the UI - await chrome.storage.session.remove('transactionPending'); - transactionService.removePending(txId, address, network); - - // Refresh the transaction list - this.refreshTransaction(address, 15, 0); - - // Tell the UI that the transaction is done - eventBus.emit('transactionDone'); - chrome.runtime.sendMessage({ - msg: 'transactionDone', - }); - } - }; + ) => {}; getNFTListCahce = async (): Promise => { const network = await this.getNetwork(); @@ -3728,6 +3657,7 @@ export class WalletController extends BaseController { }; getCadenceScripts = async () => { + console.log('getCadenceScripts'); try { const cadenceScrpts = await storage.get('cadenceScripts'); const now = new Date(); @@ -3739,6 +3669,7 @@ export class WalletController extends BaseController { now.getTime() <= cadenceScrpts['expiry'] && cadenceScrpts.network === network ) { + console.log('returning cached scripts', cadenceScrpts['data']); return cadenceScrpts['data']; } @@ -3746,9 +3677,10 @@ export class WalletController extends BaseController { // const cadencev1 = (await openapiService.cadenceScripts(network)) ?? {}; const cadenceScriptsV2 = (await openapiService.cadenceScriptsV2()) ?? {}; + console.log('cadenceScriptsV2', cadenceScriptsV2); // const { scripts, version } = cadenceScriptsV2; // const cadenceVersion = cadenceScriptsV2.version; - const cadence = cadenceScriptsV2.scripts[network]; + const cadence = cadenceScriptsV2.scripts[network === 'emulator' ? 'testnet' : network]; // for (const item of cadence) { // console.log(cadenceVersion, 'cadenceVersion'); diff --git a/src/background/fclConfig.ts b/src/background/fclConfig.ts index 404d0596..95d22a65 100644 --- a/src/background/fclConfig.ts +++ b/src/background/fclConfig.ts @@ -134,3 +134,17 @@ export const fclTestnetConfig = async () => { } } }; + +// Configure FCL for Emulator +export const fclEmulatorConfig = async () => { + const config = fcl + .config() + .put('accessNode.api', 'http://localhost:8888') + .put('sdk.transport', httpSend) + .put('flow.network', 'emulator') + // Default emulator account with contracts deployed + .put('0xFungibleToken', '0xee82856bf20e2aa6') + .put('0xFlowToken', '0x0ae53cb6e3f42a79') + .put('0xNonFungibleToken', '0xf8d6e0586b0a20c7') + .put('0xMetadataViews', '0xf8d6e0586b0a20c7'); +}; diff --git a/src/background/index.ts b/src/background/index.ts index 33aa293a..017547bb 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -16,7 +16,7 @@ import { EVENTS } from 'consts'; import { providerController, walletController } from './controller'; import { preAuthzServiceDefinition } from './controller/serviceDefinition'; -import { fclTestnetConfig, fclMainnetConfig } from './fclConfig'; +import { fclTestnetConfig, fclMainnetConfig, fclEmulatorConfig } from './fclConfig'; import { permissionService, preferenceService, @@ -99,6 +99,9 @@ async function fclSetup() { case 'testnet': await fclTestnetConfig(); break; + case 'emulator': + await fclEmulatorConfig(); + break; } } diff --git a/src/background/service/networkModel.ts b/src/background/service/networkModel.ts index 4ce5e514..b8e40f9c 100644 --- a/src/background/service/networkModel.ts +++ b/src/background/service/networkModel.ts @@ -62,6 +62,7 @@ export enum FlowNetwork { mainnet = 'mainnet', testnet = 'testnet', crescendo = 'crescendo', + emulator = 'emulator', } export enum Period { diff --git a/src/background/service/openapi.ts b/src/background/service/openapi.ts index 0368a5e8..ec7e87fb 100644 --- a/src/background/service/openapi.ts +++ b/src/background/service/openapi.ts @@ -1441,6 +1441,7 @@ class OpenApiService { const isProduction = process.env.NODE_ENV === 'production'; let url; + const isEmulator = network === 'emulator'; if (isProduction) { url = `https://raw.githubusercontent.com/Outblock/token-list-jsons/outblock/jsons/${network}/${chainType}/default.json`; } else if ( @@ -1449,6 +1450,10 @@ class OpenApiService { (network === 'testnet' || network === 'mainnet') ) { url = `https://raw.githubusercontent.com/Outblock/token-list-jsons/outblock/jsons/${network}/${chainType}/dev.json`; + } else if (isEmulator) { + // TODO: remove this after emulator is ready + const emulatorNetwork = 'testnet'; + url = `https://raw.githubusercontent.com/Outblock/token-list-jsons/outblock/jsons/${emulatorNetwork}/${chainType}/emulator.json`; } else { url = `https://raw.githubusercontent.com/Outblock/token-list-jsons/outblock/jsons/${network}/${chainType}/default.json`; } diff --git a/src/background/service/transaction.ts b/src/background/service/transaction.ts index 1e69a6db..caa6072d 100644 --- a/src/background/service/transaction.ts +++ b/src/background/service/transaction.ts @@ -42,11 +42,13 @@ class Transaction { mainnet: [], crescendo: [], testnet: [], + emulator: [], }, pendingItem: { mainnet: [], crescendo: [], testnet: [], + emulator: [], }, }, }); @@ -59,11 +61,13 @@ class Transaction { mainnet: [], crescendo: [], testnet: [], + emulator: [], }, pendingItem: { mainnet: [], testnet: [], crescendo: [], + emulator: [], }, }, }); @@ -77,11 +81,13 @@ class Transaction { mainnet: [], crescendo: [], testnet: [], + emulator: [], }, pendingItem: { mainnet: [], testnet: [], crescendo: [], + emulator: [], }, }; this.session = { @@ -91,11 +97,13 @@ class Transaction { mainnet: [], testnet: [], crescendo: [], + emulator: [], }, pendingItem: { mainnet: [], testnet: [], crescendo: [], + emulator: [], }, }; }; diff --git a/src/background/service/userWallet.ts b/src/background/service/userWallet.ts index 630b6b3a..193ebb98 100644 --- a/src/background/service/userWallet.ts +++ b/src/background/service/userWallet.ts @@ -106,7 +106,7 @@ class UserWallet { }; setUserWallets = async (filteredData, network) => { - console.log('filteredData ', filteredData); + console.log('userWalletService - setUserWallets - filteredData ', filteredData, network); this.store.wallets[network] = filteredData; let walletIndex = (await storage.get('currentWalletIndex')) || 0; if (this.store.wallets[network] && this.store.wallets[network].length > 0) { diff --git a/src/background/utils/defaultTokenList.json b/src/background/utils/defaultTokenList.json index fcd33976..f895203c 100644 --- a/src/background/utils/defaultTokenList.json +++ b/src/background/utils/defaultTokenList.json @@ -4,7 +4,8 @@ "address": { "mainnet": "0x1654653399040a61", "testnet": "0x7e60df042a9c0868", - "crescendo": "0x7e60df042a9c0868" + "crescendo": "0x7e60df042a9c0868", + "emulator": "0x0ae53cb6e3f42a79" }, "contract_name": "FlowToken", "storage_path": { diff --git a/src/background/utils/index.ts b/src/background/utils/index.ts index f3026eb9..1e728713 100644 --- a/src/background/utils/index.ts +++ b/src/background/utils/index.ts @@ -4,6 +4,7 @@ import packageJson from '@/../package.json'; import { storage } from '@/background/webapi'; const { version } = packageJson; +import { userWalletService } from '../service'; import { mixpanelTrack } from '../service/mixpanel'; import pageStateCache from '../service/pageStateCache'; @@ -97,9 +98,74 @@ export const isSameAddress = (a: string, b: string) => { return a.toLowerCase() === b.toLowerCase(); }; +export const checkEmulatorStatus = async (): Promise => { + try { + const response = await fetch('http://localhost:8888/v1/blocks?height=1'); + console.log('checkEmulatorStatus - response ', response); + const data = await response.json(); + console.log('checkEmulatorStatus - data ', data); + return !!data[0].block_status; + } catch (error) { + console.error('checkEmulatorAccount - error ', error); + + return false; + } +}; + +export const checkEmulatorAccount = async (address: string): Promise => { + try { + const response = await fetch(`http://localhost:8888/v1/accounts/${address}`); + const data = await response.json(); + return !!data.address; + } catch (error) { + console.error('checkEmulatorAccount - error ', error); + return false; + } +}; + +export const createEmulatorAccount = async (publicKey: string): Promise => { + try { + const response = await fetch('http://localhost:8888/v1/accounts', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + signer: '0xf8d6e0586b0a20c7', // Service account + keys: [ + { + publicKey: publicKey, + signAlgo: 2, // ECDSA_P256 + hashAlgo: 3, // SHA3_256 + weight: 1000, + }, + ], + contracts: {}, + }), + }); + + const data = await response.json(); + if (!data.address) { + throw new Error('Failed to create emulator account'); + } + return data.address; + } catch (error) { + console.error('Error creating emulator account:', error); + throw error; + } +}; + export const getScripts = async (folder: string, scriptName: string) => { try { const { data } = await storage.get('cadenceScripts'); + let network = await userWalletService.getNetwork(); + + // If emulator network, return default scripts + if (network === 'emulator') { + network = 'testnet'; + // return getDefaultEmulatorScript(folder, scriptName); + } + const files = data[folder]; const script = files[scriptName]; const scriptString = Buffer.from(script, 'base64').toString('utf-8'); @@ -116,6 +182,36 @@ export const getScripts = async (folder: string, scriptName: string) => { } }; +const getDefaultEmulatorScript = (type: string, name: string) => { + const defaultScripts = { + basic: { + revokeKey: ` + transaction(keyIndex: Int) { + prepare(signer: AuthAccount) { + signer.keys.revoke(keyIndex: keyIndex) + } + } + `, + }, + storage: { + getStorageInfo: ` + pub fun main(address: Address): {String: UFix64} { + let account = getAccount(address) + let used = account.storageUsed + let capacity = account.storageCapacity + return { + "used": used, + "capacity": capacity, + "available": capacity - used + } + } + `, + }, + }; + + return defaultScripts[type]?.[name] || ''; +}; + export const findKeyAndInfo = (keys, publicKey) => { const index = findPublicKeyIndex(keys, publicKey); if (index >= 0) { diff --git a/src/background/utils/modules/findAddressWithPubKey.ts b/src/background/utils/modules/findAddressWithPubKey.ts index 543b7ffa..4f08c124 100644 --- a/src/background/utils/modules/findAddressWithPubKey.ts +++ b/src/background/utils/modules/findAddressWithPubKey.ts @@ -1,6 +1,6 @@ import * as fcl from '@onflow/fcl'; -import { fclMainnetConfig, fclTestnetConfig } from 'background/fclConfig'; +import { fclEmulatorConfig, fclMainnetConfig, fclTestnetConfig } from 'background/fclConfig'; import { userWalletService } from 'background/service'; export const findAddressWithKey = async (pubKeyHex, address) => { @@ -27,6 +27,8 @@ export const findAddressOnlyKey = async (pubKeyHex, network) => { let data; if (network === 'testnet') { data = await getAddressTestnet(pubKeyHex); + } else if (network === 'emulator') { + data = await getAddressEmulator(pubKeyHex); } else { data = await getAddressByIndexer(pubKeyHex); } @@ -59,10 +61,19 @@ export async function getAddressTestnet(publicKey) { return json; } +export async function getAddressEmulator(publicKey) { + const url = `http://localhost:8080/key/${publicKey}`; + const result = await fetch(url); + const json = await result.json(); + return json; +} + const findAddres = async (address, pubKeyHex) => { const network = await userWalletService.getNetwork(); if (network === 'testnet') { await fclTestnetConfig(); + } else if (network === 'emulator') { + await fclEmulatorConfig(); } else { await fclMainnetConfig(); } diff --git a/src/messages.json b/src/messages.json index 0c2fa891..a8b347f8 100644 --- a/src/messages.json +++ b/src/messages.json @@ -187,6 +187,9 @@ "Testnet": { "message": "Testnet" }, + "Emulator": { + "message": "Emulator" + }, "Crescendo": { "message": "Crescendo" }, diff --git a/src/ui/FRWComponent/LLEmulatorIndicator.tsx b/src/ui/FRWComponent/LLEmulatorIndicator.tsx new file mode 100644 index 00000000..e1505cc2 --- /dev/null +++ b/src/ui/FRWComponent/LLEmulatorIndicator.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +export const LLEmulatorIndicator: React.FC = () => { + return ( +
+
+ {chrome.i18n.getMessage('Emulator')} +
+
+ ); +}; diff --git a/src/ui/views/Approval/components/Connect.tsx b/src/ui/views/Approval/components/Connect.tsx index 7aee7b3a..ac75620d 100644 --- a/src/ui/views/Approval/components/Connect.tsx +++ b/src/ui/views/Approval/components/Connect.tsx @@ -252,6 +252,8 @@ const Connect = ({ params: { /*icon, origin,*/ tabId } }: ConnectProps) => { return '#FF8A00'; case 'crescendo': return '#CCAF21'; + case 'emulator': + return '#4A90E2'; } }; diff --git a/src/ui/views/Approval/components/EthApproval/EthSwitch/index.tsx b/src/ui/views/Approval/components/EthApproval/EthSwitch/index.tsx index 4df14d72..d74c770e 100644 --- a/src/ui/views/Approval/components/EthApproval/EthSwitch/index.tsx +++ b/src/ui/views/Approval/components/EthApproval/EthSwitch/index.tsx @@ -90,6 +90,8 @@ const EthSwitch = ({ params: { origin, target } }: ConnectProps) => { return '#FF8A00'; case 'crescendo': return '#CCAF21'; + case 'emulator': + return '#4A90E2'; } }; diff --git a/src/ui/views/Approval/components/EthSwitch/index.tsx b/src/ui/views/Approval/components/EthSwitch/index.tsx new file mode 100644 index 00000000..3453ae5c --- /dev/null +++ b/src/ui/views/Approval/components/EthSwitch/index.tsx @@ -0,0 +1,13 @@ +const networkColor = (network: string) => { + switch (network) { + case 'mainnet': + return '#41CC5D'; + case 'testnet': + return '#FF8A00'; + case 'crescendo': + return '#CCAF21'; + case 'emulator': + return '#4A90E2'; + } +}; +// ... existing code ... diff --git a/src/ui/views/Dashboard/Components/NetworkList.tsx b/src/ui/views/Dashboard/Components/NetworkList.tsx index 004e2a83..8b4d1e3e 100644 --- a/src/ui/views/Dashboard/Components/NetworkList.tsx +++ b/src/ui/views/Dashboard/Components/NetworkList.tsx @@ -223,6 +223,30 @@ const NetworkList = ({ networkColor, currentNetwork }) => { Testnet + + switchNetwork('emulator')} + sx={{ + padding: '4px 8px', + width: '100%', + '&:hover': { + color: networkColor('emulator'), + }, + }} + > + + Emulator + + )} diff --git a/src/ui/views/Dashboard/Header.tsx b/src/ui/views/Dashboard/Header.tsx index 456f23f5..b98a90d1 100644 --- a/src/ui/views/Dashboard/Header.tsx +++ b/src/ui/views/Dashboard/Header.tsx @@ -119,11 +119,11 @@ const Header = ({ loading = false }) => { const { unreadCount } = useNews(); const toggleDrawer = () => { - setDrawer(!drawer); + setDrawer((prevDrawer) => !prevDrawer); }; const togglePop = () => { - setPop(!ispop); + setPop((prevPop) => !prevPop); }; // News Drawer @@ -351,6 +351,8 @@ const Header = ({ loading = false }) => { return '#FF8A00'; case 'crescendo': return '#CCAF21'; + case 'emulator': + return '#4A90E2'; } }; diff --git a/src/ui/views/Dashboard/index.tsx b/src/ui/views/Dashboard/index.tsx index 8cbc413c..018c2e43 100644 --- a/src/ui/views/Dashboard/index.tsx +++ b/src/ui/views/Dashboard/index.tsx @@ -6,6 +6,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import SwipeableViews from 'react-swipeable-views'; +import { LLEmulatorIndicator } from '@/ui/FRWComponent/LLEmulatorIndicator'; import { getFirbaseConfig } from 'background/utils/firebaseConfig'; import { LLTestnetIndicator } from 'ui/FRWComponent'; import { useWallet } from 'ui/utils'; @@ -108,6 +109,8 @@ const Dashboard = ({ value, setValue }) => { }} > {currentNetwork === 'testnet' && value === 0 && } + {currentNetwork === 'emulator' && value === 0 && } + {/*
*/} { )} + + + + switchNetwork('emulator')} + > + + } + checkedIcon={} + value="emulator" + checked={currentNetwork === 'emulator'} + onChange={() => switchNetwork('emulator')} + /> + } + /> + + {currentNetwork === 'emulator' && ( + + {chrome.i18n.getMessage('Selected')} + + )} + + { const inputValue = e.target[2].value; setPk(pk); const address = flowAddressRegex.test(inputValue) ? inputValue : null; + console.log('KeyImport - handleImport - address', address); const result = await usewallet.findAddressWithPrivateKey(pk, address); + console.log('KeyImport - handleImport - result', result); if (!result) { onOpen(); return;