Author SHA1 Message Date
zramsay 6f41ae5626 add yarn prior to yarn build
Lint and Build / Run lint and build checks (pull_request) Failing after 3m36s
2026-02-10 17:09:25 +00:00
afdandClaude Opus 4.6 657bc17eff [wallet-integration 4/5] Migrate localStorage to Eyre ship storage
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>
2026-02-10 09:39:04 -05:00
afdandClaude Opus 4.6 fc82acb4d5 [wallet-integration 3/5] Add Eyre channel client and glob deployment
New eyre-client.ts replaces localStorage with Urbit ship storage:
- Base layer: scryGet(), pokeAction() with SSE ack/nack handling
- Typed API: 11 functions for wallet CRUD (walletExists, getMnemonic,
  getNetworks, getAccounts, createWallet, addAccount, resetWallet, etc.)
- Compatibility layer: getWalletData/setWalletData/deleteWalletData
  mapping old key patterns for incremental migration

Glob deployment setup following Laconic self-hosted DeFi playbook:
- build-glob.sh: build, lowercase, deploy to ship
- urbit-files/: desk.docket-0 + mark files (ico, map, ttf, woff, woff2)

Includes 22 Jest tests with mocked fetch/EventSource.

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>
2026-02-10 09:38:59 -05:00
490f4ec8a4 Update lint CI to trigger on push to main branch (#13)
Lint and Build / Run lint and build checks (push) Failing after 1m36s
Co-authored-by: AdityaSalunkhe21 <adityasalunkhe2204@gmail.com>
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-authored-by: Shreerang Kale <shree@deepstacksoft.com>
Reviewed-on: #13
Co-authored-by: Pranav <jadhavpranav89@gmail.com>
Co-committed-by: Pranav <jadhavpranav89@gmail.com>
2025-12-31 06:57:59 +00:00
nabarunandpranav 770f96603b Resolve warnings in react app build (#10)
Publish wallet docker image on release / Run docker build and publish (release) Successful in 7m15s
Part of https://plan.wireit.in/deepstack/browse/VUL-251/

Co-authored-by: Pranav <jadhavpranav89@gmail.com>
Reviewed-on: #10
2025-10-07 13:42:26 +00:00
shreerangandpranav cc47ba4636 Remove testnet reference (#8)
Co-authored-by: Pranav <jadhavpranav89@gmail.com>
Reviewed-on: #8
Co-authored-by: shreerang <shreerang@noreply.git.vdb.to>
Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
2025-09-26 05:34:12 +00:00
shreerangandpranav eabdf8ee6f Remove WalletConnect ID usage from stack (#9)
Publish wallet docker image on release / Run docker build and publish (release) Successful in 3m47s
Co-authored-by: Pranav <jadhavpranav89@gmail.com>
Reviewed-on: #9
Co-authored-by: shreerang <shreerang@noreply.git.vdb.to>
Co-committed-by: shreerang <shreerang@noreply.git.vdb.to>
2025-09-22 12:18:38 +00:00
nabarun 438db6d5c6 Minimize size of docker image in stack (#7)
Publish wallet docker image on release / Run docker build and publish (release) Successful in 3m50s
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #7
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-07-30 10:20:47 +00:00
nabarun 49d454dcf7 Upgrade package version (#6)
Publish wallet docker image on release / Run docker build and publish (release) Has been cancelled
Reviewed-on: #6
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-07-17 09:27:04 +00:00
nabarun b6d05b650c Add CI for building docker image (#4)
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #4
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-07-17 14:42:49 +05:30
prathamesh 48565921d8 Update gasprice parsing to support denom $Z (#1)
Part of https://www.notion.so/zenithd-milestone-4-1d6a6b22d4728043b7e4dbf70c50aa56

Reviewed-on: #1
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-07-17 14:41:32 +05:30
nabarun 847359fb6d Add CI for publishing docker image (#5)
Publish wallet docker image on release / Run docker build and publish (release) Successful in 5m32s
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #5
Co-authored-by: Nabarun <nabarun@deepstacksoft.com>
Co-committed-by: Nabarun <nabarun@deepstacksoft.com>
2025-07-13 10:48:20 +00:00
prathamesh 112046e4d0 Fix file and directory naming in the stack (#3)
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #3
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-06-19 11:36:40 +00:00
prathamesh 01a6e63a17 Rename wallet to Zenith wallet (#2)
Part of https://www.notion.so/Implement-stacks-1b5a6b22d472806a82f5dafed6955138

Reviewed-on: #2
Co-authored-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
Co-committed-by: Prathamesh Musale <prathamesh.musale0@gmail.com>
2025-06-19 11:02:11 +00:00
42 changed files with 1672 additions and 604 deletions
-3
View File
@@ -1,9 +1,6 @@
REACT_APP_WALLET_CONNECT_PROJECT_ID=
REACT_APP_DEFAULT_GAS_PRICE=0.025
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
REACT_APP_GAS_ADJUSTMENT=2
REACT_APP_LACONICD_RPC_URL=https://laconicd-sapo.laconic.com
# Example: https://example-url-1.com,https://example-url-2.com
REACT_APP_ALLOWED_URLS=
+29
View File
@@ -0,0 +1,29 @@
name: Publish wallet docker image on release
on:
release:
types: [published]
jobs:
build:
name: Run docker build and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run docker build
run: docker build -t cerc/zenith-wallet-web -f stack/stack-orchestrator/container-build/cerc-zenith-wallet-web/Dockerfile .
- name: Get the version
id: vars
run: |
echo ::set-output name=sha::$(echo ${GITHUB_SHA:0:7})
echo ::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/})
- name: Tag docker image
run: docker tag cerc/zenith-wallet-web git.vdb.to/laconicnetwork/cerc/zenith-wallet-web:${{steps.vars.outputs.sha}}
- name: Tag docker image
run: docker tag git.vdb.to/laconicnetwork/cerc/zenith-wallet-web:${{steps.vars.outputs.sha}} git.vdb.to/laconicnetwork/cerc/zenith-wallet-web:${{steps.vars.outputs.tag}}
- name: Docker Login
run: echo ${{ secrets.CICD_PUBLISH_TOKEN }} | docker login https://git.vdb.to -u laconiccicd --password-stdin
- name: Docker Push
run: docker push git.vdb.to/laconicnetwork/cerc/zenith-wallet-web:${{steps.vars.outputs.sha}}
- name: Docker Push TAGGED
run: docker push git.vdb.to/laconicnetwork/cerc/zenith-wallet-web:${{steps.vars.outputs.tag}}
+33
View File
@@ -0,0 +1,33 @@
name: Lint and Build
on:
pull_request:
branches: [ main ]
push:
branches: [ main ]
jobs:
lint-and-build:
name: Run lint and build checks
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable corepack and yarn
run: corepack enable
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run ESLint
run: yarn lint
- name: Run build
run: yarn build
+1
View File
@@ -1 +1,2 @@
@cerc-io:registry=https://git.vdb.to/api/packages/cerc-io/npm/
@laconic-network:registry=https://git.vdb.to/api/packages/LaconicNetwork/npm/
+1 -1
View File
@@ -1 +1 @@
# laconic-wallet-web
# zenith-wallet-web
Executable
+65
View File
@@ -0,0 +1,65 @@
#!/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}"
+16
View File
@@ -48,10 +48,26 @@ module.exports = function override(config, env) {
stream: require.resolve("stream-browserify"),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
vm: require.resolve('vm-browserify'),
url: false
}
config.resolve.alias['react-native$'] = require.resolve('react-native-web');
// Ignore source map warnings from third-party packages. Ref: https://github.com/facebook/create-react-app/discussions/11767#discussioncomment-3416044
const ignoreSourceMapPackages = [
'@cosmjs',
'@confio/ics23',
'@json-rpc-tools',
'@pedrouid/environment',
'@walletconnect',
'cosmjs-types',
];
config.ignoreWarnings = ignoreSourceMapPackages.map(pkg => ({
module: new RegExp(`node_modules/${pkg.replace('/', '\\/')}`),
message: /Failed to parse source map/,
}));
return config;
};
+9 -6
View File
@@ -1,14 +1,15 @@
{
"name": "web-wallet",
"version": "0.1.7",
"version": "0.1.7-zenith-0.2.3",
"private": true,
"dependencies": {
"@laconic-network/cosmjs-util": "^0.1.0",
"@cerc-io/registry-sdk": "^0.2.5",
"@cosmjs/amino": "^0.32.3",
"@cosmjs/crypto": "^0.32.3",
"@cosmjs/amino": "^0.33.1",
"@cosmjs/crypto": "^0.33.1",
"@cosmjs/encoding": "^0.33.1",
"@cosmjs/proto-signing": "^0.32.3",
"@cosmjs/stargate": "^0.32.3",
"@cosmjs/proto-signing": "^0.33.1",
"@cosmjs/stargate": "^0.33.1",
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@ethersproject/shims": "^5.7.0",
@@ -77,6 +78,7 @@
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-modules-commonjs": "^7.24.8",
"@import-meta-env/cli": "^0.7.3",
"@import-meta-env/typescript": "^0.4.0",
@@ -96,7 +98,8 @@
"eslint-webpack-plugin": "^3.1.1",
"husky": "^9.0.11",
"react-app-rewired": "^2.2.1",
"stream-browserify": "^3.0.0"
"stream-browserify": "^3.0.0",
"vm-browserify": "^1.1.2"
},
"packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447"
}
+2 -2
View File
@@ -6,7 +6,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Laconic Wallet Web App" />
<meta name="description" content="Zenith Wallet Web App" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
@@ -27,7 +27,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Laconic Wallet</title>
<title>Zenith Wallet</title>
<script>
globalThis.import_meta_env = JSON.parse('"import_meta_env_placeholder"');
</script>
+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "Laconic Wallet",
"name": "Laconic Wallet Web App",
"short_name": "Zenith Wallet",
"name": "Zenith Wallet Web App",
"icons": [
{
"src": "favicon.ico",
+2 -6
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);
+3 -9
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;
}
+4
View File
@@ -20,6 +20,10 @@ export default function useInitialization(
}, [setWeb3wallet]);
useEffect(() => {
if (!import.meta.env.REACT_APP_WALLET_CONNECT_PROJECT_ID) {
return;
}
if (!initialized) {
onInitialize();
}
-1
View File
@@ -4,7 +4,6 @@ interface ImportMetaEnv {
readonly REACT_APP_WALLET_CONNECT_PROJECT_ID: string;
readonly REACT_APP_DEFAULT_GAS_PRICE: string;
readonly REACT_APP_GAS_ADJUSTMENT: string;
readonly REACT_APP_LACONICD_RPC_URL: string;
readonly REACT_APP_ALLOWED_URLS: string;
}
+1 -1
View File
@@ -113,7 +113,7 @@ root.render(
<NavigationContainer
linking={linking}
documentTitle={{
formatter: () => `Laconic | Wallet`,
formatter: () => `Zenith | Wallet`,
}}
theme={navigationTheme}
>
+4 -2
View File
@@ -10,9 +10,11 @@ import {
import { useNavigation } from '@react-navigation/native';
import { DirectSecp256k1Wallet, EncodeObject } from '@cosmjs/proto-signing';
import { LaconicClient } from '@cerc-io/registry-sdk';
import { GasPrice, calculateFee } from '@cosmjs/stargate';
import { calculateFee } from '@cosmjs/stargate';
import { formatJsonRpcError } from '@json-rpc-tools/utils';
import { parseCustomGasPrice } from '@laconic-network/cosmjs-util';
import { useNetworks } from '../context/NetworksContext';
import { Account, StackParamsList } from '../types';
import styles from '../styles/stylesheet';
@@ -177,7 +179,7 @@ const ApproveTransaction = ({ route }: ApproveTransactionProps) => {
}, [cosmosStargateClient, transactionMessage, requestEventId, topic, web3wallet, signer]);
useEffect(() => {
const gasPrice = GasPrice.fromString(
const gasPrice = parseCustomGasPrice(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
+4 -3
View File
@@ -19,11 +19,12 @@ import { getHeaderTitle } from '@react-navigation/elements';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
MsgSendEncodeObject,
SigningStargateClient,
} from '@cosmjs/stargate';
import { parseCustomGasPrice } from '@laconic-network/cosmjs-util';
import { Account, StackParamsList } from '../types';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
@@ -42,7 +43,7 @@ import TxErrorDialog from '../components/TxErrorDialog';
import { EIP155_SIGNING_METHODS } from '../utils/wallet-connect/EIP155Data';
import { COSMOS_METHODS } from '../utils/wallet-connect/COSMOSData';
export const MEMO = 'Sending signed tx from Laconic Wallet';
export const MEMO = 'Sending signed tx from Zenith Wallet';
// Reference: https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit
const ETH_MINIMUM_GAS = 21000;
@@ -225,7 +226,7 @@ const ApproveTransfer = ({ route }: SignRequestProps) => {
.toString();
setFees(ethFees);
} else {
const gasPrice = GasPrice.fromString(
const gasPrice = parseCustomGasPrice(
requestedNetwork?.gasPrice! + requestedNetwork?.nativeDenom,
);
+3 -8
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);
+4 -3
View File
@@ -11,10 +11,11 @@ import { BigNumber } from 'ethers';
import { DirectSecp256k1Wallet } from '@cosmjs/proto-signing';
import {
calculateFee,
GasPrice,
SigningStargateClient,
} from '@cosmjs/stargate';
import { parseCustomGasPrice } from '@laconic-network/cosmjs-util';
import { retrieveSingleAccount } from '../utils/accounts';
import AccountDetails from '../components/AccountDetails';
import styles from '../styles/stylesheet';
@@ -149,7 +150,7 @@ export const WalletEmbed = () => {
);
setGasLimit(gasLimit);
const gasPrice = GasPrice.fromString(`${network.gasPrice}${network.nativeDenom}`);
const gasPrice = parseCustomGasPrice(`${network.gasPrice}${network.nativeDenom}`);
const cosmosFees = calculateFee(Number(gasLimit), gasPrice);
setFees(cosmosFees.amount[0].amount);
@@ -195,7 +196,7 @@ export const WalletEmbed = () => {
const fee = calculateFee(
Number(gasLimit),
GasPrice.fromString(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`)
parseCustomGasPrice(`${transactionDetails.requestedNetwork.gasPrice}${transactionDetails.requestedNetwork.nativeDenom}`)
);
const txResult = await client.sendTokens(
+457
View File
@@ -0,0 +1,457 @@
/**
* 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();
});
});
});
+52 -301
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 {
-24
View File
@@ -6,30 +6,6 @@ export const EIP155 = 'eip155';
export const COSMOS = 'cosmos';
export const DEFAULT_NETWORKS: NetworksFormData[] = [
{
chainId: 'laconic-testnet-2',
networkName: 'laconicd testnet-2',
namespace: COSMOS,
rpcUrl: import.meta.env.REACT_APP_LACONICD_RPC_URL!,
blockExplorerUrl: '',
nativeDenom: 'alnt',
addressPrefix: 'laconic',
coinType: '118',
gasPrice: '0.001',
isDefault: true,
},
{
chainId: 'laconic_9000-1',
networkName: 'laconicd',
namespace: COSMOS,
rpcUrl: "https://laconicd.laconic.com",
blockExplorerUrl: '',
nativeDenom: 'alnt',
addressPrefix: 'laconic',
coinType: '118',
gasPrice: '1',
isDefault: false,
},
{
chainId: '1',
networkName: EIP155_CHAINS['eip155:1'].name,
+441
View File
@@ -0,0 +1,441 @@
/**
* 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,
};
+5
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);
};
+39 -79
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 = (
+1 -1
View File
@@ -38,5 +38,5 @@ export const COSMOS_SIGNING_METHODS = {
export const COSMOS_METHODS = {
...COSMOS_SIGNING_METHODS,
COSMOS_SEND_TOKENS: 'cosmos_sendTokens', // Added for pay.laconic.com
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for testnet onboarding app
COSMOS_SEND_TRANSACTION: 'cosmos_sendTransaction', // Added for onboarding app
};
+2 -2
View File
@@ -1,3 +1,3 @@
# laconic-wallet-web-stack
# zenith-wallet-web-stack
[laconic-wallet-web stack documentation](./stack-orchestrator/stack/laconic-wallet-web/README.md)
[zenith-wallet-web stack documentation](./stack-orchestrator/stack/zenith-wallet-web/README.md)
@@ -1,15 +1,13 @@
services:
# Builds and serves the React laconic-wallet-web app
laconic-wallet-web:
# Builds and serves the React zenith-wallet-web app
zenith-wallet-web:
restart: unless-stopped
image: cerc/laconic-wallet-web:local
image: cerc/zenith-wallet-web:local
environment:
CERC_SCRIPT_DEBUG: ${CERC_SCRIPT_DEBUG}
WALLET_CONNECT_ID: ${WALLET_CONNECT_ID}
WALLET_CONNECT_VERIFY_CODE: ${WALLET_CONNECT_VERIFY_CODE}
CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE:-0.025}
CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT:-2}
CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL:-https://laconicd.laconic.com}
CERC_ZENITHD_RPC_URL: ${CERC_ZENITHD_RPC_URL}
CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}
command: ["bash", "/scripts/run.sh"]
+1 -5
View File
@@ -6,21 +6,17 @@ if [ -n "$CERC_SCRIPT_DEBUG" ]; then
fi
echo "Using the following env variables:"
echo "WALLET_CONNECT_ID: ${WALLET_CONNECT_ID}"
echo "CERC_DEFAULT_GAS_PRICE: ${CERC_DEFAULT_GAS_PRICE}"
echo "CERC_GAS_ADJUSTMENT: ${CERC_GAS_ADJUSTMENT}"
echo "CERC_LACONICD_RPC_URL: ${CERC_LACONICD_RPC_URL}"
echo "CERC_ALLOWED_URLS: ${CERC_ALLOWED_URLS}"
# Build with required env
export REACT_APP_WALLET_CONNECT_PROJECT_ID=$WALLET_CONNECT_ID
export REACT_APP_DEFAULT_GAS_PRICE=$CERC_DEFAULT_GAS_PRICE
export REACT_APP_GAS_ADJUSTMENT=$CERC_GAS_ADJUSTMENT
export REACT_APP_LACONICD_RPC_URL=$CERC_LACONICD_RPC_URL
export REACT_APP_ALLOWED_URLS=$CERC_ALLOWED_URLS
# Set env variables in build
yarn set-env
import-meta-env -x /app/.env.example -p /app/build/index.html
# Define the directory and file path
FILE_PATH="/app/build/.well-known/walletconnect.txt"
@@ -1,7 +1,9 @@
# Originally from: https://github.com/devcontainers/images/blob/main/src/javascript-node/.devcontainer/Dockerfile
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=22-bullseye
FROM node:${VARIANT}
# Build stage
FROM node:${VARIANT} AS builder
ARG USERNAME=node
ARG NPM_GLOBAL=/usr/local/share/npm-global
@@ -26,18 +28,30 @@ RUN \
&& su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
&& npm cache clean --force > /dev/null 2>&1
# Install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends jq bash netcat
RUN mkdir -p /scripts
# Install simple web server for now (use nginx perhaps later)
RUN yarn global add serve
WORKDIR /app
COPY . .
RUN yarn install && yarn build
# Production stage
FROM node:${VARIANT}-slim AS production
# Install additional OS packages for debugging
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends bash netcat \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /scripts
# Install serve package globally
# Install import-meta-env for setting environment variables (check package.json for version)
RUN yarn global add serve@^14.2.4 @import-meta-env/cli@^0.7.3
# Create app directory
WORKDIR /app
# Copy built application and env example from builder stage
COPY --from=builder /app/build ./build
COPY --from=builder /app/.env.example ./.env.example
# Expose port for http
EXPOSE 80
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Build cerc/laconic-wallet-web
# Build cerc/zenith-wallet-web
source ${CERC_CONTAINER_BASE_DIR}/build-base.sh
@@ -8,4 +8,4 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
CERC_NPM_REGISTRY_URL="https://git.vdb.to/api/packages/cerc-io/npm/"
docker build -t cerc/laconic-wallet-web:local ${build_command_args} -f ${SCRIPT_DIR}/Dockerfile ${CERC_REPO_BASE_DIR}/laconic-wallet-web
docker build -t cerc/zenith-wallet-web:local ${build_command_args} -f ${SCRIPT_DIR}/Dockerfile ${CERC_REPO_BASE_DIR}/zenith-wallet-web
@@ -1,88 +0,0 @@
# laconic-wallet-web
Instructions for running the `laconic-wallet-web` using [laconic-so](https://git.vdb.to/cerc-io/stack-orchestrator)
## Setup
* Clone the stack repo:
```bash
laconic-so fetch-stack git.vdb.to/LaconicNetwork/laconic-wallet-web
```
* Build the container image:
```bash
laconic-so --stack ~/cerc/laconic-wallet-web/stack/stack-orchestrator/stack/laconic-wallet-web build-containers
```
This should create the `cerc/laconic-wallet-web` image locally
## Create a deployment
* Create a spec file for the deployment:
```bash
laconic-so --stack ~/cerc/laconic-wallet-web/stack/stack-orchestrator/stack/laconic-wallet-web deploy init --output laconic-wallet-web-spec.yml
```
* Edit `network` in the spec file to map container ports to host ports as required:
```bash
network:
ports:
laconic-wallet-web:
- '3000:80'
```
* Create a deployment from the spec file:
```bash
laconic-so --stack ~/cerc/laconic-wallet-web/stack/stack-orchestrator/stack/laconic-wallet-web deploy create --spec-file laconic-wallet-web-spec.yml --deployment-dir laconic-wallet-web-deployment
```
## Configuration
* Inside the `laconic-wallet-web-deployment` deployment directory, open `config.env` file and set following env variables:
```bash
# WalletConnect project ID, same should be used in the laconic-wallet
WALLET_CONNECT_ID=
# Allowed urls is a comma separated list of allowed urls
CERC_ALLOWED_URLS=
# Optional
# WalletConnect code for hostname verification
WALLET_CONNECT_VERIFY_CODE=
# Default gas price for txs (default: 0.025)
CERC_DEFAULT_GAS_PRICE=
# Gas adjustment (default: 2)
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
CERC_GAS_ADJUSTMENT=
# RPC endpoint of laconicd node (default: https://laconicd.laconic.com)
CERC_LACONICD_RPC_URL=
# Zenith RPC endpoint
CERC_ZENITHD_RPC_URL=
```
## Start the deployment
```bash
laconic-so deployment --dir laconic-wallet-web-deployment start
```
Open the wallet app in a browser at <http://localhost:3000>
## Clean up
* Stop the deployment:
```bash
laconic-so deployment --dir laconic-wallet-web-deployment stop
```
@@ -1,7 +0,0 @@
version: "1.0"
name: laconic-wallet-web
description: "Laconic web wallet"
containers:
- cerc/laconic-wallet-web
pods:
- laconic-wallet-web
@@ -0,0 +1,85 @@
# zenith-wallet-web
Instructions for running the `zenith-wallet-web` using [laconic-so](https://git.vdb.to/cerc-io/stack-orchestrator)
## Setup
* Clone the stack repo:
```bash
laconic-so fetch-stack git.vdb.to/LaconicNetwork/zenith-wallet-web
```
* Build the container image:
```bash
laconic-so --stack ~/cerc/zenith-wallet-web/stack/stack-orchestrator/stack/zenith-wallet-web build-containers
```
This should create the `cerc/zenith-wallet-web` image locally
## Create a deployment
* Create a spec file for the deployment:
```bash
laconic-so --stack ~/cerc/zenith-wallet-web/stack/stack-orchestrator/stack/zenith-wallet-web deploy init --output zenith-wallet-web-spec.yml
```
* Edit `network` in the spec file to map container ports to host ports as required:
```bash
network:
ports:
zenith-wallet-web:
- '3000:80'
```
* Create a deployment from the spec file:
```bash
laconic-so --stack ~/cerc/zenith-wallet-web/stack/stack-orchestrator/stack/zenith-wallet-web deploy create --spec-file zenith-wallet-web-spec.yml --deployment-dir zenith-wallet-web-deployment
```
## Configuration
* Inside the `zenith-wallet-web-deployment` deployment directory, open `config.env` file and set following env variables:
```bash
# WalletConnect project ID
WALLET_CONNECT_ID=
# Allowed urls is a comma separated list of allowed urls
CERC_ALLOWED_URLS=
# Optional
# WalletConnect code for hostname verification
WALLET_CONNECT_VERIFY_CODE=
# Default gas price for txs (default: 0.025)
CERC_DEFAULT_GAS_PRICE=
# Gas adjustment (default: 2)
# Reference: https://github.com/cosmos/cosmos-sdk/issues/16020
CERC_GAS_ADJUSTMENT=
# Zenith RPC endpoint
CERC_ZENITHD_RPC_URL=
```
## Start the deployment
```bash
laconic-so deployment --dir zenith-wallet-web-deployment start
```
Open the wallet app in a browser at <http://localhost:3000>
## Clean up
* Stop the deployment:
```bash
laconic-so deployment --dir zenith-wallet-web-deployment stop
```
@@ -0,0 +1,7 @@
version: "1.0"
name: zenith-wallet-web
description: "Zenith web wallet"
containers:
- cerc/zenith-wallet-web
pods:
- zenith-wallet-web
+9
View File
@@ -0,0 +1,9 @@
:~ 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'
==
+14
View File
@@ -0,0 +1,14 @@
:: 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
--
+14
View File
@@ -0,0 +1,14 @@
:: 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
--
+14
View File
@@ -0,0 +1,14 @@
:: 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
--
+14
View File
@@ -0,0 +1,14 @@
:: 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
--
+14
View File
@@ -0,0 +1,14 @@
:: 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
--
+291 -33
View File
@@ -37,6 +37,15 @@
"@babel/highlight" "^7.24.7"
picocolors "^1.0.0"
"@babel/code-frame@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be"
integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
dependencies:
"@babel/helper-validator-identifier" "^7.27.1"
js-tokens "^4.0.0"
picocolors "^1.1.1"
"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.8":
version "7.24.9"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.9.tgz#53eee4e68f1c1d0282aa0eb05ddb02d033fc43a0"
@@ -82,6 +91,24 @@
"@jridgewell/trace-mapping" "^0.3.25"
jsesc "^2.5.1"
"@babel/generator@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.28.0.tgz#9cc2f7bd6eb054d77dc66c2664148a0c5118acd2"
integrity sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==
dependencies:
"@babel/parser" "^7.28.0"
"@babel/types" "^7.28.0"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.27.1":
version "7.27.3"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==
dependencies:
"@babel/types" "^7.27.3"
"@babel/helper-annotate-as-pure@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab"
@@ -123,6 +150,19 @@
"@babel/helper-split-export-declaration" "^7.24.7"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.21.0":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281"
integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==
dependencies:
"@babel/helper-annotate-as-pure" "^7.27.1"
"@babel/helper-member-expression-to-functions" "^7.27.1"
"@babel/helper-optimise-call-expression" "^7.27.1"
"@babel/helper-replace-supers" "^7.27.1"
"@babel/helper-skip-transparent-expression-wrappers" "^7.27.1"
"@babel/traverse" "^7.27.1"
semver "^6.3.1"
"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.24.7.tgz#be4f435a80dc2b053c76eeb4b7d16dd22cfc89da"
@@ -158,6 +198,11 @@
"@babel/template" "^7.24.7"
"@babel/types" "^7.24.7"
"@babel/helper-globals@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674"
integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
"@babel/helper-hoist-variables@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz#b4ede1cde2fd89436397f30dc9376ee06b0f25ee"
@@ -173,6 +218,14 @@
"@babel/traverse" "^7.24.8"
"@babel/types" "^7.24.8"
"@babel/helper-member-expression-to-functions@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44"
integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==
dependencies:
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b"
@@ -199,6 +252,13 @@
dependencies:
"@babel/types" "^7.24.7"
"@babel/helper-optimise-call-expression@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200"
integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==
dependencies:
"@babel/types" "^7.27.1"
"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3":
version "7.24.8"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878"
@@ -222,6 +282,15 @@
"@babel/helper-member-expression-to-functions" "^7.24.7"
"@babel/helper-optimise-call-expression" "^7.24.7"
"@babel/helper-replace-supers@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0"
integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==
dependencies:
"@babel/helper-member-expression-to-functions" "^7.27.1"
"@babel/helper-optimise-call-expression" "^7.27.1"
"@babel/traverse" "^7.27.1"
"@babel/helper-simple-access@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3"
@@ -238,6 +307,14 @@
"@babel/traverse" "^7.24.7"
"@babel/types" "^7.24.7"
"@babel/helper-skip-transparent-expression-wrappers@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56"
integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==
dependencies:
"@babel/traverse" "^7.27.1"
"@babel/types" "^7.27.1"
"@babel/helper-split-export-declaration@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz#83949436890e07fa3d6873c61a96e3bbf692d856"
@@ -250,11 +327,21 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d"
integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==
"@babel/helper-string-parser@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-validator-identifier@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db"
integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==
"@babel/helper-validator-identifier@^7.27.1":
version "7.27.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8"
integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8":
version "7.24.8"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d"
@@ -293,6 +380,13 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f"
integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w==
"@babel/parser@^7.27.2", "@babel/parser@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.0.tgz#979829fbab51a29e13901e5a80713dbcb840825e"
integrity sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==
dependencies:
"@babel/types" "^7.28.0"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055"
@@ -425,6 +519,16 @@
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
version "7.21.11"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
dependencies:
"@babel/helper-annotate-as-pure" "^7.18.6"
"@babel/helper-create-class-features-plugin" "^7.21.0"
"@babel/helper-plugin-utils" "^7.20.2"
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
"@babel/plugin-syntax-async-generators@^7.8.4":
version "7.8.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d"
@@ -1225,6 +1329,15 @@
"@babel/parser" "^7.24.7"
"@babel/types" "^7.24.7"
"@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/parser" "^7.27.2"
"@babel/types" "^7.27.1"
"@babel/traverse@^7.20.0", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.7.2":
version "7.24.8"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.8.tgz#6c14ed5232b7549df3371d820fbd9abfcd7dfab7"
@@ -1241,6 +1354,19 @@
debug "^4.3.1"
globals "^11.1.0"
"@babel/traverse@^7.27.1":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.28.0.tgz#518aa113359b062042379e333db18380b537e34b"
integrity sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==
dependencies:
"@babel/code-frame" "^7.27.1"
"@babel/generator" "^7.28.0"
"@babel/helper-globals" "^7.28.0"
"@babel/parser" "^7.28.0"
"@babel/template" "^7.27.2"
"@babel/types" "^7.28.0"
debug "^4.3.1"
"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.20.0", "@babel/types@^7.20.7", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.24.9", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
version "7.24.9"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.9.tgz#228ce953d7b0d16646e755acf204f4cf3d08cc73"
@@ -1250,6 +1376,14 @@
"@babel/helper-validator-identifier" "^7.24.7"
to-fast-properties "^2.0.0"
"@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.0":
version "7.28.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.0.tgz#2fd0159a6dc7353933920c43136335a9b264d950"
integrity sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==
dependencies:
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -1331,7 +1465,7 @@
"@cosmjs/math" "0.28.13"
"@cosmjs/utils" "0.28.13"
"@cosmjs/amino@^0.32.3", "@cosmjs/amino@^0.32.4":
"@cosmjs/amino@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.32.4.tgz#3908946c0394e6d431694c8992c5147079a1c860"
integrity sha512-zKYOt6hPy8obIFtLie/xtygCkH9ZROiQ12UHfKsOkWaZfPQUvVbtgmu6R4Kn1tFLI/SRkw7eqhaogmW/3NYu/Q==
@@ -1341,6 +1475,16 @@
"@cosmjs/math" "^0.32.4"
"@cosmjs/utils" "^0.32.4"
"@cosmjs/amino@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/amino/-/amino-0.33.1.tgz#0d4957b2e755af8392627c0c0f72bee129dcdcf3"
integrity sha512-WfWiBf2EbIWpwKG9AOcsIIkR717SY+JdlXM/SL/bI66BdrhniAF+/ZNis9Vo9HF6lP2UU5XrSmFA4snAvEgdrg==
dependencies:
"@cosmjs/crypto" "^0.33.1"
"@cosmjs/encoding" "^0.33.1"
"@cosmjs/math" "^0.33.1"
"@cosmjs/utils" "^0.33.1"
"@cosmjs/crypto@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.27.1.tgz#271c853089a3baf3acd6cf0b2122fd49f8815743"
@@ -1370,7 +1514,7 @@
elliptic "^6.5.3"
libsodium-wrappers "^0.7.6"
"@cosmjs/crypto@^0.32.3", "@cosmjs/crypto@^0.32.4":
"@cosmjs/crypto@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.32.4.tgz#5d29633b661eaf092ddb3e7ea6299cfd6f4507a2"
integrity sha512-zicjGU051LF1V9v7bp8p7ovq+VyC91xlaHdsFOTo2oVry3KQikp8L/81RkXmUIT8FxMwdx1T7DmFwVQikcSDIw==
@@ -1383,6 +1527,19 @@
elliptic "^6.5.4"
libsodium-wrappers-sumo "^0.7.11"
"@cosmjs/crypto@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/crypto/-/crypto-0.33.1.tgz#761b1623e4abe8af4cbf7ca92639561314f04c3b"
integrity sha512-U4kGIj/SNBzlb2FGgA0sMR0MapVgJUg8N+oIAiN5+vl4GZ3aefmoL1RDyTrFS/7HrB+M+MtHsxC0tvEu4ic/zA==
dependencies:
"@cosmjs/encoding" "^0.33.1"
"@cosmjs/math" "^0.33.1"
"@cosmjs/utils" "^0.33.1"
"@noble/hashes" "^1"
bn.js "^5.2.0"
elliptic "^6.6.1"
libsodium-wrappers-sumo "^0.7.11"
"@cosmjs/encoding@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@cosmjs/encoding/-/encoding-0.27.1.tgz#3cd5bc0af743485eb2578cdb08cfa84c86d610e1"
@@ -1427,6 +1584,14 @@
"@cosmjs/stream" "^0.32.4"
xstream "^11.14.0"
"@cosmjs/json-rpc@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/json-rpc/-/json-rpc-0.33.1.tgz#a5b8459605750fa7d38c05aa6009a92010c0d042"
integrity sha512-T6VtWzecpmuTuMRGZWuBYHsMF/aznWCYUt/cGMWNSz7DBPipVd0w774PKpxXzpEbyt5sr61NiuLXc+Az15S/Cw==
dependencies:
"@cosmjs/stream" "^0.33.1"
xstream "^11.14.0"
"@cosmjs/launchpad@^0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@cosmjs/launchpad/-/launchpad-0.27.1.tgz#b6f1995748be96560f5f01e84d3ff907477dda77"
@@ -1461,7 +1626,14 @@
dependencies:
bn.js "^5.2.0"
"@cosmjs/proto-signing@^0.32.2", "@cosmjs/proto-signing@^0.32.3", "@cosmjs/proto-signing@^0.32.4":
"@cosmjs/math@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/math/-/math-0.33.1.tgz#04ae4cfdb05f04f1b13e908f9551ca85b13ba4d4"
integrity sha512-ytGkWdKFCPiiBU5eqjHNd59djPpIsOjbr2CkNjlnI1Zmdj+HDkSoD9MUGpz9/RJvRir5IvsXqdE05x8EtoQkJA==
dependencies:
bn.js "^5.2.0"
"@cosmjs/proto-signing@^0.32.2", "@cosmjs/proto-signing@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.32.4.tgz#5a06e087c6d677439c8c9b25b5223d5e72c4cd93"
integrity sha512-QdyQDbezvdRI4xxSlyM1rSVBO2st5sqtbEIl3IX03uJ7YiZIQHyv6vaHVf1V4mapusCqguiHJzm4N4gsFdLBbQ==
@@ -1473,6 +1645,18 @@
"@cosmjs/utils" "^0.32.4"
cosmjs-types "^0.9.0"
"@cosmjs/proto-signing@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/proto-signing/-/proto-signing-0.33.1.tgz#b084eb86410486cff30da7de34a636421db90ca8"
integrity sha512-Sv4W+MxX+0LVnd+2rU4Fw1HRsmMwSVSYULj7pRkij3wnPwUlTVoJjmKFgKz13ooIlfzPrz/dnNjGp/xnmXChFQ==
dependencies:
"@cosmjs/amino" "^0.33.1"
"@cosmjs/crypto" "^0.33.1"
"@cosmjs/encoding" "^0.33.1"
"@cosmjs/math" "^0.33.1"
"@cosmjs/utils" "^0.33.1"
cosmjs-types "^0.9.0"
"@cosmjs/socket@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.32.4.tgz#86ab6adf3a442314774c0810b7a7cfcddf4f2082"
@@ -1483,7 +1667,17 @@
ws "^7"
xstream "^11.14.0"
"@cosmjs/stargate@^0.32.2", "@cosmjs/stargate@^0.32.3":
"@cosmjs/socket@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/socket/-/socket-0.33.1.tgz#2402487e7c70c8a5c801bd3189a58a09da786513"
integrity sha512-KzAeorten6Vn20sMiM6NNWfgc7jbyVo4Zmxev1FXa5EaoLCZy48cmT3hJxUJQvJP/lAy8wPGEjZ/u4rmF11x9A==
dependencies:
"@cosmjs/stream" "^0.33.1"
isomorphic-ws "^4.0.1"
ws "^7"
xstream "^11.14.0"
"@cosmjs/stargate@^0.32.2":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.32.4.tgz#bd0e4d3bf613b629addbf5f875d3d3b50f640af1"
integrity sha512-usj08LxBSsPRq9sbpCeVdyLx2guEcOHfJS9mHGCLCXpdAPEIEQEtWLDpEUc0LEhWOx6+k/ChXTc5NpFkdrtGUQ==
@@ -1499,6 +1693,20 @@
cosmjs-types "^0.9.0"
xstream "^11.14.0"
"@cosmjs/stargate@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/stargate/-/stargate-0.33.1.tgz#13972f710942ac728474051be4f9754814ccfb52"
integrity sha512-CnJ1zpSiaZgkvhk+9aTp5IPmgWn2uo+cNEBN8VuD9sD6BA0V4DMjqe251cNFLiMhkGtiE5I/WXFERbLPww3k8g==
dependencies:
"@cosmjs/amino" "^0.33.1"
"@cosmjs/encoding" "^0.33.1"
"@cosmjs/math" "^0.33.1"
"@cosmjs/proto-signing" "^0.33.1"
"@cosmjs/stream" "^0.33.1"
"@cosmjs/tendermint-rpc" "^0.33.1"
"@cosmjs/utils" "^0.33.1"
cosmjs-types "^0.9.0"
"@cosmjs/stream@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.32.4.tgz#83e1f2285807467c56d9ea0e1113f79d9fa63802"
@@ -1506,6 +1714,13 @@
dependencies:
xstream "^11.14.0"
"@cosmjs/stream@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/stream/-/stream-0.33.1.tgz#2e928eb68c52253e64ab56a3047cd8039b66abde"
integrity sha512-bMUvEENjeQPSTx+YRzVsWT1uFIdHRcf4brsc14SOoRQ/j5rOJM/aHfsf/BmdSAnYbdOQ3CMKj/8nGAQ7xUdn7w==
dependencies:
xstream "^11.14.0"
"@cosmjs/tendermint-rpc@^0.32.2", "@cosmjs/tendermint-rpc@^0.32.4":
version "0.32.4"
resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.32.4.tgz#b36f9ec657498e42c97e21bb7368798ef6279752"
@@ -1522,6 +1737,22 @@
readonly-date "^1.0.0"
xstream "^11.14.0"
"@cosmjs/tendermint-rpc@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/tendermint-rpc/-/tendermint-rpc-0.33.1.tgz#5ab5b0b63e585badaa5827aef7e9e3d18695630a"
integrity sha512-22klDFq2MWnf//C8+rZ5/dYatr6jeGT+BmVbutXYfAK9fmODbtFcumyvB6uWaEORWfNukl8YK1OLuaWezoQvxA==
dependencies:
"@cosmjs/crypto" "^0.33.1"
"@cosmjs/encoding" "^0.33.1"
"@cosmjs/json-rpc" "^0.33.1"
"@cosmjs/math" "^0.33.1"
"@cosmjs/socket" "^0.33.1"
"@cosmjs/stream" "^0.33.1"
"@cosmjs/utils" "^0.33.1"
axios "^1.6.0"
readonly-date "^1.0.0"
xstream "^11.14.0"
"@cosmjs/utils@0.27.1":
version "0.27.1"
resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.27.1.tgz#1c8efde17256346ef142a3bd15158ee4055470e2"
@@ -1537,6 +1768,11 @@
resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.32.4.tgz#a9a717c9fd7b1984d9cefdd0ef6c6f254060c671"
integrity sha512-D1Yc+Zy8oL/hkUkFUL/bwxvuDBzRGpc4cF7/SkdhxX4iHpSLgdOuTt1mhCh9+kl6NQREy9t7SYZ6xeW5gFe60w==
"@cosmjs/utils@^0.33.1":
version "0.33.1"
resolved "https://registry.yarnpkg.com/@cosmjs/utils/-/utils-0.33.1.tgz#8882cd26172cb5b0b692c179407d6c3904493fed"
integrity sha512-UnLHDY6KMmC+UXf3Ufyh+onE19xzEXjT4VZ504Acmk4PXxqyvG4cCPprlKUFnGUX7f0z8Or9MAOHXBx41uHBcg==
"@csstools/normalize.css@*":
version "12.1.1"
resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-12.1.1.tgz#f0ad221b7280f3fc814689786fd9ee092776ef8f"
@@ -2553,6 +2789,14 @@
"@types/yargs" "^17.0.8"
chalk "^4.0.0"
"@jridgewell/gen-mapping@^0.3.12":
version "0.3.12"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz#2234ce26c62889f03db3d7fea43c1932ab3e927b"
integrity sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/gen-mapping@^0.3.2", "@jridgewell/gen-mapping@^0.3.5":
version "0.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
@@ -2593,6 +2837,14 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@jridgewell/trace-mapping@^0.3.28":
version "0.3.29"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz#a58d31eaadaf92c6695680b2e1d464a9b8fbf7fc"
integrity sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@json-rpc-tools/types@^1.7.6":
version "1.7.6"
resolved "https://registry.yarnpkg.com/@json-rpc-tools/types/-/types-1.7.6.tgz#5abd5fde01364a130c46093b501715bcce5bdc0e"
@@ -2608,6 +2860,14 @@
"@json-rpc-tools/types" "^1.7.6"
"@pedrouid/environment" "^1.0.1"
"@laconic-network/cosmjs-util@^0.1.0":
version "0.1.0"
resolved "https://git.vdb.to/api/packages/LaconicNetwork/npm/%40laconic-network%2Fcosmjs-util/-/0.1.0/cosmjs-util-0.1.0.tgz#899af1de6101834e42c68aa684a00f51f7b913ed"
integrity sha512-18Gez/2FwDkLf/de1x6httgtwrj+9jldc7V6Or0KGGtboQgKPUC6G5RebowpeWR/d9GVORV3ha8H2PpCtH4XMQ==
dependencies:
"@cosmjs/math" "^0.33.1"
"@cosmjs/stargate" "^0.33.1"
"@leichtgewicht/ip-codec@^2.0.1":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1"
@@ -6995,6 +7255,19 @@ elliptic@^6.4.0, elliptic@^6.5.2, elliptic@^6.5.3, elliptic@^6.5.4, elliptic@^6.
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
elliptic@^6.6.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06"
integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emittery@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933"
@@ -9906,6 +10179,11 @@ jsesc@^2.5.1:
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
jsesc@^3.0.2:
version "3.1.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d"
integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
jsesc@~0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
@@ -11369,7 +11647,7 @@ performance-now@^2.1.0:
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picocolors@1.1.1:
picocolors@1.1.1, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
@@ -13463,16 +13741,7 @@ string-natural-compare@^3.0.1:
resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4"
integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13575,7 +13844,7 @@ stringify-object@^3.3.0:
is-obj "^1.0.1"
is-regexp "^1.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13589,13 +13858,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -14415,6 +14677,11 @@ vlq@^1.0.0:
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
vm-browserify@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"
integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==
w3c-hr-time@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
@@ -14906,7 +15173,7 @@ workbox-window@6.6.1:
"@types/trusted-types" "^2.0.2"
workbox-core "6.6.1"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -14924,15 +15191,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"