Compare commits

..

2 Commits

Author SHA1 Message Date
986c9a431f Take private key from browser localstorage instead of accounts context (#16)
Some checks failed
Lint and Build / Run lint and build checks (push) Has been cancelled
Publish wallet docker image on release / Run docker build and publish (release) Successful in 5m7s
Part of https://plan.wireit.in/deepstack/browse/VUL-327/

Reviewed-on: #16
Co-authored-by: Shreerang Kale <shree@deepstacksoft.com>
Co-committed-by: Shreerang Kale <shree@deepstacksoft.com>
2026-02-20 11:09:04 +00:00
2bf84b2920 Use RPC URL from external app if the network already exists (#15)
All checks were successful
Lint and Build / Run lint and build checks (push) Successful in 5m13s
Publish wallet docker image on release / Run docker build and publish (release) Successful in 5m2s
Part of https://plan.wireit.in/deepstack/browse/VUL-327/

Reviewed-on: #15
Co-authored-by: Shreerang Kale <shree@deepstacksoft.com>
Co-committed-by: Shreerang Kale <shree@deepstacksoft.com>
2026-02-17 10:30:30 +00:00
18 changed files with 459 additions and 1155 deletions

View File

@ -1,65 +0,0 @@
#!/bin/bash
#
# Build and deploy zenith-wallet-web as an Urbit glob.
#
# Prerequisites:
# - yarn installed
# - An Urbit ship running with a pier at $PIER_PATH
# - The desk 'zenith-wallet' already created on the ship
# - IPFS node running (for glob upload)
#
# Usage:
# PIER_PATH=/path/to/pier ./build-glob.sh
#
set -euo pipefail
DESK_NAME="zenith-wallet"
if [ -z "${PIER_PATH:-}" ]; then
echo "Error: PIER_PATH must be set to your Urbit pier directory"
exit 1
fi
DESK_PATH="${PIER_PATH}/${DESK_NAME}"
# 1. Build the React app
echo "==> Building React app..."
yarn && yarn build
# 2. Lowercase all filenames in build/ (Urbit requires lowercase)
echo "==> Lowercasing filenames in build/..."
find build -depth -name '*[A-Z]*' | while read -r f; do
dir="$(dirname "$f")"
base="$(basename "$f")"
lower="$(echo "$base" | tr '[:upper:]' '[:lower:]')"
if [ "$base" != "$lower" ]; then
mv "$f" "${dir}/${lower}"
fi
done
# 3. Copy build output to the ship's desk
echo "==> Copying build/ to ${DESK_PATH}..."
mkdir -p "${DESK_PATH}"
cp -r build/* "${DESK_PATH}/"
# 4. Copy mark files for non-standard extensions
echo "==> Copying mark files..."
mkdir -p "${DESK_PATH}/mar"
cp urbit-files/mar/*.hoon "${DESK_PATH}/mar/"
# 5. Copy desk.docket-0
echo "==> Copying desk.docket-0..."
cp urbit-files/desk.docket-0 "${DESK_PATH}/desk.docket-0"
echo ""
echo "==> Done! Next steps:"
echo " 1. In your ship's dojo, run:"
echo " |commit %${DESK_NAME}"
echo " 2. Create the glob:"
echo " -garden!make-glob %${DESK_NAME} /build"
echo " 3. Upload the glob file from .urb/put/ to IPFS"
echo " 4. Update desk.docket-0 with the IPFS URL and hash"
echo " 5. Recommit:"
echo " |commit %${DESK_NAME}"
echo " 6. Install the desk:"
echo " |install our %${DESK_NAME}"

View File

@ -1,6 +1,6 @@
{
"name": "web-wallet",
"version": "0.1.7-zenith-0.2.3",
"version": "0.1.7-zenith-0.2.5",
"private": true,
"dependencies": {
"@laconic-network/cosmjs-util": "^0.1.0",

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 { updateNetworks } from "../utils/eyre-client";
import { setInternetCredentials } from "../utils/key-store";
import {
Accordion,
AccordionSummary,
@ -110,7 +110,11 @@ const Accounts = () => {
(networkData) => selectedNetwork!.networkId !== networkData.networkId,
);
await updateNetworks(updatedNetworks);
await setInternetCredentials(
"networks",
"_",
JSON.stringify(updatedNetworks),
);
setSelectedNetwork(updatedNetworks[0]);
setCurrentIndex(0);

View File

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

View File

@ -1,6 +1,6 @@
import { useEffect, useCallback } from "react";
import { addNewNetwork, createWallet, checkNetworkForChainID, isWalletCreated } from "../utils/accounts";
import { addNewNetwork, createWallet, checkNetworkForChainID, isWalletCreated, updateNetworkRpcUrl } from "../utils/accounts";
import { useNetworks } from "../context/NetworksContext";
import { NETWORK_ADDED_RESPONSE, NETWORK_ADD_FAILED_RESPONSE, NETWORK_ALREADY_EXISTS_RESPONSE, REQUEST_ADD_NETWORK } from "../utils/constants";
import { NetworksFormData } from "../types";
@ -52,6 +52,11 @@ const useCreateNetwork = () => {
chainId
}, sourceOrigin);
} else {
console.log("Network already exists. Updating RPC URL");
const retrievedNetworksData = await updateNetworkRpcUrl(chainId, networkData.rpcUrl);
setNetworksData(retrievedNetworksData);
sendMessage(window.parent, NETWORK_ALREADY_EXISTS_RESPONSE, {
type: NETWORK_ALREADY_EXISTS_RESPONSE,
chainId

View File

@ -1,12 +1,10 @@
import { useEffect } from 'react';
import { useAccounts } from '../context/AccountsContext';
import { retrieveNetworksData, retrieveAccounts } from '../utils/accounts';
import { getPathKey, sendMessage } from '../utils/misc';
import { ACCOUNT_PK_RESPONSE, REQUEST_ACCOUNT_PK } from '../utils/constants';
const useExportPKEmbed = () => {
const { accounts } = useAccounts();
useEffect(() => {
const handleMessage = async (event: MessageEvent) => {
const { type, chainId, address } = event.data;
@ -14,9 +12,23 @@ const useExportPKEmbed = () => {
if (type !== REQUEST_ACCOUNT_PK) return;
try {
const selectedAccount = accounts.find(account => account.address === address);
// Look up the network and accounts directly from storage
// rather than relying on the accounts context, which can be
// overwritten by HomeScreen's fetchAccounts for a different network.
const networksData = await retrieveNetworksData();
const targetNetwork = networksData.find(
net => `${net.namespace}:${net.chainId}` === chainId,
);
if (!targetNetwork) {
throw new Error("Network not found");
}
const networkAccounts = await retrieveAccounts(targetNetwork);
const selectedAccount = networkAccounts?.find(account => account.address === address);
if (!selectedAccount) {
throw new Error("Account not found")
throw new Error("Account not found");
}
const pathKey = await getPathKey(chainId, selectedAccount.index);
@ -37,7 +49,7 @@ const useExportPKEmbed = () => {
return () => {
window.removeEventListener('message', handleMessage);
};
}, [accounts]);
}, []);
};
export default useExportPKEmbed;

View File

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

View File

@ -1,457 +0,0 @@
/**
* Tests for eyre-client.ts the Eyre HTTP client for %zenith agent.
*
* Mocks: global fetch, EventSource
* Strategy: jest.resetModules() + dynamic import per test group for clean
* module-level state (channelUid, cachedShipName, messageId, pendingPokes).
*/
type EyreClient = typeof import('../eyre-client');
// ---------------------------------------------------------------------------
// Mock EventSource
// ---------------------------------------------------------------------------
type ESListener = (ev: MessageEvent) => void;
type ESErrorListener = (ev: Event) => void;
class MockEventSource {
static instance: MockEventSource | null = null;
url: string;
withCredentials: boolean;
private listeners: Record<string, Array<ESListener | ESErrorListener>> = {};
close = jest.fn();
constructor(url: string, opts?: { withCredentials?: boolean }) {
this.url = url;
this.withCredentials = opts?.withCredentials ?? false;
MockEventSource.instance = this;
}
addEventListener(type: string, cb: ESListener | ESErrorListener) {
(this.listeners[type] ??= []).push(cb);
}
/** Simulate an SSE message event */
emit(type: string, data: unknown, lastEventId = '1') {
for (const cb of this.listeners[type] ?? []) {
(cb as ESListener)({
data: JSON.stringify(data),
lastEventId,
} as MessageEvent);
}
}
emitError() {
for (const cb of this.listeners['error'] ?? []) {
(cb as ESErrorListener)(new Event('error'));
}
}
}
// Install MockEventSource globally before any module loads
(global as any).EventSource = MockEventSource;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function mockFetchResponses(
...responses: Array<{ status: number; body?: unknown; text?: string }>
) {
const impl = jest.fn();
for (const r of responses) {
impl.mockResolvedValueOnce({
ok: r.status >= 200 && r.status < 300,
status: r.status,
statusText: r.status === 404 ? 'Not Found' : 'OK',
json: () => Promise.resolve(r.body),
text: () => Promise.resolve(r.text ?? ''),
});
}
global.fetch = impl;
return impl;
}
async function loadModule(): Promise<EyreClient> {
jest.resetModules();
MockEventSource.instance = null;
return import('../eyre-client');
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('eyre-client', () => {
afterEach(() => {
jest.restoreAllMocks();
});
// =========================================================================
// scryGet
// =========================================================================
describe('scryGet', () => {
it('returns parsed JSON on 200', async () => {
const { scryGet } = await loadModule();
mockFetchResponses({ status: 200, body: { mnemonic: 'foo bar' } });
const result = await scryGet('/wallet/mnemonic');
expect(result).toEqual({ mnemonic: 'foo bar' });
expect(global.fetch).toHaveBeenCalledWith(
'/~/scry/zenith/wallet/mnemonic.json',
{ credentials: 'include' },
);
});
it('returns null on 404', async () => {
const { scryGet } = await loadModule();
mockFetchResponses({ status: 404 });
const result = await scryGet('/wallet/nonexistent');
expect(result).toBeNull();
});
it('throws on server error', async () => {
const { scryGet } = await loadModule();
mockFetchResponses({ status: 500 });
await expect(scryGet('/wallet/broken')).rejects.toThrow(
'Scry /wallet/broken failed: 500',
);
});
});
// =========================================================================
// pokeAction
// =========================================================================
describe('pokeAction', () => {
it('sends correct channel PUT body and resolves on ack', async () => {
const { pokeAction } = await loadModule();
// First fetch: getShipName() → /~/host
// Second fetch: channel PUT
// Third fetch: ack PUT (from EventSource handler)
const fetchMock = mockFetchResponses(
{ status: 200, text: '~sampel-palnet' },
{ status: 200 },
{ status: 200 },
);
const pokePromise = pokeAction('json', { action: 'test' });
// Wait for fetch calls to settle
await new Promise((r) => setTimeout(r, 10));
// Verify channel PUT body
expect(fetchMock).toHaveBeenCalledTimes(2);
const putCall = fetchMock.mock.calls[1];
expect(putCall[0]).toMatch(/^\/~\/channel\/wallet-\d+$/);
expect(putCall[1].method).toBe('PUT');
const putBody = JSON.parse(putCall[1].body);
expect(putBody).toHaveLength(1);
expect(putBody[0]).toMatchObject({
action: 'poke',
ship: '~sampel-palnet',
app: 'zenith',
mark: 'json',
json: { action: 'test' },
});
// Simulate poke ack from EventSource
const es = MockEventSource.instance!;
es.emit('message', { response: 'poke', id: putBody[0].id, ok: 'ok' });
await expect(pokePromise).resolves.toBeUndefined();
});
it('rejects on poke nack', async () => {
const { pokeAction } = await loadModule();
mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 200 },
{ status: 200 },
);
const pokePromise = pokeAction('json', { action: 'bad' });
await new Promise((r) => setTimeout(r, 10));
const putBody = JSON.parse(
(global.fetch as jest.Mock).mock.calls[1][1].body,
);
const es = MockEventSource.instance!;
es.emit('message', {
response: 'poke',
id: putBody[0].id,
ok: 'err',
err: 'Invalid mark',
});
await expect(pokePromise).rejects.toThrow('Invalid mark');
});
it('rejects when channel PUT fails', async () => {
const { pokeAction } = await loadModule();
mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 500 },
);
await expect(
pokeAction('json', { action: 'fail' }),
).rejects.toThrow('Channel PUT failed: 500');
});
it('rejects all pending pokes on EventSource error', async () => {
const { pokeAction } = await loadModule();
mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 200 },
);
const pokePromise = pokeAction('json', { action: 'x' });
await new Promise((r) => setTimeout(r, 10));
MockEventSource.instance!.emitError();
await expect(pokePromise).rejects.toThrow('Eyre channel disconnected');
});
});
// =========================================================================
// getWalletData (compatibility layer)
// =========================================================================
describe('getWalletData', () => {
it('delegates mnemonicServer to scry /wallet/mnemonic', async () => {
const { getWalletData } = await loadModule();
mockFetchResponses({ status: 200, body: { mnemonic: 'test phrase' } });
const result = await getWalletData('mnemonicServer');
expect(result).toBe('test phrase');
expect(global.fetch).toHaveBeenCalledWith(
'/~/scry/zenith/wallet/mnemonic.json',
{ credentials: 'include' },
);
});
it('returns null when no mnemonic exists', async () => {
const { getWalletData } = await loadModule();
mockFetchResponses({ status: 404 });
expect(await getWalletData('mnemonicServer')).toBeNull();
});
it('delegates networks to scry /wallet/networks', async () => {
const { getWalletData } = await loadModule();
const networks = [{ networkId: 'zenith-1', networkName: 'Zenith' }];
mockFetchResponses({ status: 200, body: networks });
const result = await getWalletData('networks');
expect(result).toBe(JSON.stringify(networks));
expect(global.fetch).toHaveBeenCalledWith(
'/~/scry/zenith/wallet/networks.json',
{ credentials: 'include' },
);
});
it('returns null when no networks exist', async () => {
const { getWalletData } = await loadModule();
mockFetchResponses({ status: 200, body: [] });
expect(await getWalletData('networks')).toBeNull();
});
it('delegates accounts/eip155:1/0 to scry /wallet/account/eip155:1/0', async () => {
const { getWalletData } = await loadModule();
const account = {
index: 0,
hdPath: "m/44'/60'/0'/0/0",
privKey: '0xabc',
pubKey: '0xdef',
address: '0x123',
};
mockFetchResponses({ status: 200, body: account });
const result = await getWalletData('accounts/eip155:1/0');
expect(result).toBe(
`${account.hdPath},${account.privKey},${account.pubKey},${account.address}`,
);
expect(global.fetch).toHaveBeenCalledWith(
'/~/scry/zenith/wallet/account/eip155:1/0.json',
{ credentials: 'include' },
);
});
it('returns null for nonexistent account', async () => {
const { getWalletData } = await loadModule();
mockFetchResponses({ status: 404 });
expect(await getWalletData('accounts/eip155:1/99')).toBeNull();
});
it('delegates accountIndices to scry /wallet/accounts', async () => {
const { getWalletData } = await loadModule();
const accounts = [
{ index: 0, pubKey: 'a', address: 'b', hdPath: 'c' },
{ index: 1, pubKey: 'd', address: 'e', hdPath: 'f' },
];
mockFetchResponses({ status: 200, body: accounts });
const result = await getWalletData('accountIndices/cosmos:zenith-1');
expect(result).toBe('0,1');
});
it('delegates addAccountCounter to scry next-account-id', async () => {
const { getWalletData } = await loadModule();
mockFetchResponses({ status: 200, body: { id: 3 } });
const result = await getWalletData('addAccountCounter/cosmos:zenith-1');
expect(result).toBe('3');
});
it('returns null for unknown key', async () => {
const { getWalletData } = await loadModule();
expect(await getWalletData('unknown-key')).toBeNull();
});
});
// =========================================================================
// setWalletData (compatibility layer)
// =========================================================================
describe('setWalletData', () => {
it('delegates networks write to poke with update-networks action', async () => {
const { setWalletData } = await loadModule();
const networks = [{ networkId: 'zenith-1', networkName: 'Zenith' }];
// getShipName fetch, channel PUT, ack fetch
const fetchMock = mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 200 },
{ status: 200 },
);
const promise = setWalletData('networks', JSON.stringify(networks));
await new Promise((r) => setTimeout(r, 10));
// Verify the poke body
const putBody = JSON.parse(fetchMock.mock.calls[1][1].body);
expect(putBody[0]).toMatchObject({
action: 'poke',
mark: 'json',
json: { action: 'update-networks', networks },
});
// Ack to resolve
MockEventSource.instance!.emit('message', {
response: 'poke',
id: putBody[0].id,
ok: 'ok',
});
await expect(promise).resolves.toBeUndefined();
});
it('throws on direct mnemonic write', async () => {
const { setWalletData } = await loadModule();
await expect(
setWalletData('mnemonicServer', 'some words'),
).rejects.toThrow('Cannot set mnemonic directly');
});
it('throws on direct account write', async () => {
const { setWalletData } = await loadModule();
await expect(
setWalletData('accounts/eip155:1/0', 'data'),
).rejects.toThrow('Direct write to');
});
it('throws on unknown key', async () => {
const { setWalletData } = await loadModule();
await expect(
setWalletData('unknown', 'data'),
).rejects.toThrow('Unknown wallet data key');
});
});
// =========================================================================
// deleteWalletData
// =========================================================================
describe('deleteWalletData', () => {
it('delegates mnemonicServer delete to resetWallet poke', async () => {
const { deleteWalletData } = await loadModule();
const fetchMock = mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 200 },
{ status: 200 },
);
const promise = deleteWalletData('mnemonicServer');
await new Promise((r) => setTimeout(r, 10));
const putBody = JSON.parse(fetchMock.mock.calls[1][1].body);
expect(putBody[0].json).toEqual({ action: 'reset-wallet' });
MockEventSource.instance!.emit('message', {
response: 'poke',
id: putBody[0].id,
ok: 'ok',
});
await expect(promise).resolves.toBeUndefined();
});
it('is a no-op for account keys', async () => {
const { deleteWalletData } = await loadModule();
// Should resolve without making any fetch calls
await expect(
deleteWalletData('accounts/eip155:1/0'),
).resolves.toBeUndefined();
});
});
// =========================================================================
// closeChannel
// =========================================================================
describe('closeChannel', () => {
it('closes EventSource and rejects pending pokes', async () => {
const { pokeAction, closeChannel } = await loadModule();
mockFetchResponses(
{ status: 200, text: '~zod' },
{ status: 200 },
);
const pokePromise = pokeAction('json', { action: 'x' });
await new Promise((r) => setTimeout(r, 10));
closeChannel();
await expect(pokePromise).rejects.toThrow('Channel closed');
expect(MockEventSource.instance!.close).toHaveBeenCalled();
});
});
});

View File

@ -7,54 +7,106 @@ 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));
await eyre.createWallet(networksData, mnemonic);
const hdNode = HDNode.fromMnemonic(mnemonic);
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
await createWalletFromMnemonic(networksData, hdNode, mnemonic);
return mnemonic;
};
const createWalletFromMnemonic = async (
networksData: NetworksDataState[],
_hdNode: HDNode,
hdNode: HDNode,
mnemonic: string
): Promise<void> => {
// HD derivation delegated to agent — hdNode param unused
await eyre.createWallet(networksData, mnemonic);
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',
),
]);
}
};
const addAccount = async (
chainId: string,
): Promise<Account | undefined> => {
try {
const networksData = await eyre.getNetworks();
let selectedNetworkAccount
const networksData = await retrieveNetworksData();
// Add account to all networks (agent handles derivation + counter)
// Add account to all networks and return account for selected network
for (const network of networksData) {
await eyre.addAccount(network.namespace, network.chainId);
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;
}
}
// 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];
return selectedNetworkAccount;
} catch (error) {
console.error('Error creating account:', error);
}
@ -65,8 +117,13 @@ const addAccountsForNetwork = async (
numberOfAccounts: number,
): Promise<void> => {
try {
const namespaceChainId = `${network.namespace}:${network.chainId}`;
for (let i = 0; i < numberOfAccounts; i++) {
await eyre.addAccount(network.namespace, network.chainId);
const id = await getNextAccountId(namespaceChainId);
const hdPath = getHDPath(namespaceChainId, `0'/0/${id}`);
await addAccountFromHDPath(hdPath, network);
await updateAccountCounter(namespaceChainId, id);
}
} catch (error) {
console.error('Error creating account:', error);
@ -78,11 +135,26 @@ const addAccountFromHDPath = async (
networkData: NetworksDataState,
): Promise<Account | undefined> => {
try {
// Agent derives from stored mnemonic using the given HD path
await eyre.addAccountFromPath(networkData.namespace, networkData.chainId, hdPath);
const account = await accountInfoFromHDPath(hdPath, networkData);
if (!account) {
throw new Error('Error while creating account');
}
const accounts = await eyre.getAccounts(networkData.namespace, networkData.chainId);
return accounts[accounts.length - 1];
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 };
} catch (error) {
console.error(error);
}
@ -91,37 +163,160 @@ const addAccountFromHDPath = async (
const addNewNetwork = async (
newNetworkData: NetworksFormData
): Promise<NetworksDataState[]> => {
await eyre.addNetwork(newNetworkData);
return eyre.getNetworks();
};
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;
}
const storeNetworkData = async (
networkData: NetworksFormData,
): Promise<NetworksDataState[]> => {
await eyre.addNetwork(networkData);
return eyre.getNetworks();
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;
};
const retrieveNetworksData = async (): Promise<NetworksDataState[]> => {
return eyre.getNetworks();
const networks = await getInternetCredentials('networks');
if (!networks) {
return [];
}
const parsedNetworks: NetworksDataState[] = JSON.parse(networks);
return parsedNetworks;
};
export const retrieveAccountsForNetwork = async (
namespaceChainId: string,
_accountsIndices?: string,
accountsIndices: string,
): Promise<Account[]> => {
const [ns, chain] = splitNsChain(namespaceChainId);
return eyre.getAccounts(ns, chain);
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 retrieveAccounts = async (
currentNetworkData: NetworksDataState,
): Promise<Account[] | undefined> => {
const accounts = await eyre.getAccounts(
currentNetworkData.namespace,
currentNetworkData.chainId,
const accountIndicesServer = await getInternetCredentials(
`accountIndices/${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
);
return accounts.length > 0 ? accounts : undefined;
const accountIndices = accountIndicesServer;
if (!accountIndices) {
return;
}
const loadedAccounts = await retrieveAccountsForNetwork(
`${currentNetworkData.namespace}:${currentNetworkData.chainId}`,
accountIndices,
)
return loadedAccounts;
};
const retrieveSingleAccount = async (
@ -129,13 +324,37 @@ const retrieveSingleAccount = async (
chainId: string,
address: string,
) => {
const accounts = await eyre.getAccounts(namespace, chainId);
return accounts.find(account => account.address === address);
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 resetWallet = async () => {
try {
await eyre.resetWallet();
await Promise.all([
resetInternetCredentials('mnemonicServer'),
resetKeyServers(EIP155),
resetKeyServers(COSMOS),
setInternetCredentials('networks', '_', JSON.stringify([])),
]);
} catch (error) {
console.error('Error resetting wallet:', error);
throw error;
@ -148,13 +367,12 @@ const accountInfoFromHDPath = async (
): Promise<
{ privKey: string; pubKey: string; address: string } | undefined
> => {
// Phase 1: still derives in browser for API compat.
// Phase 2: this moves to the agent entirely.
const mnemonic = await eyre.getMnemonic();
if (!mnemonic) {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
const hdNode = HDNode.fromMnemonic(mnemonic);
const node = hdNode.derivePath(hdPath);
@ -179,15 +397,36 @@ const accountInfoFromHDPath = async (
};
const getNextAccountId = async (namespaceChainId: string): Promise<number> => {
const [ns, chain] = splitNsChain(namespaceChainId);
return eyre.getNextAccountId(ns, chain);
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 updateAccountCounter = async (
_namespaceChainId: string,
_id: number,
namespaceChainId: string,
id: number,
): Promise<void> => {
// Agent manages account counter atomically via addAccount poke.
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,
);
};
const getCosmosAccountByHDPath = async (
@ -209,13 +448,52 @@ const getCosmosAccountByHDPath = async (
const checkNetworkForChainID = async (
chainId: string,
): Promise<boolean> => {
const networks = await eyre.getNetworks();
return networks.some((network) => network.chainId === chainId);
const networks = await getInternetCredentials('networks');
if (!networks) {
return false;
}
const networksData: NetworksFormData[] = JSON.parse(networks);
return networksData.some((network) => network.chainId === chainId);
}
const updateNetworkRpcUrl = async (
chainId: string,
rpcUrl: string,
): Promise<NetworksDataState[]> => {
const networks = await getInternetCredentials('networks');
if (!networks) {
throw new Error('Networks not found');
}
const networksData: NetworksDataState[] = JSON.parse(networks);
const networkIndex = networksData.findIndex((network) => network.chainId === chainId);
if (networkIndex === -1) {
throw new Error('Network not found');
}
networksData[networkIndex].rpcUrl = rpcUrl;
await setInternetCredentials(
'networks',
'_',
JSON.stringify(networksData),
);
return networksData;
}
const isWalletCreated = async (
): Promise<boolean> => {
return eyre.walletExists();
const mnemonicServer = await getInternetCredentials("mnemonicServer");
const mnemonic = mnemonicServer;
return mnemonic !== null;
};
export {
@ -234,5 +512,6 @@ export {
getCosmosAccountByHDPath,
addNewNetwork,
checkNetworkForChainID,
isWalletCreated
isWalletCreated,
updateNetworkRpcUrl
};

View File

@ -1,441 +0,0 @@
/**
* Eyre HTTP client for communicating with the %zenith agent.
*
* Replaces localStorage (key-store.ts) with Urbit ship storage via:
* - Scry: GET /~/scry/zenith/{path}.json
* - Poke: PUT /~/channel/{uid} with channel API
*
* Three layers:
* 1. Base helpers scryGet(), pokeAction()
* 2. Typed API getNetworks(), createWallet(), etc.
* 3. Compatibility getWalletData(), setWalletData(), deleteWalletData()
*/
import { Account, NetworksDataState, NetworksFormData } from '../types';
// ============================================================
// Types
// ============================================================
export interface AccountWithKeys extends Account {
privKey: string;
}
interface PendingPoke {
resolve: () => void;
reject: (err: Error) => void;
}
// ============================================================
// Channel state
// ============================================================
const APP_NAME = 'zenith';
const POKE_TIMEOUT_MS = 30_000;
let channelUid: string | null = null;
let eventSource: EventSource | null = null;
let messageId = 0;
let cachedShipName: string | null = null;
const pendingPokes = new Map<number, PendingPoke>();
// ============================================================
// 1. Base helpers
// ============================================================
/**
* Discover the ship name. Uses /~/host (same as @urbit/http-api),
* with fallback to the Eyre auth cookie.
*/
async function getShipName(): Promise<string> {
if (cachedShipName) return cachedShipName;
const res = await fetch('/~/host', { credentials: 'include' });
if (res.ok) {
cachedShipName = await res.text();
return cachedShipName;
}
const match = document.cookie.match(/urbauth-(~[a-z-]+)=/);
if (match) {
cachedShipName = match[1];
return cachedShipName;
}
throw new Error('Cannot determine ship name — not authenticated with Eyre');
}
function getChannelUrl(): string {
if (!channelUid) {
channelUid = `wallet-${Date.now()}`;
}
return `/~/channel/${channelUid}`;
}
/**
* Open the SSE event source for receiving poke acks.
* Eyre buffers events until the client connects, so it's safe to open
* after the first PUT.
*/
function openEventSource(): void {
if (eventSource) return;
eventSource = new EventSource(getChannelUrl(), {
withCredentials: true,
});
eventSource.addEventListener('message', (ev: MessageEvent) => {
const data = JSON.parse(ev.data);
if (data.response === 'poke') {
const pending = pendingPokes.get(data.id);
if (pending) {
if (data.ok === 'ok') {
pending.resolve();
} else {
pending.reject(new Error(data.err ?? 'Poke rejected by agent'));
}
pendingPokes.delete(data.id);
}
}
// Acknowledge every SSE event to keep the channel alive
const ackId = ++messageId;
fetch(getChannelUrl(), {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([{
id: ackId,
action: 'ack',
'event-id': Number(ev.lastEventId),
}]),
});
});
eventSource.addEventListener('error', () => {
for (const [, pending] of pendingPokes) {
pending.reject(new Error('Eyre channel disconnected'));
}
pendingPokes.clear();
eventSource?.close();
eventSource = null;
channelUid = null;
});
}
/**
* Scry the %zenith agent. Returns parsed JSON, or null on 404.
*
* @param path - Scry path starting with /, e.g. '/wallet/networks'
*/
async function scryGet<T>(path: string): Promise<T | null> {
const res = await fetch(`/~/scry/${APP_NAME}${path}.json`, {
credentials: 'include',
});
if (res.status === 404) return null;
if (!res.ok) {
throw new Error(`Scry ${path} failed: ${res.status} ${res.statusText}`);
}
return res.json();
}
/**
* Poke the %zenith agent and wait for the ack/nack.
*
* @param mark - Poke mark, e.g. 'json' or 'wallet-action'
* @param json - Poke payload
*/
async function pokeAction(mark: string, json: unknown): Promise<void> {
const shipName = await getShipName();
const id = ++messageId;
const ackPromise = new Promise<void>((resolve, reject) => {
pendingPokes.set(id, { resolve, reject });
setTimeout(() => {
if (pendingPokes.has(id)) {
pendingPokes.delete(id);
reject(new Error(`Poke timed out after ${POKE_TIMEOUT_MS}ms`));
}
}, POKE_TIMEOUT_MS);
});
const res = await fetch(getChannelUrl(), {
method: 'PUT',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([{
id,
action: 'poke',
ship: shipName,
app: APP_NAME,
mark,
json,
}]),
});
if (!res.ok) {
pendingPokes.delete(id);
throw new Error(`Channel PUT failed: ${res.status} ${res.statusText}`);
}
// Open SSE to receive ack (no-op if already open).
// Eyre buffers the ack until we connect.
openEventSource();
return ackPromise;
}
// ============================================================
// 2. Typed API — Reads (scry)
// ============================================================
async function walletExists(): Promise<boolean> {
const data = await scryGet<{ exists: boolean }>('/wallet/exists');
return data?.exists ?? false;
}
async function getMnemonic(): Promise<string | null> {
const data = await scryGet<{ mnemonic: string }>('/wallet/mnemonic');
return data?.mnemonic ?? null;
}
async function getNetworks(): Promise<NetworksDataState[]> {
return (await scryGet<NetworksDataState[]>('/wallet/networks')) ?? [];
}
async function getAccounts(
namespace: string,
chainId: string,
): Promise<Account[]> {
return (
(await scryGet<Account[]>(`/wallet/accounts/${namespace}:${chainId}`)) ?? []
);
}
async function getAccount(
namespace: string,
chainId: string,
index: number,
): Promise<AccountWithKeys | null> {
return scryGet<AccountWithKeys>(
`/wallet/account/${namespace}:${chainId}/${index}`,
);
}
async function getNextAccountId(
namespace: string,
chainId: string,
): Promise<number> {
const data = await scryGet<{ id: number }>(
`/wallet/next-account-id/${namespace}:${chainId}`,
);
return data?.id ?? 0;
}
// ============================================================
// 2. Typed API — Writes (poke)
// ============================================================
async function createWallet(
networks: NetworksFormData[],
mnemonic?: string,
): Promise<void> {
const payload: Record<string, unknown> = {
action: 'create-wallet',
networks,
};
if (mnemonic) {
payload.mnemonic = mnemonic;
}
return pokeAction('json', payload);
}
async function addAccount(
namespace: string,
chainId: string,
): Promise<void> {
return pokeAction('json', {
action: 'add-account',
namespace,
'chain-id': chainId,
});
}
async function updateNetworks(
networks: NetworksDataState[],
): Promise<void> {
return pokeAction('json', {
action: 'update-networks',
networks,
});
}
async function addNetwork(network: NetworksFormData): Promise<void> {
return pokeAction('json', {
action: 'add-network',
network,
});
}
async function addAccountFromPath(
namespace: string,
chainId: string,
hdPath: string,
): Promise<void> {
return pokeAction('json', {
action: 'add-account-from-path',
namespace,
'chain-id': chainId,
'hd-path': hdPath,
});
}
async function resetWallet(): Promise<void> {
return pokeAction('json', { action: 'reset-wallet' });
}
// ============================================================
// 3. Drop-in compatibility layer
//
// Maps the old localStorage key patterns to the typed API so
// existing callers can migrate incrementally in Phase 2.
// ============================================================
const ACCOUNTS_RE = /^accounts\/([^/]+)\/(\d+)$/;
const INDICES_RE = /^accountIndices\/(.+)$/;
const COUNTER_RE = /^addAccountCounter\/(.+)$/;
function splitNsChain(nsChain: string): [string, string] {
const i = nsChain.indexOf(':');
return [nsChain.slice(0, i), nsChain.slice(i + 1)];
}
/**
* Drop-in replacement for getInternetCredentials(key).
* Returns a string matching the old localStorage value format, or null.
*/
async function getWalletData(key: string): Promise<string | null> {
if (key === 'mnemonicServer') {
return getMnemonic();
}
if (key === 'networks') {
const networks = await getNetworks();
return networks.length > 0 ? JSON.stringify(networks) : null;
}
const accountMatch = key.match(ACCOUNTS_RE);
if (accountMatch) {
const [ns, chain] = splitNsChain(accountMatch[1]);
const account = await getAccount(ns, chain, Number(accountMatch[2]));
if (!account) return null;
return `${account.hdPath},${account.privKey},${account.pubKey},${account.address}`;
}
const indicesMatch = key.match(INDICES_RE);
if (indicesMatch) {
const [ns, chain] = splitNsChain(indicesMatch[1]);
const accounts = await getAccounts(ns, chain);
if (accounts.length === 0) return null;
return accounts.map((a) => a.index).join(',');
}
const counterMatch = key.match(COUNTER_RE);
if (counterMatch) {
const [ns, chain] = splitNsChain(counterMatch[1]);
const id = await getNextAccountId(ns, chain);
return String(id);
}
return null;
}
/**
* Drop-in replacement for setInternetCredentials(key, _, value).
* Only the `networks` key supports direct writes account mutations
* should use the typed API (createWallet, addAccount).
*/
async function setWalletData(key: string, value: string): Promise<void> {
if (key === 'networks') {
const networks: NetworksDataState[] = JSON.parse(value);
return updateNetworks(networks);
}
if (key === 'mnemonicServer') {
throw new Error(
'Cannot set mnemonic directly — use createWallet() instead',
);
}
if (ACCOUNTS_RE.test(key) || INDICES_RE.test(key) || COUNTER_RE.test(key)) {
throw new Error(
`Direct write to '${key}' not supported — use createWallet() or addAccount()`,
);
}
throw new Error(`Unknown wallet data key: ${key}`);
}
/**
* Drop-in replacement for resetInternetCredentials(key).
* Individual account key deletion is a no-op the agent manages
* account lifecycle atomically via resetWallet().
*/
async function deleteWalletData(key: string): Promise<void> {
if (key === 'mnemonicServer') {
return resetWallet();
}
// Account-related keys are managed atomically by the agent.
// Individual deletes are no-ops since resetWallet() handles cleanup.
}
// ============================================================
// 4. Channel lifecycle
// ============================================================
function closeChannel(): void {
if (eventSource) {
eventSource.close();
eventSource = null;
}
for (const [, pending] of pendingPokes) {
pending.reject(new Error('Channel closed'));
}
pendingPokes.clear();
channelUid = null;
}
// ============================================================
// Exports
// ============================================================
export {
// Base helpers
scryGet,
pokeAction,
// Typed API — reads
walletExists,
getMnemonic,
getNetworks,
getAccounts,
getAccount,
getNextAccountId,
// Typed API — writes
createWallet,
addAccount,
updateNetworks,
addNetwork,
addAccountFromPath,
resetWallet,
// Compatibility layer (Phase 1 drop-in for key-store.ts)
getWalletData,
setWalletData,
deleteWalletData,
// Lifecycle
closeChannel,
};

View File

@ -1,8 +1,3 @@
/**
* @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,23 +9,20 @@ import { stringToPath } from '@cosmjs/crypto';
import '@ethersproject/shims';
import {
getMnemonic as eyreGetMnemonic,
getAccounts,
getAccount,
} from './eyre-client';
getInternetCredentials,
resetInternetCredentials,
setInternetCredentials,
} from './key-store';
import { EIP155 } from './constants';
function splitNsChain(namespaceChainId: string): [string, string] {
const i = namespaceChainId.indexOf(':');
return [namespaceChainId.slice(0, i), namespaceChainId.slice(i + 1)];
}
import { NetworksDataState } from '../types';
const getMnemonic = async (): Promise<string> => {
const mnemonic = await eyreGetMnemonic();
if (!mnemonic) {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
throw new Error('Mnemonic not found!');
}
const mnemonic = mnemonicStore;
return mnemonic;
};
@ -56,19 +53,22 @@ const getPathKey = async (
pubKey: string;
address: string;
}> => {
const [ns, chain] = splitNsChain(namespaceChainId);
const account = await getAccount(ns, chain, accountId);
const pathKeyStore = await getInternetCredentials(
`accounts/${namespaceChainId}/${accountId}`,
);
if (!account) {
throw new Error('Error while fetching account');
if (!pathKeyStore) {
throw new Error('Error while fetching counter');
}
return {
path: account.hdPath,
privKey: account.privKey,
pubKey: account.pubKey,
address: account.address,
};
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 };
};
const getAccountIndices = async (
@ -78,35 +78,75 @@ const getAccountIndices = async (
indices: number[];
index: number;
}> => {
const [ns, chain] = splitNsChain(namespaceChainId);
const accounts = await getAccounts(ns, chain);
const counterStore = await getInternetCredentials(
`accountIndices/${namespaceChainId}`,
);
if (accounts.length === 0) {
throw new Error('Error while fetching accounts');
if (!counterStore) {
throw new Error('Error while fetching counter');
}
const indices = accounts.map(a => a.index);
const maxIndex = Math.max(...indices);
let accountIndices = counterStore;
const indices = accountIndices.split(',').map(Number);
const index = indices[indices.length - 1] + 1;
return {
accountIndices: indices.join(','),
indices,
index: maxIndex + 1,
};
return { accountIndices, indices, index };
};
const updateAccountIndices = async (
namespaceChainId: string,
): Promise<{ accountIndices: string; index: number }> => {
// 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 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 };
};
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 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 sendMessage = (

View File

@ -1,9 +0,0 @@
:~ title+'Zenith Wallet'
info+'Zenith blockchain wallet with WalletConnect support.'
color+0x4f.46e5
base+'zenith-wallet'
glob-http+['REPLACE_WITH_GLOB_URL' REPLACE_WITH_GLOB_HASH]
version+[0 0 1]
website+'https://git.vdb.to/LaconicNetwork/zenith-wallet-web'
license+'MIT'
==

View File

@ -1,14 +0,0 @@
:: mark for .ico icon files
::
|_ dat=octs
++ grow
|%
++ mime [/image/x-icon dat]
--
++ grab
|%
++ mime |=([p=mite q=octs] q)
++ noun |=(=noun ;;(octs noun))
--
++ grad %mime
--

View File

@ -1,14 +0,0 @@
:: mark for .map files (source maps)
::
|_ dat=octs
++ grow
|%
++ mime [/application/json dat]
--
++ grab
|%
++ mime |=([p=mite q=octs] q)
++ noun |=(=noun ;;(octs noun))
--
++ grad %mime
--

View File

@ -1,14 +0,0 @@
:: mark for .ttf font files
::
|_ dat=octs
++ grow
|%
++ mime [/font/ttf dat]
--
++ grab
|%
++ mime |=([p=mite q=octs] q)
++ noun |=(=noun ;;(octs noun))
--
++ grad %mime
--

View File

@ -1,14 +0,0 @@
:: mark for .woff font files
::
|_ dat=octs
++ grow
|%
++ mime [/font/woff dat]
--
++ grab
|%
++ mime |=([p=mite q=octs] q)
++ noun |=(=noun ;;(octs noun))
--
++ grad %mime
--

View File

@ -1,14 +0,0 @@
:: mark for .woff2 font files
::
|_ dat=octs
++ grow
|%
++ mime [/font/woff2 dat]
--
++ grab
|%
++ mime |=([p=mite q=octs] q)
++ noun |=(=noun ;;(octs noun))
--
++ grad %mime
--