diff --git a/backend/database_handler/accounts_manager.py b/backend/database_handler/accounts_manager.py
index 4b2838eac..9a08ea97a 100644
--- a/backend/database_handler/accounts_manager.py
+++ b/backend/database_handler/accounts_manager.py
@@ -1,7 +1,7 @@
# consensus/services/transactions_db_service.py
from eth_account import Account
-from eth_account._utils.validation import is_valid_address
+from eth_utils import is_address
from .models import CurrentState
from backend.database_handler.errors import AccountNotFoundError
@@ -38,7 +38,7 @@ def create_new_account_with_address(self, address: str):
self.session.commit()
def is_valid_address(self, address: str) -> bool:
- return is_valid_address(address)
+ return is_address(address)
def get_account(self, account_address: str) -> CurrentState | None:
"""Private method to retrieve an account from the data base"""
diff --git a/backend/database_handler/transactions_processor.py b/backend/database_handler/transactions_processor.py
index 163c6f3d3..2bd6b58aa 100644
--- a/backend/database_handler/transactions_processor.py
+++ b/backend/database_handler/transactions_processor.py
@@ -4,7 +4,7 @@
from .models import Transactions
from sqlalchemy.orm import Session
-from sqlalchemy import or_, and_
+from sqlalchemy import or_, and_, desc
from .models import TransactionStatus
from eth_utils import to_bytes, keccak, is_address
@@ -366,3 +366,51 @@ def set_transaction_appeal_failed(self, transaction_hash: str, appeal_failed: in
self.session.query(Transactions).filter_by(hash=transaction_hash).one()
)
transaction.appeal_failed = appeal_failed
+
+ def get_highest_timestamp(self) -> int:
+ transaction = (
+ self.session.query(Transactions)
+ .order_by(desc(Transactions.timestamp_accepted))
+ .first()
+ )
+ if transaction is None:
+ return 0
+ return transaction.timestamp_accepted
+
+ def get_transactions_for_block(
+ self, block_number: int, include_full_tx: bool
+ ) -> dict:
+ transactions = (
+ self.session.query(Transactions)
+ .filter(Transactions.timestamp_accepted == block_number)
+ .all()
+ )
+
+ if not transactions:
+ return None
+
+ block_hash = transactions[0].hash
+ parent_hash = "0x" + "0" * 64 # Placeholder for parent block hash
+ timestamp = transactions[0].timestamp_accepted or int(time.time())
+
+ if include_full_tx:
+ transaction_data = [self._parse_transaction_data(tx) for tx in transactions]
+ else:
+ transaction_data = [tx.hash for tx in transactions]
+
+ block_details = {
+ "number": hex(block_number),
+ "hash": block_hash,
+ "parentHash": parent_hash,
+ "nonce": "0x" + "0" * 16,
+ "transactions": transaction_data,
+ "timestamp": hex(int(timestamp)),
+ "miner": "0x" + "0" * 40,
+ "difficulty": "0x1",
+ "gasUsed": "0x0",
+ "gasLimit": "0x0",
+ "size": "0x0",
+ "extraData": "0x",
+ }
+
+ return block_details
diff --git a/backend/protocol_rpc/endpoints.py b/backend/protocol_rpc/endpoints.py
index d1500e2e9..0f8818a1a 100644
--- a/backend/protocol_rpc/endpoints.py
+++ b/backend/protocol_rpc/endpoints.py
@@ -44,7 +44,7 @@
TransactionAddressFilter,
TransactionsProcessor,
)
-from backend.node.base import Node
+from backend.node.base import Node, SIMULATOR_CHAIN_ID
from backend.node.types import ExecutionMode, ExecutionResultStatus
from backend.consensus.base import ConsensusAlgorithm
@@ -368,7 +368,7 @@ def get_balance(
def get_transaction_count(
- transactions_processor: TransactionsProcessor, address: str
+ transactions_processor: TransactionsProcessor, address: str, block: str
) -> int:
return transactions_processor.get_transaction_count(address)
@@ -390,6 +390,11 @@ async def call(
from_address = params["from"] if "from" in params else None
data = params["data"]
+ if from_address is None:
+ return base64.b64encode(b"\x00' * 31 + b'\x01").decode(
+ "ascii"
+ ) # Return '1' as a uint256
+
if from_address and not accounts_manager.is_valid_address(from_address):
raise InvalidAddressError(from_address)
@@ -531,6 +536,113 @@ def set_finality_window_time(consensus: ConsensusAlgorithm, time: int) -> None:
consensus.set_finality_window_time(time)
+def get_chain_id() -> str:
+ return hex(SIMULATOR_CHAIN_ID)
+
+
+def get_net_version() -> str:
+ return str(SIMULATOR_CHAIN_ID)
+
+
+def get_block_number(transactions_processor: TransactionsProcessor) -> str:
+ transaction_count = transactions_processor.get_highest_timestamp()
+ return hex(transaction_count)
+
+
+def get_block_by_number(
+ transactions_processor: TransactionsProcessor, block_number: str, full_tx: bool
+) -> dict | None:
+ try:
+ block_number_int = int(block_number, 16)
+ except ValueError:
+ raise JSONRPCError(f"Invalid block number format: {block_number}")
+
+ block_details = transactions_processor.get_transactions_for_block(
+ block_number_int, include_full_tx=full_tx
+ )
+
+ if not block_details:
+ raise JSONRPCError(f"Block not found for number: {block_number}")
+
+ return block_details
+
+
+def get_gas_price() -> str:
+ gas_price_in_wei = 20 * 10**9
+ return hex(gas_price_in_wei)
+
+
+def get_transaction_receipt(
+ transactions_processor: TransactionsProcessor,
+ transaction_hash: str,
+) -> dict | None:
+
+ transaction = transactions_processor.get_transaction_by_hash(transaction_hash)
+
+ if not transaction:
+ return None
+
+ receipt = {
+ "transactionHash": transaction_hash,
+ "transactionIndex": hex(0),
+ "blockHash": transaction_hash,
+ "blockNumber": hex(transaction.get("block_number", 0)),
+ "from": transaction.get("from_address"),
+ "to": transaction.get("to_address") if transaction.get("to_address") else None,
+ "cumulativeGasUsed": hex(transaction.get("gas_used", 21000)),
+ "gasUsed": hex(transaction.get("gas_used", 21000)),
+ "contractAddress": (
+ transaction.get("contract_address")
+ if transaction.get("contract_address")
+ else None
+ ),
+ "logs": transaction.get("logs", []),
+ "logsBloom": "0x" + "00" * 256,
+ "status": hex(1 if transaction.get("status", True) else 0),
+ }
+
+ return receipt
+
+
+def get_block_by_hash(
+ transactions_processor: TransactionsProcessor,
+ transaction_hash: str,
+ full_tx: bool = False,
+) -> dict | None:
+
+ transaction = transactions_processor.get_transaction_by_hash(transaction_hash)
+
+ if not transaction:
+ return None
+
+ block_details = {
+ "hash": transaction_hash,
+ "parentHash": "0x" + "00" * 32,
+ "number": hex(transaction.get("block_number", 0)),
+ "timestamp": hex(transaction.get("timestamp", 0)),
+ "nonce": "0x" + "00" * 8,
+ "transactionsRoot": "0x" + "00" * 32,
+ "stateRoot": "0x" + "00" * 32,
+ "receiptsRoot": "0x" + "00" * 32,
+ "miner": "0x" + "00" * 20,
+ "difficulty": "0x1",
+ "totalDifficulty": "0x1",
+ "size": "0x0",
+ "extraData": "0x",
+ "gasLimit": hex(transaction.get("gas_limit", 8000000)),
+ "gasUsed": hex(transaction.get("gas_used", 21000)),
+ "logsBloom": "0x" + "00" * 256,
+ "transactions": [],
+ }
+
+ if full_tx:
+ block_details["transactions"].append(transaction)
+ else:
+ block_details["transactions"].append(transaction_hash)
+
+ return block_details
+
+
def get_contract(consensus_service: ConsensusService, contract_name: str) -> dict:
"""
Get contract instance by name
@@ -689,3 +801,22 @@ def register_all_rpc_endpoints(
partial(get_contract, consensus_service),
method_name="sim_getConsensusContract",
)
+ register_rpc_endpoint(get_chain_id, method_name="eth_chainId")
+ register_rpc_endpoint(get_net_version, method_name="net_version")
+ register_rpc_endpoint(
+ partial(get_block_number, transactions_processor),
+ method_name="eth_blockNumber",
+ )
+ register_rpc_endpoint(
+ partial(get_block_by_number, transactions_processor),
+ method_name="eth_getBlockByNumber",
+ )
+ register_rpc_endpoint(get_gas_price, method_name="eth_gasPrice")
+ register_rpc_endpoint(
+ partial(get_transaction_receipt, transactions_processor),
+ method_name="eth_getTransactionReceipt",
+ )
+ register_rpc_endpoint(
+ partial(get_block_by_hash, transactions_processor),
+ method_name="eth_getBlockByHash",
+ )
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index fa1081198..937e49b44 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -23,7 +23,7 @@
"defu": "^6.1.4",
"dexie": "^4.0.4",
"floating-vue": "^5.2.2",
- "genlayer-js": "^0.4.6",
+ "genlayer-js": "^0.5.0",
"hash-sum": "^2.0.0",
"jump.js": "^1.0.2",
"lodash-es": "^4.17.21",
@@ -5547,9 +5547,9 @@
}
},
"node_modules/genlayer-js": {
- "version": "0.4.7",
- "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.4.7.tgz",
- "integrity": "sha512-vp+7spuVaX7vflZd2q7qmaYgi5Cf7S/h4lAoVhAkFdyAsDStvhtwCdJGcZDt+U77AWxo3I1mUMXz0sk9ER3JXQ==",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/genlayer-js/-/genlayer-js-0.5.0.tgz",
+ "integrity": "sha512-sW3seeKEG3TxnWbU7FzFBWMwm3oOjoktbtYg8I95uQTP74PInn7rxjYt9wea3M3VxnZnkEFu4f1bOMEIFWwAqg==",
"dependencies": {
"eslint-plugin-import": "^2.30.0",
"typescript-parsec": "^0.3.4",
diff --git a/frontend/package.json b/frontend/package.json
index d453f6c3a..60f2a4a03 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -35,7 +35,7 @@
"defu": "^6.1.4",
"dexie": "^4.0.4",
"floating-vue": "^5.2.2",
- "genlayer-js": "^0.4.6",
+ "genlayer-js": "^0.5.0",
"hash-sum": "^2.0.0",
"jump.js": "^1.0.2",
"lodash-es": "^4.17.21",
diff --git a/frontend/src/components/Simulator/AccountItem.vue b/frontend/src/components/Simulator/AccountItem.vue
index 1f35cf569..bf371a925 100644
--- a/frontend/src/components/Simulator/AccountItem.vue
+++ b/frontend/src/components/Simulator/AccountItem.vue
@@ -1,15 +1,14 @@
@@ -39,12 +48,11 @@ const handleCreateNewAccount = async () => {
@@ -57,8 +65,17 @@ const handleCreateNewAccount = async () => {
secondary
class="w-full"
:icon="PlusIcon"
- >New account
+ New account
+
+
+ Connect MetaMask
+
diff --git a/frontend/src/components/Simulator/ContractMethodItem.vue b/frontend/src/components/Simulator/ContractMethodItem.vue
index 4e5511fc3..2b5767137 100644
--- a/frontend/src/components/Simulator/ContractMethodItem.vue
+++ b/frontend/src/components/Simulator/ContractMethodItem.vue
@@ -21,16 +21,15 @@ const props = defineProps<{
const isExpanded = ref(false);
const isCalling = ref(false);
-const responseMessage = ref('');
+const responseMessage = ref();
const calldataArguments = ref({ args: [], kwargs: {} });
const handleCallReadMethod = async () => {
- responseMessage.value = '';
isCalling.value = true;
try {
- const result = await callReadMethod(
+ responseMessage.value = await callReadMethod(
props.name,
unfoldArgsData({
args: calldataArguments.value.args,
@@ -38,14 +37,6 @@ const handleCallReadMethod = async () => {
}),
);
- let repr: string;
- if (typeof result === 'string') {
- const val = Uint8Array.from(atob(result), (c) => c.charCodeAt(0));
- responseMessage.value = calldata.toString(calldata.decode(val));
- } else {
- responseMessage.value = '';
- }
-
trackEvent('called_read_method', {
contract_name: contract.value?.name || '',
method_name: props.name,
diff --git a/frontend/src/global.d.ts b/frontend/src/global.d.ts
new file mode 100644
index 000000000..96960102e
--- /dev/null
+++ b/frontend/src/global.d.ts
@@ -0,0 +1,8 @@
+// global.d.ts
+interface Window {
+ ethereum?: {
+ isMetaMask?: boolean;
+ request: (args: { method: string; params?: unknown[] }) => Promise;
+ on: (method: string, callback: Function) => {};
+ };
+}
diff --git a/frontend/src/hooks/useContractQueries.ts b/frontend/src/hooks/useContractQueries.ts
index 0da25e623..61351c455 100644
--- a/frontend/src/hooks/useContractQueries.ts
+++ b/frontend/src/hooks/useContractQueries.ts
@@ -11,11 +11,7 @@ import { notify } from '@kyvg/vue3-notification';
import { useMockContractData } from './useMockContractData';
import { useEventTracking, useGenlayer } from '@/hooks';
import * as calldata from '@/calldata';
-import type {
- Address,
- TransactionHash,
- ContractSchema,
-} from 'genlayer-js/types';
+import type { Account, Address, TransactionHash } from 'genlayer-js/types';
const schema = ref();
@@ -102,7 +98,7 @@ export function useContractQueries() {
isDeploying.value = true;
try {
- if (!contract.value || !accountsStore.currentPrivateKey) {
+ if (!contract.value || !accountsStore.currentUserAccount) {
throw new Error('Error Deploying the contract');
}
@@ -110,6 +106,7 @@ export function useContractQueries() {
const code_bytes = new TextEncoder().encode(code);
const result = await genlayer.client?.deployContract({
+ account: accountsStore.currentUserAccount as Account,
code: code_bytes as any as string, // FIXME: code should accept both bytes and string in genlayer-js
args: args.args,
leaderOnly,
@@ -143,6 +140,7 @@ export function useContractQueries() {
type: 'error',
title: 'Error deploying contract',
});
+ console.error('Error Deploying the contract', error);
throw new Error('Error Deploying the contract');
}
}
@@ -209,11 +207,16 @@ export function useContractQueries() {
leaderOnly: boolean;
}) {
try {
- if (!accountsStore.currentPrivateKey) {
+ if (!accountsStore.currentUserAccount) {
throw new Error('Error writing to contract');
}
+ console.log(
+ '🚀 ~ useContractQueries ~ accountsStore.currentUserAccount:',
+ accountsStore.currentUserAccount,
+ );
const result = await genlayer.client?.writeContract({
+ account: accountsStore.currentUserAccount as Account,
address: address.value as Address,
functionName: method,
args: args.args,
diff --git a/frontend/src/hooks/useGenlayer.ts b/frontend/src/hooks/useGenlayer.ts
index e9812c104..da1df97a1 100644
--- a/frontend/src/hooks/useGenlayer.ts
+++ b/frontend/src/hooks/useGenlayer.ts
@@ -1,6 +1,6 @@
import { simulator } from 'genlayer-js/chains';
-import { createClient, createAccount } from 'genlayer-js';
-import type { GenLayerClient } from 'genlayer-js/types';
+import { createClient } from 'genlayer-js';
+import type { Account, GenLayerClient } from 'genlayer-js/types';
import { watch } from 'vue';
import { useAccountsStore } from '@/stores';
@@ -13,18 +13,20 @@ export function useGenlayer() {
initClient();
}
- watch(
- () => accountsStore.currentUserAddress,
- () => {
- initClient();
- },
- );
+ watch([() => accountsStore.currentUserAccount?.address], () => {
+ initClient();
+ });
function initClient() {
+ const clientAccount =
+ accountsStore.currentUserAccount?.type === 'local'
+ ? (accountsStore.currentUserAccount as Account)
+ : accountsStore.currentUserAccount?.address;
+
client = createClient({
chain: simulator,
endpoint: import.meta.env.VITE_JSON_RPC_SERVER_URL,
- account: createAccount(accountsStore.currentPrivateKey || undefined),
+ account: clientAccount,
});
}
diff --git a/frontend/src/hooks/useSetupStores.ts b/frontend/src/hooks/useSetupStores.ts
index 878eaec32..4a1bd42e8 100644
--- a/frontend/src/hooks/useSetupStores.ts
+++ b/frontend/src/hooks/useSetupStores.ts
@@ -7,7 +7,6 @@ import {
} from '@/stores';
import { useDb, useGenlayer, useTransactionListener } from '@/hooks';
import { v4 as uuidv4 } from 'uuid';
-import type { Address } from '@/types';
export const useSetupStores = () => {
const setupStores = async () => {
@@ -60,14 +59,6 @@ export const useSetupStores = () => {
if (accountsStore.privateKeys.length < 1) {
accountsStore.generateNewAccount();
- } else {
- accountsStore.privateKeys = localStorage.getItem(
- 'accountsStore.privateKeys',
- )
- ? ((localStorage.getItem('accountsStore.privateKeys') || '').split(
- ',',
- ) as Address[])
- : [];
}
genlayer.initClient();
diff --git a/frontend/src/plugins/persistStore.ts b/frontend/src/plugins/persistStore.ts
index 535987dd7..dc376312e 100644
--- a/frontend/src/plugins/persistStore.ts
+++ b/frontend/src/plugins/persistStore.ts
@@ -113,23 +113,23 @@ export function persistStorePlugin(context: PiniaPluginContext): void {
default:
break;
}
- } else if (store.$id === 'accountsStore') {
- switch (name) {
- case 'generateNewAccount':
- case 'removeAccount':
- case 'setCurrentAccount':
- localStorage.setItem(
- 'accountsStore.privateKeys',
- store.privateKeys.join(','),
- );
- localStorage.setItem(
- 'accountsStore.currentPrivateKey',
- store.currentPrivateKey,
- );
- break;
- default:
- break;
- }
+ // } else if (store.$id === 'accountsStore') {
+ // switch (name) {
+ // case 'generateNewAccount':
+ // case 'removeAccount':
+ // case 'setCurrentAccount':
+ // localStorage.setItem(
+ // 'accountsStore.privateKeys',
+ // store.privateKeys.join(','),
+ // );
+ // localStorage.setItem(
+ // 'accountsStore.currentPrivateKey',
+ // store.currentPrivateKey,
+ // );
+ // break;
+ // default:
+ // break;
+ // }
} else if (store.$id === 'transactionsStore') {
switch (name) {
case 'addTransaction':
diff --git a/frontend/src/stores/accounts.ts b/frontend/src/stores/accounts.ts
index ab4079ea5..bc15af597 100644
--- a/frontend/src/stores/accounts.ts
+++ b/frontend/src/stores/accounts.ts
@@ -2,67 +2,200 @@ import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import type { Address } from '@/types';
import { createAccount, generatePrivateKey } from 'genlayer-js';
-import type { Account } from 'genlayer-js/types';
import { useShortAddress } from '@/hooks';
+export interface AccountInfo {
+ type: 'local' | 'metamask';
+ address: Address;
+ privateKey?: Address; // Only for local accounts
+}
+
export const useAccountsStore = defineStore('accountsStore', () => {
- const key = localStorage.getItem('accountsStore.currentPrivateKey');
- const currentPrivateKey = ref(key ? (key as Address) : null);
- const currentUserAddress = computed(() =>
- currentPrivateKey.value
- ? createAccount(currentPrivateKey.value).address
- : '',
- );
const { shorten } = useShortAddress();
- const privateKeys = ref(
- localStorage.getItem('accountsStore.privateKeys')
- ? ((localStorage.getItem('accountsStore.privateKeys') || '').split(
- ',',
- ) as Address[])
- : [],
- );
+ // Store all accounts (both local and MetaMask)
+ const accounts = ref([]);
+ const selectedAccount = ref(null);
+
+ // Initialize local accounts from localStorage
+ const storedKeys = localStorage.getItem('accountsStore.privateKeys');
+ if (storedKeys) {
+ const privateKeys = storedKeys.split(',') as Address[];
+ accounts.value = privateKeys.map((key) => ({
+ type: 'local',
+ address: createAccount(key).address,
+ privateKey: key,
+ }));
+
+ // Set initial selected account if stored
+ const storedSelectedKey = localStorage.getItem(
+ 'accountsStore.currentPrivateKey',
+ );
+ if (storedSelectedKey) {
+ selectedAccount.value =
+ accounts.value.find((acc) => acc.privateKey === storedSelectedKey) ||
+ null;
+ }
+ }
+
+ async function fetchMetaMaskAccount() {
+ if (window.ethereum) {
+ const ethAccounts = await window.ethereum.request({
+ method: 'eth_requestAccounts',
+ });
+
+ const metamaskAccount: AccountInfo = {
+ type: 'metamask',
+ address: ethAccounts[0] as Address,
+ };
+
+ // Update or add MetaMask account
+ const existingMetaMaskIndex = accounts.value.findIndex(
+ (acc) => acc.type === 'metamask',
+ );
+ if (existingMetaMaskIndex >= 0) {
+ accounts.value[existingMetaMaskIndex] = metamaskAccount;
+ } else {
+ accounts.value.push(metamaskAccount);
+ }
+
+ setCurrentAccount(metamaskAccount);
+ }
+ }
+
+ if (window.ethereum) {
+ window.ethereum.on('accountsChanged', (newAccounts: string[]) => {
+ if (newAccounts.length > 0) {
+ const metamaskAccount: AccountInfo = {
+ type: 'metamask',
+ address: newAccounts[0] as Address,
+ };
+
+ const existingMetaMaskIndex = accounts.value.findIndex(
+ (acc) => acc.type === 'metamask',
+ );
+ if (existingMetaMaskIndex >= 0) {
+ accounts.value[existingMetaMaskIndex] = metamaskAccount;
+ }
+
+ if (selectedAccount.value?.type === 'metamask') {
+ setCurrentAccount(metamaskAccount);
+ }
+ }
+ });
+ }
function generateNewAccount(): Address {
const privateKey = generatePrivateKey();
- privateKeys.value = [...privateKeys.value, privateKey];
- setCurrentAccount(privateKey);
+ const newAccount: AccountInfo = {
+ type: 'local',
+ address: createAccount(privateKey).address,
+ privateKey,
+ };
+
+ accounts.value.push(newAccount);
+ setCurrentAccount(newAccount);
+
+ const storedKeys = localStorage.getItem('accountsStore.privateKeys');
+ if (storedKeys) {
+ localStorage.setItem(
+ 'accountsStore.privateKeys',
+ [...storedKeys.split(','), privateKey].join(','),
+ );
+ } else {
+ localStorage.setItem('accountsStore.privateKeys', privateKey);
+ }
return privateKey;
}
- function removeAccount(privateKey: Address) {
- if (privateKeys.value.length <= 1) {
- throw new Error('You need at least 1 account');
+ function removeAccount(accountToRemove: AccountInfo) {
+ if (
+ accounts.value.filter((acc) => acc.type === 'local').length <= 1 &&
+ accountToRemove.type === 'local'
+ ) {
+ throw new Error('You need at least 1 local account');
}
- privateKeys.value = privateKeys.value.filter((k) => k !== privateKey);
+ accounts.value = accounts.value.filter((acc) =>
+ acc.type === 'metamask'
+ ? acc.address !== accountToRemove.address
+ : acc.privateKey !== accountToRemove.privateKey,
+ );
- if (currentPrivateKey.value === privateKey) {
- setCurrentAccount(privateKeys.value[0] || null);
+ if (selectedAccount.value === accountToRemove) {
+ const firstLocalAccount = accounts.value.find(
+ (acc) => acc.type === 'local',
+ );
+ if (firstLocalAccount) {
+ setCurrentAccount(firstLocalAccount);
+ }
}
}
- function setCurrentAccount(privateKey: Address) {
- currentPrivateKey.value = privateKey;
+ function setCurrentAccount(account: AccountInfo | null) {
+ selectedAccount.value = account;
+
+ // Persist local account selection to localStorage
+ if (account?.type === 'local' && account.privateKey) {
+ localStorage.setItem(
+ 'accountsStore.currentPrivateKey',
+ account.privateKey,
+ );
+ } else {
+ // Clear stored private key if no local account is selected
+ localStorage.removeItem('accountsStore.currentPrivateKey');
+ }
}
const displayAddress = computed(() => {
+ if (!selectedAccount.value) return '';
try {
- if (!currentPrivateKey.value) {
- return '';
- } else {
- return shorten(createAccount(currentPrivateKey.value).address);
- }
+ return shorten(selectedAccount.value.address);
} catch (err) {
console.error(err);
return '0x';
}
});
+ const currentUserAccount = computed(() => {
+ if (!selectedAccount.value) return undefined;
+
+ if (
+ selectedAccount.value.type === 'local' &&
+ selectedAccount.value.privateKey
+ ) {
+ return createAccount(selectedAccount.value.privateKey);
+ }
+
+ if (selectedAccount.value.type === 'metamask') {
+ // For MetaMask accounts, return a minimal account interface with just the address
+ return {
+ address: selectedAccount.value.address,
+ type: 'metamask',
+ };
+ }
+
+ return undefined;
+ });
+
+ const currentUserAddress = computed(() =>
+ selectedAccount.value ? selectedAccount.value.address : '',
+ );
+
+ // For backwards compatibility and persistence
+ const privateKeys = computed(() =>
+ accounts.value
+ .filter((acc) => acc.type === 'local')
+ .map((acc) => acc.privateKey!),
+ );
+
return {
+ accounts,
+ selectedAccount,
+ currentUserAccount,
currentUserAddress,
- currentPrivateKey,
privateKeys,
+ fetchMetaMaskAccount,
generateNewAccount,
removeAccount,
setCurrentAccount,
diff --git a/frontend/test/unit/stores/accounts.test.ts b/frontend/test/unit/stores/accounts.test.ts
index 624af4f27..3ae79de0b 100644
--- a/frontend/test/unit/stores/accounts.test.ts
+++ b/frontend/test/unit/stores/accounts.test.ts
@@ -113,3 +113,41 @@ describe('useAccountsStore', () => {
expect(accountsStore.currentUserAddress).toBe('');
});
});
+
+describe('fetchMetaMaskAccount', () => {
+ let accountsStore: ReturnType;
+
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ accountsStore = useAccountsStore();
+
+ // Mock `window.ethereum`
+ vi.stubGlobal('window', {
+ ethereum: {
+ request: vi.fn(),
+ on: vi.fn(),
+ },
+ });
+ });
+
+ it('should fetch the MetaMask account and set walletAddress', async () => {
+ const testAccount = '0x1234567890abcdef1234567890abcdef12345678';
+ (window?.ethereum?.request as Mock).mockResolvedValueOnce([testAccount]);
+
+ await accountsStore.fetchMetaMaskAccount();
+
+ expect(window?.ethereum?.request).toHaveBeenCalledWith({
+ method: 'eth_requestAccounts',
+ });
+ expect(accountsStore.walletAddress).toBe(testAccount);
+ expect(accountsStore.isWalletSelected).toBe(true);
+ });
+
+ it('should not set walletAddress if window.ethereum is undefined', async () => {
+ vi.stubGlobal('window', {}); // Remove ethereum from window object
+
+ await accountsStore.fetchMetaMaskAccount();
+
+ expect(accountsStore.walletAddress).toBe(undefined);
+ });
+});
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
index 0959803c6..24368090b 100644
--- a/frontend/tsconfig.app.json
+++ b/frontend/tsconfig.app.json
@@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
- "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "test"],
+ "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "test", "src/global.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"types": ["node", "jsdom"],