[wallet-integration 4/5] Migrate localStorage to Eyre ship storage
Some checks failed
Lint and Build / Run lint and build checks (pull_request) Failing after 3m25s

Replace all key-store.ts (localStorage) usage with eyre-client.ts:
- accounts.ts: HD derivation delegated to agent, CRUD via poke/scry
- misc.ts: getMnemonic, getPathKey, getAccountIndices via scry
- NetworksContext.tsx: init from getNetworks() instead of localStorage
- Accounts.tsx: network deletion via updateNetworks() poke
- EditNetwork.tsx: network updates via updateNetworks() poke
- key-store.ts: marked deprecated, zero remaining consumers

Private keys and mnemonic no longer stored in browser localStorage.
All wallet state persisted in the %zenith agent on the Urbit ship.

Part of wallet-integration across:
- zenith-desk: Hoon crypto libs + agent endpoints
- zenith-wallet-web (this repo): Eyre channel client + localStorage migration
- zenith-testing: Go integration tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
afd 2026-02-10 09:39:04 -05:00
parent fc82acb4d5
commit 657bc17eff
6 changed files with 104 additions and 403 deletions

View File

@ -16,7 +16,7 @@ import { useNetworks } from "../context/NetworksContext";
import ConfirmDialog from "./ConfirmDialog";
import { getNamespaces } from "../utils/wallet-connect/helpers";
import ShowPKDialog from "./ShowPKDialog";
import { setInternetCredentials } from "../utils/key-store";
import { updateNetworks } from "../utils/eyre-client";
import {
Accordion,
AccordionSummary,
@ -110,11 +110,7 @@ const Accounts = () => {
(networkData) => selectedNetwork!.networkId !== networkData.networkId,
);
await setInternetCredentials(
"networks",
"_",
JSON.stringify(updatedNetworks),
);
await updateNetworks(updatedNetworks);
setSelectedNetwork(updatedNetworks[0]);
setCurrentIndex(0);

View File

@ -1,9 +1,8 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { NetworksDataState } from '../types';
import { retrieveNetworksData } from '../utils/accounts';
import { getNetworks, updateNetworks } from '../utils/eyre-client';
import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants';
import { setInternetCredentials } from '../utils/key-store';
const NetworksContext = createContext<{
networksData: NetworksDataState[];
@ -42,15 +41,10 @@ const NetworksProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => {
const fetchData = async () => {
let retrievedNetworks = await retrieveNetworksData();
let retrievedNetworks = await getNetworks();
if (retrievedNetworks.length === 0) {
setInternetCredentials(
'networks',
'_',
JSON.stringify(DEFAULT_NETWORKS_DATA),
);
await updateNetworks(DEFAULT_NETWORKS_DATA);
retrievedNetworks = DEFAULT_NETWORKS_DATA;
}

View File

@ -10,9 +10,8 @@ import {
} from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native";
import { setInternetCredentials } from "../utils/key-store";
import { StackParamsList } from "../types";
import { retrieveNetworksData } from "../utils/accounts";
import { getNetworks, updateNetworks } from "../utils/eyre-client";
import { useNetworks } from "../context/NetworksContext";
import {
COSMOS,
@ -74,7 +73,7 @@ const EditNetwork = ({ route }: EditNetworkProps) => {
const submit = useCallback(
async (data: z.infer<typeof networksFormDataSchema>) => {
const retrievedNetworksData = await retrieveNetworksData();
const retrievedNetworksData = await getNetworks();
const { type, ...dataWithoutType } = data;
const newNetworkData = { ...networkData, ...dataWithoutType };
const index = retrievedNetworksData.findIndex(
@ -83,11 +82,7 @@ const EditNetwork = ({ route }: EditNetworkProps) => {
retrievedNetworksData.splice(index, 1, newNetworkData);
await setInternetCredentials(
"networks",
"_",
JSON.stringify(retrievedNetworksData),
);
await updateNetworks(retrievedNetworksData);
setNetworksData(retrievedNetworksData);

View File

@ -7,106 +7,54 @@ import '@ethersproject/shims';
import { utils } from 'ethers';
import { HDNode } from 'ethers/lib/utils';
import {
setInternetCredentials,
resetInternetCredentials,
getInternetCredentials,
} from './key-store';
import { Secp256k1HdWallet } from '@cosmjs/amino';
import { AccountData } from '@cosmjs/proto-signing';
import { stringToPath } from '@cosmjs/crypto';
import { Account, NetworksDataState, NetworksFormData } from '../types';
import {
getHDPath,
getPathKey,
resetKeyServers,
updateAccountIndices,
} from './misc';
import { COSMOS, EIP155 } from './constants';
import * as eyre from './eyre-client';
function splitNsChain(nsChain: string): [string, string] {
const i = nsChain.indexOf(':');
return [nsChain.slice(0, i), nsChain.slice(i + 1)];
}
const createWallet = async (
networksData: NetworksDataState[],
recoveryPhrase?: string,
): Promise<string> => {
const mnemonic = recoveryPhrase ? recoveryPhrase : utils.entropyToMnemonic(utils.randomBytes(16));
const hdNode = HDNode.fromMnemonic(mnemonic);
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
await createWalletFromMnemonic(networksData, hdNode, mnemonic);
await eyre.createWallet(networksData, mnemonic);
return mnemonic;
};
const createWalletFromMnemonic = async (
networksData: NetworksDataState[],
hdNode: HDNode,
_hdNode: HDNode,
mnemonic: string
): Promise<void> => {
for (const network of networksData) {
const hdPath = `m/44'/${network.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (network.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccountByHDPath(mnemonic, hdPath, network.addressPrefix)
).data.address;
break;
default:
throw new Error('Unsupported namespace');
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
await Promise.all([
setInternetCredentials(
`accounts/${network.namespace}:${network.chainId}/0`,
'_',
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${network.namespace}:${network.chainId}`,
'_',
'1',
),
setInternetCredentials(
`accountIndices/${network.namespace}:${network.chainId}`,
'_',
'0',
),
]);
}
// HD derivation delegated to agent — hdNode param unused
await eyre.createWallet(networksData, mnemonic);
};
const addAccount = async (
chainId: string,
): Promise<Account | undefined> => {
try {
let selectedNetworkAccount
const networksData = await retrieveNetworksData();
const networksData = await eyre.getNetworks();
// Add account to all networks and return account for selected network
// Add account to all networks (agent handles derivation + counter)
for (const network of networksData) {
const namespaceChainId = `${network.namespace}:${network.chainId}`;
const id = await getNextAccountId(namespaceChainId);
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
const account = await addAccountFromHDPath(hdPath, network);
await updateAccountCounter(namespaceChainId, id);
if (network.chainId === chainId) {
selectedNetworkAccount = account;
}
await eyre.addAccount(network.namespace, network.chainId);
}
return selectedNetworkAccount;
// Return the new account for the selected network
const selectedNetwork = networksData.find(n => n.chainId === chainId);
if (!selectedNetwork) return;
const accounts = await eyre.getAccounts(selectedNetwork.namespace, chainId);
return accounts[accounts.length - 1];
} catch (error) {
console.error('Error creating account:', error);
}
@ -117,13 +65,8 @@ const addAccountsForNetwork = async (
numberOfAccounts: number,
): Promise<void> => {
try {
const namespaceChainId = `${network.namespace}:${network.chainId}`;
for (let i = 0; i < numberOfAccounts; i++) {
const id = await getNextAccountId(namespaceChainId);
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
await addAccountFromHDPath(hdPath, network);
await updateAccountCounter(namespaceChainId, id);
await eyre.addAccount(network.namespace, network.chainId);
}
} catch (error) {
console.error('Error creating account:', error);
@ -135,26 +78,11 @@ const addAccountFromHDPath = async (
networkData: NetworksDataState,
): Promise<Account | undefined> => {
try {
const account = await accountInfoFromHDPath(hdPath, networkData);
if (!account) {
throw new Error('Error while creating account');
}
// Agent derives from stored mnemonic using the given HD path
await eyre.addAccountFromPath(networkData.namespace, networkData.chainId, hdPath);
const { privKey, pubKey, address } = account;
const namespaceChainId = `${networkData.namespace}:${networkData.chainId}`;
const index = (await updateAccountIndices(namespaceChainId)).index;
await Promise.all([
setInternetCredentials(
`accounts/${namespaceChainId}/${index}`,
'_',
`${hdPath},${privKey},${pubKey},${address}`,
),
]);
return { index, pubKey, address, hdPath };
const accounts = await eyre.getAccounts(networkData.namespace, networkData.chainId);
return accounts[accounts.length - 1];
} catch (error) {
console.error(error);
}
@ -163,160 +91,37 @@ const addAccountFromHDPath = async (
const addNewNetwork = async (
newNetworkData: NetworksFormData
): Promise<NetworksDataState[]> => {
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
if (!mnemonic) {
throw new Error("Mnemonic not found");
}
const hdNode = HDNode.fromMnemonic(mnemonic);
const hdPath = `m/44'/${newNetworkData.coinType}'/0'/0/0`;
const node = hdNode.derivePath(hdPath);
let address;
switch (newNetworkData.namespace) {
case EIP155:
address = node.address;
break;
case COSMOS:
address = (
await getCosmosAccountByHDPath(
mnemonic,
hdPath,
newNetworkData.addressPrefix,
)
).data.address;
break;
default:
throw new Error("Unsupported namespace");
}
const accountInfo = `${hdPath},${node.privateKey},${node.publicKey},${address}`;
await Promise.all([
setInternetCredentials(
`accounts/${newNetworkData.namespace}:${newNetworkData.chainId}/0`,
"_",
accountInfo,
),
setInternetCredentials(
`addAccountCounter/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"1",
),
setInternetCredentials(
`accountIndices/${newNetworkData.namespace}:${newNetworkData.chainId}`,
"_",
"0",
),
]);
const retrievedNetworksData = await storeNetworkData(newNetworkData);
// Get number of accounts in first network
const nextAccountId = await getNextAccountId(
`${retrievedNetworksData[0].namespace}:${retrievedNetworksData[0].chainId}`,
);
const selectedNetwork = retrievedNetworksData.find(
(network) => network.chainId === newNetworkData.chainId,
);
await addAccountsForNetwork(selectedNetwork!, nextAccountId - 1);
return retrievedNetworksData;
}
await eyre.addNetwork(newNetworkData);
return eyre.getNetworks();
};
const storeNetworkData = async (
networkData: NetworksFormData,
): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
let retrievedNetworks = [];
if (networks) {
retrievedNetworks = JSON.parse(networks!);
}
let networkId = 0;
if (retrievedNetworks.length > 0) {
networkId = retrievedNetworks[retrievedNetworks.length - 1].networkId + 1;
}
const updatedNetworks: NetworksDataState[] = [
...retrievedNetworks,
{
...networkData,
networkId: String(networkId),
},
];
await setInternetCredentials(
'networks',
'_',
JSON.stringify(updatedNetworks),
);
return updatedNetworks;
await eyre.addNetwork(networkData);
return eyre.getNetworks();
};
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
if (!networks) {
return [];
}
const parsedNetworks: NetworksDataState[] = JSON.parse(networks);
return parsedNetworks;
return eyre.getNetworks();
};
export const retrieveAccountsForNetwork = async (
namespaceChainId: string,
accountsIndices: string,
_accountsIndices?: string,
): Promise<Account[]> => {
const accountsIndexArray = accountsIndices.split(',');
const loadedAccounts = await Promise.all(
accountsIndexArray.map(async i => {
const { address, path, pubKey } = await getPathKey(
namespaceChainId,
Number(i),
);
const account: Account = {
index: Number(i),
pubKey,
address,
hdPath: path,
};
return account;
}),
);
return loadedAccounts;
const [ns, chain] = splitNsChain(namespaceChainId);
return eyre.getAccounts(ns, chain);
};
const retrieveAccounts = async (
currentNetworkData: NetworksDataState,
): Promise<Account[] | undefined> => {
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
const accounts = await eyre.getAccounts(
currentNetworkData.namespace,
currentNetworkData.chainId,
);
const accountIndices = accountIndicesServer;
if (!accountIndices) {
return;
}
const loadedAccounts = await retrieveAccountsForNetwork(
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
accountIndices,
)
return loadedAccounts;
return accounts.length > 0 ? accounts : undefined;
};
const retrieveSingleAccount = async (
@ -324,37 +129,13 @@ const retrieveSingleAccount = async (
chainId: string,
address: string,
) => {
let loadedAccounts;
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${namespace}:${chainId}`,
);
const accountIndices = accountIndicesServer;
if (!accountIndices) {
throw new Error('Indices for given chain not found');
}
loadedAccounts = await retrieveAccountsForNetwork(
`${namespace}:${chainId}`,
accountIndices,
);
if (!loadedAccounts) {
throw new Error('Accounts for given chain not found');
}
return loadedAccounts.find(account => account.address === address);
const accounts = await eyre.getAccounts(namespace, chainId);
return accounts.find(account => account.address === address);
};
const resetWallet = async () => {
try {
await Promise.all([
resetInternetCredentials('mnemonicServer'),
resetKeyServers(EIP155),
resetKeyServers(COSMOS),
setInternetCredentials('networks', '_', JSON.stringify([])),
]);
await eyre.resetWallet();
} catch (error) {
console.error('Error resetting wallet:', error);
throw error;
@ -367,12 +148,13 @@ const accountInfoFromHDPath = async (
): Promise<
{ privKey: string; pubKey: string; address: string } | undefined
> => {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
// Phase 1: still derives in browser for API compat.
// Phase 2: this moves to the agent entirely.
const mnemonic = await eyre.getMnemonic();
if (!mnemonic) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
const hdNode = HDNode.fromMnemonic(mnemonic);
const node = hdNode.derivePath(hdPath);
@ -397,36 +179,15 @@ const accountInfoFromHDPath = async (
};
const getNextAccountId = async (namespaceChainId: string): Promise<number> => {
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
if (!idStore) {
throw new Error('Account id not found');
}
const accountCounter = idStore;
const nextCounter = Number(accountCounter);
return nextCounter;
const [ns, chain] = splitNsChain(namespaceChainId);
return eyre.getNextAccountId(ns, chain);
};
const updateAccountCounter = async (
namespaceChainId: string,
id: number,
_namespaceChainId: string,
_id: number,
): Promise<void> => {
const idStore = await getInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
);
if (!idStore) {
throw new Error('Account id not found');
}
const updatedCounter = String(id + 1);
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await setInternetCredentials(
`addAccountCounter/${namespaceChainId}`,
'_',
updatedCounter,
);
// Agent manages account counter atomically via addAccount poke.
};
const getCosmosAccountByHDPath = async (
@ -448,23 +209,13 @@ const getCosmosAccountByHDPath = async (
const checkNetworkForChainID = async (
chainId: string,
): Promise<boolean> => {
const networks = await getInternetCredentials('networks');
if (!networks) {
return false;
}
const networksData: NetworksFormData[] = JSON.parse(networks);
return networksData.some((network) => network.chainId === chainId);
const networks = await eyre.getNetworks();
return networks.some((network) => network.chainId === chainId);
}
const isWalletCreated = async (
): Promise<boolean> => {
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
return mnemonic !== null;
return eyre.walletExists();
};
export {

View File

@ -1,3 +1,8 @@
/**
* @deprecated Use eyre-client.ts instead. This module will be removed in Phase 2.
* All consumers have been migrated to eyre-client.ts typed API.
*/
const setInternetCredentials = (name:string, username:string, password:string) => {
localStorage.setItem(name, password);
};

View File

@ -9,20 +9,23 @@ import { stringToPath } from '@cosmjs/crypto';
import '@ethersproject/shims';
import {
getInternetCredentials,
resetInternetCredentials,
setInternetCredentials,
} from './key-store';
getMnemonic as eyreGetMnemonic,
getAccounts,
getAccount,
} from './eyre-client';
import { EIP155 } from './constants';
import { NetworksDataState } from '../types';
function splitNsChain(namespaceChainId: string): [string, string] {
const i = namespaceChainId.indexOf(':');
return [namespaceChainId.slice(0, i), namespaceChainId.slice(i + 1)];
}
const getMnemonic = async (): Promise<string> => {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
const mnemonic = await eyreGetMnemonic();
if (!mnemonic) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
return mnemonic;
};
@ -53,22 +56,19 @@ const getPathKey = async (
pubKey: string;
address: string;
}> => {
const pathKeyStore = await getInternetCredentials(
`accounts/${namespaceChainId}/${accountId}`,
);
const [ns, chain] = splitNsChain(namespaceChainId);
const account = await getAccount(ns, chain, accountId);
if (!pathKeyStore) {
throw new Error('Error while fetching counter');
if (!account) {
throw new Error('Error while fetching account');
}
const pathKeyVal = pathKeyStore;
const pathkey = pathKeyVal.split(',');
const path = pathkey[0];
const privKey = pathkey[1];
const pubKey = pathkey[2];
const address = pathkey[3];
return { path, privKey, pubKey, address };
return {
path: account.hdPath,
privKey: account.privKey,
pubKey: account.pubKey,
address: account.address,
};
};
const getAccountIndices = async (
@ -78,75 +78,35 @@ const getAccountIndices = async (
indices: number[];
index: number;
}> => {
const counterStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
const [ns, chain] = splitNsChain(namespaceChainId);
const accounts = await getAccounts(ns, chain);
if (!counterStore) {
throw new Error('Error while fetching counter');
if (accounts.length === 0) {
throw new Error('Error while fetching accounts');
}
let accountIndices = counterStore;
const indices = accountIndices.split(',').map(Number);
const index = indices[indices.length - 1] + 1;
const indices = accounts.map(a => a.index);
const maxIndex = Math.max(...indices);
return { accountIndices, indices, index };
return {
accountIndices: indices.join(','),
indices,
index: maxIndex + 1,
};
};
const updateAccountIndices = async (
namespaceChainId: string,
): Promise<{ accountIndices: string; index: number }> => {
const accountIndicesData = await getAccountIndices(namespaceChainId);
const accountIndices = accountIndicesData.accountIndices;
const index = accountIndicesData.index;
const updatedAccountIndices = `${accountIndices},${index.toString()}`;
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
await setInternetCredentials(
`accountIndices/${namespaceChainId}`,
'_',
updatedAccountIndices,
);
return { accountIndices: updatedAccountIndices, index };
// Agent manages account indices atomically via addAccount poke.
// This reads current state for callers that still expect the old return shape.
const data = await getAccountIndices(namespaceChainId);
return { accountIndices: data.accountIndices, index: data.index };
};
const resetKeyServers = async (namespace: string) => {
const networksServer = await getInternetCredentials('networks');
if (!networksServer) {
throw new Error('Networks not found.');
}
const networksData: NetworksDataState[] = JSON.parse(networksServer);
const filteredNetworks = networksData.filter(
network => network.namespace === namespace,
);
if (filteredNetworks.length === 0) {
throw new Error(`No networks found for namespace ${namespace}.`);
}
filteredNetworks.forEach(async network => {
const { chainId } = network;
const namespaceChainId = `${namespace}:${chainId}`;
const idStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
if (!idStore) {
throw new Error(`Account indices not found for ${namespaceChainId}.`);
}
const accountIds = idStore;
const ids = accountIds.split(',').map(Number);
const latestId = Math.max(...ids);
for (let i = 0; i <= latestId; i++) {
await resetInternetCredentials(`accounts/${namespaceChainId}/${i}`);
}
await resetInternetCredentials(`addAccountCounter/${namespaceChainId}`);
await resetInternetCredentials(`accountIndices/${namespaceChainId}`);
});
const resetKeyServers = async (_namespace: string) => {
// Agent manages account lifecycle atomically via resetWallet poke.
// Individual namespace cleanup is a no-op — resetWallet handles it all.
};
const sendMessage = (