diff --git a/.github/workflows/deploy-firebase-dapp.yml b/.github/workflows/deploy-firebase-dapp.yml deleted file mode 100644 index e904cfb3..00000000 --- a/.github/workflows/deploy-firebase-dapp.yml +++ /dev/null @@ -1,41 +0,0 @@ ---- -# Builds the static site via pnpm, then copies that local directory's contents -# to Firebase, at: https://app.testnet.penumbra.zone -name: Deploy static site -on: - # Support ad-hoc runs via dispatch, so we can deploy from - # unmerged feature branches if necessary. - workflow_dispatch: - workflow_call: - push: - branches: - - main - -jobs: - build: - name: Deploy - runs-on: ubuntu-latest - steps: - - name: Checkout the source code - uses: actions/checkout@v3 - - - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Install dependencies - run: pnpm install - working-directory: apps/minifront - - - name: Build static site - run: pnpm build - working-directory: apps/minifront - - - name: Deploy dapp static site to firebase - uses: w9jds/firebase-action@v2.0.0 - with: - args: deploy - env: - FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} - PROJECT_ID: penumbra-dapp - PROJECT_PATH: apps/minifront/dist diff --git a/apps/minifront/.firebaserc b/apps/minifront/.firebaserc deleted file mode 100644 index 9881bf8a..00000000 --- a/apps/minifront/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "test-dapp-monorepo" - } -} diff --git a/apps/minifront/CHANGELOG.md b/apps/minifront/CHANGELOG.md deleted file mode 100644 index 3ffbed79..00000000 --- a/apps/minifront/CHANGELOG.md +++ /dev/null @@ -1,286 +0,0 @@ -# minifront - -## 5.1.0 - -### Minor Changes - -- 282eabf: Click wallet for max amount - -### Patch Changes - -- 20bb7ac: fix destination address validation in the "shield funds" page -- adf3a28: Update to june 12 testnet registry -- 6b06e04: Introduce ZQuery package and use throughout minifront -- cf2594d: add display for the both in- and out-tokens on the swap page -- 94e3240: Change styles of the unclaimed swap block -- Updated dependencies [ab9d743] -- Updated dependencies [282eabf] -- Updated dependencies [0076a1d] -- Updated dependencies [81b9536] -- Updated dependencies [3be0580] -- Updated dependencies [6b06e04] -- Updated dependencies [24c8b4f] -- Updated dependencies [c8e8d15] -- Updated dependencies [24c8b4f] -- Updated dependencies [e7d7ffc] - - @penumbra-zone/types@7.1.0 - - @penumbra-zone/ui@3.4.0 - - @penumbra-zone/protobuf@4.1.0 - - @penumbra-zone/client@6.0.1 - - @penumbra-zone/zquery@1.0.0 - - @penumbra-zone/getters@6.1.0 - - @penumbra-zone/crypto-web@3.0.10 - - @penumbra-zone/perspective@4.0.1 - -## 5.0.3 - -### Patch Changes - -- Updated dependencies [8fe4de6] - - @penumbra-zone/transport-dom@6.0.0 - - @penumbra-zone/perspective@4.0.0 - - @penumbra-zone/protobuf@4.0.0 - - @penumbra-zone/bech32m@5.0.0 - - @penumbra-zone/getters@6.0.0 - - @penumbra-zone/client@6.0.0 - - @penumbra-zone/ui@3.3.2 - - @penumbra-zone/types@7.0.1 - - @penumbra-zone/crypto-web@3.0.9 - -## 5.0.2 - -### Patch Changes - -- bb5f621: formatAmount() takes new args -- Updated dependencies [bb5f621] -- Updated dependencies [8b121ec] - - @penumbra-zone/types@7.0.0 - - @penumbra-zone/ui@3.3.1 - - @penumbra-zone/transport-dom@5.0.0 - - @penumbra-zone/perspective@3.0.0 - - @penumbra-zone/protobuf@3.0.0 - - @penumbra-zone/bech32m@4.0.0 - - @penumbra-zone/getters@5.0.0 - - @penumbra-zone/client@5.0.0 - - @penumbra-zone/crypto-web@3.0.8 - -## 5.0.1 - -### Patch Changes - -- a22d3a8: Update registry (noble/testnet channel update) - -## 5.0.0 - -### Major Changes - -- 029eebb: use service definitions from protobuf collection package - -### Minor Changes - -- 120b654: Support estimates of outputs for auctions; redesign the estimate results part of the swap/auction UI -- 3ea1e6c: update buf types dependencies - -### Patch Changes - -- Updated dependencies [fc9418c] -- Updated dependencies [120b654] -- Updated dependencies [4f8c150] -- Updated dependencies [029eebb] -- Updated dependencies [029eebb] -- Updated dependencies [3ea1e6c] - - @penumbra-zone/ui@3.3.0 - - @penumbra-zone/getters@4.1.0 - - @penumbra-zone/protobuf@2.1.0 - - @penumbra-zone/types@6.0.0 - - @penumbra-zone/transport-dom@4.1.0 - - @penumbra-zone/perspective@2.1.0 - - @penumbra-zone/bech32m@3.2.0 - - @penumbra-zone/client@4.2.0 - - @penumbra-zone/crypto-web@3.0.7 - -## 4.6.0 - -### Minor Changes - -- d8fef48: Update design of DutchAuctionComponent; add filtering to auctions - -### Patch Changes - -- Updated dependencies [d8fef48] -- Updated dependencies [5b80e7c] - - @penumbra-zone/ui@3.2.0 - - @penumbra-zone/perspective@2.0.1 - -## 4.5.0 - -### Minor Changes - -- e47a04e: Update registry to latest (fixes labs + adds starling) -- a241386: Combine the swap and auction forms -- 146b48d: Support GDAs -- cf63b30: Show swap routes in the UI; extract a component. -- e4c9fce: Add features to handle auction withdrawals - -### Patch Changes - -- d654724: Fix error splash screen -- 9563ed0: Fix a bug where multiple responses streamed to the same state variable simultaneously -- e35c6f7: Deps bumped to latest -- d6b8a23: Update registry -- 43bf99f: Add a UI to inspect an address; create a component -- Updated dependencies [146b48d] -- Updated dependencies [8ccaf30] -- Updated dependencies [8ccaf30] -- Updated dependencies [e35c6f7] -- Updated dependencies [cf63b30] -- Updated dependencies [e4c9fce] -- Updated dependencies [8a3b442] -- Updated dependencies [43bf99f] -- Updated dependencies [8ccaf30] - - @penumbra-zone/getters@4.0.0 - - @penumbra-zone/types@5.0.0 - - @penumbra-zone/perspective@2.0.0 - - @penumbra-zone/bech32m@3.1.1 - - @penumbra-zone/ui@3.1.0 - - @penumbra-zone/crypto-web@3.0.6 - - @penumbra-zone/client@4.1.2 - -## 4.4.0 - -### Minor Changes - -- v8.0.0 versioning and manifest - -### Patch Changes - -- 610a445: update osmosis channel for deimos-8 -- Updated dependencies - - @penumbra-zone/ui@3.0.0 - - @penumbra-zone/bech32m@3.1.0 - - @penumbra-zone/types@4.1.0 - - @penumbra-zone/getters@3.0.2 - - @penumbra-zone/perspective@1.0.6 - - @penumbra-zone/client@4.1.1 - - @penumbra-zone/crypto-web@3.0.5 - -## 4.3.3 - -### Patch Changes - -- Updated dependencies [8410d2f] -- Updated dependencies [8410d2f] - - @penumbra-zone/bech32m@3.0.1 - - @penumbra-zone/client@4.1.0 - - @penumbra-zone/getters@3.0.1 - - @penumbra-zone/perspective@1.0.5 - - @penumbra-zone/types@4.0.1 - - @penumbra-zone/ui@2.0.5 - - @penumbra-zone/crypto-web@3.0.4 - -## 4.3.2 - -### Patch Changes - -- Updated dependencies [fc500af] -- Updated dependencies [6fb898a] - - @penumbra-zone/transport-dom@4.0.0 - - @penumbra-zone/types@4.0.0 - - @penumbra-zone/client@4.0.1 - - @penumbra-zone/crypto-web@3.0.3 - - @penumbra-zone/perspective@1.0.4 - - @penumbra-zone/ui@2.0.4 - -## 4.3.1 - -### Patch Changes - -- Updated dependencies [3148375] -- Updated dependencies [fdd4303] - - @penumbra-zone/transport-dom@3.0.0 - - @penumbra-zone/constants@4.0.0 - - @penumbra-zone/getters@3.0.0 - - @penumbra-zone/client@4.0.0 - - @penumbra-zone/types@3.0.0 - - @penumbra-zone/bech32m@3.0.0 - - @penumbra-zone/ui@2.0.3 - - @penumbra-zone/crypto-web@3.0.2 - - @penumbra-zone/perspective@1.0.3 - -## 4.3.0 - -### Minor Changes - -- 862283c: Using external registry for ibc chains - -### Patch Changes - -- Updated dependencies [862283c] - - @penumbra-zone/constants@3.0.0 - - @penumbra-zone/perspective@1.0.2 - - @penumbra-zone/getters@2.0.1 - - @penumbra-zone/types@2.0.1 - - @penumbra-zone/ui@2.0.2 - - @penumbra-zone/client@3.0.1 - - @penumbra-zone/crypto-web@3.0.1 - -## 4.2.0 - -### Minor Changes - -- v6.3 ext updates: loading indicator, portfolio viewing, bug fixes - -### Patch Changes - -- Updated dependencies [b4082b7] - - @penumbra-zone/crypto-web@3.0.0 - -## 4.1.0 - -### Minor Changes - -- 66c2407: v6.2.0 release - -### Patch Changes - -- @penumbra-zone/perspective@1.0.1 -- @penumbra-zone/ui@2.0.1 - -## 4.0.0 - -### Major Changes - -- 929d278: barrel imports to facilitate better tree shaking - -### Patch Changes - -- Updated dependencies [7a1efed] -- Updated dependencies [7a1efed] -- Updated dependencies [8933117] -- Updated dependencies [929d278] - - @penumbra-zone/client@3.0.0 - - @penumbra-zone/ui@2.0.0 - - @penumbra-zone/constants@2.0.0 - - @penumbra-zone/perspective@1.0.0 - - @penumbra-zone/getters@2.0.0 - - @penumbra-zone/bech32@2.0.0 - - @penumbra-zone/crypto-web@2.0.0 - - @penumbra-zone/types@2.0.0 - - @penumbra-zone/transport-dom@2.0.0 - -## 3.0.0 - -### Major Changes - -- Initial changest. Git tag v5.0.0 updates. - -### Patch Changes - -- Updated dependencies - - @penumbra-zone/client@2.0.0 - - @penumbra-zone/constants@1.1.0 - - @penumbra-zone/getters@1.1.0 - - @penumbra-zone/transport-dom@1.1.0 - - @penumbra-zone/types@1.1.0 - - @penumbra-zone/ui@1.0.2 - - @penumbra-zone/crypto-web@1.0.1 diff --git a/apps/minifront/__mocks__/zustand.ts b/apps/minifront/__mocks__/zustand.ts deleted file mode 100644 index 5fa53db5..00000000 --- a/apps/minifront/__mocks__/zustand.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Mock Zustand for tests. - * - * @see https://github.com/pmndrs/zustand/blob/main/docs/guides/testing.md#vitest - */ -import * as zustand from 'zustand'; -import { act } from '@testing-library/react'; -import { afterEach, vi } from 'vitest'; - -const { create: actualCreate, createStore: actualCreateStore } = - await vi.importActual('zustand'); - -// a variable to hold reset functions for all stores declared in the app -export const storeResetFns = new Set<() => void>(); - -const createUncurried = (stateCreator: zustand.StateCreator) => { - const store = actualCreate(stateCreator); - const initialState = store.getInitialState(); - storeResetFns.add(() => { - store.setState(initialState, true); - }); - return store; -}; - -// when creating a store, we get its initial state, create a reset function and add it in the set -export const create = ((stateCreator: zustand.StateCreator) => { - // to support curried version of create - return typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried; -}) as typeof zustand.create; - -const createStoreUncurried = (stateCreator: zustand.StateCreator) => { - const store = actualCreateStore(stateCreator); - const initialState = store.getInitialState(); - storeResetFns.add(() => { - store.setState(initialState, true); - }); - return store; -}; - -// when creating a store, we get its initial state, create a reset function and add it in the set -export const createStore = ((stateCreator: zustand.StateCreator) => { - // to support curried version of createStore - return typeof stateCreator === 'function' - ? createStoreUncurried(stateCreator) - : createStoreUncurried; -}) as typeof zustand.createStore; - -// reset all stores after each test run -afterEach(() => { - act(() => { - storeResetFns.forEach(resetFn => { - resetFn(); - }); - }); -}); diff --git a/apps/minifront/eslint.config.mjs b/apps/minifront/eslint.config.mjs deleted file mode 100644 index a53ed8e5..00000000 --- a/apps/minifront/eslint.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { penumbraEslintConfig } from '@penumbra-zone/eslint-config'; -import { config, parser } from 'typescript-eslint'; - -export default config({ - ...penumbraEslintConfig, - languageOptions: { - parser, - parserOptions: { - project: true, - tsconfigRootDir: import.meta.dirname, - }, - }, -}); diff --git a/apps/minifront/firebase.json b/apps/minifront/firebase.json deleted file mode 100644 index 502ed5b1..00000000 --- a/apps/minifront/firebase.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "hosting": { - "public": "dist", - "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], - "cleanUrls": true, - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - }, - "emulators": { - "ui": { - "enabled": true - }, - "functions": { - "port": 5001 - }, - "hosting": { - "enabled": true, - "port": 5000 - } - } -} diff --git a/apps/minifront/index.html b/apps/minifront/index.html deleted file mode 100644 index b03aafee..00000000 --- a/apps/minifront/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
- - - diff --git a/apps/minifront/package.json b/apps/minifront/package.json deleted file mode 100644 index 9205fe3e..00000000 --- a/apps/minifront/package.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "name": "minifront", - "version": "5.1.0", - "private": true, - "license": "(MIT OR Apache-2.0)", - "type": "module", - "scripts": { - "build": "tsc && vite build", - "clean": "rm -rfv dist", - "dev": "vite --port 5173", - "lint": "eslint src", - "preview": "vite preview", - "test": "vitest run" - }, - "dependencies": { - "@buf/cosmos_ibc.bufbuild_es": "1.9.0-20240530142100-ad4444393387.1", - "@buf/penumbra-zone_penumbra.bufbuild_es": "1.9.0-20240528180215-8fe1c79485f8.1", - "@bufbuild/protobuf": "^1.10.0", - "@cosmjs/proto-signing": "^0.32.3", - "@cosmjs/stargate": "^0.32.3", - "@cosmos-kit/core": "^2.12.0", - "@cosmos-kit/react": "^2.15.0", - "@interchain-ui/react": "^1.23.16", - "@penumbra-labs/registry": "8.0.1", - "@penumbra-zone/bech32m": "workspace:*", - "@penumbra-zone/client": "workspace:*", - "@penumbra-zone/crypto-web": "workspace:*", - "@penumbra-zone/getters": "workspace:*", - "@penumbra-zone/perspective": "workspace:*", - "@penumbra-zone/protobuf": "workspace:*", - "@penumbra-zone/transport-dom": "workspace:*", - "@penumbra-zone/types": "workspace:*", - "@penumbra-zone/ui": "workspace:*", - "@penumbra-zone/zquery": "workspace:*", - "@radix-ui/react-icons": "^1.3.0", - "@tanstack/react-query": "4.36.1", - "bech32": "^2.0.0", - "bignumber.js": "^9.1.2", - "chain-registry": "^1.62.8", - "cosmos-kit": "^2.17.0", - "date-fns": "^3.6.0", - "framer-motion": "^11.2.4", - "immer": "^10.1.1", - "lodash": "^4.17.21", - "lucide-react": "^0.378.0", - "osmo-query": "^16.13.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-helmet": "^6.1.0", - "react-loader-spinner": "^6.1.6", - "react-router-dom": "^6.23.1", - "sonner": "1.4.3", - "tailwindcss": "^3.4.3", - "zod": "^3.23.8", - "zustand": "^4.5.2" - }, - "devDependencies": { - "@chain-registry/types": "^0.44.6", - "@penumbra-zone/polyfills": "workspace:*", - "@testing-library/jest-dom": "^6.4.5", - "@testing-library/react": "^15.0.7", - "@types/lodash": "^4.17.4", - "@types/react": "^18.3.2", - "@types/react-dom": "^18.3.0", - "@types/react-helmet": "^6.1.11", - "autoprefixer": "^10.4.19", - "firebase-tools": "^13.9.0", - "postcss": "^8.4.38", - "promise.withresolvers": "^1.0.3", - "vite-plugin-node-stdlib-browser": "^0.2.1" - } -} diff --git a/apps/minifront/postcss.config.js b/apps/minifront/postcss.config.js deleted file mode 100644 index 8e18cdcb..00000000 --- a/apps/minifront/postcss.config.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@penumbra-zone/ui/postcss.config.js'; diff --git a/apps/minifront/promise.withresolvers.d.ts b/apps/minifront/promise.withresolvers.d.ts deleted file mode 100644 index 3abe8237..00000000 --- a/apps/minifront/promise.withresolvers.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module 'promise.withresolvers' { - const withResolvers: { shim: () => void }; - export default withResolvers; -} diff --git a/apps/minifront/public/arrow-down.svg b/apps/minifront/public/arrow-down.svg deleted file mode 100644 index 31476fc5..00000000 --- a/apps/minifront/public/arrow-down.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/apps/minifront/public/arrow-replace.svg b/apps/minifront/public/arrow-replace.svg deleted file mode 100644 index 9d8216f7..00000000 --- a/apps/minifront/public/arrow-replace.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/minifront/public/auction-gradient.svg b/apps/minifront/public/auction-gradient.svg deleted file mode 100644 index d1f84043..00000000 --- a/apps/minifront/public/auction-gradient.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/minifront/public/coin-stack-icon.svg b/apps/minifront/public/coin-stack-icon.svg deleted file mode 100644 index b47d9d47..00000000 --- a/apps/minifront/public/coin-stack-icon.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/favicon.png b/apps/minifront/public/favicon.png deleted file mode 100644 index 3d28746b..00000000 Binary files a/apps/minifront/public/favicon.png and /dev/null differ diff --git a/apps/minifront/public/fuel.svg b/apps/minifront/public/fuel.svg deleted file mode 100644 index ccfd086a..00000000 --- a/apps/minifront/public/fuel.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/minifront/public/funds-gradient.svg b/apps/minifront/public/funds-gradient.svg deleted file mode 100644 index 5f804303..00000000 --- a/apps/minifront/public/funds-gradient.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/history-icon.svg b/apps/minifront/public/history-icon.svg deleted file mode 100644 index 64a282cc..00000000 --- a/apps/minifront/public/history-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/ibc-gradient.svg b/apps/minifront/public/ibc-gradient.svg deleted file mode 100644 index 7d2089bd..00000000 --- a/apps/minifront/public/ibc-gradient.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/minifront/public/incognito.svg b/apps/minifront/public/incognito.svg deleted file mode 100644 index f7fd2041..00000000 --- a/apps/minifront/public/incognito.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/minifront/public/info-icon.svg b/apps/minifront/public/info-icon.svg deleted file mode 100644 index 5f506f2f..00000000 --- a/apps/minifront/public/info-icon.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/minifront/public/logo.svg b/apps/minifront/public/logo.svg deleted file mode 100644 index 0293e288..00000000 --- a/apps/minifront/public/logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/minifront/public/more.svg b/apps/minifront/public/more.svg deleted file mode 100644 index 5bf53987..00000000 --- a/apps/minifront/public/more.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/minifront/public/negative-charts.svg b/apps/minifront/public/negative-charts.svg deleted file mode 100644 index 092ba5d7..00000000 --- a/apps/minifront/public/negative-charts.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/minifront/public/nodes-icon.svg b/apps/minifront/public/nodes-icon.svg deleted file mode 100644 index d91ff28b..00000000 --- a/apps/minifront/public/nodes-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/apps/minifront/public/penumbra-logo.svg b/apps/minifront/public/penumbra-logo.svg deleted file mode 100644 index 96b7c3f4..00000000 --- a/apps/minifront/public/penumbra-logo.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/minifront/public/positive-charts.svg b/apps/minifront/public/positive-charts.svg deleted file mode 100644 index 320f5613..00000000 --- a/apps/minifront/public/positive-charts.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/minifront/public/receive-icon.svg b/apps/minifront/public/receive-icon.svg deleted file mode 100644 index 4af7588f..00000000 --- a/apps/minifront/public/receive-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/sandpiper-gradient.svg b/apps/minifront/public/sandpiper-gradient.svg deleted file mode 100644 index f6d69448..00000000 --- a/apps/minifront/public/sandpiper-gradient.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/send-icon.svg b/apps/minifront/public/send-icon.svg deleted file mode 100644 index c2d5b192..00000000 --- a/apps/minifront/public/send-icon.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/swap-icon.svg b/apps/minifront/public/swap-icon.svg deleted file mode 100644 index 7f6148fd..00000000 --- a/apps/minifront/public/swap-icon.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/public/test-asset-icon-2.svg b/apps/minifront/public/test-asset-icon-2.svg deleted file mode 100644 index f39dba1b..00000000 --- a/apps/minifront/public/test-asset-icon-2.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - diff --git a/apps/minifront/public/test-asset-icon.svg b/apps/minifront/public/test-asset-icon.svg deleted file mode 100644 index 269eb5a9..00000000 --- a/apps/minifront/public/test-asset-icon.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/apps/minifront/src/abort-loader.ts b/apps/minifront/src/abort-loader.ts deleted file mode 100644 index 1d2977e0..00000000 --- a/apps/minifront/src/abort-loader.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - isPraxConnected, - throwIfPraxNotConnected, - throwIfPraxNotInstalled, -} from '@penumbra-zone/client/prax'; - -/** - * Retry test, resolving `true`, or resolving `false` if timeout reached. - * - * @param fn test method returning a boolean - * @param ms millisecond maximum wait. default half a second - * @param rate wait between attempts. default `ms/10`, stays above 50ms unless set. - * @returns promise that resolves to true if `fn` returns true, or false at timeout - */ -const retry = async (fn: () => boolean, ms = 500, rate = Math.max(ms / 10, 50)) => - fn() || - new Promise(resolve => { - const interval = setInterval(() => { - if (fn()) { - clearInterval(interval); - resolve(true); - } - }, rate); - setTimeout(() => { - clearInterval(interval); - resolve(false); - }, ms); - }); - -/** - * Resolves fast if Prax is connected, or rejects if Prax is not connected after - * timeout. This is a temporary solution until loaders properly await Prax - * connection. - */ -export const abortLoader = async (): Promise => { - await throwIfPraxNotInstalled(); - await retry(() => isPraxConnected()); - throwIfPraxNotConnected(); -}; diff --git a/apps/minifront/src/clients.ts b/apps/minifront/src/clients.ts deleted file mode 100644 index 98e7aa16..00000000 --- a/apps/minifront/src/clients.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createPraxClient } from '@penumbra-zone/client/prax'; -import { - CustodyService, - DexService, - IbcChannelService, - IbcClientService, - IbcConnectionService, - SctService, - SimulationService, - StakeService, - TendermintProxyService, - ViewService, -} from '@penumbra-zone/protobuf'; - -export const custodyClient = createPraxClient(CustodyService); -export const dexClient = createPraxClient(DexService); -export const ibcChannelClient = createPraxClient(IbcChannelService); -export const ibcClient = createPraxClient(IbcClientService); -export const ibcConnectionClient = createPraxClient(IbcConnectionService); -export const sctClient = createPraxClient(SctService); -export const simulationClient = createPraxClient(SimulationService); -export const stakeClient = createPraxClient(StakeService); -export const tendermintClient = createPraxClient(TendermintProxyService); -export const viewClient = createPraxClient(ViewService); diff --git a/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx b/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx deleted file mode 100644 index 0a3bd486..00000000 --- a/apps/minifront/src/components/dashboard/assets-table/equivalent-values.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { asValueView } from '@penumbra-zone/getters/equivalent-value'; -import { getDisplayDenomFromView, getEquivalentValues } from '@penumbra-zone/getters/value-view'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; - -export const EquivalentValues = ({ valueView }: { valueView?: ValueView }) => { - const equivalentValuesAsValueViews = (getEquivalentValues.optional()(valueView) ?? []).map( - asValueView, - ); - - return ( -
- {equivalentValuesAsValueViews.map(equivalentValueAsValueView => ( - - ))} -
- ); -}; diff --git a/apps/minifront/src/components/dashboard/assets-table/helpers.ts b/apps/minifront/src/components/dashboard/assets-table/helpers.ts deleted file mode 100644 index c3ce65f4..00000000 --- a/apps/minifront/src/components/dashboard/assets-table/helpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { assetPatterns } from '@penumbra-zone/types/assets'; -import { getDisplay } from '@penumbra-zone/getters/metadata'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getMetadata } from '@penumbra-zone/getters/value-view'; - -// We don't have to disclose auctionNft to the user since it is a kind of utility asset needed only -// for the implementation of the Dutch auction -const hiddenAssetPatterns = [assetPatterns.auctionNft, assetPatterns.lpNft]; - -export const isUnknown = (balancesResponse: BalancesResponse) => - balancesResponse.balanceView?.valueView.case === 'unknownAssetId'; - -export const shouldDisplay = (balance: BalancesResponse) => - isUnknown(balance) || - hiddenAssetPatterns.every( - pattern => !pattern.matches(getDisplay(getMetadata(balance.balanceView))), - ); diff --git a/apps/minifront/src/components/dashboard/assets-table/index.tsx b/apps/minifront/src/components/dashboard/assets-table/index.tsx deleted file mode 100644 index c8d8a5ae..00000000 --- a/apps/minifront/src/components/dashboard/assets-table/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { LoaderFunction, useLoaderData } from 'react-router-dom'; -import { AddressIcon } from '@penumbra-zone/ui/components/ui/address-icon'; -import { AddressComponent } from '@penumbra-zone/ui/components/ui/address-component'; -import { BalancesByAccount, getBalancesByAccount } from '../../../fetchers/balances/by-account'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@penumbra-zone/ui/components/ui/table'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { abortLoader } from '../../../abort-loader'; -import { EquivalentValues } from './equivalent-values'; -import { Fragment } from 'react'; -import { shouldDisplay } from './helpers'; - -export const AssetsLoader: LoaderFunction = async (): Promise => { - await abortLoader(); - return await getBalancesByAccount(); -}; - -export default function AssetsTable() { - const balancesByAccount = useLoaderData() as BalancesByAccount[]; - - if (balancesByAccount.length === 0) { - return ( -
-

- No balances found. Try requesting tokens by pasting your address in{' '} - - the faucet channel - {' '} - on Discord! -

-
- ); - } - - return ( - - {balancesByAccount.map(account => ( - - - - -
-
- -

- Account #{account.account} -

-
- -
- -
-
-
-
- - Balance - Estimated equivalent(s) - -
- - {account.balances.filter(shouldDisplay).map((assetBalance, index) => ( - - - - - - - - - ))} - -
- ))} -
- ); -} diff --git a/apps/minifront/src/components/dashboard/constants.ts b/apps/minifront/src/components/dashboard/constants.ts deleted file mode 100644 index 69ee87c9..00000000 --- a/apps/minifront/src/components/dashboard/constants.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DashboardTabMap } from './types'; -import { PagePath } from '../metadata/paths'; -import { EduPanel } from '../shared/edu-panels/content'; -import { Tab } from '../shared/tabs'; - -export const dashboardTabs: Tab[] = [ - { title: 'Assets', href: PagePath.DASHBOARD, enabled: true }, - { title: 'Transactions', href: PagePath.TRANSACTIONS, enabled: true }, - { title: 'NFTs', href: PagePath.NFTS, enabled: false }, -]; - -export const dashboardTabsHelper: DashboardTabMap = { - [PagePath.DASHBOARD]: { - src: './coin-stack-icon.svg', - label: 'Asset Balances', - content: EduPanel.ASSETS, - }, - [PagePath.TRANSACTIONS]: { - src: './history-icon.svg', - label: 'Transaction history', - content: EduPanel.TRANSACTIONS_LIST, - }, - [PagePath.NFTS]: { - src: './ibc-gradient.svg', - label: 'NFTs history', - content: EduPanel.TEMP_FILLER, - }, -}; diff --git a/apps/minifront/src/components/dashboard/layout.tsx b/apps/minifront/src/components/dashboard/layout.tsx deleted file mode 100644 index 75be0a76..00000000 --- a/apps/minifront/src/components/dashboard/layout.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { dashboardTabs, dashboardTabsHelper } from './constants'; -import { Outlet } from 'react-router-dom'; -import { EduInfoCard } from '../shared/edu-panels/edu-info-card'; -import { Tabs } from '../shared/tabs'; -import { usePagePath } from '../../fetchers/page-path'; -import { DashboardTab } from './types'; -import { RestrictMaxWidth } from '../shared/restrict-max-width'; - -export const DashboardLayout = () => { - const pathname = usePagePath(); - - return ( - -
- -
- -
- -
- -
-
- ); -}; diff --git a/apps/minifront/src/components/dashboard/transaction-table.tsx b/apps/minifront/src/components/dashboard/transaction-table.tsx deleted file mode 100644 index 14f720f9..00000000 --- a/apps/minifront/src/components/dashboard/transaction-table.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@penumbra-zone/ui/components/ui/table'; -import { Link } from 'react-router-dom'; -import { shorten } from '@penumbra-zone/types/string'; -import { useStore } from '../../state'; -import { memo, useEffect } from 'react'; -import { TransactionSummary } from '../../state/transactions'; - -export default function TransactionTable() { - const { summaries, loadSummaries } = useStore(store => store.transactions); - - useEffect(() => void loadSummaries(), [loadSummaries]); - - return ( - - - - Block Height - Description - Transaction Hash - - - - - {summaries.map(summary => ( - - ))} - -
- ); -} - -/** - * Split into a separate component so that we can use `memo`, which prevents - * rows from re-rendering just because other rows have been added. - */ -const Row = memo(({ summary }: { summary: TransactionSummary }) => ( - - -

{summary.height}

-
- -

{summary.description}

-
- -

- {shorten(summary.hash, 8)} -

-
- - - More - - -
-)); - -Row.displayName = 'Row'; diff --git a/apps/minifront/src/components/dashboard/types.ts b/apps/minifront/src/components/dashboard/types.ts deleted file mode 100644 index dc1bf3bf..00000000 --- a/apps/minifront/src/components/dashboard/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PagePath } from '../metadata/paths'; -import { EduPanel } from '../shared/edu-panels/content'; - -export type DashboardTab = PagePath.DASHBOARD | PagePath.TRANSACTIONS | PagePath.NFTS; - -interface DashboardMetadata { - src: string; - label: string; - content: EduPanel; -} - -export type DashboardTabMap = Record; diff --git a/apps/minifront/src/components/extension-not-connected.tsx b/apps/minifront/src/components/extension-not-connected.tsx deleted file mode 100644 index 5af63e44..00000000 --- a/apps/minifront/src/components/extension-not-connected.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Toaster } from '@penumbra-zone/ui/components/ui/toaster'; -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; -import { errorToast, warningToast } from '@penumbra-zone/ui/lib/toast/presets'; -import { HeadTag } from './metadata/head-tag'; - -import { requestPraxAccess } from '@penumbra-zone/client/prax'; -import { useState } from 'react'; -import { PenumbraRequestFailure } from '@penumbra-zone/client'; - -const handleErr = (e: unknown) => { - if (e instanceof Error && e.cause) { - switch (e.cause) { - case PenumbraRequestFailure.Denied: - errorToast( - 'You may need to un-ignore this site in your extension settings.', - 'Connection denied', - ).render(); - break; - case PenumbraRequestFailure.NeedsLogin: - warningToast( - 'Not logged in', - 'Please login into the extension and reload the page', - ).render(); - break; - default: - errorToast(e, 'Connection error').render(); - } - } else { - console.warn('Unknown connection failure', e); - errorToast(e, 'Unknown connection failure').render(); - } -}; - -const useExtConnector = () => { - const [result, setResult] = useState(); - - const request = async () => { - try { - await requestPraxAccess(); - location.reload(); - } catch (e) { - handleErr(e); - } finally { - setResult(true); - } - }; - - return { request, result }; -}; - -export const ExtensionNotConnected = () => { - const { request, result } = useExtConnector(); - - return ( - <> - - - -
-
To get started, connect the Penumbra Chrome extension.
- {!result ? ( - - ) : ( - - )} -
-
- - ); -}; diff --git a/apps/minifront/src/components/extension-not-installed.tsx b/apps/minifront/src/components/extension-not-installed.tsx deleted file mode 100644 index 5944e6fd..00000000 --- a/apps/minifront/src/components/extension-not-installed.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; -import { HeadTag } from './metadata/head-tag'; - -const CHROME_EXTENSION_ID = 'lkpmkhpnhknhmibgnmmhdhgdilepfghe'; - -export const ExtensionNotInstalled = () => { - return ( - <> - - -
- To get started, install the Penumbra Chrome extension. - -
-
- - ); -}; diff --git a/apps/minifront/src/components/extension-transport-disconnected.tsx b/apps/minifront/src/components/extension-transport-disconnected.tsx deleted file mode 100644 index 62abfab9..00000000 --- a/apps/minifront/src/components/extension-transport-disconnected.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; -import { HeadTag } from './metadata/head-tag'; -import { Button } from '@penumbra-zone/ui/components/ui/button'; - -export const ExtensionTransportDisconnected = () => { - return ( - <> - - -
-
- Communication with your Penumbra extension has been interrupted. Reloading the page may - re-establish the conneciton. -
- -
-
- - ); -}; diff --git a/apps/minifront/src/components/footer/footer.tsx b/apps/minifront/src/components/footer/footer.tsx deleted file mode 100644 index 64aab314..00000000 --- a/apps/minifront/src/components/footer/footer.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { MinifrontVersion } from './minifront-version'; -import { RightsMessage } from './rights-message'; - -export const Footer = () => ( -
-
- - -
-
-); diff --git a/apps/minifront/src/components/footer/minifront-version.tsx b/apps/minifront/src/components/footer/minifront-version.tsx deleted file mode 100644 index 93ba7d19..00000000 --- a/apps/minifront/src/components/footer/minifront-version.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { format } from 'date-fns'; - -export const MinifrontVersion = () => { - const shortenedCommitHash = __COMMIT_HASH__.slice(0, 7); - const dateObj = new Date(__COMMIT_DATE__); - const formattedDate = format(dateObj, "MMM dd yyyy HH:mm:ss 'GMT'x"); - return ( -
- Version  - - {shortenedCommitHash} - - {' - '} - {formattedDate} -
- ); -}; diff --git a/apps/minifront/src/components/footer/rights-message.tsx b/apps/minifront/src/components/footer/rights-message.tsx deleted file mode 100644 index f3e732b2..00000000 --- a/apps/minifront/src/components/footer/rights-message.tsx +++ /dev/null @@ -1,17 +0,0 @@ -export const RightsMessage = () => { - return ( -
- This software runs entirely on your device. -
- - Learn more - {' '} - about your rights. -
- ); -}; diff --git a/apps/minifront/src/components/header/constants.tsx b/apps/minifront/src/components/header/constants.tsx deleted file mode 100644 index ed26b6d6..00000000 --- a/apps/minifront/src/components/header/constants.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { PagePath } from '../metadata/paths'; -import { SwapIcon } from '../../icons/swap'; -import { ReactElement } from 'react'; -import { ArrowTopRightIcon, MixerHorizontalIcon, TextAlignLeftIcon } from '@radix-ui/react-icons'; - -export interface HeaderLink { - href: PagePath; - label: string; - active: boolean; - subLinks?: PagePath[]; - mobileIcon: ReactElement; -} - -export const headerLinks: HeaderLink[] = [ - { - href: PagePath.IBC, - label: 'Shield Funds', - active: true, - mobileIcon: , - }, - { - href: PagePath.SEND, - label: 'Send', - active: true, - subLinks: [PagePath.RECEIVE], - mobileIcon: , - }, - { - href: PagePath.SWAP, - label: 'Swap', - active: true, - mobileIcon: , - }, - { - href: PagePath.STAKING, - label: 'Stake', - active: true, - mobileIcon: , - }, -]; - -export const transactionLink = { - href: PagePath.TRANSACTION_DETAILS, - label: 'Transaction', - subLinks: [PagePath.TRANSACTION_DETAILS], -}; diff --git a/apps/minifront/src/components/header/header.tsx b/apps/minifront/src/components/header/header.tsx deleted file mode 100644 index b538b99d..00000000 --- a/apps/minifront/src/components/header/header.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { TopRow } from './top-row'; -import { SyncStatusSection } from './sync-status-section'; - -export const Header = () => { - return ( -
- - -
- ); -}; diff --git a/apps/minifront/src/components/header/mobile-nav-menu.tsx b/apps/minifront/src/components/header/mobile-nav-menu.tsx deleted file mode 100644 index d5609276..00000000 --- a/apps/minifront/src/components/header/mobile-nav-menu.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { - Sheet, - SheetContent, - SheetHeader, - SheetTrigger, -} from '@penumbra-zone/ui/components/ui/sheet'; -import { HamburgerMenuIcon } from '@radix-ui/react-icons'; -import { headerLinks } from './constants'; -import { Link } from 'react-router-dom'; - -export const MobileNavMenu = () => { - return ( - - - - - - -
- {headerLinks - .filter(link => link.active) - .map(link => ( - - - {link.mobileIcon} -

{link.label}

- -
- ))} -
-
- - - ); -}; diff --git a/apps/minifront/src/components/header/navbar.tsx b/apps/minifront/src/components/header/navbar.tsx deleted file mode 100644 index da369f82..00000000 --- a/apps/minifront/src/components/header/navbar.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { HeaderLink, headerLinks } from './constants'; -import { Link } from 'react-router-dom'; -import { usePagePath } from '../../fetchers/page-path'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useId } from 'react'; - -const ActiveIndicator = ({ layoutId }: { layoutId: string }) => ( - -); - -const isActive = (link: HeaderLink, pathname: HeaderLink['href']) => - link.href === pathname || link.subLinks?.includes(pathname); - -export const Navbar = () => { - const pathname = usePagePath(); - const layoutId = useId(); - - return ( - - ); -}; diff --git a/apps/minifront/src/components/header/sync-status-section.tsx b/apps/minifront/src/components/header/sync-status-section.tsx deleted file mode 100644 index 9522fed3..00000000 --- a/apps/minifront/src/components/header/sync-status-section.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { CondensedBlockSyncStatus } from '@penumbra-zone/ui/components/ui/block-sync-status/condensed'; -import { useStatus } from '../../state/status'; - -export const SyncStatusSection = () => { - const { data, error } = useStatus(); - - return ( -
- -
- ); -}; diff --git a/apps/minifront/src/components/header/tablet-nav-menu.tsx b/apps/minifront/src/components/header/tablet-nav-menu.tsx deleted file mode 100644 index 5da4ffae..00000000 --- a/apps/minifront/src/components/header/tablet-nav-menu.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, - navigationMenuTriggerStyle, -} from '@penumbra-zone/ui/components/ui/navigation-menu'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { useNavigate } from 'react-router-dom'; -import { headerLinks, transactionLink } from './constants'; -import { usePagePath } from '../../fetchers/page-path'; - -export const TabletNavMenu = () => { - const pathname = usePagePath(); - const navigate = useNavigate(); - - return ( -
- - - - - { - [...headerLinks, transactionLink].find( - link => link.href === pathname || link.subLinks?.includes(pathname), - )?.label - } - - - {headerLinks - .filter(link => link.active) - .map(link => ( - navigate(link.href)} - > - {link.label} - - ))} - - - - -
- ); -}; diff --git a/apps/minifront/src/components/header/top-row.tsx b/apps/minifront/src/components/header/top-row.tsx deleted file mode 100644 index 572a1294..00000000 --- a/apps/minifront/src/components/header/top-row.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Link } from 'react-router-dom'; -import { MessageWarningIcon } from '../../icons/message-warning'; -import { MobileNavMenu } from './mobile-nav-menu'; -import { Navbar } from './navbar'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@penumbra-zone/ui/components/ui/tooltip'; -import { Network } from '@penumbra-zone/ui/components/ui/network'; -import { PagePath } from '../metadata/paths'; -import { TabletNavMenu } from './tablet-nav-menu'; -import { useEffect, useState } from 'react'; -import { getChainId } from '../../fetchers/chain-id'; - -// Infinite-expiry invite link to the #web-ext-feedback channel. Provided by -// Henry (@hdevalence) and thus tied to his Discord account, so reach out to him -// if there are any problems with this link. -const WEB_EXT_FEEDBACK_DISCORD_CHANNEL = 'https://discord.gg/XDNcrhKVwV'; - -export const TopRow = () => { - const [chainId, setChainId] = useState(); - - useEffect(() => { - void getChainId().then(setChainId); - }, []); - - return ( -
-
- Penumbra logo - - Penumbra logotype - -
- -
-
- -
- - -
- - - - - - - - Send feedback via our Discord channel - - -
- - {chainId ? ( -
- -
- ) : null} -
-
- ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/asset-utils.test.ts b/apps/minifront/src/components/ibc/ibc-in/asset-utils.test.ts deleted file mode 100644 index d3b5c828..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/asset-utils.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { fromDisplayAmount, toDisplayAmount } from './asset-utils'; - -const asset = { - denom_units: [ - { denom: 'osmo', exponent: 6 }, - { denom: 'uosmo', exponent: 0 }, - ], - base: 'uosmo', - name: 'Osmosis', - display: 'osmo', - symbol: 'OSMO', -}; - -describe('toDisplayAmount', () => { - test('converts uosmo to osmo correctly', () => { - expect(toDisplayAmount(asset, { denom: 'uosmo', amount: '41000000' })).toEqual('41'); - }); - - test('high precision conversion from uosmo to osmo', () => { - expect(toDisplayAmount(asset, { denom: 'uosmo', amount: '123456789012345' })).toEqual( - '123456789.012345', - ); - }); - - test('coin denom not found in asset denom_units', () => { - expect(toDisplayAmount(asset, { denom: 'xosmo', amount: '1000000' })).toEqual('1000000'); - }); - - test('zero amount conversion from uosmo to osmo', () => { - expect(toDisplayAmount(asset, { denom: 'uosmo', amount: '0' })).toEqual('0'); - }); -}); - -describe('fromDisplayAmount', () => { - test('converts osmo to uosmo correctly for a whole number', () => { - const result = fromDisplayAmount(asset, 'osmo', '1'); - expect(result).toEqual({ denom: 'uosmo', amount: '1000000' }); - }); - - test('converts osmo to uosmo correctly for a decimal number', () => { - const result = fromDisplayAmount(asset, 'osmo', '0.5'); - expect(result).toEqual({ denom: 'uosmo', amount: '500000' }); - }); - - test('handles large numbers', () => { - const result = fromDisplayAmount(asset, 'osmo', '123456'); - expect(result).toEqual({ denom: 'uosmo', amount: '123456000000' }); - }); - - test('converts when display amount is zero', () => { - const result = fromDisplayAmount(asset, 'osmo', '0'); - expect(result).toEqual({ denom: 'uosmo', amount: '0' }); - }); - - test('returns input amount if display exponent is undefined', () => { - const result = fromDisplayAmount(asset, 'xosmo', '100'); - expect(result).toEqual({ denom: 'xosmo', amount: '100' }); - }); - - test('defaults base exponent to zero when base exponent not found in denom units array', () => { - const noExponentForBase = { - denom_units: [{ denom: 'osmo', exponent: 6 }], - base: 'uosmo', - name: 'Osmosis', - display: 'osmo', - symbol: 'OSMO', - }; - const result = fromDisplayAmount(noExponentForBase, 'osmo', '100'); - expect(result).toEqual({ denom: 'uosmo', amount: '100000000' }); - }); -}); diff --git a/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx b/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx deleted file mode 100644 index 6f7fd098..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/asset-utils.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { assets as cosmosAssetList } from 'chain-registry'; -import { Coin } from 'osmo-query'; -import { Asset } from '@chain-registry/types'; -import { BigNumber } from 'bignumber.js'; -import { AssetDenomUnit } from '@chain-registry/types/assets'; - -// Searches for corresponding denom in asset registry and returns the metadata -export const augmentToAsset = (denom: string, chainName: string): Asset => { - const match = cosmosAssetList - .find(({ chain_name }) => chain_name === chainName) - ?.assets.find(asset => asset.base === denom); - - return match ? match : fallbackAsset(denom); -}; - -const fallbackAsset = (denom: string): Asset => { - return { - base: denom, - denom_units: [{ denom, exponent: 0 }], - display: denom, - name: denom, - symbol: denom, - }; -}; - -// Helps us convert from say 41000000uosmo to the more readable 41osmo -export const toDisplayAmount = (asset: Asset, coin: Coin): string => { - const currentExponent = getExponent(asset.denom_units, coin.denom); - const displayExponent = getExponent(asset.denom_units, asset.display); - if (currentExponent === undefined || displayExponent === undefined) { - return coin.amount; - } - - const exponentDifference = currentExponent - displayExponent; - return new BigNumber(coin.amount).shiftedBy(exponentDifference).toString(); -}; - -// Converts a readable amount back to its base amount -export const fromDisplayAmount = ( - asset: Asset, - displayDenom: string, - displayAmount: string, -): Coin => { - const displayExponent = getExponent(asset.denom_units, displayDenom); - if (displayExponent === undefined) { - return { denom: displayDenom, amount: displayAmount }; - } - - // Defaults to zero if not found - const baseExponent = getExponent(asset.denom_units, asset.base) ?? 0; - - const exponentDifference = displayExponent - baseExponent; - const amount = new BigNumber(displayAmount).shiftedBy(exponentDifference).toString(); - return { denom: asset.base, amount }; -}; - -const getExponent = (denomUnits: AssetDenomUnit[], denom: string): number | undefined => { - return denomUnits.find(unit => unit.denom === denom)?.exponent; -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx b/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx deleted file mode 100644 index 4a2d0c11..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/assets-table.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useChainConnector, useCosmosChainBalances } from './hooks'; -import { useStore } from '../../../state'; -import { ibcInSelector } from '../../../state/ibc-in'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@penumbra-zone/ui/components/ui/table'; -import { Avatar, AvatarImage } from '@penumbra-zone/ui/components/ui/avatar'; -import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; -import { LineWave } from 'react-loader-spinner'; - -export const AssetsTable = () => { - const { address } = useChainConnector(); - const { selectedChain } = useStore(ibcInSelector); - const { data, isLoading, error } = useCosmosChainBalances(); - - // User has not connected their wallet yet - if (!address || !selectedChain) return <>; - - if (isLoading) { - return ( -
- Loading balances... - -
- ); - } - - if (error) { - return
{String(error)}
; - } - - return ( -
-
- Balances on {selectedChain.label} -
- - - - Denom - Amount - - - - {data?.length === 0 && noBalancesRow()} - {data?.map(b => { - return ( - - - - - - - {b.displayDenom} - - {b.displayAmount} - - ); - })} - -
-
- ); -}; - -const noBalancesRow = () => { - return ( - - No balances - - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx b/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx deleted file mode 100644 index 767591c5..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/chain-dropdown.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import * as React from 'react'; -import { useMemo } from 'react'; -import { useManager } from '@cosmos-kit/react'; -import { Popover, PopoverContent, PopoverTrigger } from '@penumbra-zone/ui/components/ui/popover'; -import { ChevronsUpDown } from 'lucide-react'; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, -} from '@penumbra-zone/ui/components/ui/command'; -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { ibcInSelector } from '../../../state/ibc-in'; -import { useStore } from '../../../state'; -import { Avatar, AvatarImage } from '@penumbra-zone/ui/components/ui/avatar'; -import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; - -export interface ChainInfo { - chainName: string; - chainId: string; - label: string; - icon?: string; -} - -const useChainInfos = (): ChainInfo[] => { - const { chainRecords, getChainLogo } = useManager(); - return useMemo( - () => - chainRecords.map(r => { - if (!r.chain?.chain_id) throw new Error(`No chain id found for ${r.name}`); - - return { - chainName: r.name, - label: r.chain.pretty_name, - icon: getChainLogo(r.name), - chainId: r.chain.chain_id, - }; - }), - [chainRecords, getChainLogo], - ); -}; - -// Note the console will display aria-label warnings (despite them being present). -// The cosmology team has been notified of the issue. -export const ChainDropdown = () => { - const chainInfos = useChainInfos(); - const { setSelectedChain } = useStore(ibcInSelector); - - const [open, setOpen] = React.useState(false); - const [value, setValue] = React.useState(''); - - const selected = chainInfos.find(c => c.chainName === value); - - return ( - - - - - - - - No framework found. - - {chainInfos.map(chain => ( - { - setOpen(false); - - if (currentValue === value) { - setValue(''); - setSelectedChain(undefined); - } else { - setValue(currentValue); - const match = chainInfos.find(options => options.chainName === currentValue); - setSelectedChain(match); - } - }} - className='flex gap-2' - > - - - - - {chain.label} - - ))} - - - - - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/chain-provider.tsx b/apps/minifront/src/components/ibc/ibc-in/chain-provider.tsx deleted file mode 100644 index d1231a0b..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/chain-provider.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ChainProvider } from '@cosmos-kit/react'; -import { aminoTypes, registry as CosmosRegistry } from './config/defaults'; -import { assets, chains } from 'chain-registry'; -import { SignerOptions, wallets } from 'cosmos-kit'; -import { ReactNode, useMemo } from 'react'; -import { Registry as PenumbraRegistry } from '@penumbra-labs/registry'; - -import '@interchain-ui/react/styles'; - -const signerOptions: SignerOptions = { - signingStargate: () => { - return { - aminoTypes, - registry: CosmosRegistry, - }; - }, -}; - -interface IbcProviderProps { - registry: PenumbraRegistry; - children: ReactNode; -} - -export const IbcChainProvider = ({ registry, children }: IbcProviderProps) => { - const chainsToDisplay = useMemo(() => chainsInPenumbraRegistry(registry), [registry]); - - return ( - - {children} - - ); -}; - -// Searches cosmos registry for chains that have ibc connections to Penumbra -const chainsInPenumbraRegistry = ({ ibcConnections }: PenumbraRegistry) => { - return chains.filter(c => ibcConnections.some(i => c.chain_id === i.chainId)); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/config/defaults.ts b/apps/minifront/src/components/ibc/ibc-in/config/defaults.ts deleted file mode 100644 index ccf6f6fe..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/config/defaults.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { GeneratedType, Registry } from '@cosmjs/proto-signing'; -import { AminoTypes } from '@cosmjs/stargate'; -import { - cosmosAminoConverters, - cosmosProtoRegistry, - cosmwasmAminoConverters, - cosmwasmProtoRegistry, - ibcAminoConverters, - ibcProtoRegistry, - osmosisAminoConverters, - osmosisProtoRegistry, -} from 'osmo-query'; - -const protoRegistry: readonly [string, GeneratedType][] = [ - ...cosmosProtoRegistry, - ...cosmwasmProtoRegistry, - ...ibcProtoRegistry, - ...osmosisProtoRegistry, -]; - -const aminoConverters = { - ...cosmosAminoConverters, - ...cosmwasmAminoConverters, - ...ibcAminoConverters, - ...osmosisAminoConverters, -}; - -export const registry = new Registry(protoRegistry); -export const aminoTypes = new AminoTypes(aminoConverters); diff --git a/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx b/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx deleted file mode 100644 index 0cf9e9c7..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/cosmos-wallet-connector.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useStore } from '../../../state'; -import { ibcInSelector } from '../../../state/ibc-in'; -import { WalletStatus } from '@cosmos-kit/core'; -import { WalletAddrCard } from './wallet-addr-card'; -import { ConnectWalletButton } from './wallet-connect-button'; -import { useChainConnector } from './hooks'; - -export const CosmosWalletConnector = () => { - const { selectedChain } = useStore(ibcInSelector); - const { username, address, status, message } = useChainConnector(); - - if (!selectedChain) return <>; - - return ( -
-
- -
- {address && } - {(status === WalletStatus.Rejected || status === WalletStatus.Error) && ( -
{message}
- )} -
- ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/destination-addr.tsx b/apps/minifront/src/components/ibc/ibc-in/destination-addr.tsx deleted file mode 100644 index 665ee794..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/destination-addr.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { AllSlices } from '../../../state'; -import { useEffect } from 'react'; -import { IncognitoIcon } from '@penumbra-zone/ui/components/ui/icons/incognito'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; - -const addrsSelector = ({ ibcIn }: AllSlices) => ({ - fetchPenumbraAddrs: ibcIn.fetchPenumbraAddrs, - penumbraAddrs: ibcIn.penumbraAddrs, - selectedChain: ibcIn.selectedChain, -}); - -export const DestinationAddr = () => { - const { penumbraAddrs, fetchPenumbraAddrs, selectedChain } = useStoreShallow(addrsSelector); - - // TODO: allow for user account selection - // On mount, get normal+ephemeral address for Account #0 - useEffect(() => void fetchPenumbraAddrs(), [fetchPenumbraAddrs, selectedChain]); - - return ( -
-
- Sending to your Account #0 -
- {penumbraAddrs && ( - <> -
Here is your normal address
-
{penumbraAddrs.normal}
-
- But you will IBC to an ephemeral address representing{' '} - Account #0 to maintain privacy{' '} - - - -
-
- {penumbraAddrs.ephemeral} -
- - )} -
- ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/hooks.ts b/apps/minifront/src/components/ibc/ibc-in/hooks.ts deleted file mode 100644 index 5315d7b0..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/hooks.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { useStore } from '../../../state'; -import { ibcInSelector } from '../../../state/ibc-in'; -import { useChain, useManager } from '@cosmos-kit/react'; -import { UseQueryResult } from '@tanstack/react-query'; -import { ProtobufRpcClient } from '@cosmjs/stargate'; -import { Coin, createRpcQueryHooks, useRpcClient, useRpcEndpoint } from 'osmo-query'; -import { augmentToAsset, toDisplayAmount } from './asset-utils'; - -// This is sad, but osmo-query's custom hooks require calling .toJSON() on all fields. -// This will throw an error for bigint, so needs to be added to the prototype. -declare global { - interface BigInt { - toJSON(): string; - } -} - -BigInt.prototype.toJSON = function () { - return this.toString(); -}; - -export const useChainConnector = () => { - const { selectedChain } = useStore(ibcInSelector); - const { chainRecords } = useManager(); - const defaultChain = chainRecords[0]!.name; - return useChain(selectedChain?.chainName ?? defaultChain); -}; - -const useCosmosQueryHooks = () => { - const { address, getRpcEndpoint, chain } = useChainConnector(); - - const rpcEndpointQuery = useRpcEndpoint({ - getter: getRpcEndpoint, - options: { - enabled: !!address, - staleTime: Infinity, - queryKey: ['rpc endpoint', address, chain.chain_name], - // Needed for osmo-query's internal caching - queryKeyHashFn: queryKey => { - return JSON.stringify([...queryKey, chain.chain_name]); - }, - }, - }) as UseQueryResult; - - const rpcClientQuery = useRpcClient({ - rpcEndpoint: rpcEndpointQuery.data ?? '', - options: { - enabled: !!address && !!rpcEndpointQuery.data, - staleTime: Infinity, - queryKey: ['rpc client', address, rpcEndpointQuery.data, chain.chain_name], - // Needed for osmo-query's internal caching - queryKeyHashFn: queryKey => { - return JSON.stringify([...queryKey, chain.chain_name]); - }, - }, - }) as UseQueryResult; - - const { cosmos: cosmosQuery, osmosis: osmosisQuery } = createRpcQueryHooks({ - rpc: rpcClientQuery.data, - }); - - const isReady = !!address && !!rpcClientQuery.data; - const isFetching = rpcEndpointQuery.isFetching || rpcClientQuery.isFetching; - - return { cosmosQuery, osmosisQuery, isReady, isFetching, address, chain }; -}; - -interface BalancesResponse { - balances: Coin[]; - pagination: { nexKey: Uint8Array; total: bigint }; -} - -// Reference: https://github.com/cosmos/chain-registry/blob/master/assetlist.schema.json#L60 -const ASSET_TYPES = [ - 'sdk.coin', - 'cw20', - 'erc20', - 'ics20', - 'snip20', - 'snip25', - 'bitcoin-like', - 'evm-base', - 'svm-base', - 'substrate', -] as const; - -type AssetType = (typeof ASSET_TYPES)[number]; - -export interface CosmosAssetBalance { - raw: Coin; - displayDenom: string; - displayAmount: string; - icon?: string; - assetType?: AssetType; -} - -interface UseCosmosChainBalancesRes { - data?: CosmosAssetBalance[]; - isLoading: boolean; - error: unknown; -} - -export const useCosmosChainBalances = (): UseCosmosChainBalancesRes => { - const { address, cosmosQuery, isReady, chain } = useCosmosQueryHooks(); - - const { data, isLoading, error } = cosmosQuery.bank.v1beta1.useAllBalances({ - request: { - address: address ?? '', - pagination: { - offset: 0n, - limit: 100n, - key: new Uint8Array(), - countTotal: true, - reverse: false, - }, - }, - options: { - enabled: isReady, - }, - }) as UseQueryResult; - - const augmentedAssets = data?.balances.map(coin => { - const asset = augmentToAsset(coin.denom, chain.chain_name); - return { - raw: coin, - displayDenom: asset.display, - displayAmount: toDisplayAmount(asset, coin), - icon: asset.logo_URIs?.svg ?? asset.logo_URIs?.png, - assetType: assetTypeCheck(asset.type_asset), - }; - }); - return { data: augmentedAssets, isLoading, error }; -}; - -const assetTypeCheck = (type?: string): AssetType | undefined => { - return typeof type === 'string' && ASSET_TYPES.includes(type as AssetType) - ? (type as AssetType) - : undefined; -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx b/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx deleted file mode 100644 index da94adeb..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/ibc-in-form.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { ChainDropdown } from './chain-dropdown'; -import { CosmosWalletConnector } from './cosmos-wallet-connector'; -import { AssetsTable } from './assets-table'; -import { IbcInRequest } from './ibc-in-request'; -import { AllSlices, useStore } from '../../../state'; -import { useChainConnector } from './hooks'; -import { FormEvent, MouseEvent } from 'react'; - -export const IbcInForm = () => { - const issueTx = useStore(({ ibcIn }: AllSlices) => ibcIn.issueTx); - const { address, getSigningStargateClient } = useChainConnector(); - - const handleSubmit = (event: FormEvent | MouseEvent) => { - event.preventDefault(); - - void issueTx(getSigningStargateClient, address); - }; - - return ( -
-
- -
- - - - - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/ibc-in-request.tsx b/apps/minifront/src/components/ibc/ibc-in/ibc-in-request.tsx deleted file mode 100644 index aacefe48..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/ibc-in-request.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { useChainConnector, useCosmosChainBalances } from './hooks'; -import { AllSlices, useStore } from '../../../state'; -import { ibcErrorSelector, ibcInSelector } from '../../../state/ibc-in'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@penumbra-zone/ui/components/ui/select'; -import { Avatar, AvatarImage } from '@penumbra-zone/ui/components/ui/avatar'; -import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { DestinationAddr } from './destination-addr'; -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { LockClosedIcon } from '@radix-ui/react-icons'; - -const isReadySelector = (state: AllSlices) => { - const { amount, coin, selectedChain, penumbraAddrs } = state.ibcIn; - const errorsPresent = Object.values(ibcErrorSelector(state)).some(Boolean); - const formsFilled = - Boolean(amount) && Boolean(coin) && Boolean(selectedChain) && Boolean(penumbraAddrs); - return !errorsPresent && formsFilled; -}; - -export const IbcInRequest = () => { - const { address } = useChainConnector(); - const { selectedChain, setCoin } = useStore(ibcInSelector); - const { data } = useCosmosChainBalances(); - - const { isUnsupportedAsset } = useStore(ibcErrorSelector); - const isReady = useStore(isReadySelector); - - // User is not ready to issue request - if (!address || !selectedChain || !data?.length) return <>; - - return ( -
-
Issue Ibc-in Request
- {isUnsupportedAsset && ( -
- Note: only native assets at this time are eligible for ibc'ing in. Unwind them - through their home chain to get them to Penumbra. -
- )} -
- - -
- - -
- ); -}; - -const AmountInput = () => { - const { setAmount, coin } = useStore(ibcInSelector); - const { amountErr } = useStore(ibcErrorSelector); - - return ( - setAmount(e.target.value)} - required - /> - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx b/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx deleted file mode 100644 index 218af9f7..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/interchain-ui.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { IbcChainProvider } from './chain-provider'; -import { useRegistry } from '../../../fetchers/registry'; -import { IbcInForm } from './ibc-in-form'; - -export const InterchainUi = () => { - const { data, isLoading, error } = useRegistry(); - - if (isLoading) return
Loading registry...
; - if (error) return
Error trying to load registry!
; - if (!data) return <>; - - return ( - - - - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/wallet-addr-card.tsx b/apps/minifront/src/components/ibc/ibc-in/wallet-addr-card.tsx deleted file mode 100644 index 3c34414c..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/wallet-addr-card.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; - -interface UserInfoProps { - address: string; - username?: string; -} - -export const WalletAddrCard = ({ address, username }: UserInfoProps) => { - return ( -
- -
{address}
- {username} -
- ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx b/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx deleted file mode 100644 index 89b0b1fb..00000000 --- a/apps/minifront/src/components/ibc/ibc-in/wallet-connect-button.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { WalletStatus } from 'cosmos-kit'; -import { WalletIcon } from '@penumbra-zone/ui/components/ui/icons/wallet'; -import { MouseEventHandler } from 'react'; -import { useStore } from '../../../state'; -import { ibcInSelector } from '../../../state/ibc-in'; - -import { useChainConnector } from './hooks'; -import { cn } from '@penumbra-zone/ui/lib/utils'; - -export const ConnectWalletButton = () => { - const { connect, openView, status } = useChainConnector(); - const { selectedChain } = useStore(ibcInSelector); - - if (!selectedChain) { - return ; - } - - const onClickConnect: MouseEventHandler = e => { - e.preventDefault(); - void connect(); - }; - - const onClickOpenView: MouseEventHandler = e => { - e.preventDefault(); - openView(); - }; - - switch (status) { - case WalletStatus.Disconnected: - return ; - case WalletStatus.Connecting: - return ; - case WalletStatus.Connected: - return ; - case WalletStatus.Rejected: - return ; - case WalletStatus.Error: - return ; - case WalletStatus.NotExist: - return ; - default: - return ; - } -}; - -interface BaseProps { - buttonText?: string; - isLoading?: boolean; - isDisabled?: boolean; - onClick?: MouseEventHandler; -} - -const WalletButtonBase = ({ buttonText, isLoading, isDisabled, onClick }: BaseProps) => { - return ( - - ); -}; diff --git a/apps/minifront/src/components/ibc/ibc-loader.ts b/apps/minifront/src/components/ibc/ibc-loader.ts deleted file mode 100644 index f9543d4e..00000000 --- a/apps/minifront/src/components/ibc/ibc-loader.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LoaderFunction } from 'react-router-dom'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getBalances } from '../../fetchers/balances'; -import { useStore } from '../../state'; -import { Chain } from '@penumbra-labs/registry'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getIbcConnections, getStakingTokenMetadata } from '../../fetchers/registry'; -import { getAllAssets } from '../../fetchers/assets'; -import { filterBalancesPerChain } from '../../state/ibc-out'; -import { abortLoader } from '../../abort-loader'; - -export interface IbcLoaderResponse { - balances: BalancesResponse[]; - chains: Chain[]; - stakingTokenMetadata: Metadata; - assets: Metadata[]; -} - -export const IbcLoader: LoaderFunction = async (): Promise => { - await abortLoader(); - const assetBalances = await getBalances(); - const ibcConnections = await getIbcConnections(); - const stakingTokenMetadata = await getStakingTokenMetadata(); - const assets = await getAllAssets(); - - if (assetBalances[0]) { - const initialChain = ibcConnections[0]; - const initialSelection = filterBalancesPerChain( - assetBalances, - initialChain, - stakingTokenMetadata, - assets, - )[0]; - - // set initial account if accounts exist and asset if account has asset list - useStore.setState(state => { - state.ibcOut.selection = initialSelection; - state.ibcOut.chain = initialChain; - }); - } - - return { balances: assetBalances, chains: ibcConnections, stakingTokenMetadata, assets }; -}; diff --git a/apps/minifront/src/components/ibc/ibc-out/chain-selector.tsx b/apps/minifront/src/components/ibc/ibc-out/chain-selector.tsx deleted file mode 100644 index ef137ab3..00000000 --- a/apps/minifront/src/components/ibc/ibc-out/chain-selector.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@penumbra-zone/ui/components/ui/select'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { useState } from 'react'; -import { useStore } from '../../../state'; -import { ibcOutSelector } from '../../../state/ibc-out'; -import { useLoaderData } from 'react-router-dom'; -import { IbcLoaderResponse } from '../ibc-loader'; -import { Chain } from '@penumbra-labs/registry'; - -export const ChainSelector = () => { - const { chain, setChain } = useStore(ibcOutSelector); - const { chains: ibcConnections } = useLoaderData() as IbcLoaderResponse; - const [openSelect, setOpenSelect] = useState(false); - - return ( -
-

Chain

- -
- ); -}; - -const ChainIcon = ({ chain }: { chain: Chain }) => { - const imgUrl = getChainImgUrl(chain); - if (!imgUrl) return undefined; - - return Chain; -}; - -const getChainImgUrl = (chain?: Chain) => { - const chainImgObj = chain?.images[0]; - if (!chainImgObj) return undefined; - - return chainImgObj.png ?? chainImgObj.svg; -}; diff --git a/apps/minifront/src/components/ibc/ibc-out/ibc-out-form.tsx b/apps/minifront/src/components/ibc/ibc-out/ibc-out-form.tsx deleted file mode 100644 index 5f279f83..00000000 --- a/apps/minifront/src/components/ibc/ibc-out/ibc-out-form.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { ChainSelector } from './chain-selector'; -import { useLoaderData } from 'react-router-dom'; -import { useStore } from '../../../state'; -import { - filterBalancesPerChain, - ibcOutSelector, - ibcValidationErrors, -} from '../../../state/ibc-out'; -import InputToken from '../../shared/input-token'; -import { InputBlock } from '../../shared/input-block'; -import { IbcLoaderResponse } from '../ibc-loader'; -import { LockOpen2Icon } from '@radix-ui/react-icons'; - -export const IbcOutForm = () => { - const { balances, stakingTokenMetadata, assets } = useLoaderData() as IbcLoaderResponse; - const { - sendIbcWithdraw, - destinationChainAddress, - setDestinationChainAddress, - amount, - setAmount, - selection, - setSelection, - chain, - } = useStore(ibcOutSelector); - const filteredBalances = filterBalancesPerChain(balances, chain, stakingTokenMetadata, assets); - const validationErrors = useStore(ibcValidationErrors); - - return ( -
{ - e.preventDefault(); - void sendIbcWithdraw(); - }} - > - - { - if (Number(amount) < 0) return; - setAmount(amount); - }} - validations={[ - { - type: 'error', - issue: 'insufficient funds', - checkFn: () => validationErrors.amountErr, - }, - ]} - balances={filteredBalances} - /> - validationErrors.recipientErr, - }, - ]} - > - setDestinationChainAddress(e.target.value)} - /> - - - - ); -}; diff --git a/apps/minifront/src/components/ibc/layout.tsx b/apps/minifront/src/components/ibc/layout.tsx deleted file mode 100644 index 637560e8..00000000 --- a/apps/minifront/src/components/ibc/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { IbcOutForm } from './ibc-out/ibc-out-form'; -import { InterchainUi } from './ibc-in/interchain-ui'; -import { LongArrowIcon } from './long-arrow'; - -export const IbcLayout = () => { - return ( - <> -
-
- - - - - - - - -
- - ); -}; diff --git a/apps/minifront/src/components/ibc/long-arrow.tsx b/apps/minifront/src/components/ibc/long-arrow.tsx deleted file mode 100644 index e992e790..00000000 --- a/apps/minifront/src/components/ibc/long-arrow.tsx +++ /dev/null @@ -1,31 +0,0 @@ -interface LongArrowIconProps { - className?: string; - direction: 'left' | 'right'; -} - -export const LongArrowIcon = ({ className, direction }: LongArrowIconProps) => { - const rotationStyle = { - transform: direction === 'right' ? 'rotate(0deg)' : 'rotate(180deg)', - }; - - return ( - - - - ); -}; diff --git a/apps/minifront/src/components/layout.tsx b/apps/minifront/src/components/layout.tsx deleted file mode 100644 index 2dd0ccff..00000000 --- a/apps/minifront/src/components/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Outlet } from 'react-router-dom'; -import { HeadTag } from './metadata/head-tag'; -import { Header } from './header/header'; -import { Toaster } from '@penumbra-zone/ui/components/ui/toaster'; -import { Footer } from './footer/footer'; -import '@penumbra-zone/ui/styles/globals.css'; -import { getChainId } from '../fetchers/chain-id'; -import { useEffect, useState } from 'react'; -import { TestnetBanner } from '@penumbra-zone/ui/components/ui/testnet-banner'; -import { MotionConfig } from 'framer-motion'; - -export const Layout = () => { - const [chainId, setChainId] = useState(); - - useEffect(() => { - void getChainId().then(id => setChainId(id)); - }, []); - - return ( - - - -
-
-
- -
-
-
- -
- ); -}; diff --git a/apps/minifront/src/components/metadata/content.ts b/apps/minifront/src/components/metadata/content.ts deleted file mode 100644 index 070c1145..00000000 --- a/apps/minifront/src/components/metadata/content.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { PagePath } from './paths'; -import { EduPanel, eduPanelContent } from '../shared/edu-panels/content'; - -export interface PageMetadata { - title: string; - description: string; -} - -export const metadata: Record = { - [PagePath.INDEX]: { - title: 'Penumbra', - description: '', - }, - [PagePath.DASHBOARD]: { - title: 'Penumbra | Assets', - description: eduPanelContent[EduPanel.ASSETS], - }, - [PagePath.TRANSACTIONS]: { - title: 'Penumbra | Transactions', - description: eduPanelContent[EduPanel.TRANSACTIONS_LIST], - }, - [PagePath.NFTS]: { - title: 'Penumbra | NFTs', - description: eduPanelContent[EduPanel.TEMP_FILLER], - }, - [PagePath.SEND]: { - title: 'Penumbra | Send', - description: eduPanelContent[EduPanel.SENDING_FUNDS], - }, - [PagePath.RECEIVE]: { - title: 'Penumbra | Receive', - description: eduPanelContent[EduPanel.RECEIVING_FUNDS], - }, - [PagePath.IBC]: { - title: 'Penumbra | IBC', - description: eduPanelContent[EduPanel.TEMP_FILLER], - }, - [PagePath.SWAP]: { - title: 'Penumbra | Swap', - description: eduPanelContent[EduPanel.SWAP], - }, - [PagePath.STAKING]: { - title: 'Penumbra | Staking', - description: eduPanelContent[EduPanel.STAKING], - }, - [PagePath.TRANSACTION_DETAILS]: { - title: 'Penumbra | Transaction', - description: 'More details about transaction', - }, -}; diff --git a/apps/minifront/src/components/metadata/head-tag.tsx b/apps/minifront/src/components/metadata/head-tag.tsx deleted file mode 100644 index 810d7037..00000000 --- a/apps/minifront/src/components/metadata/head-tag.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { metadata } from './content'; -import { Helmet } from 'react-helmet'; -import { usePagePath } from '../../fetchers/page-path'; - -export const HeadTag = () => { - const pathname = usePagePath(); - - return ( - - - - {metadata[pathname].title} - - - ); -}; diff --git a/apps/minifront/src/components/metadata/paths.ts b/apps/minifront/src/components/metadata/paths.ts deleted file mode 100644 index 82aa0fb4..00000000 --- a/apps/minifront/src/components/metadata/paths.ts +++ /dev/null @@ -1,12 +0,0 @@ -export enum PagePath { - INDEX = '/', - SWAP = '/swap', - SEND = '/send', - STAKING = '/staking', - RECEIVE = '/send/receive', - TRANSACTIONS = '/dashboard/transactions', - DASHBOARD = '/dashboard', - IBC = '/ibc', - NFTS = '/dashboard/nfts', - TRANSACTION_DETAILS = '/tx/:hash', -} diff --git a/apps/minifront/src/components/not-found.tsx b/apps/minifront/src/components/not-found.tsx deleted file mode 100644 index e15f3e3e..00000000 --- a/apps/minifront/src/components/not-found.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; - -export const NotFound = () => { - return That page could not be found. ; -}; diff --git a/apps/minifront/src/components/root-router.tsx b/apps/minifront/src/components/root-router.tsx deleted file mode 100644 index 466cadd4..00000000 --- a/apps/minifront/src/components/root-router.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { createHashRouter, redirect } from 'react-router-dom'; -import { PagePath } from './metadata/paths'; -import { Layout } from './layout'; -import AssetsTable, { AssetsLoader } from './dashboard/assets-table'; -import TransactionTable from './dashboard/transaction-table'; -import { DashboardLayout } from './dashboard/layout'; -import { TxDetails, TxDetailsErrorBoundary, TxDetailsLoader } from './tx-details'; -import { SendLayout } from './send/layout'; -import { SendAssetBalanceLoader, SendForm } from './send/send-form'; -import { Receive } from './send/receive'; -import { ErrorBoundary } from './shared/error-boundary'; -import { SwapLayout } from './swap/layout'; -import { SwapLoader } from './swap/swap-loader'; -import { StakingLayout, StakingLoader } from './staking/layout'; -import { IbcLoader } from './ibc/ibc-loader'; -import { IbcLayout } from './ibc/layout'; - -export const rootRouter = createHashRouter([ - { - path: '/', - element: , - errorElement: , - children: [ - { index: true, loader: () => redirect(PagePath.DASHBOARD) }, - { - path: PagePath.DASHBOARD, - element: , - children: [ - { - index: true, - loader: AssetsLoader, - element: , - }, - { - path: PagePath.TRANSACTIONS, - element: , - }, - ], - }, - { - path: PagePath.SEND, - element: , - children: [ - { - index: true, - loader: SendAssetBalanceLoader, - element: , - }, - { - path: PagePath.RECEIVE, - element: , - }, - ], - }, - { - path: PagePath.SWAP, - loader: SwapLoader, - element: , - }, - { - path: PagePath.TRANSACTION_DETAILS, - loader: TxDetailsLoader, - element: , - errorElement: , - }, - { - path: PagePath.STAKING, - loader: StakingLoader, - element: , - }, - { - path: PagePath.IBC, - loader: IbcLoader, - element: , - }, - ], - }, -]); diff --git a/apps/minifront/src/components/send/constants.ts b/apps/minifront/src/components/send/constants.ts deleted file mode 100644 index 86a8b6f6..00000000 --- a/apps/minifront/src/components/send/constants.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SendTabMap } from './types'; -import { PagePath } from '../metadata/paths'; -import { EduPanel } from '../shared/edu-panels/content'; - -export const sendTabsHelper: SendTabMap = { - [PagePath.SEND]: { - src: './send-icon.svg', - label: 'Sending Funds', - content: EduPanel.SENDING_FUNDS, - }, - [PagePath.RECEIVE]: { - src: './receive-icon.svg', - label: 'Receiving Funds', - content: EduPanel.RECEIVING_FUNDS, - }, -}; - -export const sendTabs = [ - { title: 'Send', href: PagePath.SEND, enabled: true }, - { title: 'Receive', href: PagePath.RECEIVE, enabled: true }, -]; diff --git a/apps/minifront/src/components/send/helpers.ts b/apps/minifront/src/components/send/helpers.ts deleted file mode 100644 index 9a15825e..00000000 --- a/apps/minifront/src/components/send/helpers.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { isAddress } from '@penumbra-zone/bech32m/penumbra'; -import { Validation } from '../shared/validation-result'; -import { assetPatterns } from '@penumbra-zone/types/assets'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getDisplay } from '@penumbra-zone/getters/metadata'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getBalances } from '../../fetchers/balances'; -import { getMetadata } from '@penumbra-zone/getters/value-view'; -import { isUnknown } from '../dashboard/assets-table/helpers'; - -export const penumbraAddrValidation = (): Validation => { - return { - type: 'error', - issue: 'invalid address', - checkFn: (addr: string) => Boolean(addr) && !isAddress(addr), - }; -}; - -const nonTransferableAssetPatterns = [ - assetPatterns.proposalNft, - assetPatterns.auctionNft, - assetPatterns.lpNft, -]; - -export const isTransferable = (metadata: Metadata) => - nonTransferableAssetPatterns.every(pattern => !pattern.matches(getDisplay(metadata))); - -export const getTransferableBalancesResponses = async (): Promise => { - const balancesResponses = await getBalances(); - return balancesResponses.filter( - balance => isUnknown(balance) || isTransferable(getMetadata(balance.balanceView)), - ); -}; diff --git a/apps/minifront/src/components/send/layout.tsx b/apps/minifront/src/components/send/layout.tsx deleted file mode 100644 index 56f99ef6..00000000 --- a/apps/minifront/src/components/send/layout.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { sendTabs, sendTabsHelper } from './constants'; -import { SendTab } from './types'; -import { usePagePath } from '../../fetchers/page-path'; -import { Tabs } from '../shared/tabs'; -import { Outlet } from 'react-router-dom'; -import { EduInfoCard } from '../shared/edu-panels/edu-info-card'; -import { RestrictMaxWidth } from '../shared/restrict-max-width'; - -export const SendLayout = () => { - const pathname = usePagePath(); - - return ( - -
-
- - - - - -
- - ); -}; diff --git a/apps/minifront/src/components/send/receive.tsx b/apps/minifront/src/components/send/receive.tsx deleted file mode 100644 index 3433883f..00000000 --- a/apps/minifront/src/components/send/receive.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { SelectAccount } from '@penumbra-zone/ui/components/ui/select-account'; -import { getAddrByIndex } from '../../fetchers/address'; - -export const Receive = () => { - return ( -
- -
- ); -}; diff --git a/apps/minifront/src/components/send/send-form/index.tsx b/apps/minifront/src/components/send/send-form/index.tsx deleted file mode 100644 index f705e1aa..00000000 --- a/apps/minifront/src/components/send/send-form/index.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { useStore } from '../../../state'; -import { sendSelector, sendValidationErrors } from '../../../state/send'; -import { InputBlock } from '../../shared/input-block'; -import { LoaderFunction, useLoaderData } from 'react-router-dom'; -import { useMemo } from 'react'; -import { getTransferableBalancesResponses, penumbraAddrValidation } from '../helpers'; -import { abortLoader } from '../../../abort-loader'; -import InputToken from '../../shared/input-token'; -import { useRefreshFee } from './use-refresh-fee'; -import { GasFee } from '../../shared/gas-fee'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getStakingTokenMetadata } from '../../../fetchers/registry'; - -export interface SendLoaderResponse { - assetBalances: BalancesResponse[]; - feeAssetMetadata: Metadata; -} - -export const SendAssetBalanceLoader: LoaderFunction = async (): Promise => { - await abortLoader(); - const assetBalances = await getTransferableBalancesResponses(); - - if (assetBalances[0]) { - // set initial account if accounts exist and asset if account has asset list - useStore.setState(state => { - state.send.selection = assetBalances[0]; - }); - } - const feeAssetMetadata = await getStakingTokenMetadata(); - - return { assetBalances, feeAssetMetadata }; -}; - -export const SendForm = () => { - const { assetBalances, feeAssetMetadata } = useLoaderData() as SendLoaderResponse; - const { - selection, - amount, - recipient, - memo, - fee, - feeTier, - setAmount, - setSelection, - setRecipient, - setFeeTier, - setMemo, - sendTx, - txInProgress, - } = useStore(sendSelector); - - useRefreshFee(); - - const validationErrors = useMemo(() => { - return sendValidationErrors(selection, amount, recipient); - }, [selection, amount, recipient]); - - return ( -
{ - e.preventDefault(); - void sendTx(); - }} - > - - setRecipient(e.target.value)} - /> - - { - if (Number(amount) < 0) return; - setAmount(amount); - }} - validations={[ - { - type: 'error', - issue: 'insufficient funds', - checkFn: () => validationErrors.amountErr, - }, - ]} - balances={assetBalances} - /> - - - - 369 bytes)', - checkFn: () => validationErrors.memoErr, - }, - ]} - > - setMemo(e.target.value)} - /> - - - - ); -}; diff --git a/apps/minifront/src/components/send/send-form/use-refresh-fee.ts b/apps/minifront/src/components/send/send-form/use-refresh-fee.ts deleted file mode 100644 index f0631299..00000000 --- a/apps/minifront/src/components/send/send-form/use-refresh-fee.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useCallback, useEffect, useRef } from 'react'; -import { sendSelector } from '../../../state/send'; -import { useStore } from '../../../state'; - -const DEBOUNCE_MS = 500; - -/** - * Refreshes the fee in the state when the amount, recipient, selection, or memo - * changes. - */ -export const useRefreshFee = () => { - const { amount, feeTier, recipient, selection, refreshFee } = useStore(sendSelector); - const timeoutId = useRef(null); - - const debouncedRefreshFee = useCallback(() => { - if (timeoutId.current) { - window.clearTimeout(timeoutId.current); - timeoutId.current = null; - } - - timeoutId.current = window.setTimeout(() => { - timeoutId.current = null; - void refreshFee(); - }, DEBOUNCE_MS); - }, [refreshFee]); - - useEffect(debouncedRefreshFee, [amount, feeTier, recipient, selection, debouncedRefreshFee]); -}; diff --git a/apps/minifront/src/components/send/types.ts b/apps/minifront/src/components/send/types.ts deleted file mode 100644 index 236bc33e..00000000 --- a/apps/minifront/src/components/send/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { PagePath } from '../metadata/paths'; -import { EduPanel } from '../shared/edu-panels/content'; - -export type SendTab = PagePath.SEND | PagePath.RECEIVE; - -interface SendTabMetadata { - src: string; - label: string; - content: EduPanel; -} - -export type SendTabMap = Record; diff --git a/apps/minifront/src/components/shared/asset-selector.tsx b/apps/minifront/src/components/shared/asset-selector.tsx deleted file mode 100644 index 1bbd870f..00000000 --- a/apps/minifront/src/components/shared/asset-selector.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { - Dialog, - DialogClose, - DialogContent, - DialogHeader, -} from '@penumbra-zone/ui/components/ui/dialog'; -import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { useEffect, useId, useMemo, useState } from 'react'; -import { IconInput } from '@penumbra-zone/ui/components/ui/icon-input'; -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { Box } from '@penumbra-zone/ui/components/ui/box'; -import { motion } from 'framer-motion'; - -interface AssetSelectorProps { - assets: Metadata[]; - value?: Metadata; - onChange: (metadata: Metadata) => void; - /** - * If passed, this function will be called for every asset that - * `AssetSelector` plans to display. It should return `true` or `false` - * depending on whether that asset should be displayed. - */ - filter?: (metadata: Metadata) => boolean; -} - -/** - * If the `filter` rejects the currently selected `asset`, switch to a different - * `asset`. - */ -const switchAssetIfNecessary = ({ - value, - onChange, - filter, - assets, -}: AssetSelectorProps & { assets: Metadata[] }) => { - if (!filter || !value) return; - - if (!filter(value)) { - const firstAssetThatPassesTheFilter = assets.find(filter); - if (firstAssetThatPassesTheFilter) onChange(firstAssetThatPassesTheFilter); - } -}; - -const useFilteredAssets = ({ assets, value, onChange, filter }: AssetSelectorProps) => { - const sortedAssets = useMemo( - () => - [...assets].sort((a, b) => - a.symbol.toLocaleLowerCase() < b.symbol.toLocaleLowerCase() ? -1 : 1, - ), - [assets], - ); - - const [search, setSearch] = useState(''); - - let filteredAssets = filter ? sortedAssets.filter(filter) : sortedAssets; - filteredAssets = search ? assets.filter(bySearch(search)) : assets; - - useEffect( - () => switchAssetIfNecessary({ value, onChange, filter, assets: filteredAssets }), - [filter, value, filteredAssets, onChange], - ); - - return { filteredAssets, search, setSearch }; -}; - -const bySearch = (search: string) => (asset: Metadata) => - asset.display.toLocaleLowerCase().includes(search.toLocaleLowerCase()) || - asset.symbol.toLocaleLowerCase().includes(search.toLocaleLowerCase()); - -/** - * Allows the user to select any asset known to Penumbra, optionally filtered by - * a filter function. - * - * For an asset selector that picks from the user's balances, use - * ``. - */ -export const AssetSelector = ({ assets, onChange, value, filter }: AssetSelectorProps) => { - const { filteredAssets, search, setSearch } = useFilteredAssets({ - assets, - value, - onChange, - filter, - }); - - const layoutId = useId(); - const [isOpen, setIsOpen] = useState(false); - - /** - * @todo: Refactor to not use `ValueViewComponent`, since it's not intended to - * just display an asset icon/symbol without a value. - */ - const valueView = useMemo( - () => new ValueView({ valueView: { case: 'knownAssetId', value: { metadata: value } } }), - [value], - ); - - return ( - <> - {!isOpen && ( - setIsOpen(true)} - > - - - )} - - {isOpen && ( - <> - {/* 0-opacity placeholder for layout's sake */} -
- -
- - )} - - - -
- Select asset - -
- - } - value={search} - onChange={setSearch} - placeholder='Search assets...' - /> - - - {filteredAssets.map(metadata => ( -
- -
onChange(metadata)} - > - -

{metadata.symbol || 'Unknown asset'}

-
-
-
- ))} -
-
-
-
- - ); -}; diff --git a/apps/minifront/src/components/shared/balance-selector.tsx b/apps/minifront/src/components/shared/balance-selector.tsx deleted file mode 100644 index a3c41a57..00000000 --- a/apps/minifront/src/components/shared/balance-selector.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { useId, useState } from 'react'; -import { IconInput } from '@penumbra-zone/ui/components/ui/icon-input'; -import { - Dialog, - DialogClose, - DialogContent, - DialogHeader, -} from '@penumbra-zone/ui/components/ui/dialog'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { getDisplayDenomFromView, getSymbolFromValueView } from '@penumbra-zone/getters/value-view'; -import { Box } from '@penumbra-zone/ui/components/ui/box'; -import { motion } from 'framer-motion'; - -const bySearch = (search: string) => (balancesResponse: BalancesResponse) => - getDisplayDenomFromView(balancesResponse.balanceView) - .toLocaleLowerCase() - .includes(search.toLocaleLowerCase()) || - getSymbolFromValueView(balancesResponse.balanceView) - .toLocaleLowerCase() - .includes(search.toLocaleLowerCase()); - -interface BalanceSelectorProps { - value: BalancesResponse | undefined; - onChange: (selection: BalancesResponse) => void; - balances: BalancesResponse[]; -} - -/** - * Renders balances the user holds, and allows the user to select one. This is - * useful for a form where the user wants to send/sell/swap an asset that they - * already hold. - * - * Use `` if you want to render assets that aren't tied to any - * balance. - */ -export default function BalanceSelector({ value, balances, onChange }: BalanceSelectorProps) { - const [search, setSearch] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const filteredBalances = search ? balances.filter(bySearch(search)) : balances; - const layoutId = useId(); - - return ( - <> - {!isOpen && ( - setIsOpen(true)} - > - - - )} - - {isOpen && ( - <> - {/* 0-opacity placeholder for layout's sake */} -
- -
- - )} - - - -
- Select asset -
- - } - value={search} - onChange={setSearch} - placeholder='Search assets...' - /> - -
-

Account

-

Asset

-
-
- {filteredBalances.map((b, i) => { - const index = getAddressIndex(b.accountAddress).account; - - return ( -
- -
onChange(b)} - > -

{index}

-
- -
-
-
-
- ); - })} -
-
-
-
-
- - ); -} diff --git a/apps/minifront/src/components/shared/edu-panels/content.tsx b/apps/minifront/src/components/shared/edu-panels/content.tsx deleted file mode 100644 index 0d71de6d..00000000 --- a/apps/minifront/src/components/shared/edu-panels/content.tsx +++ /dev/null @@ -1,35 +0,0 @@ -export enum EduPanel { - ASSETS, - TRANSACTIONS_LIST, - SHIELDED_TRANSACTION, - SENDING_FUNDS, - RECEIVING_FUNDS, - IBC_WITHDRAW, - SWAP, - SWAP_AUCTION, - STAKING, - TEMP_FILLER, -} - -export const eduPanelContent: Record = { - [EduPanel.ASSETS]: - 'Your balances are shielded, and are known only to you. They are not visible on chain. Each Penumbra wallet controls many numbered accounts, each with its own balance. Account information is never revealed on-chain.', - [EduPanel.TRANSACTIONS_LIST]: - 'Your wallet scans shielded chain data locally and indexes all relevant transactions it detects, both incoming and outgoing.', - [EduPanel.SHIELDED_TRANSACTION]: - 'Penumbra transactions are shielded and don’t reveal any information about the sender, receiver, or amount. Use the toggle to see what information is revealed on-chain.', - [EduPanel.SENDING_FUNDS]: - 'Penumbra transactions include a shielded memo only visible to the sender and receiver. The sender’s address is included in the memo so the receiver can identify the payment.', - [EduPanel.RECEIVING_FUNDS]: - 'Every Penumbra account has a stable default address and many one-time IBC deposit addresses. All addresses for the same account deposit to the same pool of funds. Use a freshly generated IBC deposit address to preserve privacy when sending funds from a transparent chain to Penumbra.', - [EduPanel.IBC_WITHDRAW]: - 'IBC to a connected chain. Note that if the chain is a transparent chain, the transaction will be visible to others.', - [EduPanel.SWAP]: - 'Shielded swaps between any kind of cryptoasset, with sealed-bid, batch pricing and no frontrunning. Only the batch totals are revealed, providing long-term privacy. Penumbra has no MEV, because transactions do not leak data about user activity.', - [EduPanel.SWAP_AUCTION]: - "Offer a specific quantity of cryptocurrency at decreasing prices until all the tokens are sold. Buyers can place bids at the price they're willing to pay, with the auction concluding when all tokens are sold or when the auction time expires. This mechanism allows for price discovery based on market demand, with participants potentially acquiring tokens at prices lower than initially offered.", - [EduPanel.STAKING]: - 'Explore the available validator nodes and their associated rewards, performance metrics, and staking requirements. Select the validator you wish to delegate your tokens to, based on factors like uptime, reputation, and expected returns. Stay informed about validator performance updates, rewards distribution, and any network upgrades to ensure a seamless staking experience.', - [EduPanel.TEMP_FILLER]: - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", -}; diff --git a/apps/minifront/src/components/shared/edu-panels/edu-info-card.tsx b/apps/minifront/src/components/shared/edu-panels/edu-info-card.tsx deleted file mode 100644 index 74204548..00000000 --- a/apps/minifront/src/components/shared/edu-panels/edu-info-card.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header'; -import { EduPanel, eduPanelContent } from './content'; -import { motion } from 'framer-motion'; - -interface HelperCardProps { - src: string; - label: string; - className?: string; - content: EduPanel; - layout?: boolean; -} - -export const EduInfoCard = ({ src, label, className, content, layout }: HelperCardProps) => { - return ( - - - icons - {label} - -

{eduPanelContent[content]}

-
- ); -}; diff --git a/apps/minifront/src/components/shared/error-boundary.tsx b/apps/minifront/src/components/shared/error-boundary.tsx deleted file mode 100644 index f7a7d9f7..00000000 --- a/apps/minifront/src/components/shared/error-boundary.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { isRouteErrorResponse, useRouteError } from 'react-router-dom'; -import { PenumbraNotInstalledError, PenumbraNotConnectedError } from '@penumbra-zone/client'; -import { ExtensionNotConnected } from '../extension-not-connected'; -import { NotFound } from '../not-found'; -import { ExtensionTransportDisconnected } from '../extension-transport-disconnected'; -import { ExtensionNotInstalled } from '../extension-not-installed'; -import { Code, ConnectError } from '@connectrpc/connect'; -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; - -export const ErrorBoundary = () => { - const error = useRouteError(); - - if (error instanceof ConnectError && error.code === Code.Unavailable) - return ; - if (error instanceof PenumbraNotInstalledError) return ; - if (error instanceof PenumbraNotConnectedError) return ; - if (isRouteErrorResponse(error) && error.status === 404) return ; - - console.error('ErrorBoundary caught error:', error); - - return ( - - {String(error)} - - ); -}; diff --git a/apps/minifront/src/components/shared/gas-fee.tsx b/apps/minifront/src/components/shared/gas-fee.tsx deleted file mode 100644 index 6347dc7f..00000000 --- a/apps/minifront/src/components/shared/gas-fee.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { - Fee, - FeeTier_Tier, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1/fee_pb'; -import { - SegmentedPicker, - SegmentedPickerOption, -} from '@penumbra-zone/ui/components/ui/segmented-picker'; -import { InputBlock } from './input-block'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; - -const FEE_TIER_OPTIONS: SegmentedPickerOption[] = [ - { - label: 'Low', - value: FeeTier_Tier.LOW, - }, - { - label: 'Medium', - value: FeeTier_Tier.MEDIUM, - }, - { - label: 'High', - value: FeeTier_Tier.HIGH, - }, -]; - -export const GasFee = ({ - fee, - feeTier, - feeAssetMetadata, - setFeeTier, -}: { - fee: Fee | undefined; - feeTier: FeeTier_Tier; - feeAssetMetadata: Metadata; - setFeeTier: (feeTier: FeeTier_Tier) => void; -}) => { - let feeValueView: ValueView | undefined; - if (fee?.amount) - feeValueView = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { amount: fee.amount, metadata: feeAssetMetadata }, - }, - }); - - return ( - /** - * @todo: Change label from 'Fee tier' to 'Gas fee' if/when we support - * manual fee entry. - */ - -
- - - {feeValueView && ( -
- Gas fee - - -
- )} -
-
- ); -}; diff --git a/apps/minifront/src/components/shared/input-block.tsx b/apps/minifront/src/components/shared/input-block.tsx deleted file mode 100644 index 2061f7ad..00000000 --- a/apps/minifront/src/components/shared/input-block.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { Validation, validationResult } from './validation-result'; -import { ReactNode } from 'react'; -import { Box } from '@penumbra-zone/ui/components/ui/box'; - -interface InputBlockProps { - label: string; - className?: string | undefined; - validations?: Validation[] | undefined; - value?: unknown; - children: ReactNode; - layout?: boolean; - layoutId?: string; -} - -export const InputBlock = ({ - label, - className, - validations, - value, - children, - layout, - layoutId, -}: InputBlockProps) => { - const vResult = typeof value === 'string' ? validationResult(value, validations) : undefined; - - return ( - {vResult.issue}
: null - } - layout={layout} - layoutId={layoutId} - > -
- {children} -
- - ); -}; diff --git a/apps/minifront/src/components/shared/input-token.tsx b/apps/minifront/src/components/shared/input-token.tsx deleted file mode 100644 index a6837dc7..00000000 --- a/apps/minifront/src/components/shared/input-token.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import BalanceSelector from './balance-selector'; -import { Validation } from './validation-result'; -import { InputBlock } from './input-block'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view'; -import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view'; - -interface InputTokenProps { - label: string; - selection: BalancesResponse | undefined; - placeholder: string; - className?: string; - inputClassName?: string; - value: string; - setSelection: (selection: BalancesResponse) => void; - validations?: Validation[]; - balances: BalancesResponse[]; - onInputChange: (amount: string) => void; -} - -export default function InputToken({ - label, - placeholder, - selection, - className, - validations, - value, - inputClassName, - setSelection, - balances, - onInputChange, -}: InputTokenProps) { - const setInputToBalanceMax = () => { - const match = balances.find(b => b.balanceView?.equals(selection?.balanceView)); - if (match?.balanceView) { - const formattedAmt = getFormattedAmtFromValueView(match.balanceView); - onInputChange(formattedAmt); - } - }; - - return ( - -
- onInputChange(e.target.value)} - /> - -
- -
-
- {selection?.balanceView && ( - - )} -
-
-
- ); -} diff --git a/apps/minifront/src/components/shared/restrict-max-width.tsx b/apps/minifront/src/components/shared/restrict-max-width.tsx deleted file mode 100644 index c901c1ba..00000000 --- a/apps/minifront/src/components/shared/restrict-max-width.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ReactNode } from 'react'; - -export const RestrictMaxWidth = ({ children }: { children: ReactNode }) => { - return
{children}
; -}; diff --git a/apps/minifront/src/components/shared/tabs.tsx b/apps/minifront/src/components/shared/tabs.tsx deleted file mode 100644 index 2a0488bf..00000000 --- a/apps/minifront/src/components/shared/tabs.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { PagePath } from '../metadata/paths'; -import { useNavigate } from 'react-router-dom'; -import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picker'; -import { ComponentProps } from 'react'; - -export interface Tab { - title: string; - enabled: boolean; - href: PagePath; -} - -interface TabsProps { - tabs: Tab[]; - activeTab: PagePath; - className?: string; -} - -export const Tabs = ({ tabs, activeTab }: TabsProps) => { - const navigate = useNavigate(); - const options: ComponentProps['options'] = tabs - .filter(tab => tab.enabled) - .map(tab => ({ - label: tab.title, - value: tab.href, - })); - - return ( - navigate(value.toString())} - options={options} - grow - size='lg' - /> - ); -}; diff --git a/apps/minifront/src/components/shared/validation-result.ts b/apps/minifront/src/components/shared/validation-result.ts deleted file mode 100644 index 7aea5986..00000000 --- a/apps/minifront/src/components/shared/validation-result.ts +++ /dev/null @@ -1,15 +0,0 @@ -export interface Validation { - checkFn: (txt: string) => boolean; - type: 'warn' | 'error'; // corresponds to red or yellow - issue: string; -} - -export const validationResult = ( - value: string, - validations?: Validation[], -): undefined | Validation => { - if (!validations) return; - const results = validations.filter(v => v.checkFn(value)); - const error = results.find(v => v.type === 'error'); - return error ? error : results.find(v => v.type === 'warn'); -}; diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/index.test.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/index.test.tsx deleted file mode 100644 index 0017e8b7..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/index.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { DelegationValueView } from '.'; -import { render } from '@testing-library/react'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { bech32mIdentityKey } from '@penumbra-zone/bech32m/penumbravalid'; - -const u8 = (length: number) => Uint8Array.from({ length }, () => Math.floor(Math.random() * 256)); - -const validatorIk = { ik: u8(32) }; -const validatorIkString = bech32mIdentityKey(validatorIk); -const delString = 'delegation_' + validatorIkString; -const udelString = 'udelegation_' + validatorIkString; -const delAsset = { inner: u8(32) }; - -const otherAsset = { inner: u8(32) }; - -const DELEGATION_TOKEN_METADATA = new Metadata({ - display: delString, - base: udelString, - denomUnits: [{ denom: udelString }, { denom: delString, exponent: 6 }], - name: 'Delegation token', - penumbraAssetId: delAsset, - symbol: 'delUM(abc...xyz)', -}); - -const SOME_OTHER_TOKEN_METADATA = new Metadata({ - display: 'someOtherToken', - base: 'usomeOtherToken', - denomUnits: [{ denom: 'usomeOtherToken' }, { denom: 'someOtherToken', exponent: 6 }], - name: 'Some Other Token', - penumbraAssetId: otherAsset, - symbol: 'SOT', -}); - -const STAKING_TOKEN_METADATA = new Metadata({ - display: 'penumbra', - base: 'penumbra', - denomUnits: [{ denom: 'upenumbra' }, { denom: 'penumbra', exponent: 6 }], - name: 'penumbra', - penumbraAssetId: { inner: new Uint8Array([2, 5, 6, 7]) }, - symbol: 'UM', -}); - -const validatorInfo = new ValidatorInfo({ - validator: { - identityKey: {}, - fundingStreams: [ - { - recipient: { - case: 'toAddress', - value: { - rateBps: 1, - }, - }, - }, - ], - }, -}); - -const valueView = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { - hi: 0n, - lo: 1_000_000n, - }, - metadata: DELEGATION_TOKEN_METADATA, - equivalentValues: [ - { - asOfHeight: 123n, - equivalentAmount: { - hi: 0n, - lo: 1_330_000n, - }, - numeraire: STAKING_TOKEN_METADATA, - }, - - { - asOfHeight: 123n, - equivalentAmount: { - hi: 0n, - lo: 2_660_000n, - }, - numeraire: SOME_OTHER_TOKEN_METADATA, - }, - ], - - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo.toBinary(), - }, - }, - }, -}); - -describe('', () => { - it('shows balance of the delegation token', () => { - const { container } = render( - , - ); - - expect(container).toHaveTextContent('1delUM(abc...xyz)'); - }); - - it("shows the delegation token's equivalent value in terms of the staking token", () => { - const { container } = render( - , - ); - - expect(container).toHaveTextContent('1.33UM'); - }); - - it('does not show other equivalent values', () => { - const { container } = render( - , - ); - - expect(container).not.toHaveTextContent('2.66SOT'); - }); -}); diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/index.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/index.tsx deleted file mode 100644 index 05373aaf..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/index.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { ValidatorInfoComponent } from './validator-info-component'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { StakingActions } from './staking-actions'; -import { memo, useMemo } from 'react'; -import { - getDisplayDenomFromView, - getEquivalentValues, - getMetadata, - getValidatorInfoFromValueView, -} from '@penumbra-zone/getters/value-view'; -import { asValueView } from '@penumbra-zone/getters/equivalent-value'; - -/** - * Renders a `ValueView` that contains a delegation token, along with the - * validator that the token is staked in. - * - * @todo: Depending on the outcome of - * https://github.com/penumbra-zone/penumbra/issues/3882, we may be able to - * remove `votingPowerAsIntegerPercentage`. - */ -export const DelegationValueView = memo( - ({ - valueView, - votingPowerAsIntegerPercentage, - unstakedTokens, - stakingTokenMetadata, - }: { - /** - * A `ValueView` representing the address's balance of the given delegation - * token. - */ - valueView: ValueView; - votingPowerAsIntegerPercentage?: number; - /** - * A `ValueView` representing the address's balance of staking (UM) tokens. - * Used to show the user how many tokens they have available to delegate. - */ - unstakedTokens?: ValueView; - stakingTokenMetadata: Metadata; - }) => { - const validatorInfo = getValidatorInfoFromValueView(valueView); - const metadata = getMetadata(valueView); - - const equivalentValueOfStakingToken = useMemo(() => { - const equivalentValue = getEquivalentValues(valueView).find(equivalentValue => - equivalentValue.numeraire?.penumbraAssetId?.equals(stakingTokenMetadata.penumbraAssetId), - ); - - if (equivalentValue) return asValueView(equivalentValue); - return undefined; - }, [valueView, stakingTokenMetadata.penumbraAssetId]); - - return ( -
-
- -
- -
- - - {equivalentValueOfStakingToken && ( - - )} -
- - -
- ); - }, -); -DelegationValueView.displayName = 'DelegationValueView'; diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx deleted file mode 100644 index 95a5b341..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/form-dialog.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Dialog, DialogContent, DialogHeader } from '@penumbra-zone/ui/components/ui/dialog'; -import { IdentityKeyComponent } from '@penumbra-zone/ui/components/ui/identity-key-component'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { InputBlock } from '../../../../shared/input-block'; -import { Validator } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { FormEvent } from 'react'; -import { getIdentityKey } from '@penumbra-zone/getters/validator'; -import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view'; -import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view'; - -const getCapitalizedAction = (action: 'delegate' | 'undelegate') => - action.replace(/^./, firstCharacter => firstCharacter.toLocaleUpperCase()); - -/** - * Renders a dialog with a form for delegating to, or undelegating from, a - * validator. - */ -export const FormDialog = ({ - action, - validator, - amount, - delegationTokens, - unstakedTokens, - open, - onChangeAmount, - onClose, - onSubmit, -}: { - /** When defined, the dialog will be open. */ - action?: 'delegate' | 'undelegate'; - /** The validator we're delegating to or undelegating from. */ - validator: Validator; - amount: string; - /** - * A `ValueView` representing the address's balance of delegation tokens. Used - * to show the user how many tokens they have available to undelegate. - */ - delegationTokens: ValueView; - /** - * A `ValueView` representing the address's balance of staking (UM) tokens. - * Used to show the user how many tokens they have available to delegate. - */ - unstakedTokens?: ValueView; - /** - * Whether the form is open. - */ - open: boolean; - onChangeAmount: (amount: string) => void; - onClose: () => void; - onSubmit: () => void; -}) => { - const handleOpenChange = (open: boolean) => { - if (!open) onClose(); - }; - - const handleSubmit = (e: FormEvent) => { - e.preventDefault(); - onSubmit(); - }; - - const setInputToBalanceMax = () => { - const type = action === 'delegate' ? unstakedTokens : delegationTokens; - if (type) { - const formattedAmt = getFormattedAmtFromValueView(type); - onChangeAmount(formattedAmt); - } - }; - - return ( - - - {!!open && !!action && ( - <> - {getCapitalizedAction(action)} -
-
-
{validator.name}
- -
-
- Please verify that the identity key above is the one you expect, rather than relying - on the validator name (as that can be spoofed). -
- - {/** @todo: Refactor this block to use `InputToken` (with a new - boolean `showSelectModal` prop) once asset balances are - refactored as `ValueView`s. */} - - onChangeAmount(e.currentTarget.value)} - type='number' - inputMode='decimal' - autoFocus - /> - -
- {action === 'delegate' && unstakedTokens && ( - - )} - - {action === 'undelegate' && ( - - )} -
-
- - -
- - )} -
-
- ); -}; diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx deleted file mode 100644 index 3d9812ef..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { StakingActions } from '.'; -import { render } from '@testing-library/react'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; - -const nonZeroBalance = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 1n }, - }, - }, -}); - -const zeroBalance = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 0n }, - }, - }, -}); - -const validatorInfo = new ValidatorInfo({ validator: {} }); - -describe('', () => { - it('renders an enabled Delegate button there is a non-zero balance of unstaked tokens', () => { - const { getByText } = render( - , - ); - - expect(getByText('Delegate')).toBeEnabled(); - }); - - it('renders an enabled Undelegate button when there is a non-zero balance of delegation tokens', () => { - const { getByText } = render( - , - ); - - expect(getByText('Undelegate')).toBeEnabled(); - }); - - it('renders a disabled Delegate button when there is a zero balance of unstaked tokens', () => { - const { getByText } = render( - , - ); - - expect(getByText('Delegate')).toBeDisabled(); - }); - - it('renders a disabled Delegate button when unstaked tokens are undefined', () => { - const { getByText } = render( - , - ); - - expect(getByText('Delegate')).toBeDisabled(); - }); - - it('renders a disabled Undelegate button when there is a zero balance of delegation tokens', () => { - const { getByText } = render( - , - ); - - expect(getByText('Undelegate')).toBeDisabled(); - }); -}); diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.tsx deleted file mode 100644 index b83fe82e..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/staking-actions/index.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { FormDialog } from './form-dialog'; -import { useMemo } from 'react'; -import { AllSlices } from '../../../../../state'; -import { useStoreShallow } from '../../../../../utils/use-store-shallow'; -import { getValidator } from '@penumbra-zone/getters/validator-info'; -import { getAmount } from '@penumbra-zone/getters/value-view'; -import { joinLoHiAmount } from '@penumbra-zone/types/amount'; - -const stakingActionsSelector = (state: AllSlices) => ({ - action: state.staking.action, - amount: state.staking.amount, - delegate: state.staking.delegate, - undelegate: state.staking.undelegate, - onClickActionButton: state.staking.onClickActionButton, - onClose: state.staking.onClose, - setAmount: state.staking.setAmount, - validatorInfo: state.staking.validatorInfo, -}); - -/** - * Renders Delegate/Undelegate buttons for a validator, as well as a form inside - * a dialog that opens when the user clicks one of those buttons. - */ -export const StakingActions = ({ - validatorInfo, - delegationTokens, - unstakedTokens, -}: { - /** The validator that these actions will apply to. */ - validatorInfo: ValidatorInfo; - /** - * A `ValueView` representing the address's balance of delegation tokens. Used - * to show the user how many tokens they have available to undelegate. - */ - delegationTokens: ValueView; - /** - * A `ValueView` representing the address's balance of staking (UM) tokens. - * Used to show the user how many tokens they have available to delegate. - */ - unstakedTokens?: ValueView; -}) => { - const state = useStoreShallow(stakingActionsSelector); - const validator = getValidator(validatorInfo); - - const canDelegate = useMemo( - () => (unstakedTokens ? !!joinLoHiAmount(getAmount(unstakedTokens)) : false), - [unstakedTokens], - ); - const canUndelegate = useMemo( - () => !!joinLoHiAmount(getAmount(delegationTokens)), - [delegationTokens], - ); - - const handleSubmit = () => { - if (state.action === 'delegate') void state.delegate(); - else void state.undelegate(); - }; - - return ( - <> -
-
- - -
-
- - - - ); -}; diff --git a/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx b/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx deleted file mode 100644 index e803c8e6..00000000 --- a/apps/minifront/src/components/staking/account/delegation-value-view/validator-info-component.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { IdentityKeyComponent } from '@penumbra-zone/ui/components/ui/identity-key-component'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@penumbra-zone/ui/components/ui/tooltip'; -import { useStore } from '../../../../state'; -import { - getIdentityKeyFromValidatorInfo, - getValidator, -} from '@penumbra-zone/getters/validator-info'; -import { calculateCommissionAsPercentage } from '@penumbra-zone/types/staking'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon'; - -/** - * Renders a single `ValidatorInfo`: its name and identity key, - * voting power, and commission. - */ -export const ValidatorInfoComponent = ({ - validatorInfo, - votingPowerAsIntegerPercentage, - delegationTokenMetadata, -}: { - validatorInfo: ValidatorInfo; - votingPowerAsIntegerPercentage?: number; - delegationTokenMetadata: Metadata; -}) => { - // The tooltip component is a bit heavy to render, so we'll wait to render it - // until all loading completes. - const showTooltips = useStore(state => !state.staking.loading); - const validator = getValidator(validatorInfo); - const identityKey = getIdentityKeyFromValidatorInfo(validatorInfo); - - return ( - -
-
- -
- -
- -
- {validator.name} - - {votingPowerAsIntegerPercentage !== undefined && ( - - {showTooltips && ( - - - VP: - - Voting power - - )} - {!showTooltips && VP:} {votingPowerAsIntegerPercentage}% - - )} - - - {showTooltips && ( - - - Com: - - Commission - - )} - {!showTooltips && Com:} {calculateCommissionAsPercentage(validatorInfo)}% - -
-
-
-
- ); -}; diff --git a/apps/minifront/src/components/staking/account/delegations.tsx b/apps/minifront/src/components/staking/account/delegations.tsx deleted file mode 100644 index 8fbff661..00000000 --- a/apps/minifront/src/components/staking/account/delegations.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { AnimatePresence, motion } from 'framer-motion'; -import { AllSlices } from '../../../state'; -import { DelegationValueView } from './delegation-value-view'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; -import { getValidatorIdentityKeyFromValueView } from '@penumbra-zone/getters/value-view'; -import { bech32mIdentityKey } from '@penumbra-zone/bech32m/penumbravalid'; -import { VotingPowerAsIntegerPercentage } from '@penumbra-zone/types/staking'; - -const getVotingPowerAsIntegerPercentage = ( - votingPowerByValidatorInfo: Record, - delegation: ValueView, -) => - votingPowerByValidatorInfo[bech32mIdentityKey(getValidatorIdentityKeyFromValueView(delegation))]; - -const delegationsSelector = (state: AllSlices) => ({ - delegations: state.staking.delegationsByAccount.get(state.staking.account) ?? [], - unstakedTokens: state.staking.unstakedTokensByAccount.get(state.staking.account), - votingPowerByValidatorInfo: state.staking.votingPowerByValidatorInfo, -}); - -export const Delegations = ({ stakingTokenMetadata }: { stakingTokenMetadata: Metadata }) => { - const { delegations, unstakedTokens, votingPowerByValidatorInfo } = - useStoreShallow(delegationsSelector); - - return ( -
- - {delegations.map(delegation => ( - - - - ))} - -
- ); -}; diff --git a/apps/minifront/src/components/staking/account/header/index.tsx b/apps/minifront/src/components/staking/account/header/index.tsx deleted file mode 100644 index 1dc5c92e..00000000 --- a/apps/minifront/src/components/staking/account/header/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Card, CardContent } from '@penumbra-zone/ui/components/ui/card'; -import { AccountSwitcher } from '@penumbra-zone/ui/components/ui/account-switcher'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { Stat } from './stat'; -import { AllSlices } from '../../../../state'; -import { UnbondingTokens } from './unbonding-tokens'; -import { useStoreShallow } from '../../../../utils/use-store-shallow'; -import { useLoaderData } from 'react-router-dom'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { zeroValueView } from '../../../../utils/zero-value-view'; - -const headerSelector = (state: AllSlices) => ({ - account: state.staking.account, - setAccount: state.staking.setAccount, - accountSwitcherFilter: state.staking.accountSwitcherFilter, - unstakedTokensByAccount: state.staking.unstakedTokensByAccount, - unbondingTokensByAccount: state.staking.unbondingTokensByAccount, - undelegateClaim: state.staking.undelegateClaim, -}); - -/** - * The header of the account view, with an account switcher and balances of - * various token types related to staking. - */ -export const Header = () => { - const { - account, - setAccount, - accountSwitcherFilter, - unstakedTokensByAccount, - unbondingTokensByAccount, - undelegateClaim, - } = useStoreShallow(headerSelector); - const unstakedTokens = unstakedTokensByAccount.get(account); - const unbondingTokens = unbondingTokensByAccount.get(account); - - const stakingTokenMetadata = useLoaderData() as Metadata; - return ( - - -
- - -
- - - - - - - - - - - {!!unbondingTokens?.claimable.tokens.length && ( - - )} - - -
-
-
-
- ); -}; diff --git a/apps/minifront/src/components/staking/account/header/stat.tsx b/apps/minifront/src/components/staking/account/header/stat.tsx deleted file mode 100644 index 63358536..00000000 --- a/apps/minifront/src/components/staking/account/header/stat.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { ReactNode } from 'react'; - -/** - * A statistic that shows up at the top of the staking page. - */ -export const Stat = ({ label, children }: { label: string; children: ReactNode }) => { - return ( -
- {label} - {children} -
- ); -}; diff --git a/apps/minifront/src/components/staking/account/header/unbonding-tokens.tsx b/apps/minifront/src/components/staking/account/header/unbonding-tokens.tsx deleted file mode 100644 index eed53b9b..00000000 --- a/apps/minifront/src/components/staking/account/header/unbonding-tokens.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getDisplayDenomFromView } from '@penumbra-zone/getters/value-view'; -import { - TooltipProvider, - Tooltip, - TooltipTrigger, - TooltipContent, -} from '@penumbra-zone/ui/components/ui/tooltip'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { ReactNode } from 'react'; -import { zeroValueView } from '../../../../utils/zero-value-view'; - -export const UnbondingTokens = ({ - total, - tokens, - helpText, - children, - stakingTokenMetadata, -}: { - total?: ValueView; - tokens?: ValueView[]; - helpText: string; - children?: ReactNode; - stakingTokenMetadata: Metadata; -}) => { - return ( - - - - - - -
-
{helpText}
- - {!!tokens?.length && - tokens.map(token => ( - - ))} - - {children} -
-
-
-
- ); -}; diff --git a/apps/minifront/src/components/staking/layout.tsx b/apps/minifront/src/components/staking/layout.tsx deleted file mode 100644 index 8fe41733..00000000 --- a/apps/minifront/src/components/staking/layout.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect } from 'react'; -import { AllSlices, useStore } from '../../state'; -import { abortLoader } from '../../abort-loader'; -import { Card, CardContent, CardHeader, CardTitle } from '@penumbra-zone/ui/components/ui/card'; -import { Header } from './account/header'; -import { Delegations } from './account/delegations'; -import { LoaderFunction, useLoaderData } from 'react-router-dom'; -import { useStoreShallow } from '../../utils/use-store-shallow'; -import { RestrictMaxWidth } from '../shared/restrict-max-width'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getStakingTokenMetadata } from '../../fetchers/registry'; - -export const StakingLoader: LoaderFunction = async (): Promise => { - await abortLoader(); - // Await to avoid screen flicker. - await useStore.getState().staking.loadAndReduceBalances(); - - return await getStakingTokenMetadata(); -}; - -const stakingLayoutSelector = (state: AllSlices) => ({ - account: state.staking.account, - loadDelegationsForCurrentAccount: state.staking.loadDelegationsForCurrentAccount, - loadUnbondingTokensForCurrentAccount: state.staking.loadUnbondingTokensForCurrentAccount, -}); - -export const StakingLayout = () => { - const stakingTokenMetadata = useLoaderData() as Metadata; - const { account, loadDelegationsForCurrentAccount, loadUnbondingTokensForCurrentAccount } = - useStoreShallow(stakingLayoutSelector); - - /** Load delegations every time the account changes. */ - useEffect(() => { - void loadDelegationsForCurrentAccount(); - void loadUnbondingTokensForCurrentAccount(); - }, [account, loadDelegationsForCurrentAccount, loadUnbondingTokensForCurrentAccount]); - - return ( - -
-
- - - Delegation tokens - - - - - -
-
- ); -}; diff --git a/apps/minifront/src/components/staking/validator-info-row.tsx b/apps/minifront/src/components/staking/validator-info-row.tsx deleted file mode 100644 index c53271b8..00000000 --- a/apps/minifront/src/components/staking/validator-info-row.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { TableCell, TableRow } from '@penumbra-zone/ui/components/ui/table'; -import { ReactNode } from 'react'; -import { Oval } from 'react-loader-spinner'; -import { getValidator } from '@penumbra-zone/getters/validator-info'; -import { calculateCommissionAsPercentage } from '@penumbra-zone/types/staking'; - -export const ValidatorInfoRow = ({ - loading, - validatorInfo, - votingPowerByValidatorInfo, - staking, -}: { - loading: boolean; - validatorInfo: ValidatorInfo; - votingPowerByValidatorInfo: Map; - staking: ReactNode; -}) => ( - - {getValidator(validatorInfo).name} - - {loading ? ( - - ) : ( - `${votingPowerByValidatorInfo.get(validatorInfo)}%` - )} - - - {calculateCommissionAsPercentage(validatorInfo)}% - - {staking} - -); diff --git a/apps/minifront/src/components/staking/validators-table.tsx b/apps/minifront/src/components/staking/validators-table.tsx deleted file mode 100644 index 2c7f731e..00000000 --- a/apps/minifront/src/components/staking/validators-table.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@penumbra-zone/ui/components/ui/table'; -import { Oval } from 'react-loader-spinner'; -import { ValidatorInfoRow } from './validator-info-row'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { ReactNode } from 'react'; -import { getValidator } from '@penumbra-zone/getters/validator-info'; -import { VotingPowerAsIntegerPercentage } from '@penumbra-zone/types/staking'; - -const HEADERS = ['Validator', 'Voting power', 'Commission', 'Staking']; - -interface ValidatorsTableProps { - loading: boolean; - error: unknown; - validatorInfos: ValidatorInfo[]; - votingPowerByValidatorInfo: Map; - /** - * Content to display inside the Staking cell. - */ - renderStakingActions: (validatorInfo: ValidatorInfo) => ReactNode; -} - -export const ValidatorsTable = ({ - loading, - error, - validatorInfos, - votingPowerByValidatorInfo, - renderStakingActions, -}: ValidatorsTableProps) => { - const showError = !!error; - const showLoading = loading && !validatorInfos.length; - const showValidators = !showError && !showLoading; - - return ( - - - - {HEADERS.map(header => ( - {header} - ))} - - - - {showError && ( - - - There was an error loading validators. Please reload the page. - - - )} - - {showLoading && ( - - - - - - )} - - {showValidators && - validatorInfos.map(validatorInfo => ( - - ))} - -
- ); -}; diff --git a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts deleted file mode 100644 index fe129123..00000000 --- a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getFilteredAuctionInfos } from './get-filtered-auction-infos'; -import { - AuctionId, - DutchAuction, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb'; -import { AuctionInfo } from '../../../fetchers/auction-infos'; - -const MOCK_AUCTION_1 = new DutchAuction({ - description: { - startHeight: 11n, - endHeight: 20n, - }, - state: { - seq: 0n, - }, -}); -const MOCK_AUCTION_ID_1 = new AuctionId({ inner: new Uint8Array([1]) }); -const MOCK_AUCTION_INFO_1: AuctionInfo = { - auction: MOCK_AUCTION_1, - id: MOCK_AUCTION_ID_1, -}; - -const MOCK_AUCTION_2 = new DutchAuction({ - description: { - startHeight: 11n, - endHeight: 20n, - }, - state: { - seq: 1n, - }, -}); -const MOCK_AUCTION_ID_2 = new AuctionId({ inner: new Uint8Array([2]) }); -const MOCK_AUCTION_INFO_2: AuctionInfo = { - auction: MOCK_AUCTION_2, - id: MOCK_AUCTION_ID_2, -}; - -const MOCK_AUCTION_3 = new DutchAuction({ - description: { - startHeight: 1n, - endHeight: 10n, - }, - state: { - seq: 0n, - }, -}); -const MOCK_AUCTION_ID_3 = new AuctionId({ inner: new Uint8Array([3]) }); -const MOCK_AUCTION_INFO_3: AuctionInfo = { - auction: MOCK_AUCTION_3, - id: MOCK_AUCTION_ID_3, -}; - -const MOCK_AUCTION_4 = new DutchAuction({ - description: { - startHeight: 21n, - endHeight: 30n, - }, - state: { - seq: 0n, - }, -}); -const MOCK_AUCTION_ID_4 = new AuctionId({ inner: new Uint8Array([4]) }); -const MOCK_AUCTION_INFO_4: AuctionInfo = { - auction: MOCK_AUCTION_4, - id: MOCK_AUCTION_ID_4, -}; - -const MOCK_FULL_SYNC_HEIGHT = 15n; - -const AUCTION_INFOS: AuctionInfo[] = [ - MOCK_AUCTION_INFO_1, - MOCK_AUCTION_INFO_2, - MOCK_AUCTION_INFO_3, - MOCK_AUCTION_INFO_4, -]; - -describe('getFilteredAuctionInfos()', () => { - describe('when the `filter` is `all`', () => { - it('returns the `auctionInfos` array as-is', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'all', MOCK_FULL_SYNC_HEIGHT)).toBe( - AUCTION_INFOS, - ); - }); - }); - - describe('when the `filter` is `active`', () => { - it('includes active auctions', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'active', MOCK_FULL_SYNC_HEIGHT)).toContain( - MOCK_AUCTION_INFO_1, - ); - }); - - it('filters out auctions with a nonzero `seq`', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'active', MOCK_FULL_SYNC_HEIGHT)).not.toContain( - MOCK_AUCTION_INFO_2, - ); - }); - - it('filters out auctions that end before `fullSyncHeight`', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'active', MOCK_FULL_SYNC_HEIGHT)).not.toContain( - MOCK_AUCTION_INFO_3, - ); - }); - - it('filters out auctions that start after `fullSyncHeight`', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'active', MOCK_FULL_SYNC_HEIGHT)).not.toContain( - MOCK_AUCTION_INFO_4, - ); - }); - - it('filters out everything if `fullSyncHeight` is undefined', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'active', undefined)).toEqual([]); - }); - }); - - describe('when the `filter` is `upcoming`', () => { - it('filters out active auctions', () => { - expect( - getFilteredAuctionInfos(AUCTION_INFOS, 'upcoming', MOCK_FULL_SYNC_HEIGHT), - ).not.toContain(MOCK_AUCTION_INFO_1); - }); - - it('filters out auctions with a nonzero `seq`', () => { - expect( - getFilteredAuctionInfos(AUCTION_INFOS, 'upcoming', MOCK_FULL_SYNC_HEIGHT), - ).not.toContain(MOCK_AUCTION_INFO_2); - }); - - it('filters out auctions that end before `fullSyncHeight`', () => { - expect( - getFilteredAuctionInfos(AUCTION_INFOS, 'upcoming', MOCK_FULL_SYNC_HEIGHT), - ).not.toContain(MOCK_AUCTION_INFO_3); - }); - - it('includes auctions that start after `fullSyncHeight`', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'upcoming', MOCK_FULL_SYNC_HEIGHT)).toContain( - MOCK_AUCTION_INFO_4, - ); - }); - - it('filters out everything if `fullSyncHeight` is undefined', () => { - expect(getFilteredAuctionInfos(AUCTION_INFOS, 'upcoming', undefined)).toEqual([]); - }); - }); -}); diff --git a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts deleted file mode 100644 index 49df7f57..00000000 --- a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AuctionInfo } from '../../../fetchers/auction-infos'; -import { Filter } from '../../../state/swap/dutch-auction'; - -type FilterMatchableAuctionInfo = AuctionInfo & { - auction: { - description: { - startHeight: bigint; - endHeight: bigint; - }; - state: { - seq: bigint; - }; - }; -}; - -const haveEnoughDataToDetermineIfAuctionMatchesFilter = ( - auctionInfo: AuctionInfo, -): auctionInfo is FilterMatchableAuctionInfo => { - return !!auctionInfo.auction.description && !!auctionInfo.auction.state; -}; - -const auctionIsActive = (auctionInfo: FilterMatchableAuctionInfo, fullSyncHeight: bigint) => - auctionInfo.auction.state.seq === 0n && - fullSyncHeight >= auctionInfo.auction.description.startHeight && - fullSyncHeight <= auctionInfo.auction.description.endHeight; - -const auctionIsUpcoming = (auctionInfo: FilterMatchableAuctionInfo, fullSyncHeight: bigint) => - auctionInfo.auction.state.seq === 0n && - fullSyncHeight < auctionInfo.auction.description.startHeight; - -export const getFilteredAuctionInfos = ( - auctionInfos: AuctionInfo[], - filter: Filter, - fullSyncHeight?: bigint, -): AuctionInfo[] => { - if (filter === 'all') return auctionInfos; - - return auctionInfos.filter(auctionInfo => { - if (!fullSyncHeight) return false; - if (!haveEnoughDataToDetermineIfAuctionMatchesFilter(auctionInfo)) return false; - if (filter === 'active') return auctionIsActive(auctionInfo, fullSyncHeight); - return auctionIsUpcoming(auctionInfo, fullSyncHeight); - }); -}; diff --git a/apps/minifront/src/components/swap/auction-list/helpers.ts b/apps/minifront/src/components/swap/auction-list/helpers.ts deleted file mode 100644 index d26f1955..00000000 --- a/apps/minifront/src/components/swap/auction-list/helpers.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AuctionInfo } from '../../../fetchers/auction-infos'; -import { Filter } from '../../../state/swap/dutch-auction'; - -const byStartHeight = - (direction: 'ascending' | 'descending') => (a: AuctionInfo, b: AuctionInfo) => { - if (!a.auction.description?.startHeight || !b.auction.description?.startHeight) return 0; - if (direction === 'ascending') { - return Number(a.auction.description.startHeight - b.auction.description.startHeight); - } - return Number(b.auction.description.startHeight - a.auction.description.startHeight); - }; - -export const SORT_FUNCTIONS: Record number> = { - all: byStartHeight('ascending'), - active: byStartHeight('descending'), - upcoming: byStartHeight('ascending'), -}; diff --git a/apps/minifront/src/components/swap/auction-list/index.tsx b/apps/minifront/src/components/swap/auction-list/index.tsx deleted file mode 100644 index 4e1b8af4..00000000 --- a/apps/minifront/src/components/swap/auction-list/index.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { AllSlices } from '../../../state'; -import { DutchAuctionComponent } from '@penumbra-zone/ui/components/ui/dutch-auction-component'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; -import { AuctionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb'; -import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header'; -import { QueryLatestStateButton } from './query-latest-state-button'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { bech32mAuctionId } from '@penumbra-zone/bech32m/pauctid'; -import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picker'; -import { useMemo } from 'react'; -import { getFilteredAuctionInfos } from './get-filtered-auction-infos'; -import { LayoutGroup, motion } from 'framer-motion'; -import { SORT_FUNCTIONS } from './helpers'; -import { useAuctionInfos } from '../../../state/swap/dutch-auction'; -import { useStatus } from '../../../state/status'; - -const auctionListSelector = (state: AllSlices) => ({ - endAuction: state.swap.dutchAuction.endAuction, - withdraw: state.swap.dutchAuction.withdraw, - filter: state.swap.dutchAuction.filter, - setFilter: state.swap.dutchAuction.setFilter, -}); - -const getButtonProps = ( - auctionId: AuctionId, - endAuction: (auctionId: AuctionId) => Promise, - withdraw: (auctionId: AuctionId, seqNum: bigint) => Promise, - seqNum?: bigint, -): - | { buttonType: 'end' | 'withdraw'; onClickButton: VoidFunction } - | { buttonType: undefined; onClickButton: undefined } => { - if (seqNum === 0n) return { buttonType: 'end', onClickButton: () => void endAuction(auctionId) }; - - if (seqNum === 1n) - return { buttonType: 'withdraw', onClickButton: () => void withdraw(auctionId, seqNum) }; - - return { buttonType: undefined, onClickButton: undefined }; -}; - -export const AuctionList = () => { - const auctionInfos = useAuctionInfos(); - const { endAuction, withdraw, filter, setFilter } = useStoreShallow(auctionListSelector); - const { data: status } = useStatus(); - - const filteredAuctionInfos = useMemo( - () => - [...getFilteredAuctionInfos(auctionInfos.data ?? [], filter, status?.fullSyncHeight)].sort( - SORT_FUNCTIONS[filter], - ), - [auctionInfos, filter, status?.fullSyncHeight], - ); - - return ( - -
- My Auctions - - - {!!auctionInfos.data?.length && } - - - -
- -
- {!filteredAuctionInfos.length && - filter === 'all' && - "You don't currently have any auctions."} - - {!filteredAuctionInfos.length && - filter !== 'all' && - `You don't currently have any ${filter} auctions.`} - - - {filteredAuctionInfos.map(auctionInfo => ( -
- -
- ))} -
-
-
- ); -}; diff --git a/apps/minifront/src/components/swap/auction-list/query-latest-state-button.tsx b/apps/minifront/src/components/swap/auction-list/query-latest-state-button.tsx deleted file mode 100644 index ae3289e3..00000000 --- a/apps/minifront/src/components/swap/auction-list/query-latest-state-button.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@penumbra-zone/ui/components/ui/tooltip'; -import { ReloadIcon } from '@radix-ui/react-icons'; -import { useRevalidateAuctionInfos } from '../../../state/swap/dutch-auction'; - -export const QueryLatestStateButton = () => { - const revalidate = useRevalidateAuctionInfos(); - - return ( - - - revalidate({ queryLatestState: true })} - aria-label='Get the current auction reserves (makes a request to a fullnode)' - > -
- -
-
- - Get the current auction reserves -
- (makes a request to a fullnode) -
-
-
- ); -}; diff --git a/apps/minifront/src/components/swap/duration-slider.tsx b/apps/minifront/src/components/swap/duration-slider.tsx deleted file mode 100644 index 4c6ef5b2..00000000 --- a/apps/minifront/src/components/swap/duration-slider.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Slider } from '@penumbra-zone/ui/components/ui/slider'; -import { DURATION_OPTIONS, GDA_RECIPES } from '../../state/swap/constants'; -import { useStoreShallow } from '../../utils/use-store-shallow'; -import { AllSlices } from '../../state'; - -const durationSliderSelector = (state: AllSlices) => ({ - duration: state.swap.duration, - setDuration: state.swap.setDuration, -}); - -export const DurationSlider = () => { - const { duration, setDuration } = useStoreShallow(durationSliderSelector); - - const handleChange = (newValue: number[]) => { - const value = newValue[0]!; // We don't use multiple values in the slider - const option = DURATION_OPTIONS[value]!; - - setDuration(option); - }; - - return ( -
-
- - Instant -
- Price -
- - - - - Averaged -
- Price -
-
- - {duration === 'instant' && ( -
- Now - single trade at market price -
- )} - - {duration !== 'instant' && ( -
- ~ {duration}{' '} - - distributed across {GDA_RECIPES[duration].numberOfSubAuctions.toString()} auctions - -
- )} -
- ); -}; diff --git a/apps/minifront/src/components/swap/helpers.ts b/apps/minifront/src/components/swap/helpers.ts deleted file mode 100644 index 8e7c45d8..00000000 --- a/apps/minifront/src/components/swap/helpers.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { assetPatterns } from '@penumbra-zone/types/assets'; -import { getBalances } from '../../fetchers/balances'; -import { - getAmount, - getDisplayDenomExponentFromValueView, - getMetadata, -} from '@penumbra-zone/getters/value-view'; -import { fromBaseUnitAmount } from '@penumbra-zone/types/amount'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getDisplay } from '@penumbra-zone/getters/metadata'; - -const byBalanceDescending = (a: BalancesResponse, b: BalancesResponse) => { - const aExponent = getDisplayDenomExponentFromValueView(a.balanceView); - const bExponent = getDisplayDenomExponentFromValueView(b.balanceView); - const aAmount = fromBaseUnitAmount(getAmount(a.balanceView), aExponent); - const bAmount = fromBaseUnitAmount(getAmount(b.balanceView), bExponent); - - return bAmount.comparedTo(aAmount); -}; - -const nonSwappableAssetPatterns = [ - assetPatterns.lpNft, - assetPatterns.proposalNft, - assetPatterns.votingReceipt, - assetPatterns.auctionNft, - assetPatterns.lpNft, - - // In theory, these asset types are swappable, but we have removed them for now to get a better UX - assetPatterns.delegationToken, - assetPatterns.unbondingToken, -]; - -export const isSwappable = (metadata: Metadata) => - nonSwappableAssetPatterns.every(pattern => !pattern.matches(getDisplay(metadata))); - -const isKnown = (balancesResponse: BalancesResponse) => - balancesResponse.balanceView?.valueView.case === 'knownAssetId'; - -export const getSwappableBalancesResponses = async (): Promise => { - const balancesResponses = await getBalances(); - - return balancesResponses - .filter(isKnown) - .filter(balance => isSwappable(getMetadata(balance.balanceView))) - .sort(byBalanceDescending); -}; diff --git a/apps/minifront/src/components/swap/layout.tsx b/apps/minifront/src/components/swap/layout.tsx deleted file mode 100644 index 4ca72eb5..00000000 --- a/apps/minifront/src/components/swap/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { RestrictMaxWidth } from '../shared/restrict-max-width'; -import { SwapForm } from './swap-form'; -import { UnclaimedSwaps } from './unclaimed-swaps'; -import { AuctionList } from './auction-list'; -import { SwapInfoCard } from './swap-info-card'; -import { LayoutGroup } from 'framer-motion'; - -export const SwapLayout = () => { - return ( - - -
-
- - - - - -
- -
- -
-
-
-
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/estimate-button.tsx b/apps/minifront/src/components/swap/swap-form/estimate-button.tsx deleted file mode 100644 index 602852bc..00000000 --- a/apps/minifront/src/components/swap/swap-form/estimate-button.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { buttonVariants } from '@penumbra-zone/ui/components/ui/button'; -import { - Tooltip, - TooltipProvider, - TooltipTrigger, - TooltipContent, -} from '@penumbra-zone/ui/components/ui/tooltip'; -import { cn } from '@penumbra-zone/ui/lib/utils'; - -export const EstimateButton = ({ - disabled, - onClick, -}: { - disabled: boolean; - onClick: () => void; -}) => { - return ( - - - { - e.preventDefault(); - onClick(); - }} - disabled={disabled} - > - Estimate - - -

- Privacy note: This makes a request to your config's gRPC node to simulate a swap of - these assets. That means you are possibly revealing your intent to this node. -

-
-
-
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/index.tsx b/apps/minifront/src/components/swap/swap-form/index.tsx deleted file mode 100644 index 4db93f21..00000000 --- a/apps/minifront/src/components/swap/swap-form/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { AllSlices } from '../../../state'; -import { TokenSwapInput } from './token-swap-input'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; -import { DurationSlider } from '../duration-slider'; -import { InputBlock } from '../../shared/input-block'; -import { Output } from './output'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { SimulateSwap } from './simulate-swap'; -import { LayoutGroup } from 'framer-motion'; -import { useId } from 'react'; - -const swapFormSelector = (state: AllSlices) => ({ - onSubmit: - state.swap.duration === 'instant' - ? state.swap.instantSwap.initiateSwapTx - : state.swap.dutchAuction.onSubmit, - submitButtonLabel: state.swap.duration === 'instant' ? 'Swap' : 'Start auctions', - submitButtonDisabled: state.swap.dutchAuction.txInProgress || !state.swap.amount, - duration: state.swap.duration, -}); - -export const SwapForm = () => { - const { onSubmit, submitButtonLabel, duration, submitButtonDisabled } = - useStoreShallow(swapFormSelector); - - const sharedLayoutId = useId(); - - return ( - - -
{ - e.preventDefault(); - void onSubmit(); - }} - > - - - - - - - {duration === 'instant' ? ( - - ) : ( - - )} - - - -
-
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx b/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx deleted file mode 100644 index ec42fba7..00000000 --- a/apps/minifront/src/components/swap/swap-form/output/estimated-output-explanation.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { AllSlices } from '../../../../state'; -import { useStoreShallow } from '../../../../utils/use-store-shallow'; -import { formatAmount } from '@penumbra-zone/types/amount'; -import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { getSymbolFromValueView } from '@penumbra-zone/getters/value-view'; - -const estimatedOutputExplanationSelector = (state: AllSlices) => ({ - estimatedOutput: state.swap.dutchAuction.estimatedOutput, - amount: state.swap.amount, - assetIn: state.swap.assetIn, - assetOut: state.swap.assetOut, -}); - -export const EstimatedOutputExplanation = () => { - const { amount, assetIn, estimatedOutput, assetOut } = useStoreShallow( - estimatedOutputExplanationSelector, - ); - - if (!estimatedOutput) return null; - const formattedAmount = formatAmount({ - amount: estimatedOutput, - exponent: getDisplayDenomExponent.optional()(assetOut), - }); - const asssetInSymbol = getSymbolFromValueView.optional()(assetIn?.balanceView); - - return ( -
- Based on the current estimated market price of {formattedAmount} {assetOut?.symbol} for{' '} - {amount} {asssetInSymbol}. -
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/output/index.tsx b/apps/minifront/src/components/swap/swap-form/output/index.tsx deleted file mode 100644 index b9769c4d..00000000 --- a/apps/minifront/src/components/swap/swap-form/output/index.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Box } from '@penumbra-zone/ui/components/ui/box'; -import { AllSlices } from '../../../../state'; -import { useStoreShallow } from '../../../../utils/use-store-shallow'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { EstimateButton } from '../estimate-button'; -import { EstimatedOutputExplanation } from './estimated-output-explanation'; -import { motion } from 'framer-motion'; - -const outputSelector = (state: AllSlices) => ({ - assetOut: state.swap.assetOut, - minOutput: state.swap.dutchAuction.minOutput, - setMinOutput: state.swap.dutchAuction.setMinOutput, - maxOutput: state.swap.dutchAuction.maxOutput, - setMaxOutput: state.swap.dutchAuction.setMaxOutput, - estimate: state.swap.dutchAuction.estimate, - estimateButtonDisabled: - state.swap.txInProgress || !state.swap.amount || state.swap.dutchAuction.estimateLoading, -}); - -export const Output = ({ layoutId }: { layoutId: string }) => { - const { - assetOut, - minOutput, - setMinOutput, - maxOutput, - setMaxOutput, - estimate, - estimateButtonDisabled, - } = useStoreShallow(outputSelector); - - return ( - void estimate()} /> - } - > - - -
- Maximum: - setMaxOutput(e.target.value)} - type='number' - inputMode='decimal' - step='any' - className='text-right' - /> - - {assetOut?.symbol && ( - {assetOut.symbol} - )} -
- -
- Minimum: - - setMinOutput(e.target.value)} - type='number' - inputMode='decimal' - step='any' - className='text-right' - /> - - {assetOut?.symbol && ( - {assetOut.symbol} - )} -
-
- - -
-
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx deleted file mode 100644 index d4041902..00000000 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/index.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { PriceImpact } from './price-impact'; -import { Trace } from './trace'; -import { motion } from 'framer-motion'; -import { SimulateSwapResult as TSimulateSwapResult } from '../../../../state/swap'; -import { joinLoHiAmount } from '@penumbra-zone/types/amount'; -import { getAmount } from '@penumbra-zone/getters/value-view'; - -const HIDE = { clipPath: 'polygon(0 0, 100% 0, 100% 0, 0 0)' }; -const SHOW = { clipPath: 'polygon(0 0, 100% 0, 100% 100%, 0 100%)' }; - -export const SimulateSwapResult = ({ result }: { result: TSimulateSwapResult }) => { - const { unfilled, output, priceImpact, traces, metadataByAssetId } = result; - - const hasUnfilled = joinLoHiAmount(getAmount(unfilled)) > 0n; - - return ( - -
-
- - Price impact -
-
- - Filled amount -
- {hasUnfilled && ( -
- - Unfilled amount -
- )} -
- - {!!traces?.length && ( - <> -
-
- {traces.map((trace, index) => ( - - ))} -
-
- - )} -
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/price-impact.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/price-impact.tsx deleted file mode 100644 index a234ccc9..00000000 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/price-impact.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { formatNumber } from '@penumbra-zone/types/amount'; -import { cn } from '@penumbra-zone/ui/lib/utils'; - -// The price hit the user takes as a consequence of moving the market with the size of their trade -export const PriceImpact = ({ amount = 0 }: { amount?: number }) => { - // e.g .041234245245 becomes 4.123 - const percent = formatNumber(amount * 100, { precision: 3 }); - - return ( -
- {percent}% -
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/index.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/index.tsx deleted file mode 100644 index 784464e4..00000000 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - Metadata, - Value, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { SwapExecution_Trace } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { bech32mAssetId } from '@penumbra-zone/bech32m/passet'; -import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; -import { Fragment } from 'react'; -import { Price } from './price'; -import { Separator } from '@penumbra-zone/ui/components/ui/separator'; - -const getValueView = (metadataByAssetId: Record, { amount, assetId }: Value) => - new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount, - metadata: assetId ? metadataByAssetId[bech32mAssetId(assetId)] : undefined, - }, - }, - }); - -export const Trace = ({ - trace, - metadataByAssetId, -}: { - trace: SwapExecution_Trace; - metadataByAssetId: Record; -}) => { - return ( -
-
- {trace.value.map((value, index) => ( - -
- -
- - {index < trace.value.length - 1 && } -
- ))} -
- - -
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/price.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/price.tsx deleted file mode 100644 index f8dbe90c..00000000 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap-result/trace/price.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { SwapExecution_Trace } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { bech32mAssetId } from '@penumbra-zone/bech32m/passet'; -import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { formatAmount, removeTrailingZeros } from '@penumbra-zone/types/amount'; -import { BigNumber } from 'bignumber.js'; - -export const Price = ({ - trace, - metadataByAssetId, -}: { - trace: SwapExecution_Trace; - metadataByAssetId: Record; -}) => { - const inputValue = trace.value[0]; - const outputValue = trace.value[trace.value.length - 1]; - let price: string | undefined; - - if (inputValue?.amount && outputValue?.amount && inputValue.assetId && outputValue.assetId) { - const firstValueMetadata = metadataByAssetId[bech32mAssetId(inputValue.assetId)]; - const lastValueMetadata = metadataByAssetId[bech32mAssetId(outputValue.assetId)]; - - if (firstValueMetadata?.symbol && lastValueMetadata?.symbol) { - const inputDisplayDenomExponent = getDisplayDenomExponent.optional()(firstValueMetadata) ?? 0; - const outputDisplayDenomExponent = getDisplayDenomExponent.optional()(lastValueMetadata) ?? 0; - const formattedInputAmount = formatAmount({ - amount: inputValue.amount, - exponent: inputDisplayDenomExponent, - }); - const formattedOutputAmount = formatAmount({ - amount: outputValue.amount, - exponent: outputDisplayDenomExponent, - }); - - const outputToInputRatio = new BigNumber(formattedOutputAmount) - .dividedBy(formattedInputAmount) - .toFormat(outputDisplayDenomExponent); - - const outputToInputFormatted = removeTrailingZeros(outputToInputRatio); - - price = `1 ${firstValueMetadata.symbol} = ${outputToInputFormatted} ${lastValueMetadata.symbol}`; - } - } - - if (!price) return null; - return {price}; -}; diff --git a/apps/minifront/src/components/swap/swap-form/simulate-swap.tsx b/apps/minifront/src/components/swap/swap-form/simulate-swap.tsx deleted file mode 100644 index e37f476f..00000000 --- a/apps/minifront/src/components/swap/swap-form/simulate-swap.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Box } from '@penumbra-zone/ui/components/ui/box'; -import { SimulateSwapResult } from './simulate-swap-result'; -import { AllSlices } from '../../../state'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; -import { EstimateButton } from './estimate-button'; - -const simulateSwapSelector = (state: AllSlices) => ({ - simulateSwap: state.swap.instantSwap.simulateSwap, - disabled: - state.swap.txInProgress || !state.swap.amount || state.swap.instantSwap.simulateSwapLoading, - result: state.swap.instantSwap.simulateSwapResult, -}); - -export const SimulateSwap = ({ layoutId }: { layoutId: string }) => { - const { simulateSwap, disabled, result } = useStoreShallow(simulateSwapSelector); - - return ( - void simulateSwap()} />} - layoutId={layoutId} - > - {result && } - - ); -}; diff --git a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx b/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx deleted file mode 100644 index 96ad94ad..00000000 --- a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { BalanceValueView } from '@penumbra-zone/ui/components/ui/balance-value-view'; -import { Box } from '@penumbra-zone/ui/components/ui/box'; -import { CandlestickPlot } from '@penumbra-zone/ui/components/ui/candlestick-plot'; -import { Input } from '@penumbra-zone/ui/components/ui/input'; -import { joinLoHiAmount } from '@penumbra-zone/types/amount'; -import { - getAmount, - getBalanceView, - getMetadataFromBalancesResponse, -} from '@penumbra-zone/getters/balances-response'; -import { ArrowRight } from 'lucide-react'; -import { useEffect } from 'react'; -import { getBlockDate } from '../../../fetchers/block-date'; -import { AllSlices } from '../../../state'; -import { amountMoreThanBalance } from '../../../state/send'; -import { useStoreShallow } from '../../../utils/use-store-shallow'; -import { getFormattedAmtFromValueView } from '@penumbra-zone/types/value-view'; -import { getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { AssetSelector } from '../../shared/asset-selector'; -import BalanceSelector from '../../shared/balance-selector'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { useStatus } from '../../../state/status'; - -const isValidAmount = (amount: string, assetIn?: BalancesResponse) => - Number(amount) >= 0 && (!assetIn || !amountMoreThanBalance(assetIn, amount)); - -const getKnownZeroValueView = (metadata?: Metadata) => { - return new ValueView({ - valueView: { - case: 'knownAssetId', - value: { amount: new Amount({ lo: 0n }), metadata }, - }, - }); -}; - -const assetOutBalanceSelector = ({ swap: { balancesResponses, assetIn, assetOut } }: AllSlices) => { - if (!assetIn || !assetOut) return getKnownZeroValueView(); - - const match = balancesResponses.find(balance => { - const balanceViewMetadata = getMetadataFromBalancesResponse(balance); - - return ( - balance.accountAddress?.equals(assetIn.accountAddress) && assetOut.equals(balanceViewMetadata) - ); - }); - const matchedBalance = getBalanceView.optional()(match); - return matchedBalance ?? getKnownZeroValueView(assetOut); -}; - -const tokenSwapInputSelector = (state: AllSlices) => ({ - swappableAssets: state.swap.swappableAssets, - assetIn: state.swap.assetIn, - setAssetIn: state.swap.setAssetIn, - assetOut: state.swap.assetOut, - setAssetOut: state.swap.setAssetOut, - amount: state.swap.amount, - setAmount: state.swap.setAmount, - balancesResponses: state.swap.balancesResponses, - priceHistory: state.swap.priceHistory, - assetOutBalance: assetOutBalanceSelector(state), -}); - -/** - * Exposes a UI with three interactive elements: an asset selector for the user - * to choose which asset to swap _from_, an asset selector for the user to - * choose which asset to swap _to_, and a text field for the user to enter an - * amount. - */ -export const TokenSwapInput = () => { - const status = useStatus(); - const latestKnownBlockHeight = status.data?.latestKnownBlockHeight ?? 0n; - const { - swappableAssets, - amount, - setAmount, - assetIn, - setAssetIn, - assetOut, - setAssetOut, - balancesResponses, - priceHistory, - assetOutBalance, - } = useStoreShallow(tokenSwapInputSelector); - - useEffect(() => { - if (!assetIn || !assetOut) return; - else return priceHistory.load(); - }, [assetIn, assetOut]); - - useEffect(() => { - if (!priceHistory.candles.length) return; - else if (latestKnownBlockHeight % 10n) return; - else return priceHistory.load(); - }, [priceHistory, latestKnownBlockHeight]); - - const maxAmount = getAmount.optional()(assetIn); - const maxAmountAsString = maxAmount ? joinLoHiAmount(maxAmount).toString() : undefined; - - const setInputToBalanceMax = () => { - if (assetIn?.balanceView) { - const formattedAmt = getFormattedAmtFromValueView(assetIn.balanceView); - setAmount(formattedAmt); - } - }; - - return ( - -
- { - if (!isValidAmount(e.target.value, assetIn)) return; - setAmount(e.target.value); - }} - /> - -
- {assetIn && ( -
- - Account #{getAddressIndex(assetIn.accountAddress).account} - -
- )} - -
- - {assetIn?.balanceView && ( - - )} -
- -
- -
- -
- - {assetOut && } -
-
- {priceHistory.startMetadata && priceHistory.endMetadata && priceHistory.candles.length ? ( - - ) : null} -
-
- ); -}; diff --git a/apps/minifront/src/components/swap/swap-info-card.tsx b/apps/minifront/src/components/swap/swap-info-card.tsx deleted file mode 100644 index 641360c5..00000000 --- a/apps/minifront/src/components/swap/swap-info-card.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { AllSlices } from '../../state'; -import { useStoreShallow } from '../../utils/use-store-shallow'; -import { EduPanel } from '../shared/edu-panels/content'; -import { EduInfoCard } from '../shared/edu-panels/edu-info-card'; - -const swapInfoCardSelector = (state: AllSlices) => { - if (state.swap.duration === 'instant') { - return { - src: './swap-icon.svg', - label: 'Shielded Swap', - content: EduPanel.SWAP, - }; - } - - return { - src: './auction-gradient.svg', - label: 'Dutch Auction', - content: EduPanel.SWAP_AUCTION, - }; -}; - -/** - * Renders an `EduInfoCard` for either swaps or Dutch auctions, depending on the - * value of the duration slider. - */ -export const SwapInfoCard = () => { - const props = useStoreShallow(swapInfoCardSelector); - - return ; -}; diff --git a/apps/minifront/src/components/swap/swap-loader.ts b/apps/minifront/src/components/swap/swap-loader.ts deleted file mode 100644 index 9adc5fe3..00000000 --- a/apps/minifront/src/components/swap/swap-loader.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { LoaderFunction } from 'react-router-dom'; -import { useStore } from '../../state'; -import { abortLoader } from '../../abort-loader'; -import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getSwappableBalancesResponses, isSwappable } from './helpers'; -import { getAllAssets } from '../../fetchers/assets'; - -export interface UnclaimedSwapsWithMetadata { - swap: SwapRecord; - asset1: Metadata; - asset2: Metadata; -} - -export type SwapLoaderResponse = UnclaimedSwapsWithMetadata[]; - -const getAndSetDefaultAssetBalances = async (swappableAssets: Metadata[]) => { - const balancesResponses = await getSwappableBalancesResponses(); - - // set initial denom in if there is an available balance - if (balancesResponses[0]) { - useStore.getState().swap.setAssetIn(balancesResponses[0]); - useStore.getState().swap.setAssetOut(swappableAssets[0]!); - } - - return balancesResponses; -}; - -export const SwapLoader: LoaderFunction = async (): Promise => { - await abortLoader(); - const assets = await getAllAssets(); - const swappableAssets = assets.filter(isSwappable); - - const balancesResponses = await getAndSetDefaultAssetBalances(swappableAssets); - useStore.getState().swap.setBalancesResponses(balancesResponses); - useStore.getState().swap.setSwappableAssets(swappableAssets); - - return null; -}; diff --git a/apps/minifront/src/components/swap/unclaimed-swaps.tsx b/apps/minifront/src/components/swap/unclaimed-swaps.tsx deleted file mode 100644 index 573c5e3b..00000000 --- a/apps/minifront/src/components/swap/unclaimed-swaps.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon'; -import { AllSlices } from '../../state'; -import { useUnclaimedSwaps } from '../../state/unclaimed-swaps'; -import { getSwapRecordCommitment } from '@penumbra-zone/getters/swap-record'; -import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; -import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header'; -import { useStoreShallow } from '../../utils/use-store-shallow'; - -const unclaimedSwapsSelector = (state: AllSlices) => ({ - claimSwap: state.unclaimedSwaps.claimSwap, - isInProgress: state.unclaimedSwaps.isInProgress, -}); - -export const UnclaimedSwaps = () => { - const unclaimedSwaps = useUnclaimedSwaps(); - const { claimSwap, isInProgress } = useStoreShallow(unclaimedSwapsSelector); - - return !unclaimedSwaps.data?.length ? ( -
- ) : ( - - Unclaimed Swaps - {unclaimedSwaps.data.map(({ swap, asset1, asset2 }) => { - const id = uint8ArrayToBase64(getSwapRecordCommitment(swap).inner); - - return ( -
-
- -

{asset1.symbol || 'Unknown asset'}

- - -

{asset2.symbol || 'Unknown asset'}

-
- -
Block Height: {Number(swap.outputData?.height)}
- - -
- ); - })} -
- ); -}; diff --git a/apps/minifront/src/components/tx-details/hooks.ts b/apps/minifront/src/components/tx-details/hooks.ts deleted file mode 100644 index 67321b61..00000000 --- a/apps/minifront/src/components/tx-details/hooks.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { asReceiverTransactionView } from '@penumbra-zone/perspective/translators/transaction-view'; -import { viewClient } from '../../clients'; -import { TransactionView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb'; -import { TransactionInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; - -const fetchReceiverView = async (txInfo: TransactionInfo): Promise => { - return await asReceiverTransactionView(txInfo.view, { - isControlledAddress: async address => - viewClient.indexByAddress({ address }).then(({ addressIndex }) => Boolean(addressIndex)), - }); -}; - -export default fetchReceiverView; diff --git a/apps/minifront/src/components/tx-details/index.tsx b/apps/minifront/src/components/tx-details/index.tsx deleted file mode 100644 index 29a64bbc..00000000 --- a/apps/minifront/src/components/tx-details/index.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { FadeTransition } from '@penumbra-zone/ui/components/ui/fade-transition'; -import { TxViewer } from './tx-viewer'; -import { EduInfoCard } from '../shared/edu-panels/edu-info-card'; -import { EduPanel } from '../shared/edu-panels/content'; -import { LoaderFunction, useLoaderData, useRouteError } from 'react-router-dom'; -import { getTxInfoByHash } from '../../fetchers/tx-info-by-hash'; -import { TransactionInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { abortLoader } from '../../abort-loader'; -import { RestrictMaxWidth } from '../shared/restrict-max-width'; - -export interface TxDetailsLoaderResult { - hash: string; - txInfo: TransactionInfo; -} - -export const TxDetailsLoader: LoaderFunction = async ({ - params, -}): Promise => { - await abortLoader(); - const hash = params['hash']!; - const txInfo = await getTxInfoByHash(hash); - return { txInfo, hash }; -}; - -export const TxDetailsErrorBoundary = () => { - const error = useRouteError(); - - return
{String(error)}
; -}; - -export const TxDetails = () => { - const { txInfo, hash } = useLoaderData() as TxDetailsLoaderResult; - - return ( - - -
- - - - -
-
-
- ); -}; diff --git a/apps/minifront/src/components/tx-details/tx-viewer.tsx b/apps/minifront/src/components/tx-details/tx-viewer.tsx deleted file mode 100644 index fc471328..00000000 --- a/apps/minifront/src/components/tx-details/tx-viewer.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { JsonViewer } from '@penumbra-zone/ui/components/ui/json-viewer'; -import { TransactionViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/transaction'; -import { TxDetailsLoaderResult } from '.'; -import { TransactionInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import type { Jsonified } from '@penumbra-zone/types/jsonified'; -import { useState } from 'react'; -import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picker'; -import { asPublicTransactionView } from '@penumbra-zone/perspective/translators/transaction-view'; -import { typeRegistry } from '@penumbra-zone/protobuf'; -import { useQuery } from '@tanstack/react-query'; -import fetchReceiverView from './hooks'; -import { classifyTransaction } from '@penumbra-zone/perspective/transaction/classify'; - -export enum TxDetailsTab { - PUBLIC = 'public', - PRIVATE = 'private', - RECIEVER = 'reciever', -} - -const OPTIONS = [ - { label: 'Your View', value: TxDetailsTab.PRIVATE }, - { label: 'Public View', value: TxDetailsTab.PUBLIC }, - { label: 'Reciever View', value: TxDetailsTab.RECIEVER }, -]; - -export const TxViewer = ({ txInfo, hash }: TxDetailsLoaderResult) => { - const [option, setOption] = useState(TxDetailsTab.PRIVATE); - - // classify the transaction type - const transactionClassification = classifyTransaction(txInfo.view); - - // filter for reciever view - const showReceiverTransactionView = transactionClassification === 'send'; - const filteredOptions = showReceiverTransactionView - ? OPTIONS - : OPTIONS.filter(option => option.value !== TxDetailsTab.RECIEVER); - - // use React-Query to invoke custom hooks that call async translators. - const { data: receiverView } = useQuery( - ['receiverView', txInfo, option], - () => fetchReceiverView(txInfo), - { - enabled: option === TxDetailsTab.RECIEVER && !!txInfo, - }, - ); - - return ( -
-
Transaction View
-
{hash}
- -
- -
- {option === TxDetailsTab.PRIVATE && ( - <> - -
-
Raw JSON
- } /> -
- - )} - {option === TxDetailsTab.RECIEVER && receiverView && showReceiverTransactionView && ( - - )} - {option === TxDetailsTab.PUBLIC && ( - - )} -
- ); -}; diff --git a/apps/minifront/src/fetchers/address.ts b/apps/minifront/src/fetchers/address.ts deleted file mode 100644 index c2dab62b..00000000 --- a/apps/minifront/src/fetchers/address.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { viewClient } from '../clients'; -import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra'; - -type Index = number; -type Bech32Address = string; - -export type IndexAddrRecord = Record; - -export const getAddresses = async (accounts: (number | undefined)[]): Promise => { - const allReqs = accounts.map(getAddressByIndex); - - const responses = await Promise.all(allReqs); - return responses - .map((address, i) => { - return { - index: accounts[i] ?? 0, - address: bech32mAddress(address), - }; - }) - .reduce((acc, curr) => { - acc[curr.index] = curr.address; - return acc; - }, {}); -}; - -export const getAddressByIndex = async (account = 0): Promise
=> { - const { address } = await viewClient.addressByIndex({ addressIndex: { account } }); - if (!address) throw new Error('Address not in getAddressByIndex response'); - return address; -}; - -export const getEphemeralAddress = async (account = 0): Promise
=> { - const { address } = await viewClient.ephemeralAddress({ addressIndex: { account } }); - if (!address) throw new Error('Address not in getEphemeralAddress response'); - return address; -}; - -export const getAddrByIndex = async (index: number, ephemeral: boolean) => { - return ephemeral ? await getEphemeralAddress(index) : await getAddressByIndex(index); -}; diff --git a/apps/minifront/src/fetchers/assets.ts b/apps/minifront/src/fetchers/assets.ts deleted file mode 100644 index 7d52814f..00000000 --- a/apps/minifront/src/fetchers/assets.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Array from '@penumbra-zone/polyfills/Array.fromAsync'; -import { AssetMetadataByIdRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { viewClient } from '../clients'; -import { - AssetId, - Metadata, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getDenomMetadata } from '@penumbra-zone/getters/assets-response'; - -export const getAllAssets = async (): Promise => { - const responses = await Array.fromAsync(viewClient.assets({})); - return responses.map(getDenomMetadata); -}; - -export const getAssetMetadataById = async (assetId: AssetId): Promise => { - const req = new AssetMetadataByIdRequest({ assetId }); - const { denomMetadata } = await viewClient.assetMetadataById(req); - return denomMetadata; -}; diff --git a/apps/minifront/src/fetchers/auction-infos.ts b/apps/minifront/src/fetchers/auction-infos.ts deleted file mode 100644 index 84716ece..00000000 --- a/apps/minifront/src/fetchers/auction-infos.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - AuctionId, - DutchAuction, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb'; -import { viewClient } from '../clients'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { getInputAssetId, getOutputAssetId } from '@penumbra-zone/getters/dutch-auction'; - -export interface AuctionInfo { - id: AuctionId; - auction: DutchAuction; - inputMetadata?: Metadata; - outputMetadata?: Metadata; -} - -export const getAuctionInfos = async function* ({ - queryLatestState = false, -}: { - queryLatestState?: boolean; -} = {}): AsyncGenerator { - for await (const response of viewClient.auctions({ queryLatestState, includeInactive: true })) { - if (!response.auction || !response.id) continue; - - const auction = DutchAuction.fromBinary(response.auction.value); - - const inputAssetId = getInputAssetId.optional()(auction); - const outputAssetId = getOutputAssetId.optional()(auction); - - const inputMetadataPromise = inputAssetId - ? viewClient.assetMetadataById({ assetId: inputAssetId }) - : undefined; - const outputMetadataPromise = outputAssetId - ? viewClient.assetMetadataById({ assetId: outputAssetId }) - : undefined; - - const [inputMetadata, outputMetadata] = await Promise.all([ - inputMetadataPromise, - outputMetadataPromise, - ]); - - yield { - id: response.id, - auction, - inputMetadata: inputMetadata?.denomMetadata, - outputMetadata: outputMetadata?.denomMetadata, - }; - } -}; diff --git a/apps/minifront/src/fetchers/balances/by-account.ts b/apps/minifront/src/fetchers/balances/by-account.ts deleted file mode 100644 index 3e2b8bb8..00000000 --- a/apps/minifront/src/fetchers/balances/by-account.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { getBalances } from '.'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getAddress, getAddressIndex } from '@penumbra-zone/getters/address-view'; - -export interface BalancesByAccount { - account: number; - address: Address; - balances: BalancesResponse[]; -} - -const groupByAccount = (acc: BalancesByAccount[], curr: BalancesResponse): BalancesByAccount[] => { - const index = getAddressIndex(curr.accountAddress); - const grouping = acc.find(a => a.account === index.account); - - if (grouping) { - grouping.balances.push(curr); - } else { - acc.push({ - account: index.account, - address: getAddress(curr.accountAddress), - balances: [curr], - }); - } - - return acc; -}; - -export const getBalancesByAccount = async (): Promise => { - const balances = await getBalances(); - return balances.reduce(groupByAccount, []); -}; diff --git a/apps/minifront/src/fetchers/balances/by-asset.ts b/apps/minifront/src/fetchers/balances/by-asset.ts deleted file mode 100644 index e7e7ddc0..00000000 --- a/apps/minifront/src/fetchers/balances/by-asset.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getAssetIdFromValueView } from '@penumbra-zone/getters/value-view'; -import { addAmounts } from '@penumbra-zone/types/amount'; - -const hasMatchingAssetId = (vA: ValueView, vB: ValueView) => { - return getAssetIdFromValueView(vA).equals(getAssetIdFromValueView(vB)); -}; - -// Use for doing a .reduce() on BalancesResponse[] -export const groupByAsset = (acc: ValueView[], curr: BalancesResponse): ValueView[] => { - if (!curr.balanceView?.valueView.value?.amount) throw new Error('No amount in value view'); - - const grouping = acc.find(v => hasMatchingAssetId(v, curr.balanceView!)); - - if (grouping) { - // Combine the amounts - if (!grouping.valueView.value?.amount) throw new Error('Grouping without amount'); - grouping.valueView.value.amount = addAmounts( - grouping.valueView.value.amount, - curr.balanceView.valueView.value.amount, - ); - } else { - // Add a new entry to the array - // clone so we don't mutate the original - acc.push(curr.balanceView.clone()); - } - - return acc; -}; diff --git a/apps/minifront/src/fetchers/balances/index.ts b/apps/minifront/src/fetchers/balances/index.ts deleted file mode 100644 index d4e9d56e..00000000 --- a/apps/minifront/src/fetchers/balances/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BalancesRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { viewClient } from '../../clients'; -import Array from '@penumbra-zone/polyfills/Array.fromAsync'; - -interface BalancesProps { - accountFilter?: AddressIndex; - assetIdFilter?: AssetId; -} - -export const getBalances = ({ accountFilter, assetIdFilter }: BalancesProps = {}) => { - const req = new BalancesRequest(); - if (accountFilter) req.accountFilter = accountFilter; - if (assetIdFilter) req.assetIdFilter = assetIdFilter; - - const iterable = viewClient.balances(req); - return Array.fromAsync(iterable); -}; diff --git a/apps/minifront/src/fetchers/block-date.ts b/apps/minifront/src/fetchers/block-date.ts deleted file mode 100644 index 4f13e3d5..00000000 --- a/apps/minifront/src/fetchers/block-date.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { tendermintClient } from '../clients'; - -export const getBlockDate = async ( - height: bigint, - signal?: AbortSignal, -): Promise => { - const { block } = await tendermintClient.getBlockByHeight({ height }, { signal }); - return block?.header?.time?.toDate(); -}; diff --git a/apps/minifront/src/fetchers/chain-id.ts b/apps/minifront/src/fetchers/chain-id.ts deleted file mode 100644 index 0036a888..00000000 --- a/apps/minifront/src/fetchers/chain-id.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { viewClient } from '../clients'; - -export const getChainId = async (): Promise => { - const { parameters } = await viewClient.appParameters({}); - return parameters?.chainId; -}; diff --git a/apps/minifront/src/fetchers/page-path.test.ts b/apps/minifront/src/fetchers/page-path.test.ts deleted file mode 100644 index e935b49d..00000000 --- a/apps/minifront/src/fetchers/page-path.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { matchPagePath, removeTrailingSlash } from './page-path'; -import { PagePath } from '../components/metadata/paths'; - -describe('removeTrailingSlash', () => { - test('should remove trailing slash when present', () => { - const url = '/example/'; - const result = removeTrailingSlash(url); - expect(result).toBe('/example'); - }); - - test('should return original string when trailing slash is not present', () => { - const url = '/example'; - const result = removeTrailingSlash(url); - expect(result).toBe('/example'); - }); - - test('should handle empty strings', () => { - const url = ''; - const result = removeTrailingSlash(url); - expect(result).toBe(''); - }); - - test('should handle strings with only a slash', () => { - const url = '/'; - const result = removeTrailingSlash(url); - expect(result).toBe(''); - }); - - test('should not remove slashes that are not at the end', () => { - const url = '/example/test/'; - const result = removeTrailingSlash(url); - expect(result).toBe('/example/test'); - }); -}); - -describe('matchPagePath', () => { - test('should match exact paths', () => { - expect(matchPagePath('/')).toBe(PagePath.INDEX); - expect(matchPagePath('/swap')).toBe(PagePath.SWAP); - expect(matchPagePath('/send')).toBe(PagePath.SEND); - }); - - test('should match paths with variable parts', () => { - expect(matchPagePath('/tx/123')).toBe(PagePath.TRANSACTION_DETAILS); - expect(matchPagePath('/tx/abc')).toBe(PagePath.TRANSACTION_DETAILS); - expect(matchPagePath('/tx/abc123')).toBe(PagePath.TRANSACTION_DETAILS); - }); - - test('should throw an error for unmatched paths', () => { - expect(() => matchPagePath('/unmatched')).toThrowError('No match found for path: /unmatched'); - expect(() => matchPagePath('/tx/')).toThrowError('No match found for path: /tx/'); - expect(() => matchPagePath('/tx/123/abc')).toThrowError('No match found for path: /tx/123/abc'); - }); -}); diff --git a/apps/minifront/src/fetchers/page-path.ts b/apps/minifront/src/fetchers/page-path.ts deleted file mode 100644 index d8c6b81e..00000000 --- a/apps/minifront/src/fetchers/page-path.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { useLocation } from 'react-router-dom'; -import { PagePath } from '../components/metadata/paths'; - -// Some pages have query params like: /tx/?hash=12342 -// This normalizes to return a path of /tx instead of /tx/ -export const removeTrailingSlash = (url: string): string => { - return url.endsWith('/') ? url.slice(0, -1) : url; -}; - -export const usePagePath = () => { - const location = useLocation(); - return matchPagePath(removeTrailingSlash(location.pathname)) as T; -}; - -export const matchPagePath = (str: string): PagePath => { - const pathValues = Object.values(PagePath); - - if (pathValues.includes(str as PagePath)) { - return str as PagePath; - } - - for (const pathValue of pathValues) { - if (pathValue.includes(':')) { - const regex = new RegExp('^' + pathValue.replace(/:(\w+)/g, '([^/]+)') + '$'); - const match = str.match(regex); - if (match) { - return pathValue as PagePath; - } - } - } - - throw new Error(`No match found for path: ${str}`); -}; diff --git a/apps/minifront/src/fetchers/registry.ts b/apps/minifront/src/fetchers/registry.ts deleted file mode 100644 index 96770629..00000000 --- a/apps/minifront/src/fetchers/registry.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ChainRegistryClient, Registry } from '@penumbra-labs/registry'; -import { useQuery } from '@tanstack/react-query'; -import { getChainId } from './chain-id'; -import { getAssetMetadataById } from './assets'; - -export const chainRegistryClient = new ChainRegistryClient(); - -export const useRegistry = () => { - return useQuery({ - queryKey: ['penumbraRegistry'], - queryFn: async (): Promise => { - const chainId = await getChainId(); - if (!chainId) throw new Error('No chain id in response'); - return chainRegistryClient.get(chainId); - }, - staleTime: Infinity, - }); -}; - -export const getStakingTokenMetadata = async () => { - const chainId = await getChainId(); - if (!chainId) { - throw new Error('Could not fetch chain id'); - } - - const { stakingAssetId } = chainRegistryClient.get(chainId); - const stakingAssetsMetadata = await getAssetMetadataById(stakingAssetId); - - if (!stakingAssetsMetadata) { - throw new Error('Could not fetch staking asset metadata'); - } - return stakingAssetsMetadata; -}; - -export const getIbcConnections = async () => { - const chainId = await getChainId(); - if (!chainId) throw new Error('Could not fetch chain id'); - - const registryClient = new ChainRegistryClient(); - const { ibcConnections } = registryClient.get(chainId); - return ibcConnections; -}; diff --git a/apps/minifront/src/fetchers/status.ts b/apps/minifront/src/fetchers/status.ts deleted file mode 100644 index 76b17c4a..00000000 --- a/apps/minifront/src/fetchers/status.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { viewClient } from '../clients'; - -const getInitialStatus = () => - viewClient.status({}).then(status => ({ - fullSyncHeight: status.fullSyncHeight, - latestKnownBlockHeight: status.catchingUp ? undefined : status.fullSyncHeight, - })); - -export async function* getStatusStream(): AsyncGenerator<{ - fullSyncHeight?: bigint; - latestKnownBlockHeight?: bigint; -}> { - // `statusStream` sends new data to stream only when a new block is detected. - // This can take up to 5 seconds (time of new block generated). - // Therefore, we need to do a unary request to start us off. - yield await getInitialStatus(); - - for await (const result of viewClient.statusStream({})) { - yield { - fullSyncHeight: result.fullSyncHeight, - latestKnownBlockHeight: result.latestKnownBlockHeight, - }; - } -} diff --git a/apps/minifront/src/fetchers/stream.ts b/apps/minifront/src/fetchers/stream.ts deleted file mode 100644 index 3b72ea80..00000000 --- a/apps/minifront/src/fetchers/stream.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useEffect, useState } from 'react'; - -interface StreamQueryResult { - data: T | undefined; - end: boolean; - error: unknown; -} - -interface CollectedStreamQueryResult { - data: T[]; - end: boolean; - error: unknown; -} - -type DataHandler = (prevData: U, newData: T) => U; - -// Common hook for handling streams -const useStreamCommon = ( - query: AsyncIterable, - initialData: U, - dataHandler: DataHandler, -): { data: U; end: boolean; error: unknown } => { - const [data, setData] = useState(initialData); - const [end, setEnd] = useState(false); - const [error, setError] = useState(); - - useEffect(() => { - const streamData = async () => { - try { - for await (const res of query) { - setData(prevData => dataHandler(prevData, res)); - } - setEnd(true); - } catch (e) { - setError(e); - } - }; - - void streamData(); - }, [query, dataHandler]); - - return { data, end, error }; -}; - -// Every new stream result will replace the old value -export const useStream = (query: AsyncIterable): StreamQueryResult => { - return useStreamCommon(query, undefined as T | undefined, (_, newData) => newData); -}; - -// Will take every stream result and append it to an array. Will ever grow until stream finished. -export const useCollectedStream = (query: AsyncIterable): CollectedStreamQueryResult => { - return useStreamCommon(query, [] as T[], (prevData, newData) => [...prevData, newData]); -}; diff --git a/apps/minifront/src/fetchers/transactions.ts b/apps/minifront/src/fetchers/transactions.ts deleted file mode 100644 index a5d6cde0..00000000 --- a/apps/minifront/src/fetchers/transactions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { viewClient } from '../clients'; -import Array from '@penumbra-zone/polyfills/Array.fromAsync'; -import { getTransactionClassificationLabel } from '@penumbra-zone/perspective/transaction/classify'; -import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; - -export interface TransactionSummary { - height: number; - hash: string; - description: string; -} - -export const getAllTransactions = async (): Promise => { - const responses = await Array.fromAsync(viewClient.transactionInfo({})); - return responses - .map(tx => { - return { - height: Number(tx.txInfo?.height ?? 0n), - hash: tx.txInfo?.id?.inner ? uint8ArrayToHex(tx.txInfo.id.inner) : 'unknown', - description: getTransactionClassificationLabel(tx.txInfo?.view), - }; - }) - .sort((a, b) => b.height - a.height); -}; diff --git a/apps/minifront/src/fetchers/tx-info-by-hash.ts b/apps/minifront/src/fetchers/tx-info-by-hash.ts deleted file mode 100644 index b5d0bdfd..00000000 --- a/apps/minifront/src/fetchers/tx-info-by-hash.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { viewClient } from '../clients'; -import { TransactionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/txhash/v1/txhash_pb'; -import { TransactionInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { hexToUint8Array } from '@penumbra-zone/types/hex'; - -export const getTxInfoByHash = async (hash: string): Promise => { - const res = await viewClient.transactionInfoByHash({ - id: new TransactionId({ inner: hexToUint8Array(hash) }), - }); - - const txInfo = res.txInfo; - if (!txInfo) throw new Error('Transaction info not found'); - - return txInfo; -}; diff --git a/apps/minifront/src/fetchers/unclaimed-swaps.ts b/apps/minifront/src/fetchers/unclaimed-swaps.ts deleted file mode 100644 index c8e5cb2f..00000000 --- a/apps/minifront/src/fetchers/unclaimed-swaps.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { viewClient } from '../clients'; -import Array from '@penumbra-zone/polyfills/Array.fromAsync'; -import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getUnclaimedSwaps } from '@penumbra-zone/getters/unclaimed-swaps-response'; -import { UnclaimedSwapsWithMetadata } from '../state/unclaimed-swaps'; -import { getSwapAsset1, getSwapAsset2 } from '@penumbra-zone/getters/swap-record'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; - -const fetchMetadataForSwap = async (swap: SwapRecord): Promise => { - const assetId1 = getSwapAsset1(swap); - const assetId2 = getSwapAsset2(swap); - - const [{ denomMetadata: asset1Metadata }, { denomMetadata: asset2Metadata }] = await Promise.all([ - viewClient.assetMetadataById({ assetId: assetId1 }), - viewClient.assetMetadataById({ assetId: assetId2 }), - ]); - - return { - swap, - // If no metadata, uses assetId for asset icon display - asset1: asset1Metadata - ? asset1Metadata - : new Metadata({ display: uint8ArrayToBase64(assetId1.inner) }), - asset2: asset2Metadata - ? asset2Metadata - : new Metadata({ display: uint8ArrayToBase64(assetId2.inner) }), - }; -}; - -const byHeightDescending = (a: UnclaimedSwapsWithMetadata, b: UnclaimedSwapsWithMetadata): number => - Number(b.swap.outputData?.height) - Number(a.swap.outputData?.height); - -export const fetchUnclaimedSwaps = async (): Promise => { - const responses = await Array.fromAsync(viewClient.unclaimedSwaps({})); - const unclaimedSwaps = responses.map(getUnclaimedSwaps); - const unclaimedSwapsWithMetadata = await Promise.all(unclaimedSwaps.map(fetchMetadataForSwap)); - - return unclaimedSwapsWithMetadata.sort(byHeightDescending); -}; diff --git a/apps/minifront/src/icons/box.tsx b/apps/minifront/src/icons/box.tsx deleted file mode 100644 index e4422bc1..00000000 --- a/apps/minifront/src/icons/box.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { cn } from '@penumbra-zone/ui/lib/utils'; - -export const BoxIcon = ({ - stroke = '#BDB8B8', - className, -}: { - stroke?: string; - className?: string; -}) => { - return ( - - - - - - ); -}; diff --git a/apps/minifront/src/icons/drag-handle-dots.tsx b/apps/minifront/src/icons/drag-handle-dots.tsx deleted file mode 100644 index f5c45a3b..00000000 --- a/apps/minifront/src/icons/drag-handle-dots.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { cn } from '@penumbra-zone/ui/lib/utils'; - -export const DragHandleDotsIcon = ({ - stroke = '#BDB8B8', - className, -}: { - stroke?: string; - className?: string; -}) => { - return ( - - - - - - - - - - - - ); -}; diff --git a/apps/minifront/src/icons/message-warning.tsx b/apps/minifront/src/icons/message-warning.tsx deleted file mode 100644 index a3be8117..00000000 --- a/apps/minifront/src/icons/message-warning.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const MessageWarningIcon = ({ stroke = '#BDB8B8' }: { stroke?: string }) => { - return ( - - - - - - ); -}; diff --git a/apps/minifront/src/icons/swap.tsx b/apps/minifront/src/icons/swap.tsx deleted file mode 100644 index f8564e01..00000000 --- a/apps/minifront/src/icons/swap.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { cn } from '@penumbra-zone/ui/lib/utils'; - -export const SwapIcon = ({ - stroke = '#BDB8B8', - className, -}: { - stroke?: string; - className?: string; -}) => { - return ( - - - - - - - ); -}; diff --git a/apps/minifront/src/main.tsx b/apps/minifront/src/main.tsx deleted file mode 100644 index d1a3b0f7..00000000 --- a/apps/minifront/src/main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// Importing `./state` before any components ensures that `useStore` gets -// defined before any slices get used. Otherwise, we'll get an error like -// `Cannot access 'createXSlice' before initialization` due to circular -// references. -import './state'; - -import { createRoot } from 'react-dom/client'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useState } from 'react'; -import { RouterProvider } from 'react-router-dom'; -import { rootRouter } from './components/root-router'; - -const Main = () => { - const [queryClient] = useState(() => new QueryClient()); - - return ( - - - - ); -}; - -const rootElement = document.getElementById('root') as HTMLDivElement; -createRoot(rootElement).render(
); diff --git a/apps/minifront/src/state/constants.ts b/apps/minifront/src/state/constants.ts deleted file mode 100644 index d85a2278..00000000 --- a/apps/minifront/src/state/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -const APPROX_BLOCK_DURATION_MS = 5_000n; -const MINUTE_MS = 60_000n; -export const BLOCKS_PER_MINUTE = MINUTE_MS / APPROX_BLOCK_DURATION_MS; -export const BLOCKS_PER_HOUR = BLOCKS_PER_MINUTE * 60n; diff --git a/apps/minifront/src/state/helpers.ts b/apps/minifront/src/state/helpers.ts deleted file mode 100644 index c3928c16..00000000 --- a/apps/minifront/src/state/helpers.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { - AuthorizeAndBuildRequest, - AuthorizeAndBuildResponse, - BroadcastTransactionRequest, - BroadcastTransactionResponse, - TransactionPlannerRequest, - WitnessAndBuildRequest, - WitnessAndBuildResponse, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { viewClient } from '../clients'; -import { sha256Hash } from '@penumbra-zone/crypto-web/sha256'; -import { - Transaction, - TransactionPlan, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb'; -import { TransactionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/txhash/v1/txhash_pb'; -import { PartialMessage } from '@bufbuild/protobuf'; -import { TransactionToast } from '@penumbra-zone/ui/lib/toast/transaction-toast'; -import { TransactionClassification } from '@penumbra-zone/perspective/transaction/classification'; -import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; - -/** - * Handles the common use case of planning, building, and broadcasting a - * transaction, along with the appropriate toasts. Throws if there is an - * unhandled error (i.e., any error other than the user denying authorization - * for the transaction) so that consuming code can take different actions based - * on whether the transaction succeeded or failed. - */ -export const planBuildBroadcast = async ( - transactionClassification: TransactionClassification, - req: PartialMessage, - options?: { - /** - * If set to `true`, the `ViewService#witnessAndBuild` method will be used, - * which does not prompt the user to authorize the transaction. If `false`, - * the `ViewService#authorizeAndBuild` method will be used, which _does_ - * prompt the user to authorize the transaction. (This is required in the - * case of most transactions.) Default: `false` - */ - skipAuth?: boolean; - }, -): Promise => { - const toast = new TransactionToast(transactionClassification); - toast.onStart(); - - const rpcMethod = options?.skipAuth ? viewClient.witnessAndBuild : viewClient.authorizeAndBuild; - - try { - const transactionPlan = await plan(req); - - const transaction = await build({ transactionPlan }, rpcMethod, status => - toast.onBuildStatus(status), - ); - - const txHash = await getTxHash(transaction); - toast.txHash(txHash); - - const { detectionHeight } = await broadcast({ transaction, awaitDetection: true }, status => - toast.onBroadcastStatus(status), - ); - toast.onSuccess(detectionHeight); - - return transaction; - } catch (e) { - if (userDeniedTransaction(e)) { - toast.onDenied(); - } else if (unauthenticated(e)) { - toast.onUnauthenticated(); - } else { - toast.onFailure(e); - throw e; - } - } - - return undefined; -}; - -export const plan = async ( - req: PartialMessage, -): Promise => { - const { plan } = await viewClient.transactionPlanner(req); - if (!plan) throw new Error('No plan in planner response'); - return plan; -}; - -const build = async ( - req: PartialMessage | PartialMessage, - buildFn: (typeof viewClient)['authorizeAndBuild' | 'witnessAndBuild'], - onStatusUpdate: ( - status?: (AuthorizeAndBuildResponse | WitnessAndBuildResponse)['status'], - ) => void, -) => { - for await (const { status } of buildFn(req)) { - onStatusUpdate(status); - - switch (status.case) { - case undefined: - case 'buildProgress': - break; - case 'complete': - return status.value.transaction!; - default: - console.warn(`unknown ${buildFn.name} status`, status); - } - } - throw new Error('did not build transaction'); -}; - -const broadcast = async ( - req: PartialMessage, - onStatusUpdate: (status?: BroadcastTransactionResponse['status']) => void, -): Promise<{ txHash: string; detectionHeight?: bigint }> => { - const { awaitDetection, transaction } = req; - if (!transaction) throw new Error('no transaction'); - const txId = await getTxId(transaction); - const txHash = getTxHash(txId); - onStatusUpdate(undefined); - for await (const { status } of viewClient.broadcastTransaction({ awaitDetection, transaction })) { - if (!txId.equals(status.value?.id)) throw new Error('unexpected transaction id'); - onStatusUpdate(status); - switch (status.case) { - case 'broadcastSuccess': - if (!awaitDetection) return { txHash, detectionHeight: undefined }; - break; - case 'confirmed': - return { txHash, detectionHeight: status.value.detectionHeight }; - default: - console.warn(`unknown broadcastTransaction status: ${status.case}`); - } - } - // TODO: detail broadcastSuccess status - throw new Error('did not broadcast transaction'); -}; - -const getTxHash = > | PartialMessage>( - t: T, -): T extends Required> ? string : Promise => - 'inner' in t && t.inner instanceof Uint8Array - ? (uint8ArrayToHex(t.inner) as T extends Required> - ? string - : never) - : (getTxId(t as PartialMessage).then(({ inner }) => - uint8ArrayToHex(inner), - ) as T extends Required> ? never : Promise); - -const getTxId = (tx: Transaction | PartialMessage) => - sha256Hash(tx instanceof Transaction ? tx.toBinary() : new Transaction(tx).toBinary()).then( - inner => new TransactionId({ inner }), - ); - -// We don't have ConnectError in this scope, so we only detect standard Error. -// Any ConnectError code is named at the beginning of the message value. - -export const userDeniedTransaction = (e: unknown): boolean => - e instanceof Error && e.message.startsWith('[permission_denied]'); - -export const unauthenticated = (e: unknown): boolean => - e instanceof Error && e.message.startsWith('[unauthenticated]'); diff --git a/apps/minifront/src/state/ibc-in.test.ts b/apps/minifront/src/state/ibc-in.test.ts deleted file mode 100644 index b6396320..00000000 --- a/apps/minifront/src/state/ibc-in.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { parseRevisionNumberFromChainId } from './ibc-in'; - -describe('parseRevisionNumberFromChainId', () => { - test('should extract the number at the end of a well-formatted string as a BigInt', () => { - expect(parseRevisionNumberFromChainId('grand-1')).toEqual(1n); - expect(parseRevisionNumberFromChainId('osmo-test-5')).toEqual(5n); - expect(parseRevisionNumberFromChainId('penumbra-testnet-deimos-7')).toEqual(7n); - }); - - test('should throw an error if there is no number at the end', () => { - expect(() => parseRevisionNumberFromChainId('grand')).toThrow( - 'No revision number found within chain id: grand', - ); - expect(() => parseRevisionNumberFromChainId('osmo-test-beta')).toThrow( - 'No revision number found within chain id: osmo-test-beta', - ); - }); - - test('should throw an error if the string ends with a hyphen', () => { - expect(() => parseRevisionNumberFromChainId('test-chain-')).toThrow( - 'No revision number found within chain id: test-chain-', - ); - }); - - test('should throw an error if the string does not contain any hyphens', () => { - expect(() => parseRevisionNumberFromChainId('testchain5')).toThrow( - 'No revision number found within chain id: testchain5', - ); - }); - - test('should handle cases with multiple hyphens correctly', () => { - expect(parseRevisionNumberFromChainId('multi-part-chain-id-123')).toEqual(123n); - }); -}); diff --git a/apps/minifront/src/state/ibc-in.tsx b/apps/minifront/src/state/ibc-in.tsx deleted file mode 100644 index 5721de10..00000000 --- a/apps/minifront/src/state/ibc-in.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { ChainInfo } from '../components/ibc/ibc-in/chain-dropdown'; -import { CosmosAssetBalance } from '../components/ibc/ibc-in/hooks'; -import { ChainWalletContext } from '@cosmos-kit/core'; -import { AllSlices, SliceCreator } from '.'; -import { getAddrByIndex } from '../fetchers/address'; -import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra'; -import { Toast } from '@penumbra-zone/ui/lib/toast/toast'; -import { shorten } from '@penumbra-zone/types/string'; -import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { bech32CompatAddress } from '@penumbra-zone/bech32m/penumbracompat1'; -import { calculateFee, GasPrice, SigningStargateClient } from '@cosmjs/stargate'; -import { chains } from 'chain-registry'; -import { getChainId } from '../fetchers/chain-id'; -import { augmentToAsset, fromDisplayAmount } from '../components/ibc/ibc-in/asset-utils'; -import { cosmos, ibc } from 'osmo-query'; -import { chainRegistryClient } from '../fetchers/registry'; -import { tendermintClient } from '../clients'; -import { BLOCKS_PER_HOUR } from './constants'; -import { currentTimePlusTwoDaysRounded } from './ibc-out'; -import { EncodeObject } from '@cosmjs/proto-signing'; -import { MsgTransfer } from 'osmo-query/ibc/applications/transfer/v1/tx'; - -interface PenumbraAddrs { - ephemeral: string; - normal: string; -} - -export interface IbcInSlice { - selectedChain?: ChainInfo; - setSelectedChain: (chain?: ChainInfo) => void; - coin?: CosmosAssetBalance; - setCoin: (coin?: CosmosAssetBalance) => void; - amount?: string; - setAmount: (amount?: string) => void; - penumbraAddrs?: PenumbraAddrs; - fetchPenumbraAddrs: () => Promise; - issueTx: ( - getClient: ChainWalletContext['getSigningStargateClient'], - address?: string, - ) => Promise; -} - -export const createIbcInSlice = (): SliceCreator => (set, get) => { - return { - coin: undefined, - setCoin: coin => { - set(({ ibcIn }) => { - ibcIn.coin = coin; - }); - }, - amount: undefined, - setAmount: amount => { - set(({ ibcIn }) => { - ibcIn.amount = amount; - }); - }, - selectedChain: undefined, - setSelectedChain: chain => { - set(({ ibcIn }) => { - ibcIn.selectedChain = chain; - ibcIn.amount = undefined; - ibcIn.coin = undefined; - }); - }, - penumbraAddrs: undefined, - fetchPenumbraAddrs: async () => { - const normalAddr = await getAddrByIndex(0, false); - const ephemeralAddr = await getAddrByIndex(0, true); - - const chain = get().ibcIn.selectedChain; - if (!chain) throw new Error('No chain selected'); - - set(({ ibcIn }) => { - ibcIn.penumbraAddrs = { - normal: bech32mAddress(normalAddr), - ephemeral: getCompatibleBech32(chain.chainName, ephemeralAddr), - }; - }); - }, - issueTx: async (getClient, address) => { - const toast = new Toast(); - try { - toast.loading().message('Issuing IBC transaction').render(); - - if (!address) throw new Error('Address not selected'); - const { code, transactionHash, height } = await execute(get().ibcIn, address, getClient); - - // The transaction succeeded if and only if code is 0. - if (code !== 0) { - throw new Error(`Tendermint error: ${code}`); - } - - // If we have a block explorer tx page link for this chain id, include it in toast - const chainId = get().ibcIn.selectedChain?.chainId; - const explorerTxPage = getExplorerPage(transactionHash, chainId); - if (explorerTxPage) { - toast.action( - - See details - , - ); - } - - const chainName = get().ibcIn.selectedChain?.chainName; - toast - .success() - .message(`IBC transaction succeeded! 🎉`) - .description( - `Transaction ${shorten(transactionHash, 8)} appeared on ${chainName} at height ${height}.`, - ) - .render(); - } catch (e) { - toast.error().message('Transaction error ❌').description(String(e)).render(); - } - }, - }; -}; - -const getExplorerPage = (txHash: string, chainId?: string) => { - if (!chainId) return undefined; - - // They come in the format of "https://mintscan.io/noble-testnet/txs/${txHash}" - const txPage = chains.find(({ chain_id }) => chain_id === chainId)?.explorers?.[0]?.tx_page; - if (!txPage) return undefined; - - return txPage.replace('${txHash}', txHash); -}; - -/** - * For Noble specifically we need to use a Bech32 encoding rather than Bech32m, - * because Noble currently has a middleware that decodes as Bech32. - * Noble plans to change this at some point in the future but until then we need - * to use a special encoding just for Noble specifically. - */ -const bech32Chains = ['noble', 'nobletestnet']; -const getCompatibleBech32 = (chainName: string, address: Address): string => { - return bech32Chains.includes(chainName) ? bech32CompatAddress(address) : bech32mAddress(address); -}; - -const estimateFee = async ({ - chainId, - client, - signerAddress, - message, -}: { - chainId: string; - client: SigningStargateClient; - signerAddress: string; - message: EncodeObject; -}) => { - const feeToken = chains.find(({ chain_id }) => chain_id === chainId)?.fees?.fee_tokens[0]; - const avgGasPrice = feeToken?.average_gas_price; - if (!feeToken) throw new Error(`Fee token not found in registry for ${chainId}`); - if (!avgGasPrice) throw new Error(`Average gas price not found for ${chainId}`); - - const estimatedGas = await client.simulate(signerAddress, [message], ''); - const gasLimit = Math.round(estimatedGas * 1.5); // Give some padding to the limit due to fluctuations - const gasPrice = GasPrice.fromString(`${feeToken.average_gas_price}${feeToken.denom}`); // e.g. 132uosmo - return calculateFee(gasLimit, gasPrice); -}; - -async function execute( - slice: IbcInSlice, - address: string, - getStargateClient: ChainWalletContext['getSigningStargateClient'], -) { - const { penumbraAddrs, selectedChain, coin, amount } = slice; - if (!penumbraAddrs) throw new Error('Penumbra address not available'); - if (!coin) throw new Error('No token is selected'); - if (!amount) throw new Error('Amount has not been entered'); - if (!selectedChain) throw new Error('No chain has been selected'); - - const penumbraChainId = await getChainId(); - if (!penumbraChainId) throw new Error('Penumbra chain id could not be retrieved'); - - const { timeoutHeight, timeoutTimestamp } = await getTimeout(penumbraChainId); - const assetMetadata = augmentToAsset(coin.raw.denom, selectedChain.chainName); - - const transferToken = fromDisplayAmount(assetMetadata, coin.displayDenom, amount); - const params: MsgTransfer = { - sourcePort: 'transfer', - sourceChannel: getCounterpartyChannelId(selectedChain, penumbraChainId), - sender: address, - receiver: penumbraAddrs.ephemeral, - token: transferToken, - timeoutHeight, - timeoutTimestamp, - memo: '', - }; - const ibcTransferMsg = ibc.applications.transfer.v1.MessageComposer.withTypeUrl.transfer(params); - - const client = await getStargateClient(); - const fee = await estimateFee({ - chainId: selectedChain.chainId, - client, - signerAddress: address, - message: ibcTransferMsg, - }); - - const signedTx = await client.sign(address, [ibcTransferMsg], fee, ''); - return await client.broadcastTx(cosmos.tx.v1beta1.TxRaw.encode(signedTx).finish()); -} - -const getCounterpartyChannelId = ( - counterpartyChain: ChainInfo, - penumbraChainId: string, -): string => { - const registry = chainRegistryClient.get(penumbraChainId); - - const counterpartyChannelId = registry.ibcConnections.find( - c => c.chainId === counterpartyChain.chainId, - )?.counterpartyChannelId; - if (!counterpartyChannelId) { - throw new Error( - `Counterparty channel could not be found in registry for chain id: ${counterpartyChain.chainId}`, - ); - } - - return counterpartyChannelId; -}; - -/** - * Examples: - * getRevisionNumberFromChainId("grand-1") returns 1n - * getRevisionNumberFromChainId("osmo-test-5") returns 5n - * getRevisionNumberFromChainId("penumbra-testnet-deimos-7") returns 7n - */ -export const parseRevisionNumberFromChainId = (chainId: string): bigint => { - const match = chainId.match(/-(\d+)$/); - if (match?.[1]) { - return BigInt(match[1]); - } else { - throw new Error(`No revision number found within chain id: ${chainId}`); - } -}; - -// Get timeout from penumbra chain blocks -const getTimeout = async (chainId: string) => { - const { syncInfo } = await tendermintClient.getStatus({}); - const height = syncInfo?.latestBlockHeight; - if (height === undefined) { - throw new Error('Could not retrieve latest block height from Tendermint'); - } - - const timeoutHeight = { - revisionNumber: parseRevisionNumberFromChainId(chainId), - // We don't know the average block times for the counterparty chain, so just putting in the Penumbra average - revisionHeight: height + BLOCKS_PER_HOUR * 3n, - }; - const timeoutTimestamp = currentTimePlusTwoDaysRounded(Date.now()); - - return { timeoutHeight, timeoutTimestamp }; -}; - -const isIbcAsset = (denom: string): boolean => { - const ibcRegex = /^ibc\/[0-9A-F]{64}$/i; - return ibcRegex.test(denom); -}; - -export const ibcErrorSelector = (state: AllSlices) => { - const { amount, coin } = state.ibcIn; - - const numberTooLow = Number(amount) <= 0; - const inputAmountTooBig = Number(coin?.displayAmount) < Number(amount); - const isNotValidAmount = Boolean(coin) && Boolean(amount) && (inputAmountTooBig || numberTooLow); - - return { - amountErr: isNotValidAmount, - // Testnet coins don't seem to have assetType field. Checking manually for ibc address first. - isUnsupportedAsset: - coin && (isIbcAsset(coin.raw.denom) || (coin.assetType && coin.assetType !== 'sdk.coin')), - }; -}; - -export const ibcInSelector = (state: AllSlices) => state.ibcIn; diff --git a/apps/minifront/src/state/ibc-out.test.ts b/apps/minifront/src/state/ibc-out.test.ts deleted file mode 100644 index 668ceb00..00000000 --- a/apps/minifront/src/state/ibc-out.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { beforeEach, describe, expect, test } from 'vitest'; -import { create, StoreApi, UseBoundStore } from 'zustand'; -import { AllSlices, initializeStore } from '.'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { sendValidationErrors } from './send'; -import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { produce } from 'immer'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; -import { Chain } from '@penumbra-labs/registry'; -import { currentTimePlusTwoDaysRounded, ibcValidationErrors } from './ibc-out'; - -describe('IBC Slice', () => { - const selectionExample = new BalancesResponse({ - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ - lo: 0n, - hi: 0n, - }), - metadata: new Metadata({ display: 'test_usd', denomUnits: [{ exponent: 18 }] }), - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'opaque', - value: { - address: addressFromBech32m( - 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4', - ), - }, - }, - }), - }); - - let useStore: UseBoundStore>; - - beforeEach(() => { - useStore = create()(initializeStore()) as UseBoundStore>; - }); - - test('the default is empty, false or undefined', () => { - expect(useStore.getState().ibcOut.amount).toBe(''); - expect(useStore.getState().ibcOut.selection).toBeUndefined(); - expect(useStore.getState().ibcOut.chain).toBeUndefined(); - }); - - describe('setAmount', () => { - test('amount can be set', () => { - useStore.getState().ibcOut.setAmount('2'); - expect(useStore.getState().ibcOut.amount).toBe('2'); - }); - - // TODO [vanishmax, 2024-06-04]: Remove test skipping - test.skip('validate high enough amount validates', () => { - const assetBalance = new Amount({ hi: 1n }); - const state = produce(selectionExample, draft => { - draft.balanceView!.valueView.value!.amount = assetBalance; - }); - useStore.getState().send.setSelection(state); - useStore.getState().send.setAmount('1'); - const { selection, amount } = useStore.getState().send; - - const { amountErr } = sendValidationErrors(selection, amount, 'xyz'); - expect(amountErr).toBeFalsy(); - }); - - test.skip('validate error when too low the balance of the asset', () => { - const assetBalance = new Amount({ lo: 2n }); - const state = produce(selectionExample, draft => { - draft.balanceView!.valueView.value!.amount = assetBalance; - }); - useStore.getState().send.setSelection(state); - useStore.getState().send.setAmount('6'); - const { selection, amount } = useStore.getState().send; - const { amountErr } = sendValidationErrors(selection, amount, 'xyz'); - expect(amountErr).toBeTruthy(); - }); - }); - - describe('setChain', () => { - const chain = { - displayName: 'Osmosis', - chainId: 'osmosis-test-5', - channelId: 'channel-0', - counterpartyChannelId: 'channel-999', - images: [{ svg: '/test.svg' }], - addressPrefix: 'osmo', - } satisfies Chain; - - test('chain can be set', () => { - useStore.getState().ibcOut.setChain(chain); - expect(useStore.getState().ibcOut.chain).toBe(chain); - }); - - test('destination address validation per selected chain', () => { - const osmoAddress = 'osmo1dyrr4r42ql4em7d46srcmnn5ymxk9asvcv95sg'; - - useStore.getState().ibcOut.setChain(chain); - useStore.getState().ibcOut.setDestinationChainAddress(osmoAddress); - - const validationErrors = ibcValidationErrors(useStore.getState()); - - expect(validationErrors.recipientErr).toBeFalsy(); - }); - - test('destination address validation per selected chain fails with incorrect address', () => { - const osmoAddress = 'osmo1xxxxxx'; - - useStore.getState().ibcOut.setChain(chain); - useStore.getState().ibcOut.setDestinationChainAddress(osmoAddress); - - const validationErrors = ibcValidationErrors(useStore.getState()); - - expect(validationErrors.recipientErr).toBeTruthy(); - }); - }); - - describe('setSelection', () => { - test('asset and account can be set', () => { - useStore.getState().send.setSelection(selectionExample); - expect(useStore.getState().send.selection).toStrictEqual(selectionExample); - }); - }); -}); - -describe('currentTimePlusTwoDaysRounded', () => { - test('should add exactly two days to the current time and round up to the nearest ten minutes', () => { - const currentTimeMs = 1713519156000; // Apr 19 2024 9:32:36 - const twoDaysRoundedNano = 1713692400000000000n; // Apr 21 2024 9:40:00 - - const result = currentTimePlusTwoDaysRounded(currentTimeMs); - expect(result).toEqual(twoDaysRoundedNano); - }); -}); diff --git a/apps/minifront/src/state/ibc-out.ts b/apps/minifront/src/state/ibc-out.ts deleted file mode 100644 index 0bf3187f..00000000 --- a/apps/minifront/src/state/ibc-out.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { AllSlices, SliceCreator } from '.'; -import { - BalancesResponse, - TransactionPlannerRequest, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { BigNumber } from 'bignumber.js'; -import { ClientState } from '@buf/cosmos_ibc.bufbuild_es/ibc/lightclients/tendermint/v1/tendermint_pb'; -import { Height } from '@buf/cosmos_ibc.bufbuild_es/ibc/core/client/v1/client_pb'; -import { ibcChannelClient, ibcClient, ibcConnectionClient, viewClient } from '../clients'; -import { - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, - getMetadata, -} from '@penumbra-zone/getters/value-view'; -import { getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { toBaseUnit } from '@penumbra-zone/types/lo-hi'; -import { planBuildBroadcast } from './helpers'; -import { amountMoreThanBalance } from './send'; -import { getAssetId } from '@penumbra-zone/getters/metadata'; -import { assetPatterns } from '@penumbra-zone/types/assets'; -import { bech32, bech32m } from 'bech32'; -import { errorToast } from '@penumbra-zone/ui/lib/toast/presets'; -import { Chain } from '@penumbra-labs/registry'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { Channel } from '@buf/cosmos_ibc.bufbuild_es/ibc/core/channel/v1/channel_pb'; -import { BLOCKS_PER_HOUR } from './constants'; - -export interface IbcOutSlice { - selection: BalancesResponse | undefined; - setSelection: (selection: BalancesResponse) => void; - amount: string; - setAmount: (amount: string) => void; - chain: Chain | undefined; - destinationChainAddress: string; - setDestinationChainAddress: (addr: string) => void; - setChain: (chain: Chain | undefined) => void; - sendIbcWithdraw: () => Promise; - txInProgress: boolean; -} - -export const createIbcOutSlice = (): SliceCreator => (set, get) => { - return { - amount: '', - selection: undefined, - chain: undefined, - destinationChainAddress: '', - txInProgress: false, - setSelection: selection => { - set(state => { - state.ibcOut.selection = selection; - }); - }, - setAmount: amount => { - set(state => { - state.ibcOut.amount = amount; - }); - }, - setChain: chain => { - set(state => { - state.ibcOut.chain = chain; - }); - }, - setDestinationChainAddress: addr => { - set(state => { - state.ibcOut.destinationChainAddress = addr; - }); - }, - sendIbcWithdraw: async () => { - set(state => { - state.send.txInProgress = true; - }); - - try { - const req = await getPlanRequest(get().ibcOut); - await planBuildBroadcast('ics20Withdrawal', req); - - // Reset form - set(state => { - state.ibcOut.amount = ''; - }); - } catch (e) { - errorToast(e, 'Ics20 withdrawal error').render(); - } finally { - set(state => { - state.ibcOut.txInProgress = false; - }); - } - }, - }; -}; - -const tenMinsMs = 1000 * 60 * 10; -const twoDaysMs = 1000 * 60 * 60 * 24 * 2; - -// Timeout is two days. However, in order to prevent identifying oneself by clock skew, -// timeout time is rounded up to the nearest 10 minute interval. -// Reference in core: https://github.com/penumbra-zone/penumbra/blob/1376d4b4f47f44bcc82e8bbdf18262942edf461e/crates/bin/pcli/src/command/tx.rs#L1066-L1067 -export const currentTimePlusTwoDaysRounded = (currentTimeMs: number): bigint => { - const twoDaysFromNowMs = currentTimeMs + twoDaysMs; - - // round to next ten-minute interval - const roundedTimeoutMs = twoDaysFromNowMs + tenMinsMs - (twoDaysFromNowMs % tenMinsMs); - - // 1 million nanoseconds per millisecond (converted to bigint) - const roundedTimeoutNs = BigInt(roundedTimeoutMs) * 1_000_000n; - - return roundedTimeoutNs; -}; - -const clientStateForChannel = async (channel?: Channel): Promise => { - const connectionId = channel?.connectionHops[0]; - if (!connectionId) { - throw new Error('no connectionId in channel returned from ibcChannelClient request'); - } - - const { connection } = await ibcConnectionClient.connection({ - connectionId, - }); - const clientId = connection?.clientId; - if (!clientId) { - throw new Error('no clientId ConnectionEnd returned from ibcConnectionClient request'); - } - - const { clientState: anyClientState } = await ibcClient.clientState({ clientId: clientId }); - if (!anyClientState) { - throw new Error(`Could not get state for client id ${clientId}`); - } - - const clientState = new ClientState(); - const success = anyClientState.unpackTo(clientState); // Side effect of augmenting input clientState with data - if (!success) { - throw new Error(`Error while trying to unpack Any to ClientState for client id ${clientId}`); - } - - return clientState; -}; - -// Reference in core: https://github.com/penumbra-zone/penumbra/blob/1376d4b4f47f44bcc82e8bbdf18262942edf461e/crates/bin/pcli/src/command/tx.rs#L998-L1050 -const getTimeout = async ( - ibcChannelId: string, -): Promise<{ timeoutTime: bigint; timeoutHeight: Height }> => { - const { channel } = await ibcChannelClient.channel({ - portId: 'transfer', - channelId: ibcChannelId, - }); - - const clientState = await clientStateForChannel(channel); - if (!clientState.latestHeight) { - throw new Error(`latestHeight not provided in client state for ${clientState.chainId}`); - } - - return { - timeoutTime: currentTimePlusTwoDaysRounded(Date.now()), - timeoutHeight: new Height({ - revisionHeight: clientState.latestHeight.revisionHeight + BLOCKS_PER_HOUR * 3n, - revisionNumber: clientState.latestHeight.revisionNumber, - }), - }; -}; - -const getPlanRequest = async ({ - amount, - selection, - chain, - destinationChainAddress, -}: IbcOutSlice): Promise => { - if (!destinationChainAddress) throw new Error('no destination chain address set'); - if (!chain) throw new Error('Chain not set'); - if (!selection) throw new Error('No asset selected'); - - const addressIndex = getAddressIndex(selection.accountAddress); - const { address: returnAddress } = await viewClient.ephemeralAddress({ addressIndex }); - if (!returnAddress) throw new Error('Error with generating IBC deposit address'); - - const { timeoutHeight, timeoutTime } = await getTimeout(chain.channelId); - - return new TransactionPlannerRequest({ - ics20Withdrawals: [ - { - amount: toBaseUnit( - BigNumber(amount), - getDisplayDenomExponentFromValueView(selection.balanceView), - ), - denom: { denom: getMetadata(selection.balanceView).base }, - destinationChainAddress, - returnAddress, - timeoutHeight, - timeoutTime, - sourceChannel: chain.channelId, - }, - ], - source: addressIndex, - }); -}; - -export const ibcOutSelector = (state: AllSlices) => state.ibcOut; - -export const ibcValidationErrors = (state: AllSlices) => { - return { - recipientErr: !state.ibcOut.destinationChainAddress - ? false - : !unknownAddrIsValid(state.ibcOut.chain, state.ibcOut.destinationChainAddress), - amountErr: !state.ibcOut.selection - ? false - : amountMoreThanBalance(state.ibcOut.selection, state.ibcOut.amount), - }; -}; - -/** - * Matches the given address to the chain's address prefix. - * We don't know what format foreign addresses are in, so this only checks: - * - it's valid bech32 OR valid bech32m - * - the prefix matches the chain - */ -const unknownAddrIsValid = (chain: Chain | undefined, address: string): boolean => { - if (!chain || address === '') return false; - const { prefix, words } = - bech32.decodeUnsafe(address, Infinity) ?? bech32m.decodeUnsafe(address, Infinity) ?? {}; - return !!words && prefix === chain.addressPrefix; -}; - -/** - * Filters the given IBC loader response balances by checking if any of the assets - * in the balance view match the staking token's asset ID or are of the same ibc channel. - * - * Until unwind support is implemented (https://github.com/penumbra-zone/web/issues/344), - * we need to ensure ics20 withdraws match these conditions. - */ -export const filterBalancesPerChain = ( - allBalances: BalancesResponse[], - chain: Chain | undefined, - stakingTokenMetadata: Metadata, - registryAssets: Metadata[], -): BalancesResponse[] => { - const penumbraAssetId = getAssetId(stakingTokenMetadata); - const assetsWithMatchingChannel = registryAssets - .filter(a => { - const match = assetPatterns.ibc.capture(a.base); - if (!match) return false; - return chain?.channelId === match.channel; - }) - .map(m => m.penumbraAssetId!); - - const assetIdsToCheck = [penumbraAssetId, ...assetsWithMatchingChannel]; - - return allBalances.filter(({ balanceView }) => { - return assetIdsToCheck.some(assetId => assetId.equals(getAssetIdFromValueView(balanceView))); - }); -}; diff --git a/apps/minifront/src/state/index.ts b/apps/minifront/src/state/index.ts deleted file mode 100644 index 110008fb..00000000 --- a/apps/minifront/src/state/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { create, StateCreator } from 'zustand'; -import { enableMapSet } from 'immer'; -import { immer } from 'zustand/middleware/immer'; -import { createSwapSlice, SwapSlice } from './swap'; -import { createIbcOutSlice, IbcOutSlice } from './ibc-out'; -import { createSendSlice, SendSlice } from './send'; -import { createStakingSlice, StakingSlice } from './staking'; -import { createStatusSlice, StatusSlice } from './status'; -import { createUnclaimedSwapsSlice, UnclaimedSwapsSlice } from './unclaimed-swaps'; -import { createTransactionsSlice, TransactionsSlice } from './transactions'; -import { createIbcInSlice, IbcInSlice } from './ibc-in'; - -/** - * Required to enable use of `Map`s in Zustand state when using Immer - * middleware. Without this, calling `.set()` on a `Map` in Zustand state - * results in an error. - */ -enableMapSet(); - -export interface AllSlices { - ibcIn: IbcInSlice; - ibcOut: IbcOutSlice; - send: SendSlice; - staking: StakingSlice; - status: StatusSlice; - swap: SwapSlice; - transactions: TransactionsSlice; - unclaimedSwaps: UnclaimedSwapsSlice; -} - -export type SliceCreator = StateCreator< - AllSlices, - [['zustand/immer', never]], - [], - SliceInterface ->; - -export const initializeStore = () => { - return immer((setState, getState: () => AllSlices, store) => ({ - ibcIn: createIbcInSlice()(setState, getState, store), - ibcOut: createIbcOutSlice()(setState, getState, store), - send: createSendSlice()(setState, getState, store), - staking: createStakingSlice()(setState, getState, store), - status: createStatusSlice()(setState, getState, store), - swap: createSwapSlice()(setState, getState, store), - transactions: createTransactionsSlice()(setState, getState, store), - unclaimedSwaps: createUnclaimedSwapsSlice()(setState, getState, store), - })); -}; - -export const useStore = create()(initializeStore()); diff --git a/apps/minifront/src/state/send.test.ts b/apps/minifront/src/state/send.test.ts deleted file mode 100644 index 2f7fc31b..00000000 --- a/apps/minifront/src/state/send.test.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { create, StoreApi, UseBoundStore } from 'zustand'; -import { AllSlices, initializeStore } from '.'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { sendValidationErrors } from './send'; -import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { viewClient } from '../clients'; -import { - AddressByIndexResponse, - BalancesResponse, - TransactionPlannerResponse, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Fee } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1/fee_pb'; -import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; - -vi.mock('../fetchers/address', () => ({ - getAddressByIndex: vi.fn(), -})); - -describe('Send Slice', () => { - const selectionExample = new BalancesResponse({ - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ - lo: 0n, - hi: 0n, - }), - metadata: new Metadata({ - display: 'xyz', - denomUnits: [{ denom: 'xyz', exponent: 6 }], - penumbraAssetId: { inner: new Uint8Array(32) }, - }), - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'decoded', - value: { - address: addressFromBech32m( - 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4', - ), - index: { account: 12 }, - }, - }, - }), - }); - - let useStore: UseBoundStore>; - - beforeEach(() => { - useStore = create()(initializeStore()) as UseBoundStore>; - }); - - test('the default is empty, false or undefined', () => { - const { amount, memo, recipient, selection, txInProgress } = useStore.getState().send; - - expect(amount).toBe(''); - expect(selection).toBeUndefined(); - expect(memo).toBe(''); - expect(recipient).toBe(''); - - expect(txInProgress).toBeFalsy(); - - const { amountErr, recipientErr } = sendValidationErrors( - selectionExample, - amount, - recipient, - memo, - ); - expect(amountErr).toBeFalsy(); - expect(recipientErr).toBeFalsy(); - }); - - describe('setAmount', () => { - test('amount can be set', () => { - useStore.getState().send.setAmount('2'); - expect(useStore.getState().send.amount).toBe('2'); - }); - - test('validate high enough amount validates', () => { - const assetBalance = new Amount({ hi: 1n }); - const state = selectionExample.clone(); - state.balanceView!.valueView.value!.amount = assetBalance; - - useStore.getState().send.setSelection(state); - useStore.getState().send.setAmount('1000'); - const { selection, amount } = useStore.getState().send; - - const { amountErr } = sendValidationErrors(selection, amount, 'xyz', 'a memo'); - expect(amountErr).toBeFalsy(); - }); - - test('validate error when too low the balance of the asset', () => { - const assetBalance = new Amount({ lo: 2n }); - const state = selectionExample.clone(); - state.balanceView!.valueView.value!.amount = assetBalance; - - useStore.getState().send.setSelection(state); - useStore.getState().send.setAmount('6'); - const { selection, amount } = useStore.getState().send; - const { amountErr } = sendValidationErrors(selection, amount, 'xyz', 'a memo'); - expect(amountErr).toBeTruthy(); - }); - }); - - describe('setMemo', () => { - test('memo can be set', () => { - useStore.getState().send.setMemo('memo-test'); - expect(useStore.getState().send.memo).toBe('memo-test'); - }); - }); - - describe('setRecipient and validate', () => { - const rightAddress = - 'penumbra1ftmn2a3hf8pxe0e48es8u9rqhny4xggq9wn2caxcjnfwfhwr5s0t3y6nzs9gx3ty5czd0sd9ssfgjt2pcxrq93yvgk2gu3ynmayuwgddkxthce8l445v8x6v07y2sjd8djcr6v'; - - test('recipient can be set and validate', () => { - useStore.getState().send.setSelection(selectionExample); - useStore.getState().send.setRecipient(rightAddress); - expect(useStore.getState().send.recipient).toBe(rightAddress); - const { selection, amount, recipient, memo } = useStore.getState().send; - const { recipientErr } = sendValidationErrors(selection, amount, recipient, memo); - expect(recipientErr).toBeFalsy(); - }); - - test('recipient will have a validation error after entering an incorrect address length', () => { - const badAddressLength = - 'penumbra1lsqlh43cxh6amvtu0g84v9s8sq0zef4mz8jvje9lxwarancqg9qjf6nthhnjzlwngplepq7vaam8h4z530gys7x2s82zn0sgvxneea442q63sumem7r096p7rd'; - - useStore.getState().send.setSelection(selectionExample); - useStore.getState().send.setRecipient(badAddressLength); - const { selection, amount, recipient, memo } = useStore.getState().send; - const { recipientErr } = sendValidationErrors(selection, amount, recipient, memo); - expect(recipientErr).toBeTruthy(); - }); - - test('recipient will have a validation error after entering an address without penumbra as prefix', () => { - const badAddressPrefix = - 'wwwwwwwwww1lsqlh43cxh6amvtu0g84v9s8sq0zef4mz8jvje9lxwarancqg9qjf6nthhnjzlwngplepq7vaam8h4z530gys7x2s82zn0sgvxneea442q63sumem7r096p7rd2tywm2v6ppc4d'; - - useStore.getState().send.setSelection(selectionExample); - useStore.getState().send.setRecipient(badAddressPrefix); - const { selection, amount, recipient, memo } = useStore.getState().send; - const { recipientErr } = sendValidationErrors(selection, amount, recipient, memo); - expect(recipientErr).toBeTruthy(); - }); - - test('recipient will have a validation error after entering a very long memo', () => { - useStore.getState().send.setMemo('b'.repeat(512)); - const { selection, amount, recipient, memo } = useStore.getState().send; - const { memoErr } = sendValidationErrors(selection, amount, recipient, memo); - expect(memoErr).toBeTruthy(); - }); - }); - - describe('setSelection', () => { - test('asset and account can be set', () => { - useStore.getState().send.setSelection(selectionExample); - expect(useStore.getState().send.selection).toStrictEqual(selectionExample); - }); - }); - - describe('refreshFee', () => { - const amount = '1'; - const recipient = - 'penumbra1lsqlh43cxh6amvtu0g84v9s8sq0zef4mz8jvje9lxwarancqg9qjf6nthhnjzlwngplepq7vaam8h4z530gys7x2s82zn0sgvxneea442q63sumem7r096p7rd2tywm2v6ppc4'; - const memo = 'hello'; - const mockFee = new Fee({ amount: { hi: 1n, lo: 2n } }); - - beforeEach(() => { - vi.spyOn(viewClient, 'addressByIndex').mockResolvedValue(new AddressByIndexResponse()); - - vi.spyOn(viewClient, 'transactionPlanner').mockResolvedValue( - new TransactionPlannerResponse({ plan: { transactionParameters: { fee: mockFee } } }), - ); - }); - - afterEach(() => { - vi.spyOn(viewClient, 'transactionPlanner').mockReset(); - }); - - describe('when `fee` is not yet present in the state`', () => { - test('sets `fee` to the one found in the transaction planner response', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: selectionExample, - memo, - fee: undefined, - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBe(mockFee); - }); - - test('sets `fee` to the one found in the transaction planner response even if `memo` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: selectionExample, - memo: '', - fee: undefined, - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBe(mockFee); - }); - - test('sets `fee` to `undefined` if `amount` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount: '', - recipient, - selection: selectionExample, - memo, - fee: undefined, - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - - test('sets `fee` to `undefined` if `recipient` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient: '', - selection: selectionExample, - memo, - fee: undefined, - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - - test('sets `fee` to `undefined` if `selection` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: undefined, - memo, - fee: undefined, - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - }); - - describe('when `fee` is already present in the state`', () => { - test('sets `fee` to the one found in the transaction planner response', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: selectionExample, - memo, - fee: new Fee({ amount: { hi: 0n, lo: 0n } }), - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBe(mockFee); - }); - - test('sets `fee` to the one found in the transaction planner response even if `memo` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: selectionExample, - memo: '', - fee: new Fee({ amount: { hi: 0n, lo: 0n } }), - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBe(mockFee); - }); - - test('sets `fee` to `undefined` if `amount` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount: '', - recipient, - selection: selectionExample, - memo, - fee: new Fee({ amount: { hi: 0n, lo: 0n } }), - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - - test('sets `fee` to `undefined` if `recipient` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient: '', - selection: selectionExample, - memo, - fee: new Fee({ amount: { hi: 0n, lo: 0n } }), - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - - test('sets `fee` to `undefined` if `selection` is falsey', async () => { - const prev = useStore.getState(); - useStore.setState({ - ...prev, - send: { - ...prev.send, - amount, - recipient, - selection: undefined, - memo, - fee: new Fee({ amount: { hi: 0n, lo: 0n } }), - }, - }); - - await useStore.getState().send.refreshFee(); - - expect(useStore.getState().send.fee).toBeUndefined(); - }); - }); - }); -}); diff --git a/apps/minifront/src/state/send.ts b/apps/minifront/src/state/send.ts deleted file mode 100644 index bf532b49..00000000 --- a/apps/minifront/src/state/send.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { AllSlices, SliceCreator } from '.'; - -import { - BalancesResponse, - TransactionPlannerRequest, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { BigNumber } from 'bignumber.js'; -import { MemoPlaintext } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb'; -import { plan, planBuildBroadcast } from './helpers'; - -import { - Fee, - FeeTier_Tier, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/fee/v1/fee_pb'; -import { - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, -} from '@penumbra-zone/getters/value-view'; -import { getAddress, getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { toBaseUnit } from '@penumbra-zone/types/lo-hi'; -import { fromValueView } from '@penumbra-zone/types/amount'; -import { isAddress } from '@penumbra-zone/bech32m/penumbra'; - -export interface SendSlice { - selection: BalancesResponse | undefined; - setSelection: (selection: BalancesResponse) => void; - amount: string; - setAmount: (amount: string) => void; - recipient: string; - setRecipient: (addr: string) => void; - memo: string; - setMemo: (txt: string) => void; - fee: Fee | undefined; - refreshFee: () => Promise; - feeTier: FeeTier_Tier; - setFeeTier: (feeTier: FeeTier_Tier) => void; - sendTx: () => Promise; - txInProgress: boolean; -} - -export const createSendSlice = (): SliceCreator => (set, get) => { - return { - selection: undefined, - amount: '', - recipient: '', - memo: '', - fee: undefined, - feeTier: FeeTier_Tier.LOW, - txInProgress: false, - setAmount: amount => { - set(state => { - state.send.amount = amount; - }); - }, - setSelection: selection => { - set(state => { - state.send.selection = selection; - }); - }, - setRecipient: addr => { - set(state => { - state.send.recipient = addr; - }); - }, - setMemo: txt => { - set(state => { - state.send.memo = txt; - }); - }, - refreshFee: async () => { - const { amount, recipient, selection } = get().send; - - if (!amount || !recipient || !selection) { - set(state => { - state.send.fee = undefined; - }); - return; - } - - const txPlan = await plan(assembleRequest(get().send)); - const fee = txPlan.transactionParameters?.fee; - if (!fee?.amount) return; - - set(state => { - state.send.fee = fee; - }); - }, - setFeeTier: feeTier => { - set(state => { - state.send.feeTier = feeTier; - }); - }, - sendTx: async () => { - set(state => { - state.send.txInProgress = true; - }); - - try { - const req = assembleRequest(get().send); - await planBuildBroadcast('send', req); - - set(state => { - state.send.amount = ''; - }); - } finally { - set(state => { - state.send.txInProgress = false; - }); - } - }, - }; -}; - -const assembleRequest = ({ amount, feeTier, recipient, selection, memo }: SendSlice) => { - return new TransactionPlannerRequest({ - outputs: [ - { - address: { altBech32m: recipient }, - value: { - amount: toBaseUnit( - BigNumber(amount), - getDisplayDenomExponentFromValueView(selection?.balanceView), - ), - assetId: getAssetIdFromValueView(selection?.balanceView), - }, - }, - ], - source: getAddressIndex(selection?.accountAddress), - - // Note: we currently don't provide a UI for setting the fee manually. Thus, - // a `feeMode` of `manualFee` is not supported here. - feeMode: - typeof feeTier === 'undefined' - ? { case: undefined } - : { - case: 'autoFee', - value: { feeTier }, - }, - - memo: new MemoPlaintext({ - returnAddress: getAddress(selection?.accountAddress), - text: memo, - }), - }); -}; - -export const amountMoreThanBalance = ( - asset: BalancesResponse, - /** - * The amount that a user types into the interface will always be in the - * display denomination -- e.g., in `penumbra`, not in `upenumbra`. - */ - amountInDisplayDenom: string, -): boolean => { - if (!asset.balanceView) { - throw new Error('Missing balanceView'); - } - - const balanceAmt = fromValueView(asset.balanceView); - return Boolean(amountInDisplayDenom) && BigNumber(amountInDisplayDenom).gt(balanceAmt); -}; - -export interface SendValidationFields { - recipientErr: boolean; - amountErr: boolean; - memoErr: boolean; -} - -export const sendValidationErrors = ( - asset: BalancesResponse | undefined, - amount: string, - recipient: string, - memo?: string, -): SendValidationFields => { - return { - recipientErr: Boolean(recipient) && !isAddress(recipient), - amountErr: !asset ? false : amountMoreThanBalance(asset, amount), - // The memo cannot exceed 512 bytes - // return address uses 80 bytes - // so 512-80=432 bytes for memo text - memoErr: new TextEncoder().encode(memo).length > 432, - }; -}; - -export const sendSelector = (state: AllSlices) => state.send; diff --git a/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts b/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts deleted file mode 100644 index e1804b80..00000000 --- a/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { - TransactionPlannerRequest, - TransactionPlannerRequest_UndelegateClaim, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; - -import { sctClient, stakeClient, viewClient } from '../../clients'; -import { - getAmount, - getValidatorIdentityKeyFromValueView, - getMetadata, -} from '@penumbra-zone/getters/value-view'; -import { getUnbondingStartHeight } from '@penumbra-zone/types/assets'; - -const getUndelegateClaimPlannerRequest = - (endEpochIndex: bigint) => async (unbondingToken: ValueView) => { - const unbondingStartHeight = getUnbondingStartHeight(getMetadata(unbondingToken)); - const identityKey = getValidatorIdentityKeyFromValueView(unbondingToken); - const { epoch: startEpoch } = await sctClient.epochByHeight({ height: unbondingStartHeight }); - - const { penalty } = await stakeClient.validatorPenalty({ - startEpochIndex: startEpoch?.index, - endEpochIndex, - identityKey, - }); - - return new TransactionPlannerRequest_UndelegateClaim({ - validatorIdentity: identityKey, - unbondingStartHeight, - penalty, - unbondingAmount: getAmount(unbondingToken), - }); - }; - -export const assembleUndelegateClaimRequest = async ({ - account, - unbondingTokens, -}: { - account: number; - unbondingTokens: ValueView[]; -}) => { - const { fullSyncHeight } = await viewClient.status({}); - const { epoch } = await sctClient.epochByHeight({ height: fullSyncHeight }); - const endEpochIndex = epoch?.index; - if (!endEpochIndex) return; - - return new TransactionPlannerRequest({ - undelegationClaims: await Promise.all( - unbondingTokens.map(getUndelegateClaimPlannerRequest(endEpochIndex)), - ), - source: { account }, - }); -}; diff --git a/apps/minifront/src/state/staking/index.test.ts b/apps/minifront/src/state/staking/index.test.ts deleted file mode 100644 index e73f9bd7..00000000 --- a/apps/minifront/src/state/staking/index.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { create, StoreApi, UseBoundStore } from 'zustand'; -import { AllSlices, initializeStore } from '..'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { bech32mIdentityKey } from '@penumbra-zone/bech32m/penumbravalid'; -import { getValidatorInfoFromValueView } from '@penumbra-zone/getters/value-view'; -import { - AddressView, - IdentityKey, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { THROTTLE_MS } from '.'; -import { DelegationsByAddressIndexResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; - -const u8 = (length: number) => Uint8Array.from({ length }, () => Math.floor(Math.random() * 256)); -const validator1IdentityKey = new IdentityKey({ ik: u8(32) }); -const validator1Bech32IdentityKey = bech32mIdentityKey(validator1IdentityKey); -const validatorInfo1 = new ValidatorInfo({ - status: { - votingPower: { hi: 0n, lo: 2n }, - }, - validator: { - name: 'Validator 1', - identityKey: validator1IdentityKey, - }, -}); - -const validator2IdentityKey = new IdentityKey({ - ik: u8(32), -}); -const validator2Bech32IdentityKey = bech32mIdentityKey(validator2IdentityKey); -const validatorInfo2 = new ValidatorInfo({ - status: { - votingPower: { hi: 0n, lo: 5n }, - }, - validator: { - name: 'Validator 2', - identityKey: validator2IdentityKey, - }, -}); - -const validator3IdentityKey = new IdentityKey({ - ik: u8(32), -}); -const validatorInfo3 = new ValidatorInfo({ - status: { - votingPower: { hi: 0n, lo: 3n }, - }, - validator: { - name: 'Validator 3', - identityKey: validator3IdentityKey, - }, -}); - -const validator4IdentityKey = new IdentityKey({ ik: u8(32) }); -const validatorInfo4 = new ValidatorInfo({ - status: { - votingPower: { hi: 0n, lo: 9n }, - }, - validator: { - name: 'Validator 4', - identityKey: validator4IdentityKey, - }, -}); - -vi.mock('../../fetchers/registry', () => ({ - getStakingTokenMetadata: vi.fn(async () => Promise.resolve(new Metadata())), -})); - -vi.mock('../../fetchers/balances', () => ({ - getBalances: vi.fn(async () => - Promise.resolve([ - { - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 1n }, - metadata: { - display: `delegation_${validator1Bech32IdentityKey}`, - }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo1.toBinary(), - }, - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'decoded', - value: { - index: { - account: 0, - }, - address: {}, - }, - }, - }), - }, - { - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 2n }, - metadata: { - display: `delegation_${validator2Bech32IdentityKey}`, - }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo2.toBinary(), - }, - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'decoded', - value: { - index: { - account: 0, - }, - address: {}, - }, - }, - }), - }, - { - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 5n }, - metadata: { - display: 'penumbra', - }, - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'decoded', - value: { - index: { - account: 0, - }, - address: {}, - }, - }, - }), - }, - ]), - ), -})); - -const mockViewClient = vi.hoisted(() => ({ - assetMetadataById: vi.fn(() => new Metadata()), - delegationsByAddressIndex: vi.fn(async function* () { - yield await Promise.resolve( - new DelegationsByAddressIndexResponse({ - valueView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 1n }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo1.toBinary(), - }, - }, - }, - }, - }), - ); - yield await Promise.resolve( - new DelegationsByAddressIndexResponse({ - valueView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 2n }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo2.toBinary(), - }, - }, - }, - }, - }), - ); - yield await Promise.resolve( - new DelegationsByAddressIndexResponse({ - valueView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 0n }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo3.toBinary(), - }, - }, - }, - }, - }), - ); - yield await Promise.resolve( - new DelegationsByAddressIndexResponse({ - valueView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 0n }, - extendedMetadata: { - typeUrl: ValidatorInfo.typeName, - value: validatorInfo4.toBinary(), - }, - }, - }, - }, - }), - ); - }), -})); - -vi.mock('../../clients', () => ({ - viewClient: mockViewClient, -})); - -describe('Staking Slice', () => { - let useStore: UseBoundStore>; - - beforeEach(() => { - useStore = create()(initializeStore()) as UseBoundStore>; - vi.useFakeTimers(); - }); - - it('has correct initial state', () => { - expect(useStore.getState().staking).toEqual({ - account: 0, - accountSwitcherFilter: [], - action: undefined, - amount: '', - validatorInfo: undefined, - delegationsByAccount: new Map(), - unstakedTokensByAccount: new Map(), - unbondingTokensByAccount: new Map(), - setAccount: expect.any(Function) as unknown, - loadDelegationsForCurrentAccount: expect.any(Function) as unknown, - loadUnbondingTokensForCurrentAccount: expect.any(Function) as unknown, - loadAndReduceBalances: expect.any(Function) as unknown, - delegate: expect.any(Function) as unknown, - undelegate: expect.any(Function) as unknown, - undelegateClaim: expect.any(Function) as unknown, - onClickActionButton: expect.any(Function) as unknown, - onClose: expect.any(Function) as unknown, - setAmount: expect.any(Function) as unknown, - error: undefined, - loading: false, - votingPowerByValidatorInfo: {}, - }); - }); - - it('adds the delegation tokens from responses to the state, sorted by balance (descending) then voting power (descending)', async () => { - const { getState } = useStore; - - await getState().staking.loadDelegationsForCurrentAccount(); - vi.advanceTimersByTime(THROTTLE_MS); - - const delegations = getState().staking.delegationsByAccount.get(0)!; - - /** - * Note sorting - validator 2 comes before validator 1, since we have a - * higher balance of validator 2's delegation tokens. And validator 4 comes - * before validator 3 at the end: we have a 0 balance of both, but validator - * 4 has more voting power. - */ - expect(getValidatorInfoFromValueView(delegations[0])).toEqual(validatorInfo2); - expect(getValidatorInfoFromValueView(delegations[1])).toEqual(validatorInfo1); - expect(getValidatorInfoFromValueView(delegations[2])).toEqual(validatorInfo4); - expect(getValidatorInfoFromValueView(delegations[3])).toEqual(validatorInfo3); - }); - - it('calculates the percentage voting power once all delegations are loaded', async () => { - const { getState } = useStore; - - expect(getState().staking.votingPowerByValidatorInfo).toEqual({}); - await getState().staking.loadDelegationsForCurrentAccount(); - expect(Object.values(getState().staking.votingPowerByValidatorInfo).length).toBe(4); - }); - - describe('loadAndReduceBalances()', () => { - beforeEach(async () => { - await useStore.getState().staking.loadAndReduceBalances(); - }); - - it('includes accounts with tokens relevant to staking', () => { - expect(useStore.getState().staking.accountSwitcherFilter).toEqual([0]); - }); - }); -}); diff --git a/apps/minifront/src/state/staking/index.ts b/apps/minifront/src/state/staking/index.ts deleted file mode 100644 index f23917e1..00000000 --- a/apps/minifront/src/state/staking/index.ts +++ /dev/null @@ -1,544 +0,0 @@ -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { SliceCreator } from '..'; -import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { BalancesByAccount, getBalancesByAccount } from '../../fetchers/balances/by-account'; -import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { planBuildBroadcast } from '../helpers'; -import { - TransactionPlannerRequest, - UnbondingTokensByAddressIndexResponse, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { BigNumber } from 'bignumber.js'; -import { assembleUndelegateClaimRequest } from './assemble-undelegate-claim-request'; -import throttle from 'lodash/throttle'; -import { - getAmount, - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, - getDisplayDenomFromView, - getValidatorInfoFromValueView, -} from '@penumbra-zone/getters/value-view'; -import { - getRateData, - getVotingPowerFromValidatorInfo, -} from '@penumbra-zone/getters/validator-info'; -import { - getVotingPowerByValidatorInfo, - isDelegationTokenForValidator, - VotingPowerAsIntegerPercentage, -} from '@penumbra-zone/types/staking'; -import { joinLoHiAmount } from '@penumbra-zone/types/amount'; -import { splitLoHi, toBaseUnit } from '@penumbra-zone/types/lo-hi'; -import { viewClient } from '../../clients'; -import { getValueView as getValueViewFromDelegationsByAddressIndexResponse } from '@penumbra-zone/getters/delegations-by-address-index-response'; -import { getValueView as getValueViewFromUnbondingTokensByAddressIndexResponse } from '@penumbra-zone/getters/unbonding-tokens-by-address-index-response'; -import Array from '@penumbra-zone/polyfills/Array.fromAsync'; -import { getStakingTokenMetadata } from '../../fetchers/registry'; -import { zeroValueView } from '../../utils/zero-value-view'; -import { assetPatterns } from '@penumbra-zone/types/assets'; - -interface UnbondingTokensForAccount { - claimable: { - /** - * The total value of all claimable unbonding tokens in this account, in the - * staking token. This is what they will be worth once claimed, assuming no - * slashing. - */ - total: ValueView; - tokens: ValueView[]; - }; - notYetClaimable: { - /** - * The total value of all not-yet-claimable unbonding tokens in this - * account, in the staking token. This is what they will be worth once - * claimed, assuming no slashing. - */ - total: ValueView; - tokens: ValueView[]; - }; -} - -export interface StakingSlice { - /** The account for which we're viewing delegations. */ - account: number; - /** Switch to view a different account. */ - setAccount: (account: number) => void; - /** - * All accounts for which staking is relevant, to be passed to - * `` as the `filter` prop. This includes accounts with: - * - delegation tokens - * - unbonding tokens - * - staking (UM) tokens (since they can be delegated) - */ - accountSwitcherFilter: number[]; - /** A map of numeric account indexes to delegations for that account. */ - delegationsByAccount: Map; - /** - * A map of numeric account indexes to unstaked (UM) tokens for that account. - */ - unstakedTokensByAccount: Map; - /** - * A map of numeric account indexes to information about unbonding tokens for - * that account. - */ - unbondingTokensByAccount: Map; - /** - * Load all delegations for the currently selected account, and save them into - * `delegationsByAccount`. Should be called each time `account` is changed. - */ - loadDelegationsForCurrentAccount: () => Promise; - loadDelegationsForCurrentAccountAbortController?: AbortController; - /** - * Load all unbonding tokens for the currently selected account, and save them - * into `unbondingTokensByAccount`. Should be called each time `account` is - * changed. - */ - loadUnbondingTokensForCurrentAccount: () => Promise; - loadUnbondingTokensForCurrentAccountAbortController?: AbortController; - /** - * Build and submit the Delegate transaction. - */ - delegate: () => Promise; - /** - * Build and submit the Undelegate transaction. - */ - undelegate: () => Promise; - /** - * Build and submit Undelegate Claim transaction(s). - */ - undelegateClaim: () => Promise; - /** - * Loads all the user's balances and reduces them to: - * 1. The `unstakedTokensByAccount` property on the state. - * 2. The `accountSwitcherFilter` property on the state. - */ - loadAndReduceBalances: () => Promise; - loading: boolean; - error: unknown; - votingPowerByValidatorInfo: Record; - /** - * Called when the user clicks either the Delegate or Undelegate button for a - * given validator (represented by `validatorInfo`). - */ - onClickActionButton: (action: 'delegate' | 'undelegate', validatorInfo: ValidatorInfo) => void; - /** - * Called when the user closes the delegate or undelegate form without - * submitting it. - */ - onClose: () => void; - setAmount: (amount: string) => void; - /** - * The action that the user is currently taking. This is populated once the - * user clicks the "Delegate" or "Undelegate" button, and it is reset to - * `undefined` when the transaction starts or the user cancels. - */ - action?: 'delegate' | 'undelegate'; - /** - * The amount the user has typed into the form that appears after clicking the - * "Delegate" or "Undelegate" button. - */ - amount: string; - /** - * The `ValidatorInfo` for the validator that the user has clicked the - * delegate or undelegate button for. - */ - validatorInfo?: ValidatorInfo; -} - -/** - * Used with `.sort()` to sort value views by balance and then voting power - * (both descending). - */ -const byBalanceAndVotingPower = (valueViewA: ValueView, valueViewB: ValueView): number => { - const byBalance = Number( - joinLoHiAmount(getAmount(valueViewB)) - joinLoHiAmount(getAmount(valueViewA)), - ); - if (byBalance !== 0) return byBalance; - - const validatorInfoA = getValidatorInfoFromValueView(valueViewA); - const validatorInfoB = getValidatorInfoFromValueView(valueViewB); - - const byVotingPower = Number( - joinLoHiAmount(getVotingPowerFromValidatorInfo(validatorInfoB)) - - joinLoHiAmount(getVotingPowerFromValidatorInfo(validatorInfoA)), - ); - - return byVotingPower; -}; - -/** - * Tuned to give optimal performance when throttling the rendering delegation - * tokens. - */ -export const THROTTLE_MS = 200; - -export const createStakingSlice = (): SliceCreator => (set, get) => ({ - account: 0, - accountSwitcherFilter: [], - setAccount: (account: number) => - set(state => { - state.staking.account = account; - }), - action: undefined, - amount: '', - validatorInfo: undefined, - onClickActionButton: (action, validatorInfo) => - set(state => { - state.staking.action = action; - state.staking.validatorInfo = validatorInfo; - }), - onClose: () => - set(state => { - state.staking.action = undefined; - }), - setAmount: amount => - set(state => { - state.staking.amount = amount; - }), - delegationsByAccount: new Map(), - unstakedTokensByAccount: new Map(), - unbondingTokensByAccount: new Map(), - loadDelegationsForCurrentAccount: async () => { - const existingAbortController = get().staking.loadDelegationsForCurrentAccountAbortController; - if (existingAbortController) existingAbortController.abort(); - const newAbortController = new AbortController(); - set(state => { - state.staking.loadDelegationsForCurrentAccountAbortController = newAbortController; - }); - - const addressIndex = new AddressIndex({ account: get().staking.account }); - const validatorInfos: ValidatorInfo[] = []; - - set(state => { - state.staking.delegationsByAccount.set(addressIndex.account, []); - state.staking.votingPowerByValidatorInfo = {}; - state.staking.loading = true; - }); - - let delegationsToFlush: ValueView[] = []; - - /** - * Per the RPC call, we get delegations in a stream, one-by-one. If we push - * them to state as we receive them, React has to re-render super - * frequently. Rendering happens synchronously, which means that the `for` - * loop below has to wait until rendering is done before moving on to the - * next delegation. Thus, the staking page loads super slowly if we render - * delegations as soon as we receive them. - * - * To resolve this performance issue, we instead queue up a number of - * delegations and then flush them to state in batches. - */ - const flushToState = () => { - if (!delegationsToFlush.length) return; - - const delegations = get().staking.delegationsByAccount.get(addressIndex.account) ?? []; - - const sortedDelegations = [...delegations, ...delegationsToFlush].sort( - byBalanceAndVotingPower, - ); - - set(state => { - state.staking.delegationsByAccount.set(addressIndex.account, sortedDelegations); - }); - - delegationsToFlush = []; - }; - const throttledFlushToState = throttle(flushToState, THROTTLE_MS, { trailing: true }); - - for await (const response of viewClient.delegationsByAddressIndex({ addressIndex })) { - if (newAbortController.signal.aborted) { - throttledFlushToState.cancel(); - return; - } - - const delegation = getValueViewFromDelegationsByAddressIndexResponse(response); - delegationsToFlush.push(delegation); - validatorInfos.push(getValidatorInfoFromValueView(delegation)); - - throttledFlushToState(); - } - - /** - * We can only calculate _each_ validator's percentage voting power once - * we've loaded _all_ voting powers. - */ - set(state => { - state.staking.votingPowerByValidatorInfo = getVotingPowerByValidatorInfo(validatorInfos); - state.staking.loading = false; - }); - }, - loadUnbondingTokensForCurrentAccount: async () => { - const existingAbortController = - get().staking.loadUnbondingTokensForCurrentAccountAbortController; - if (existingAbortController) existingAbortController.abort(); - const newAbortController = new AbortController(); - set(state => { - state.staking.loadUnbondingTokensForCurrentAccountAbortController = newAbortController; - }); - - const addressIndex = new AddressIndex({ account: get().staking.account }); - - set(state => { - state.staking.unbondingTokensByAccount.delete(addressIndex.account); - }); - - const responses = await Array.fromAsync( - viewClient.unbondingTokensByAddressIndex({ addressIndex }), - ); - const stakingTokenMetadata = await getStakingTokenMetadata(); - - const unbondingTokensForAccount = responses.reduce( - (acc, curr) => toUnbondingTokensForAccount(acc, curr, stakingTokenMetadata), - { - claimable: { total: zeroValueView(stakingTokenMetadata), tokens: [] }, - notYetClaimable: { total: zeroValueView(stakingTokenMetadata), tokens: [] }, - }, - ); - - set(state => { - state.staking.unbondingTokensByAccount.set(addressIndex.account, unbondingTokensForAccount); - }); - }, - loadAndReduceBalances: async () => { - const balancesByAccount = await getBalancesByAccount(); - - const stakingTokenMetadata = await getStakingTokenMetadata(); - - // It's slightly inefficient to reduce over an array twice, rather than - // combining the reducers into one. But this is much more readable; and - // anyway, `balancesByAccount` will be a single-item array for the vast - // majority of users. - const unstakedTokensByAccount = balancesByAccount.reduce( - (acc: Map, cur: BalancesByAccount) => - toUnstakedTokensByAccount(acc, cur, stakingTokenMetadata), - new Map(), - ); - const accountSwitcherFilter = balancesByAccount.reduce( - (acc: number[], cur: BalancesByAccount) => - toAccountSwitcherFilter(acc, cur, stakingTokenMetadata), - [], - ); - set(state => { - state.staking.unstakedTokensByAccount = unstakedTokensByAccount; - state.staking.accountSwitcherFilter = accountSwitcherFilter; - }); - }, - delegate: async () => { - try { - const stakingTokenMetadata = await getStakingTokenMetadata(); - - const req = assembleDelegateRequest(get().staking, stakingTokenMetadata); - - // Reset form _after_ building the transaction planner request, since it depends on - // the state. - set(state => { - state.staking.action = undefined; - state.staking.validatorInfo = undefined; - }); - - await planBuildBroadcast('delegate', req); - - // Reload delegation tokens and unstaked tokens to reflect their updated - // balances. - void get().staking.loadDelegationsForCurrentAccount(); - void get().staking.loadAndReduceBalances(); - } finally { - set(state => { - state.staking.amount = ''; - }); - } - }, - undelegate: async () => { - try { - const req = assembleUndelegateRequest(get().staking); - - // Reset form _after_ assembling the transaction planner request, since it - // depends on the state. - set(state => { - state.staking.action = undefined; - state.staking.validatorInfo = undefined; - }); - - await planBuildBroadcast('undelegate', req); - - // Reload delegation tokens and unstaked tokens to reflect their updated - // balances. - void get().staking.loadDelegationsForCurrentAccount(); - void get().staking.loadAndReduceBalances(); - void get().staking.loadUnbondingTokensForCurrentAccount(); - } finally { - set(state => { - state.staking.amount = ''; - }); - } - }, - undelegateClaim: async () => { - const { account, unbondingTokensByAccount } = get().staking; - const unbondingTokens = unbondingTokensByAccount.get(account)?.claimable.tokens; - if (!unbondingTokens) return; - - try { - const req = await assembleUndelegateClaimRequest({ account, unbondingTokens }); - if (!req) return; - - await planBuildBroadcast('undelegateClaim', req); - - // Reset form _after_ assembling the transaction planner request, since it - // depends on the state. - set(state => { - state.staking.action = undefined; - state.staking.validatorInfo = undefined; - }); - - // Reload unbonding tokens and unstaked tokens to reflect their updated - // balances. - void get().staking.loadAndReduceBalances(); - void get().staking.loadUnbondingTokensForCurrentAccount(); - } finally { - set(state => { - state.staking.amount = ''; - }); - } - }, - loading: false, - error: undefined, - votingPowerByValidatorInfo: {}, -}); - -const assembleDelegateRequest = ( - { account, amount, validatorInfo }: StakingSlice, - stakingAssetMetadata: Metadata, -) => { - return new TransactionPlannerRequest({ - delegations: [ - { - amount: toBaseUnit(BigNumber(amount), getDisplayDenomExponent(stakingAssetMetadata)), - rateData: getRateData(validatorInfo), - }, - ], - source: { account }, - }); -}; - -const assembleUndelegateRequest = ({ - account, - amount, - delegationsByAccount, - validatorInfo, -}: StakingSlice) => { - const delegation = delegationsByAccount - .get(account) - ?.find(delegation => isDelegationTokenForValidator(delegation, validatorInfo!)); - if (!delegation) - throw new Error('Tried to assemble undelegate request from account with no delegation tokens'); - - return new TransactionPlannerRequest({ - undelegations: [ - { - rateData: getRateData(validatorInfo), - value: { - amount: toBaseUnit(BigNumber(amount), getDisplayDenomExponentFromValueView(delegation)), - assetId: getAssetIdFromValueView(delegation), - }, - }, - ], - source: { account }, - }); -}; - -/** - * Function to use with `reduce()` over an array of `BalancesByAccount` objects. - * Returns a map of accounts to `ValueView`s of the staking token. - */ -const toUnstakedTokensByAccount = ( - unstakedTokensByAccount: Map, - curr: BalancesByAccount, - stakingTokenMetadata: Metadata, -) => { - const stakingTokenBalance = curr.balances.find( - ({ balanceView }) => getDisplayDenomFromView(balanceView) === stakingTokenMetadata.display, - ); - - if (stakingTokenBalance?.balanceView) { - unstakedTokensByAccount.set(curr.account, stakingTokenBalance.balanceView); - } - - return unstakedTokensByAccount; -}; - -/** - * Function to use with `reduce()` over an array of `BalancesByAccount` objects. - * Reduces to an array of accounts that have relevant balances for staking. - */ -const toAccountSwitcherFilter = ( - accountSwitcherFilter: number[], - curr: BalancesByAccount, - stakingTokenMetadata: Metadata, -) => { - const isRelevantAccount = curr.balances.some(({ balanceView }) => { - const displayDenom = getDisplayDenomFromView(balanceView); - - return ( - assetPatterns.delegationToken.matches(displayDenom) || - assetPatterns.unbondingToken.matches(displayDenom) || - displayDenom === stakingTokenMetadata.display - ); - }); - - if (isRelevantAccount) return [...accountSwitcherFilter, curr.account]; - return accountSwitcherFilter; -}; - -/** - * Function to use with `reduce()` over an array of `BalancesByAccount` objects. - * Returns a map of accounts to `ValueView`s of the staking token. - */ -const toUnbondingTokensForAccount = ( - unbondingTokensForAccount: UnbondingTokensForAccount, - curr: UnbondingTokensByAddressIndexResponse, - stakingTokenMetadata: Metadata, -): UnbondingTokensForAccount => { - const valueView = getValueViewFromUnbondingTokensByAddressIndexResponse(curr); - - if (curr.claimable) { - unbondingTokensForAccount.claimable.tokens.push(valueView); - } else { - unbondingTokensForAccount.notYetClaimable.tokens.push(valueView); - } - - const claimableTotal = unbondingTokensForAccount.claimable.tokens.reduce( - (prev, curr) => prev + joinLoHiAmount(getAmount(curr)), - 0n, - ); - - unbondingTokensForAccount.claimable.total = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: splitLoHi(claimableTotal), - metadata: stakingTokenMetadata, - }, - }, - }); - - const notYetClaimableTotal = unbondingTokensForAccount.notYetClaimable.tokens.reduce( - (prev, curr) => prev + joinLoHiAmount(getAmount(curr)), - 0n, - ); - - unbondingTokensForAccount.notYetClaimable.total = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: splitLoHi(notYetClaimableTotal), - metadata: stakingTokenMetadata, - }, - }, - }); - - return unbondingTokensForAccount; -}; diff --git a/apps/minifront/src/state/status.ts b/apps/minifront/src/state/status.ts deleted file mode 100644 index 3fb111ab..00000000 --- a/apps/minifront/src/state/status.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ZQueryState } from '@penumbra-zone/zquery/src/types'; -import { SliceCreator, useStore } from '.'; -import { createZQuery } from '@penumbra-zone/zquery'; -import { getStatusStream } from '../fetchers/status'; - -interface Status { - fullSyncHeight?: bigint; - latestKnownBlockHeight?: bigint; -} - -export const { status, useStatus } = createZQuery({ - name: 'status', - fetch: getStatusStream, - stream: (_prevState: Status | undefined, item: Status): Status => ({ - fullSyncHeight: item.fullSyncHeight, - latestKnownBlockHeight: item.latestKnownBlockHeight, - }), - getUseStore: () => useStore, - get: state => state.status.status, - set: setter => { - const newState = setter(useStore.getState().status.status); - useStore.setState(state => { - state.status.status = newState; - }); - }, -}); - -export interface StatusSlice { - status: ZQueryState<{ - fullSyncHeight?: bigint; - latestKnownBlockHeight?: bigint; - }>; -} - -export const createStatusSlice = (): SliceCreator => () => ({ - status, -}); diff --git a/apps/minifront/src/state/swap/constants.test.ts b/apps/minifront/src/state/swap/constants.test.ts deleted file mode 100644 index 13c31c54..00000000 --- a/apps/minifront/src/state/swap/constants.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { GDA_RECIPES, STEP_COUNT } from './constants'; - -const isCleanlyDivisible = (numerator: bigint, denominator: bigint): boolean => - Number(numerator) % Number(denominator) === 0; - -/** - * "Why are we testing a constants file?!" Good question! - * - * Each duration for a sub-auction needs to be cleanly divisible by the step - * count so that sub-auctions can be evenly distributed. If an unsuspecting - * developer changes some of the constants in `./constants.ts` in the future, - * there could be durations that aren't cleanly divisible by the step count. So - * this test suite ensures that that case never happens. - */ -describe('`GDA_RECIPES` and `STEP_COUNT`', () => { - test('every sub-auction option is cleanly divisible by `STEP_COUNT`', () => { - Object.values(GDA_RECIPES).forEach(recipe => { - expect(isCleanlyDivisible(recipe.subAuctionDurationInBlocks, STEP_COUNT)).toBe(true); - }); - }); -}); diff --git a/apps/minifront/src/state/swap/constants.ts b/apps/minifront/src/state/swap/constants.ts deleted file mode 100644 index dfb48795..00000000 --- a/apps/minifront/src/state/swap/constants.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { BLOCKS_PER_MINUTE, BLOCKS_PER_HOUR } from '../constants'; - -export const DURATION_OPTIONS = [ - 'instant', - '10min', - '30min', - '1h', - '2h', - '6h', - '12h', - '24h', - '48h', -] as const; -export type DurationOption = (typeof DURATION_OPTIONS)[number]; - -export const STEP_COUNT = 60n; - -/** - * When a user creates an auction in minifront, minifront actually creates a - * series of "sub-auctions", rather than creating a single auction with the - * entirety of the user's input funds. This minimizes the user's exposure to - * price fluctuations. The user's input and output amounts are equally divided - * into the number of sub-auctions, and the sub-auctions are spaced apart to - * take up roughly the duration that the user specifies (although they're spaced - * with some randomness to avoid having too many auctions on the same blocks). - */ -export interface GdaRecipe { - /** - * The target overall duration of _all_ sub-auctions, from the start height of - * the first to the end height of the last. - * - * Note that the actual duration won't be exactly equal to this due to the - * randomness of a Poisson distribution. - */ - durationInBlocks: bigint; - /** Used to generate the poisson distribution of sub-auctions. */ - poissonIntensityPerBlock: number; - /** The number of sub-auctions for a given overall duration. */ - numberOfSubAuctions: bigint; - /** The duration of each sub-auction for a given overall duration. */ - subAuctionDurationInBlocks: bigint; -} - -/** - * Configuration parameters for generating sub-auctions based on the user's - * desired duration. - */ -export const GDA_RECIPES: Record, GdaRecipe> = { - '10min': { - durationInBlocks: 10n * BLOCKS_PER_MINUTE, - poissonIntensityPerBlock: 0.0645833333333, - numberOfSubAuctions: 4n, - subAuctionDurationInBlocks: 60n, - }, - '30min': { - durationInBlocks: 30n * BLOCKS_PER_MINUTE, - poissonIntensityPerBlock: 0.05058333333, - numberOfSubAuctions: 12n, - subAuctionDurationInBlocks: 60n, - }, - '1h': { - durationInBlocks: BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.02525, - numberOfSubAuctions: 12n, - subAuctionDurationInBlocks: 120n, - }, - '2h': { - durationInBlocks: 2n * BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.02266666666, - numberOfSubAuctions: 24n, - subAuctionDurationInBlocks: 120n, - }, - '6h': { - durationInBlocks: 6n * BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.01075, - numberOfSubAuctions: 36n, - subAuctionDurationInBlocks: 240n, - }, - '12h': { - durationInBlocks: 12n * BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.0053333333, - numberOfSubAuctions: 36n, - subAuctionDurationInBlocks: 480n, - }, - '24h': { - durationInBlocks: 24n * BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.035, - numberOfSubAuctions: 48n, - subAuctionDurationInBlocks: 720n, - }, - '48h': { - durationInBlocks: 48n * BLOCKS_PER_HOUR, - poissonIntensityPerBlock: 0.00175, - numberOfSubAuctions: 48n, - subAuctionDurationInBlocks: 1440n, - }, -}; diff --git a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.test.ts b/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.test.ts deleted file mode 100644 index 52d9d0f3..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { assembleScheduleRequest } from './assemble-schedule-request'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; - -const MOCK_START_HEIGHT = vi.hoisted(() => 1234n); - -const mockViewClient = vi.hoisted(() => ({ - status: () => Promise.resolve({ fullSyncHeight: MOCK_START_HEIGHT }), -})); - -vi.mock('../../../clients', () => ({ - viewClient: mockViewClient, -})); - -const metadata = new Metadata({ - base: 'uasset', - display: 'asset', - denomUnits: [ - { - denom: 'uasset', - exponent: 0, - }, - { - denom: 'asset', - exponent: 6, - }, - ], - penumbraAssetId: {}, -}); - -const balancesResponse = new BalancesResponse({ - balanceView: { - valueView: { - case: 'knownAssetId', - value: { - metadata, - }, - }, - }, - accountAddress: { - addressView: { - case: 'decoded', - value: { - index: { - account: 1234, - }, - }, - }, - }, -}); - -const ARGS: Parameters[0] = { - amount: '123', - duration: '10min', - minOutput: '1', - maxOutput: '1000', - assetIn: balancesResponse, - assetOut: metadata, -}; - -describe('assembleScheduleRequest()', () => { - it('uses the correct source for the transaction', async () => { - const req = await assembleScheduleRequest(ARGS); - - expect(req.source).toEqual(new AddressIndex({ account: 1234 })); - }); -}); diff --git a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts b/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts deleted file mode 100644 index d71dc94b..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/assemble-schedule-request.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { DutchAuctionSlice } from '.'; -import { getSubAuctions } from './get-sub-auctions'; -import { getAddressIndex } from '@penumbra-zone/getters/balances-response'; -import { SwapSlice } from '..'; - -export const assembleScheduleRequest = async ({ - amount, - assetIn, - assetOut, - minOutput, - maxOutput, - duration, -}: Pick & - Pick): Promise => { - const source = getAddressIndex.optional()(assetIn); - - return new TransactionPlannerRequest({ - dutchAuctionScheduleActions: await getSubAuctions({ - amount, - assetIn, - assetOut, - minOutput, - maxOutput, - duration, - }), - source, - }); -}; diff --git a/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.test.ts b/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.test.ts deleted file mode 100644 index 88f3ae8f..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.test.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { getSubAuctions } from './get-sub-auctions'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; - -const MOCK_START_HEIGHT = vi.hoisted(() => 1234n); - -const mockViewClient = vi.hoisted(() => ({ - status: () => Promise.resolve({ fullSyncHeight: MOCK_START_HEIGHT }), -})); - -vi.mock('../../../clients', () => ({ - viewClient: mockViewClient, -})); - -describe('getSubAuctions()', () => { - const inputAssetMetadata = new Metadata({ - display: 'input', - base: 'input', - denomUnits: [ - { denom: 'uinput', exponent: 0 }, - { denom: 'input', exponent: 6 }, - ], - penumbraAssetId: { inner: new Uint8Array([1]) }, - }); - const outputAssetMetadata = new Metadata({ - display: 'output', - base: 'uoutput', - denomUnits: [ - { denom: 'moutput', exponent: 0 }, - { denom: 'output', exponent: 3 }, - ], - penumbraAssetId: { inner: new Uint8Array([2]) }, - }); - - const ARGS = { - amount: '123', - duration: '10min', - maxOutput: '1000', - minOutput: '1', - assetIn: new BalancesResponse({ - balanceView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 1234n, lo: 0n }, - metadata: inputAssetMetadata, - }, - }, - }, - }), - assetOut: outputAssetMetadata, - } satisfies Parameters[0]; - - it('uses a step count of 60', async () => { - expect.hasAssertions(); - - const subAuctions = await getSubAuctions(ARGS); - - subAuctions.forEach(auction => { - expect(auction.description?.stepCount).toBe(60n); - }); - }); - - it('correctly divides the input across sub-auctions, using the display denom exponent', async () => { - expect.assertions(4); - - const subAuctions = await getSubAuctions({ ...ARGS, duration: '10min', amount: '100' }); - - subAuctions.forEach(subAuction => { - expect(subAuction.description?.input?.amount).toEqual( - new Amount({ hi: 0n, lo: 25_000_000n }), - ); - }); - }); - - it('rounds down to the nearest whole number input', async () => { - expect.assertions(4); - - const subAuctions = await getSubAuctions({ ...ARGS, duration: '10min', amount: '2.666666' }); - - subAuctions.forEach(subAuction => { - expect(subAuction.description?.input?.amount).toEqual(new Amount({ hi: 0n, lo: 666_666n })); - }); - }); - - it('correctly divides the min/max outputs across sub-auctions, using the display denom exponent', async () => { - expect.assertions(8); - - const subAuctions = await getSubAuctions({ - ...ARGS, - duration: '10min', - minOutput: '1', - maxOutput: '10', - }); - - subAuctions.forEach(subAuction => { - expect(subAuction.description?.minOutput).toEqual(new Amount({ hi: 0n, lo: 250n })); - expect(subAuction.description?.maxOutput).toEqual(new Amount({ hi: 0n, lo: 2_500n })); - }); - }); - - it('rounds down to the nearest whole number output', async () => { - expect.assertions(8); - - const subAuctions = await getSubAuctions({ - ...ARGS, - duration: '10min', - maxOutput: '10.666', - minOutput: '2.666', - }); - - subAuctions.forEach(subAuction => { - expect(subAuction.description?.minOutput).toEqual(new Amount({ hi: 0n, lo: 666n })); - expect(subAuction.description?.maxOutput).toEqual(new Amount({ hi: 0n, lo: 2_666n })); - }); - }); - - it("doesn't choke when the user enters too many decimal places for the given asset type", async () => { - expect.assertions(12); - - const subAuctions = await getSubAuctions({ - ...ARGS, - duration: '10min', - amount: '2.666666666666666666666666666666', - maxOutput: '10.666666666666666666666666666666', - minOutput: '2.666666666666666666666666666666', - }); - - subAuctions.forEach(subAuction => { - expect(subAuction.description?.input?.amount).toEqual(new Amount({ hi: 0n, lo: 666_666n })); - expect(subAuction.description?.minOutput).toEqual(new Amount({ hi: 0n, lo: 666n })); - expect(subAuction.description?.maxOutput).toEqual(new Amount({ hi: 0n, lo: 2_666n })); - }); - }); -}); diff --git a/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.ts b/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.ts deleted file mode 100644 index 30068aea..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/get-sub-auctions.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { TransactionPlannerRequest_ActionDutchAuctionSchedule } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { getAssetId, getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, -} from '@penumbra-zone/getters/value-view'; -import { divideAmounts, fromString } from '@penumbra-zone/types/amount'; -import { DutchAuctionSlice } from '.'; -import { viewClient } from '../../../clients'; -import { GDA_RECIPES, GdaRecipe, STEP_COUNT } from '../constants'; -import { BLOCKS_PER_MINUTE } from '../../constants'; -import { timeUntilNextEvent } from './time-until-next-event'; -import { splitLoHi } from '@penumbra-zone/types/lo-hi'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { BigNumber } from 'bignumber.js'; -import { SwapSlice } from '..'; - -/** - * The start height of an auction must be, at minimum, the current block height. - * Since the transaction may take a while to build, and the user may take a - * while to approve it, we need to add some buffer time to the start height. - * Roughly a minute seems appropriate. - */ -const getStartHeight = (fullSyncHeight: bigint) => fullSyncHeight + BLOCKS_PER_MINUTE; - -/** - * Sub-auctions are spaced apart using a Poisson distribution to ensure random - * yet statistically predictable spacing. This means that sub-auctions will have - * some overlap, and prevents too many users' auctions from starting/ending on - * identical block heights. - */ -const getSubAuctionStartHeights = (overallStartHeight: bigint, recipe: GdaRecipe): bigint[] => { - const startHeights: bigint[] = []; - const lambda = recipe.poissonIntensityPerBlock; - let currentHeight = overallStartHeight; - - for (let i = 0n; i < recipe.numberOfSubAuctions; i++) { - const fastForwardClock = timeUntilNextEvent(lambda); - currentHeight += BigInt(Math.ceil(fastForwardClock)); - startHeights.push(currentHeight); - } - - return startHeights; -}; - -export const getSubAuctions = async ({ - amount: amountAsString, - assetIn, - assetOut, - minOutput, - maxOutput, - duration, -}: Pick & - Pick): Promise< - TransactionPlannerRequest_ActionDutchAuctionSchedule[] -> => { - if (duration === 'instant') return []; - const inputAssetId = getAssetIdFromValueView(assetIn?.balanceView); - const outputAssetId = getAssetId(assetOut); - const assetInExponent = getDisplayDenomExponentFromValueView(assetIn?.balanceView); - const assetOutExponent = getDisplayDenomExponent(assetOut); - const inputAmount = fromString(amountAsString, assetInExponent); - const minOutputAmount = fromString(minOutput, assetOutExponent); - const maxOutputAmount = fromString(maxOutput, assetOutExponent); - - const recipe = GDA_RECIPES[duration]; - - const scaledInputAmount = splitLoHi( - BigInt( - divideAmounts(inputAmount, new Amount(splitLoHi(recipe.numberOfSubAuctions))) - .decimalPlaces(0, BigNumber.ROUND_FLOOR) - .toString(), - ), - ); - - const scaledMinOutputAmount = splitLoHi( - BigInt( - divideAmounts(minOutputAmount, new Amount(splitLoHi(recipe.numberOfSubAuctions))) - .decimalPlaces(0, BigNumber.ROUND_FLOOR) - .toString(), - ), - ); - - const scaledMaxOutputAmount = splitLoHi( - BigInt( - divideAmounts(maxOutputAmount, new Amount(splitLoHi(recipe.numberOfSubAuctions))) - .decimalPlaces(0, BigNumber.ROUND_FLOOR) - .toString(), - ), - ); - - const { fullSyncHeight } = await viewClient.status({}); - - const overallStartHeight = getStartHeight(fullSyncHeight); - - return getSubAuctionStartHeights(overallStartHeight, recipe).map(startHeight => { - return new TransactionPlannerRequest_ActionDutchAuctionSchedule({ - description: { - startHeight, - endHeight: startHeight + recipe.subAuctionDurationInBlocks, - input: { amount: scaledInputAmount, assetId: inputAssetId }, - outputId: outputAssetId, - stepCount: STEP_COUNT, - minOutput: scaledMinOutputAmount, - maxOutput: scaledMaxOutputAmount, - }, - }); - }); -}; diff --git a/apps/minifront/src/state/swap/dutch-auction/index.test.ts b/apps/minifront/src/state/swap/dutch-auction/index.test.ts deleted file mode 100644 index 59f6095e..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/index.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { StoreApi, UseBoundStore, create } from 'zustand'; -import { AllSlices, initializeStore } from '../..'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; - -const mockSimulationClient = vi.hoisted(() => ({ - simulateTrade: vi.fn(), -})); - -vi.mock('../../../clients', () => ({ - simulationClient: mockSimulationClient, -})); - -describe('Dutch auction slice', () => { - let useStore: UseBoundStore>; - - beforeEach(() => { - useStore = create()(initializeStore()) as UseBoundStore>; - }); - - describe('estimate()', () => { - beforeEach(() => { - mockSimulationClient.simulateTrade.mockResolvedValue({ - output: { - output: { - amount: { - hi: 0n, - lo: 222n, - }, - }, - }, - }); - - useStore.setState(state => ({ - ...state, - swap: { - ...state.swap, - assetIn: new BalancesResponse({ - balanceView: { - valueView: { - case: 'knownAssetId', - value: { - amount: { - hi: 0n, - lo: 123n, - }, - metadata: { - base: 'upenumbra', - display: 'penumbra', - denomUnits: [{ denom: 'upenumbra' }, { denom: 'penumbra', exponent: 6 }], - penumbraAssetId: { inner: new Uint8Array([1]) }, - }, - }, - }, - }, - }), - assetOut: new Metadata({ - base: 'ugm', - display: 'gm', - denomUnits: [{ denom: 'ugm' }, { denom: 'gm', exponent: 6 }], - penumbraAssetId: { inner: new Uint8Array([2]) }, - }), - }, - })); - }); - - it('sets `maxOutput` to twice the estimated market price', async () => { - await useStore.getState().swap.dutchAuction.estimate(); - - expect(useStore.getState().swap.dutchAuction.maxOutput).toEqual('0.000444'); - }); - - it('sets `minOutput` to half the estimated market price', async () => { - await useStore.getState().swap.dutchAuction.estimate(); - - expect(useStore.getState().swap.dutchAuction.minOutput).toEqual('0.000111'); - }); - }); -}); diff --git a/apps/minifront/src/state/swap/dutch-auction/index.ts b/apps/minifront/src/state/swap/dutch-auction/index.ts deleted file mode 100644 index 5da2e2c0..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/index.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { SliceCreator, useStore } from '../..'; -import { planBuildBroadcast } from '../../helpers'; -import { assembleScheduleRequest } from './assemble-schedule-request'; -import { AuctionId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb'; -import { sendSimulateTradeRequest } from '../helpers'; -import { fromBaseUnitAmount, multiplyAmountByNumber } from '@penumbra-zone/types/amount'; -import { getDisplayDenomExponent } from '@penumbra-zone/getters/metadata'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { errorToast } from '@penumbra-zone/ui/lib/toast/presets'; -import { ZQueryState, createZQuery } from '@penumbra-zone/zquery'; -import { AuctionInfo, getAuctionInfos } from '../../../fetchers/auction-infos'; - -/** - * Multipliers to use with the output of the swap simulation, to determine - * reasonable maximum and minimimum outputs for the auction. - */ -const MAX_OUTPUT_ESTIMATE_MULTIPLIER = 2; -const MIN_OUTPUT_ESTIMATE_MULTIPLIER = 0.5; - -export type Filter = 'active' | 'upcoming' | 'all'; - -interface Actions { - setMinOutput: (minOutput: string) => void; - setMaxOutput: (maxOutput: string) => void; - onSubmit: () => Promise; - endAuction: (auctionId: AuctionId) => Promise; - withdraw: (auctionId: AuctionId, currentSeqNum: bigint) => Promise; - reset: VoidFunction; - setFilter: (filter: Filter) => void; - estimate: () => Promise; -} - -interface State { - minOutput: string; - maxOutput: string; - txInProgress: boolean; - auctionInfos: ZQueryState>; - filter: Filter; - estimateLoading: boolean; - estimatedOutput?: Amount; -} - -export const { auctionInfos, useAuctionInfos, useRevalidateAuctionInfos } = createZQuery({ - name: 'auctionInfos', - fetch: getAuctionInfos, - stream: (prevState: AuctionInfo[] | undefined, auctionInfo: AuctionInfo) => [ - ...(prevState ?? []), - auctionInfo, - ], - getUseStore: () => useStore, - set: setter => { - const newState = setter(useStore.getState().swap.dutchAuction.auctionInfos); - useStore.setState(state => { - state.swap.dutchAuction.auctionInfos = newState; - }); - }, - get: state => state.swap.dutchAuction.auctionInfos, -}); - -export type DutchAuctionSlice = Actions & State; - -const INITIAL_STATE: State = { - minOutput: '1', - maxOutput: '1000', - txInProgress: false, - auctionInfos, - filter: 'active', - estimateLoading: false, - estimatedOutput: undefined, -}; - -export const createDutchAuctionSlice = (): SliceCreator => (set, get) => ({ - ...INITIAL_STATE, - setMinOutput: minOutput => { - set(({ swap }) => { - swap.dutchAuction.minOutput = minOutput; - swap.dutchAuction.estimatedOutput = undefined; - }); - }, - setMaxOutput: maxOutput => { - set(({ swap }) => { - swap.dutchAuction.maxOutput = maxOutput; - swap.dutchAuction.estimatedOutput = undefined; - }); - }, - - onSubmit: async () => { - set(({ swap }) => { - swap.dutchAuction.txInProgress = true; - }); - - try { - const req = await assembleScheduleRequest({ - ...get().swap.dutchAuction, - amount: get().swap.amount, - assetIn: get().swap.assetIn, - assetOut: get().swap.assetOut, - duration: get().swap.duration, - }); - await planBuildBroadcast('dutchAuctionSchedule', req); - - get().swap.setAmount(''); - get().swap.dutchAuction.auctionInfos.revalidate(); - } finally { - set(state => { - state.swap.dutchAuction.txInProgress = false; - }); - } - }, - - endAuction: async auctionId => { - const req = new TransactionPlannerRequest({ dutchAuctionEndActions: [{ auctionId }] }); - await planBuildBroadcast('dutchAuctionEnd', req); - get().swap.dutchAuction.auctionInfos.revalidate(); - }, - - withdraw: async (auctionId, currentSeqNum) => { - const req = new TransactionPlannerRequest({ - dutchAuctionWithdrawActions: [{ auctionId, seq: currentSeqNum + 1n }], - }); - await planBuildBroadcast('dutchAuctionWithdraw', req); - get().swap.dutchAuction.auctionInfos.revalidate(); - }, - - reset: () => - set(({ swap }) => { - swap.dutchAuction = { - ...swap.dutchAuction, - ...INITIAL_STATE, - - // preserve loaded auctions and filter: - auctionInfos: swap.dutchAuction.auctionInfos, - filter: swap.dutchAuction.filter, - }; - }), - - setFilter: filter => { - set(({ swap }) => { - swap.dutchAuction.filter = filter; - }); - }, - - estimate: async () => { - try { - set(({ swap }) => { - swap.dutchAuction.estimateLoading = true; - }); - - const res = await sendSimulateTradeRequest(get().swap); - const estimatedOutputAmount = res.output?.output?.amount; - - if (estimatedOutputAmount) { - const assetOut = get().swap.assetOut; - const exponent = getDisplayDenomExponent(assetOut); - - set(({ swap }) => { - swap.dutchAuction.maxOutput = fromBaseUnitAmount( - multiplyAmountByNumber(estimatedOutputAmount, MAX_OUTPUT_ESTIMATE_MULTIPLIER), - exponent, - ).toString(); - swap.dutchAuction.minOutput = fromBaseUnitAmount( - multiplyAmountByNumber(estimatedOutputAmount, MIN_OUTPUT_ESTIMATE_MULTIPLIER), - exponent, - ).toString(); - swap.dutchAuction.estimatedOutput = estimatedOutputAmount; - }); - } - } catch (e) { - errorToast(e, 'Error estimating swap').render(); - } finally { - set(({ swap }) => { - swap.dutchAuction.estimateLoading = false; - }); - } - }, -}); diff --git a/apps/minifront/src/state/swap/dutch-auction/time-until-next-event.ts b/apps/minifront/src/state/swap/dutch-auction/time-until-next-event.ts deleted file mode 100644 index 45c0f296..00000000 --- a/apps/minifront/src/state/swap/dutch-auction/time-until-next-event.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const timeUntilNextEvent = (lambda: number): number => { - const sampleNextEventTime = () => { - // This is modeling a - return Math.abs(Math.log(Math.random()) / lambda); - }; - - return sampleNextEventTime(); -}; diff --git a/apps/minifront/src/state/swap/helpers.ts b/apps/minifront/src/state/swap/helpers.ts deleted file mode 100644 index 887c207b..00000000 --- a/apps/minifront/src/state/swap/helpers.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Value } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { - CandlestickData, - SimulateTradeRequest, - SimulateTradeResponse, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { getAssetId } from '@penumbra-zone/getters/metadata'; -import { - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, -} from '@penumbra-zone/getters/value-view'; -import { toBaseUnit } from '@penumbra-zone/types/lo-hi'; -import { BigNumber } from 'bignumber.js'; -import { SwapSlice } from '.'; -import { dexClient, simulationClient } from '../../clients'; -import { PriceHistorySlice } from './price-history'; - -export const sendSimulateTradeRequest = ({ - assetIn, - assetOut, - amount, -}: Pick): Promise => { - if (!assetIn || !assetOut) throw new Error('Both asset in and out need to be set'); - - const swapInValue = new Value({ - assetId: getAssetIdFromValueView(assetIn.balanceView), - amount: toBaseUnit( - BigNumber(amount || 0), - getDisplayDenomExponentFromValueView(assetIn.balanceView), - ), - }); - - const req = new SimulateTradeRequest({ - input: swapInValue, - output: getAssetId(assetOut), - }); - - return simulationClient.simulateTrade(req); -}; - -export const sendCandlestickDataRequest = async ( - { startMetadata, endMetadata }: Pick, - limit: bigint, - signal?: AbortSignal, -): Promise => { - const start = startMetadata?.penumbraAssetId; - const end = endMetadata?.penumbraAssetId; - - if (!start || !end) throw new Error('Asset pair incomplete'); - if (start.equals(end)) throw new Error('Asset pair equivalent'); - - try { - const { data } = await dexClient.candlestickData( - { - pair: { start, end }, - limit, - }, - { signal }, - ); - return data; - } catch (err) { - if (err instanceof Error && err.name === 'AbortError') return; - else throw err; - } -}; diff --git a/apps/minifront/src/state/swap/index.test.ts b/apps/minifront/src/state/swap/index.test.ts deleted file mode 100644 index ffd69fc5..00000000 --- a/apps/minifront/src/state/swap/index.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { create, StoreApi, UseBoundStore } from 'zustand'; -import { AllSlices, initializeStore } from '..'; -import { beforeEach, describe, expect, test } from 'vitest'; -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; - -describe('Swap Slice', () => { - const selectionExample = new BalancesResponse({ - balanceView: new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: new Amount({ - lo: 0n, - hi: 0n, - }), - metadata: new Metadata({ display: 'test_usd', denomUnits: [{ exponent: 18 }] }), - }, - }, - }), - accountAddress: new AddressView({ - addressView: { - case: 'opaque', - value: { - address: addressFromBech32m( - 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4', - ), - }, - }, - }), - }); - - let useStore: UseBoundStore>; - let registryAssets: Metadata[]; - - beforeEach(() => { - registryAssets = []; - useStore = create()(initializeStore()) as UseBoundStore>; - }); - - test('the defaults are correct', () => { - expect(useStore.getState().swap.amount).toBe(''); - expect(useStore.getState().swap.assetIn).toBeUndefined(); - expect(useStore.getState().swap.assetOut).toBeUndefined(); - expect(useStore.getState().swap.swappableAssets).toEqual([]); - expect(useStore.getState().swap.balancesResponses).toEqual([]); - expect(useStore.getState().swap.duration).toBe('instant'); - expect(useStore.getState().swap.txInProgress).toBe(false); - }); - - test('assetIn can be set', () => { - expect(useStore.getState().swap.assetIn).toBeUndefined(); - useStore.getState().swap.setAssetIn(selectionExample); - expect(useStore.getState().swap.assetIn).toBe(selectionExample); - }); - - test('assetOut can be set', () => { - expect(useStore.getState().swap.assetOut).toBeUndefined(); - useStore.getState().swap.setAssetOut(registryAssets[0]!); - expect(useStore.getState().swap.assetOut).toBe(registryAssets[0]); - }); - - test('amount can be set', () => { - expect(useStore.getState().swap.amount).toBe(''); - useStore.getState().swap.setAmount('22.44'); - expect(useStore.getState().swap.amount).toBe('22.44'); - }); - - test('changing assetIn clears simulation', () => { - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - useStore.setState(state => { - state.swap.instantSwap.simulateSwapResult = { - output: new ValueView(), - unfilled: new ValueView(), - priceImpact: undefined, - metadataByAssetId: {}, - }; - return state; - }); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeDefined(); - useStore.getState().swap.setAssetIn({} as BalancesResponse); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - }); - - test('changing assetOut clears simulation', () => { - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - useStore.setState(state => { - state.swap.instantSwap.simulateSwapResult = { - output: new ValueView(), - unfilled: new ValueView(), - priceImpact: undefined, - metadataByAssetId: {}, - }; - return state; - }); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeDefined(); - useStore.getState().swap.setAssetOut({} as Metadata); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - }); - - test('changing amount clears simulation', () => { - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - useStore.setState(state => { - state.swap.instantSwap.simulateSwapResult = { - output: new ValueView(), - unfilled: new ValueView(), - priceImpact: undefined, - metadataByAssetId: {}, - }; - return state; - }); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeDefined(); - useStore.getState().swap.setAmount('123'); - expect(useStore.getState().swap.instantSwap.simulateSwapResult).toBeUndefined(); - }); -}); diff --git a/apps/minifront/src/state/swap/index.ts b/apps/minifront/src/state/swap/index.ts deleted file mode 100644 index 17be8d8a..00000000 --- a/apps/minifront/src/state/swap/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { SwapExecution_Trace } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { SliceCreator } from '..'; -import { DurationOption } from './constants'; -import { DutchAuctionSlice, createDutchAuctionSlice } from './dutch-auction'; -import { InstantSwapSlice, createInstantSwapSlice } from './instant-swap'; -import { PriceHistorySlice, createPriceHistorySlice } from './price-history'; - -export interface SimulateSwapResult { - metadataByAssetId: Record; - output: ValueView; - priceImpact: number | undefined; - traces?: SwapExecution_Trace[]; - unfilled: ValueView; -} - -interface Actions { - setBalancesResponses: (balancesResponses: BalancesResponse[]) => void; - setSwappableAssets: (assets: Metadata[]) => void; - setAssetIn: (asset: BalancesResponse) => void; - setAmount: (amount: string) => void; - setAssetOut: (metadata: Metadata) => void; - setDuration: (duration: DurationOption) => void; - resetSubslices: VoidFunction; -} - -interface State { - balancesResponses: BalancesResponse[]; - swappableAssets: Metadata[]; - assetIn?: BalancesResponse; - amount: string; - assetOut?: Metadata; - duration: DurationOption; - txInProgress: boolean; -} - -interface Subslices { - dutchAuction: DutchAuctionSlice; - instantSwap: InstantSwapSlice; - priceHistory: PriceHistorySlice; -} - -const INITIAL_STATE: State = { - amount: '', - swappableAssets: [], - balancesResponses: [], - duration: 'instant', - txInProgress: false, -}; - -export type SwapSlice = Actions & State & Subslices; - -export const createSwapSlice = (): SliceCreator => (set, get, store) => ({ - ...INITIAL_STATE, - dutchAuction: createDutchAuctionSlice()(set, get, store), - instantSwap: createInstantSwapSlice()(set, get, store), - priceHistory: createPriceHistorySlice()(set, get, store), - setBalancesResponses: balancesResponses => { - set(state => { - state.swap.balancesResponses = balancesResponses; - }); - }, - setSwappableAssets: swappableAssets => { - set(state => { - state.swap.swappableAssets = swappableAssets; - }); - }, - setAssetIn: asset => { - get().swap.resetSubslices(); - set(({ swap }) => { - swap.assetIn = asset; - }); - }, - setAssetOut: metadata => { - get().swap.resetSubslices(); - set(({ swap }) => { - swap.assetOut = metadata; - }); - }, - setAmount: amount => { - get().swap.resetSubslices(); - set(({ swap }) => { - swap.amount = amount; - }); - }, - setDuration: duration => { - get().swap.resetSubslices(); - set(state => { - state.swap.duration = duration; - }); - }, - resetSubslices: () => { - get().swap.dutchAuction.reset(); - get().swap.instantSwap.reset(); - }, -}); diff --git a/apps/minifront/src/state/swap/instant-swap.ts b/apps/minifront/src/state/swap/instant-swap.ts deleted file mode 100644 index 95555a6d..00000000 --- a/apps/minifront/src/state/swap/instant-swap.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { SliceCreator } from '..'; -import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { planBuildBroadcast } from '../helpers'; -import { - AssetId, - Metadata, - Value, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { BigNumber } from 'bignumber.js'; -import { getAddressByIndex } from '../../fetchers/address'; -import { StateCommitment } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/crypto/tct/v1/tct_pb'; -import { errorToast } from '@penumbra-zone/ui/lib/toast/presets'; -import { - SwapExecution, - SwapExecution_Trace, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { viewClient } from '../../clients'; -import { - getAssetIdFromValueView, - getDisplayDenomExponentFromValueView, - getMetadata, -} from '@penumbra-zone/getters/value-view'; -import { getAssetId } from '@penumbra-zone/getters/metadata'; -import { getSwapCommitmentFromTx } from '@penumbra-zone/getters/transaction'; -import { getAddressIndex } from '@penumbra-zone/getters/address-view'; -import { toBaseUnit } from '@penumbra-zone/types/lo-hi'; -import { getAmountFromValue, getAssetIdFromValue } from '@penumbra-zone/getters/value'; -import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; -import { divideAmounts } from '@penumbra-zone/types/amount'; -import { bech32mAssetId } from '@penumbra-zone/bech32m/passet'; -import { SwapSlice } from '.'; -import { sendSimulateTradeRequest } from './helpers'; - -const getMetadataByAssetId = async ( - traces: SwapExecution_Trace[] = [], -): Promise> => { - const map: Record = {}; - - const promises = traces.flatMap(trace => - trace.value.map(async value => { - if (!value.assetId || map[bech32mAssetId(value.assetId)]) return; - - const { denomMetadata } = await viewClient.assetMetadataById({ assetId: value.assetId }); - - if (denomMetadata) { - map[bech32mAssetId(value.assetId)] = denomMetadata; - } - }), - ); - - await Promise.all(promises); - - return map; -}; - -export interface SimulateSwapResult { - output: ValueView; - unfilled: ValueView; - priceImpact: number | undefined; - traces?: SwapExecution_Trace[]; - metadataByAssetId: Record; -} - -interface Actions { - initiateSwapTx: () => Promise; - simulateSwap: () => Promise; - reset: VoidFunction; -} - -interface State { - txInProgress: boolean; - simulateSwapResult?: SimulateSwapResult; - simulateSwapLoading: boolean; -} - -export type InstantSwapSlice = Actions & State; - -const INITIAL_STATE: State = { - txInProgress: false, - simulateSwapLoading: false, - simulateSwapResult: undefined, -}; - -export const createInstantSwapSlice = (): SliceCreator => (set, get) => { - return { - ...INITIAL_STATE, - simulateSwap: async () => { - try { - set(({ swap }) => { - swap.instantSwap.simulateSwapLoading = true; - }); - - const res = await sendSimulateTradeRequest(get().swap); - - const output = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: res.output?.output?.amount, - metadata: get().swap.assetOut, - }, - }, - }); - - const unfilled = new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: res.unfilled?.amount, - metadata: getMetadata(get().swap.assetIn?.balanceView), - }, - }, - }); - - const metadataByAssetId = await getMetadataByAssetId(res.output?.traces); - - set(({ swap }) => { - swap.instantSwap.simulateSwapResult = { - output, - unfilled, - priceImpact: calculatePriceImpact(res.output), - traces: res.output?.traces, - metadataByAssetId, - }; - }); - } catch (e) { - errorToast(e, 'Error estimating swap').render(); - } finally { - set(({ swap }) => { - swap.instantSwap.simulateSwapLoading = false; - }); - } - }, - initiateSwapTx: async () => { - set(state => { - state.swap.instantSwap.txInProgress = true; - }); - - try { - const swapReq = await assembleSwapRequest(get().swap); - const swapTx = await planBuildBroadcast('swap', swapReq); - const swapCommitment = getSwapCommitmentFromTx(swapTx); - await issueSwapClaim(swapCommitment); - - set(state => { - state.swap.amount = ''; - }); - } finally { - set(state => { - state.swap.instantSwap.txInProgress = false; - }); - } - }, - reset: () => { - set(state => { - state.swap.instantSwap = { - ...state.swap.instantSwap, - ...INITIAL_STATE, - }; - }); - }, - }; -}; - -const assembleSwapRequest = async ({ - assetIn, - amount, - assetOut, -}: Pick) => { - if (!assetIn) throw new Error('`assetIn` was undefined'); - - const addressIndex = getAddressIndex(assetIn.accountAddress); - - return new TransactionPlannerRequest({ - swaps: [ - { - targetAsset: getAssetId(assetOut), - value: { - amount: toBaseUnit( - BigNumber(amount), - getDisplayDenomExponentFromValueView(assetIn.balanceView), - ), - assetId: getAssetIdFromValueView(assetIn.balanceView), - }, - claimAddress: await getAddressByIndex(addressIndex.account), - }, - ], - source: getAddressIndex(assetIn.accountAddress), - }); -}; - -// Swap claims don't need authenticationData, so `witnessAndBuild` is used. -// This way it won't trigger a second, unnecessary approval popup. -// @see https://protocol.penumbra.zone/main/zswap/swap.html#claiming-swap-outputs -export const issueSwapClaim = async (swapCommitment: StateCommitment) => { - const req = new TransactionPlannerRequest({ swapClaims: [{ swapCommitment }] }); - await planBuildBroadcast('swapClaim', req, { skipAuth: true }); -}; - -/* - Price impact is the change in price as a consequence of the trade's size. In SwapExecution, the \ - first trace in the array is the best execution for the swap. To calculate price impact, take - the price of the trade and see the % diff off the best execution trace. - */ -const calculatePriceImpact = (swapExec?: SwapExecution): number | undefined => { - if (!swapExec?.traces.length || !swapExec.output || !swapExec.input) return undefined; - - // Get the price of the estimate for the swap total - const inputAmount = getAmountFromValue(swapExec.input); - const outputAmount = getAmountFromValue(swapExec.output); - const swapEstimatePrice = divideAmounts(outputAmount, inputAmount); - - // Get the price in the best execution trace - const inputAssetId = getAssetIdFromValue(swapExec.input); - const outputAssetId = getAssetIdFromValue(swapExec.output); - const bestTrace = swapExec.traces[0]!; - const bestInputAmount = getMatchingAmount(bestTrace.value, inputAssetId); - const bestOutputAmount = getMatchingAmount(bestTrace.value, outputAssetId); - const bestTraceEstimatedPrice = divideAmounts(bestOutputAmount, bestInputAmount); - - // Difference = (priceB - priceA) / priceA - const percentDifference = swapEstimatePrice - .minus(bestTraceEstimatedPrice) - .div(bestTraceEstimatedPrice); - - return percentDifference.toNumber(); -}; - -const getMatchingAmount = (values: Value[], toMatch: AssetId): Amount => { - const match = values.find(v => toMatch.equals(v.assetId)); - if (!match?.amount) throw new Error('No match in values array found'); - - return match.amount; -}; diff --git a/apps/minifront/src/state/swap/price-history.ts b/apps/minifront/src/state/swap/price-history.ts deleted file mode 100644 index 2b5014cb..00000000 --- a/apps/minifront/src/state/swap/price-history.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { CandlestickData } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/dex/v1/dex_pb'; -import { getMetadataFromBalancesResponseOptional } from '@penumbra-zone/getters/balances-response'; -import { AllSlices, SliceCreator } from '..'; -import { sendCandlestickDataRequest } from './helpers'; - -interface Actions { - load: (ac?: AbortController) => AbortController['abort']; -} - -interface State { - candles: CandlestickData[]; - endMetadata?: Metadata; - startMetadata?: Metadata; -} - -export type PriceHistorySlice = Actions & State; - -const INITIAL_STATE: State = { - candles: [], -}; - -export const createPriceHistorySlice = (): SliceCreator => (set, get) => ({ - ...INITIAL_STATE, - load: (ac = new AbortController()): AbortController['abort'] => { - const { assetIn, assetOut } = get().swap; - const startMetadata = getMetadataFromBalancesResponseOptional(assetIn); - const endMetadata = assetOut; - void sendCandlestickDataRequest( - { startMetadata, endMetadata }, - // there's no UI to set limit yet, and most ranges don't always happen to - // include price records. 2500 at least scales well when there is data - 2500n, - ac.signal, - ).then(data => { - if (data) - set(({ swap }) => { - swap.priceHistory.startMetadata = startMetadata; - swap.priceHistory.endMetadata = endMetadata; - swap.priceHistory.candles = data; - }); - }); - - return () => ac.abort('Returned slice abort'); - }, -}); - -export const priceHistorySelector = (state: AllSlices) => state.swap.priceHistory; diff --git a/apps/minifront/src/state/transactions.ts b/apps/minifront/src/state/transactions.ts deleted file mode 100644 index 83da8a92..00000000 --- a/apps/minifront/src/state/transactions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; -import { SliceCreator } from '.'; -import { viewClient } from '../clients'; -import { getTransactionClassificationLabel } from '@penumbra-zone/perspective/transaction/classify'; - -export interface TransactionSummary { - height: number; - hash: string; - description: string; -} - -export interface TransactionsSlice { - summaries: TransactionSummary[]; - loadSummaries: () => Promise; -} - -export const createTransactionsSlice = (): SliceCreator => (set, get) => ({ - summaries: [], - - loadSummaries: async () => { - set(state => { - state.transactions.summaries = []; - }); - - for await (const tx of viewClient.transactionInfo({})) { - const summary = { - height: Number(tx.txInfo?.height ?? 0n), - hash: tx.txInfo?.id?.inner ? uint8ArrayToHex(tx.txInfo.id.inner) : 'unknown', - description: getTransactionClassificationLabel(tx.txInfo?.view), - }; - - const summaries = [...get().transactions.summaries, summary].sort( - (a, b) => b.height - a.height, - ); - - set(state => { - state.transactions.summaries = summaries; - }); - } - }, -}); diff --git a/apps/minifront/src/state/unclaimed-swaps.test.ts b/apps/minifront/src/state/unclaimed-swaps.test.ts deleted file mode 100644 index d0fd6994..00000000 --- a/apps/minifront/src/state/unclaimed-swaps.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, test } from 'vitest'; -import { useStore } from '.'; - -describe('Unclaimed Swaps Slice', () => { - test('the default in progress list is empty', () => { - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(0); - }); - - test('adding to status list works as expected', () => { - const setStatus = useStore.getState().unclaimedSwaps.setProgressStatus; - setStatus('add', '123'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(1); - setStatus('add', '456'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(2); - expect(useStore.getState().unclaimedSwaps.isInProgress('123')).toBeTruthy(); - expect(useStore.getState().unclaimedSwaps.isInProgress('456')).toBeTruthy(); - }); - - test('adds only once', () => { - const setStatus = useStore.getState().unclaimedSwaps.setProgressStatus; - setStatus('add', '123'); - setStatus('add', '123'); - setStatus('add', '123'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(1); - }); - - test('removing from list works', () => { - const setStatus = useStore.getState().unclaimedSwaps.setProgressStatus; - setStatus('add', '123'); - setStatus('add', '456'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(2); - setStatus('remove', '123'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(1); - expect(useStore.getState().unclaimedSwaps.isInProgress('123')).toBeFalsy(); - expect(useStore.getState().unclaimedSwaps.isInProgress('456')).toBeTruthy(); - }); - - test('removing multiple times does nothing', () => { - const setStatus = useStore.getState().unclaimedSwaps.setProgressStatus; - setStatus('add', '123'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(1); - setStatus('remove', '123'); - setStatus('remove', '123'); - setStatus('remove', '123'); - expect(useStore.getState().unclaimedSwaps.inProgress.length).toBe(0); - }); -}); diff --git a/apps/minifront/src/state/unclaimed-swaps.ts b/apps/minifront/src/state/unclaimed-swaps.ts deleted file mode 100644 index 78332c76..00000000 --- a/apps/minifront/src/state/unclaimed-swaps.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { SliceCreator, useStore } from '.'; -import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; -import { issueSwapClaim } from './swap/instant-swap'; -import { getSwapRecordCommitment } from '@penumbra-zone/getters/swap-record'; -import { ZQueryState, createZQuery } from '@penumbra-zone/zquery'; -import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { fetchUnclaimedSwaps } from '../fetchers/unclaimed-swaps'; - -type SwapCommitmentId = string; - -export interface UnclaimedSwapsWithMetadata { - swap: SwapRecord; - asset1: Metadata; - asset2: Metadata; -} - -export interface UnclaimedSwapsSlice { - inProgress: SwapCommitmentId[]; - isInProgress: (id: SwapCommitmentId) => boolean; - setProgressStatus: (action: 'add' | 'remove', id: SwapCommitmentId) => void; - claimSwap: (id: SwapCommitmentId, swap: SwapRecord) => Promise; - unclaimedSwaps: ZQueryState; -} - -export const { unclaimedSwaps, useUnclaimedSwaps, useRevalidateUnclaimedSwaps } = createZQuery({ - name: 'unclaimedSwaps', - fetch: fetchUnclaimedSwaps, - - getUseStore: () => useStore, - - set: setter => { - const newState = setter(useStore.getState().unclaimedSwaps.unclaimedSwaps); - - useStore.setState(state => { - Object.assign(state.unclaimedSwaps.unclaimedSwaps, newState); - }); - }, - - get: state => state.unclaimedSwaps.unclaimedSwaps, -}); - -export const createUnclaimedSwapsSlice = (): SliceCreator => (set, get) => ({ - inProgress: [], - isInProgress: id => { - return get().unclaimedSwaps.inProgress.includes(id); - }, - setProgressStatus: (action, id) => { - if (action === 'add') { - if (get().unclaimedSwaps.isInProgress(id)) return; - set(({ unclaimedSwaps: { inProgress } }) => { - inProgress.push(id); - }); - } - if (action === 'remove') { - set(({ unclaimedSwaps }) => { - unclaimedSwaps.inProgress = get().unclaimedSwaps.inProgress.filter(item => item !== id); - }); - } - }, - claimSwap: async (id, swap) => { - const setStatus = get().unclaimedSwaps.setProgressStatus; - setStatus('add', id); - - const commitment = getSwapRecordCommitment(swap); - await issueSwapClaim(commitment); - setStatus('remove', id); - get().unclaimedSwaps.unclaimedSwaps.revalidate(); - }, - unclaimedSwaps, -}); diff --git a/apps/minifront/src/utils/commit-info-vite-plugin.ts b/apps/minifront/src/utils/commit-info-vite-plugin.ts deleted file mode 100644 index b7eb4d3d..00000000 --- a/apps/minifront/src/utils/commit-info-vite-plugin.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Plugin } from 'vite'; -import { execSync } from 'child_process'; - -// Vite plugin used to inject the current commit hash + commit date + origin into React -// so the user can be informed the version of minifront they are using -export const commitInfoPlugin = (): Plugin => { - const commitHash = execSync('git rev-parse HEAD').toString().trim(); - const commitDate = execSync('git log -1 --format=%cI').toString().trim(); - const gitOriginUrl = execSync('git remote get-url origin') - .toString() - .trim() - .replace(/\.git$/, ''); // Origin urls often appended with .git - - return { - name: 'vite-plugin-commit-info', - enforce: 'pre', - config() { - return { - // Inject the env variables into the code - define: { - __COMMIT_HASH__: JSON.stringify(commitHash), - __COMMIT_DATE__: JSON.stringify(commitDate), - __GIT_ORIGIN_URL__: JSON.stringify(gitOriginUrl), - }, - }; - }, - }; -}; diff --git a/apps/minifront/src/utils/use-store-shallow.ts b/apps/minifront/src/utils/use-store-shallow.ts deleted file mode 100644 index b3643033..00000000 --- a/apps/minifront/src/utils/use-store-shallow.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useShallow } from 'zustand/react/shallow'; -import { AllSlices, useStore } from '../state'; - -/** - * Like `useStore()`, but checks for shallow equality to prevent unnecessary - * re-renders if none of the properties returned by `selector` have changed. - * - * Calling `useStoreShallow(selector)` is the same as calling - * `useStore(useShallow(selector))`. But it's so common to use those two - * together that this function combines both for ease of use. - * - * @example - * ```tsx - * import { useStoreShallow } from '../utils/use-store-shallow'; - * - * const myComponentSelector = (state: AllSlices) => ({ - * prop1: state.mySlice.prop1, - * prop2: state.mySlice.prop2, - * }); - * - * const MyComponent = () => { - * const state = useStoreShallow(myComponentSelector); - * }; - * ``` - */ -export const useStoreShallow = (selector: (state: AllSlices) => U) => - useStore(useShallow(selector)); diff --git a/apps/minifront/src/utils/zero-value-view.tsx b/apps/minifront/src/utils/zero-value-view.tsx deleted file mode 100644 index 85d3fe78..00000000 --- a/apps/minifront/src/utils/zero-value-view.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { - Metadata, - ValueView, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -/** - * A default `ValueView` to render when we don't have any balance data for a - * particular token. - */ -export const zeroValueView = (metadata: Metadata) => - new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { hi: 0n, lo: 0n }, - metadata: metadata, - }, - }, - }); diff --git a/apps/minifront/src/vite-env.d.ts b/apps/minifront/src/vite-env.d.ts deleted file mode 100644 index 4c831f6b..00000000 --- a/apps/minifront/src/vite-env.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -/// -declare const __COMMIT_HASH__: string; -declare const __COMMIT_DATE__: string; -declare const __GIT_ORIGIN_URL__: string; diff --git a/apps/minifront/tailwind.config.js b/apps/minifront/tailwind.config.js deleted file mode 100644 index c97ca2ed..00000000 --- a/apps/minifront/tailwind.config.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@penumbra-zone/tailwind-config'; diff --git a/apps/minifront/tests-setup.ts b/apps/minifront/tests-setup.ts deleted file mode 100644 index db273e3b..00000000 --- a/apps/minifront/tests-setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { afterEach, vi } from 'vitest'; -import { cleanup } from '@testing-library/react'; -import withResolvers from 'promise.withresolvers'; - -import '@testing-library/jest-dom/vitest'; - -withResolvers.shim(); - -vi.mock('zustand'); - -afterEach(() => { - // Clear anything rendered by jsdom. (Without this, previous tests can leave - // React nodes in the DOM, which can interfere with subsequent tests.) - cleanup(); -}); diff --git a/apps/minifront/tsconfig.json b/apps/minifront/tsconfig.json deleted file mode 100644 index 4d6d6b47..00000000 --- a/apps/minifront/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "tsconfig/vite.json", - "include": ["src", "*.ts", "__mocks__"], - "exclude": ["node_modules"] -} diff --git a/apps/minifront/vite-plugin-node-stdlib-browser.d.ts b/apps/minifront/vite-plugin-node-stdlib-browser.d.ts deleted file mode 100644 index aa692b98..00000000 --- a/apps/minifront/vite-plugin-node-stdlib-browser.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'vite-plugin-node-stdlib-browser' { - export default function (): import('vite').Plugin; -} diff --git a/apps/minifront/vite.config.ts b/apps/minifront/vite.config.ts deleted file mode 100644 index 27f67c3e..00000000 --- a/apps/minifront/vite.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react-swc'; -import basicSsl from '@vitejs/plugin-basic-ssl'; -import { commitInfoPlugin } from './src/utils/commit-info-vite-plugin'; -import polyfillNode from 'vite-plugin-node-stdlib-browser'; - -export default defineConfig({ - clearScreen: false, - base: './', - plugins: [polyfillNode(), react(), basicSsl(), commitInfoPlugin()], -}); diff --git a/apps/minifront/vitest.config.ts b/apps/minifront/vitest.config.ts deleted file mode 100644 index ee721304..00000000 --- a/apps/minifront/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - snapshotSerializers: [], - environment: 'jsdom', - setupFiles: ['./tests-setup.ts'], - }, -}); diff --git a/apps/node-status/CHANGELOG.md b/apps/node-status/CHANGELOG.md deleted file mode 100644 index f84be797..00000000 --- a/apps/node-status/CHANGELOG.md +++ /dev/null @@ -1,167 +0,0 @@ -# node-status - -## 3.0.3 - -### Patch Changes - -- Updated dependencies [ab9d743] -- Updated dependencies [282eabf] -- Updated dependencies [0076a1d] -- Updated dependencies [81b9536] -- Updated dependencies [6b06e04] -- Updated dependencies [24c8b4f] -- Updated dependencies [c8e8d15] -- Updated dependencies [24c8b4f] -- Updated dependencies [e7d7ffc] - - @penumbra-zone/types@7.1.0 - - @penumbra-zone/ui@3.4.0 - - @penumbra-zone/protobuf@4.1.0 - - @penumbra-zone/crypto-web@3.0.10 - -## 3.0.2 - -### Patch Changes - -- Updated dependencies [8fe4de6] - - @penumbra-zone/protobuf@4.0.0 - - @penumbra-zone/ui@3.3.2 - - @penumbra-zone/types@7.0.1 - - @penumbra-zone/crypto-web@3.0.9 - -## 3.0.1 - -### Patch Changes - -- Updated dependencies [bb5f621] -- Updated dependencies [8b121ec] - - @penumbra-zone/types@7.0.0 - - @penumbra-zone/ui@3.3.1 - - @penumbra-zone/protobuf@3.0.0 - - @penumbra-zone/crypto-web@3.0.8 - -## 3.0.0 - -### Major Changes - -- 029eebb: use service definitions from protobuf collection package - -### Minor Changes - -- 3ea1e6c: update buf types dependencies - -### Patch Changes - -- Updated dependencies [fc9418c] -- Updated dependencies [120b654] -- Updated dependencies [4f8c150] -- Updated dependencies [029eebb] -- Updated dependencies [029eebb] -- Updated dependencies [3ea1e6c] - - @penumbra-zone/ui@3.3.0 - - @penumbra-zone/protobuf@2.1.0 - - @penumbra-zone/types@6.0.0 - - @penumbra-zone/crypto-web@3.0.7 - -## 2.0.9 - -### Patch Changes - -- Updated dependencies [d8fef48] -- Updated dependencies [5b80e7c] - - @penumbra-zone/ui@3.2.0 - -## 2.0.8 - -### Patch Changes - -- e35c6f7: Deps bumped to latest -- Updated dependencies [146b48d] -- Updated dependencies [e35c6f7] -- Updated dependencies [cf63b30] -- Updated dependencies [e4c9fce] -- Updated dependencies [8a3b442] -- Updated dependencies [43bf99f] -- Updated dependencies [8ccaf30] - - @penumbra-zone/types@5.0.0 - - @penumbra-zone/ui@3.1.0 - - @penumbra-zone/crypto-web@3.0.6 - -## 2.0.7 - -### Patch Changes - -- Updated dependencies - - @penumbra-zone/ui@3.0.0 - - @penumbra-zone/types@4.1.0 - - @penumbra-zone/crypto-web@3.0.5 - -## 2.0.6 - -### Patch Changes - -- @penumbra-zone/types@4.0.1 -- @penumbra-zone/ui@2.0.5 -- @penumbra-zone/crypto-web@3.0.4 - -## 2.0.5 - -### Patch Changes - -- Updated dependencies [6fb898a] - - @penumbra-zone/types@4.0.0 - - @penumbra-zone/crypto-web@3.0.3 - - @penumbra-zone/ui@2.0.4 - -## 2.0.4 - -### Patch Changes - -- Updated dependencies [3148375] - - @penumbra-zone/types@3.0.0 - - @penumbra-zone/ui@2.0.3 - - @penumbra-zone/crypto-web@3.0.2 - -## 2.0.3 - -### Patch Changes - -- @penumbra-zone/types@2.0.1 -- @penumbra-zone/ui@2.0.2 -- @penumbra-zone/crypto-web@3.0.1 - -## 2.0.2 - -### Patch Changes - -- Updated dependencies [b4082b7] - - @penumbra-zone/crypto-web@3.0.0 - -## 2.0.1 - -### Patch Changes - -- @penumbra-zone/ui@2.0.1 - -## 2.0.0 - -### Major Changes - -- 929d278: barrel imports to facilitate better tree shaking - -### Patch Changes - -- Updated dependencies [7a1efed] -- Updated dependencies [8933117] -- Updated dependencies [929d278] - - @penumbra-zone/ui@2.0.0 - - @penumbra-zone/crypto-web@2.0.0 - - @penumbra-zone/types@2.0.0 - -## 1.0.1 - -### Patch Changes - -- Updated dependencies - - @penumbra-zone/types@1.1.0 - - @penumbra-zone/ui@1.0.2 - - @penumbra-zone/crypto-web@1.0.1 diff --git a/apps/node-status/README.md b/apps/node-status/README.md deleted file mode 100644 index 9164caab..00000000 --- a/apps/node-status/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Penumbra node status page - -![Screenshot 2024-02-22 at 1 21 54 PM](https://github.com/penumbra-zone/web/assets/16624263/7422ff48-fe33-4f16-a13f-4e109998c7ec) - -### Overview - -This static site serves as a status page for the Penumbra node, -displaying output from [GetStatus](https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.util.tendermint_proxy.v1#penumbra.util.tendermint_proxy.v1.TendermintProxyService.GetStatus) rpc method -and linking to minifront. Designed to be hosted by PD. - -### Run - -``` -pnpm install -pnpm dev # for local development - -pnpm build # for getting build output for deployment on pd -``` diff --git a/apps/node-status/eslint.config.mjs b/apps/node-status/eslint.config.mjs deleted file mode 100644 index a53ed8e5..00000000 --- a/apps/node-status/eslint.config.mjs +++ /dev/null @@ -1,13 +0,0 @@ -import { penumbraEslintConfig } from '@penumbra-zone/eslint-config'; -import { config, parser } from 'typescript-eslint'; - -export default config({ - ...penumbraEslintConfig, - languageOptions: { - parser, - parserOptions: { - project: true, - tsconfigRootDir: import.meta.dirname, - }, - }, -}); diff --git a/apps/node-status/index.html b/apps/node-status/index.html deleted file mode 100644 index 6c635a33..00000000 --- a/apps/node-status/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Penumbra Node Status - - -
- - - diff --git a/apps/node-status/package.json b/apps/node-status/package.json deleted file mode 100644 index 20501b35..00000000 --- a/apps/node-status/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "node-status", - "version": "3.0.3", - "private": true, - "license": "(MIT OR Apache-2.0)", - "type": "module", - "scripts": { - "build": "tsc && vite build", - "clean": "rm -rfv dist", - "dev": "vite --port 5174", - "lint": "eslint src", - "preview": "vite preview" - }, - "dependencies": { - "@buf/penumbra-zone_penumbra.bufbuild_es": "1.9.0-20240528180215-8fe1c79485f8.1", - "@buf/tendermint_tendermint.bufbuild_es": "1.9.0-20231117195010-33ed361a9051.1", - "@connectrpc/connect-web": "^1.4.0", - "@penumbra-zone/crypto-web": "workspace:*", - "@penumbra-zone/protobuf": "workspace:*", - "@penumbra-zone/types": "workspace:*", - "@penumbra-zone/ui": "workspace:*", - "date-fns": "^3.6.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-loader-spinner": "^6.1.6", - "react-router-dom": "^6.23.1", - "tailwindcss": "^3.4.3" - }, - "devDependencies": { - "@types/react": "^18.3.2", - "@types/react-dom": "^18.3.0" - } -} diff --git a/apps/node-status/postcss.config.js b/apps/node-status/postcss.config.js deleted file mode 100644 index 8e18cdcb..00000000 --- a/apps/node-status/postcss.config.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@penumbra-zone/ui/postcss.config.js'; diff --git a/apps/node-status/public/favicon.png b/apps/node-status/public/favicon.png deleted file mode 100644 index 5d7949b4..00000000 Binary files a/apps/node-status/public/favicon.png and /dev/null differ diff --git a/apps/node-status/public/penumbra-rays.svg b/apps/node-status/public/penumbra-rays.svg deleted file mode 100644 index 96b7c3f4..00000000 --- a/apps/node-status/public/penumbra-rays.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/node-status/public/penumbra-text-logo.svg b/apps/node-status/public/penumbra-text-logo.svg deleted file mode 100644 index 0293e288..00000000 --- a/apps/node-status/public/penumbra-text-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/apps/node-status/src/clients/grpc.ts b/apps/node-status/src/clients/grpc.ts deleted file mode 100644 index 464b0f90..00000000 --- a/apps/node-status/src/clients/grpc.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createGrpcWebTransport } from '@connectrpc/connect-web'; -import { createPromiseClient } from '@connectrpc/connect'; -import { TendermintProxyService } from '@penumbra-zone/protobuf'; -import { devBaseUrl, prodBaseUrl } from '../constants'; - -const transport = createGrpcWebTransport({ - baseUrl: import.meta.env.MODE === 'production' ? prodBaseUrl : devBaseUrl, -}); - -export const tendermintClient = createPromiseClient(TendermintProxyService, transport); diff --git a/apps/node-status/src/components/error-boundary.tsx b/apps/node-status/src/components/error-boundary.tsx deleted file mode 100644 index 31104631..00000000 --- a/apps/node-status/src/components/error-boundary.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { SplashPage } from '@penumbra-zone/ui/components/ui/splash-page'; -import { useRouteError } from 'react-router-dom'; - -export const ErrorBoundary = () => { - const error = useRouteError(); - - console.error('ErrorBoundary caught error:', error); - - return ( - - {String(error)} - - ); -}; diff --git a/apps/node-status/src/components/frontend-referral.tsx b/apps/node-status/src/components/frontend-referral.tsx deleted file mode 100644 index 4189228f..00000000 --- a/apps/node-status/src/components/frontend-referral.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Button } from '@penumbra-zone/ui/components/ui/button'; -import { devFrontend, prodFrontend } from '../constants'; - -export const FrontendReferral = () => { - const onClickHandler = () => { - window.open(import.meta.env.MODE === 'production' ? prodFrontend : devFrontend); - }; - - return ( - - ); -}; diff --git a/apps/node-status/src/components/header.tsx b/apps/node-status/src/components/header.tsx deleted file mode 100644 index 7581827b..00000000 --- a/apps/node-status/src/components/header.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Link } from 'react-router-dom'; -import { LineWave } from 'react-loader-spinner'; -import { cn } from '@penumbra-zone/ui/lib/utils'; -import { useDelayedIsLoading } from '../fetching/refetch-hook'; - -export const Header = () => { - const isLoading = useDelayedIsLoading(); - - return ( -
-
- Penumbra logo - - Penumbra logo - -
-
- Node Status - -
-
-
- ); -}; diff --git a/apps/node-status/src/components/index.tsx b/apps/node-status/src/components/index.tsx deleted file mode 100644 index 09faa8ed..00000000 --- a/apps/node-status/src/components/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Header } from './header'; -import { FrontendReferral } from './frontend-referral'; -import { NodeInfo } from './node-info'; -import { SyncInfo } from './sync-info'; -import { ValidatorInfo } from './validator-info'; -import { useRefetchStatusOnInterval } from '../fetching/refetch-hook'; - -export const Index = () => { - useRefetchStatusOnInterval(); - - return ( - <> -
-
-
- - -
- - -
-
-
- - ); -}; diff --git a/apps/node-status/src/components/node-info.tsx b/apps/node-status/src/components/node-info.tsx deleted file mode 100644 index c6b7c31c..00000000 --- a/apps/node-status/src/components/node-info.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useLoaderData } from 'react-router-dom'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { Identicon } from '@penumbra-zone/ui/components/ui/identicon'; -import { IndexLoaderResponse } from '../fetching/loader'; -import { uint8ArrayToString } from '@penumbra-zone/types/string'; - -export const NodeInfo = () => { - const { - status: { nodeInfo }, - } = useLoaderData() as IndexLoaderResponse; - if (!nodeInfo) return <>; - - return ( - -
- Network -
- - {nodeInfo.network} -
- Version - {nodeInfo.version} -
-
- Default Node ID - {nodeInfo.defaultNodeId} -
- {nodeInfo.protocolVersion && ( -
- Protocol Version - Block: {nodeInfo.protocolVersion.block.toString()} - P2P: {nodeInfo.protocolVersion.p2p.toString()} - App: {nodeInfo.protocolVersion.app.toString()} -
- )} -
- Listen Address - {nodeInfo.listenAddr} -
-
- Channels - {uint8ArrayToString(nodeInfo.channels)} -
-
- Moniker - {nodeInfo.moniker} -
- {nodeInfo.other && ( -
- Transaction Index - {nodeInfo.other.txIndex} - RPC Address - {nodeInfo.other.rpcAddress} -
- )} -
- ); -}; diff --git a/apps/node-status/src/components/router.tsx b/apps/node-status/src/components/router.tsx deleted file mode 100644 index 749463c9..00000000 --- a/apps/node-status/src/components/router.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { createHashRouter } from 'react-router-dom'; -import { ErrorBoundary } from './error-boundary'; -import { Index } from '.'; -import { IndexLoader } from '../fetching/loader'; - -export const router = createHashRouter([ - { - path: '/', - loader: IndexLoader, - element: , - errorElement: , - }, -]); diff --git a/apps/node-status/src/components/sync-info.tsx b/apps/node-status/src/components/sync-info.tsx deleted file mode 100644 index db6d6309..00000000 --- a/apps/node-status/src/components/sync-info.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useLoaderData } from 'react-router-dom'; -import { IndexLoaderResponse } from '../fetching/loader'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { format } from 'date-fns'; -import { SyncInfo as SyncInfoProto } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/util/tendermint_proxy/v1/tendermint_proxy_pb'; - -const getFormattedTime = (syncInfo: SyncInfoProto): { date?: string; time?: string } => { - const dateObj = syncInfo.latestBlockTime?.toDate(); - if (!dateObj) return {}; - - const date = format(dateObj, 'EEE MMM dd yyyy'); - const time = format(dateObj, "HH:mm:ss 'GMT'x"); - - return { date, time }; -}; - -export const SyncInfo = () => { - const { - status: { syncInfo }, - latestBlockHash, - latestAppHash, - } = useLoaderData() as IndexLoaderResponse; - if (!syncInfo) return <>; - - const { date, time } = getFormattedTime(syncInfo); - - return ( - -
-
- Latest Block Height{' '} - {syncInfo.latestBlockHeight.toString()} -
-
- Caught Up{' '} - {syncInfo.catchingUp ? ( -
- False -
- ) : ( -
- True -
- )} -
-
- Latest Block Time - {date} - {time} -
-
- -
-
- Latest Block Hash: - {latestBlockHash} -
-
- Latest App Hash: - {latestAppHash} -
-
-
- ); -}; diff --git a/apps/node-status/src/components/validator-info.tsx b/apps/node-status/src/components/validator-info.tsx deleted file mode 100644 index 7baaf8b3..00000000 --- a/apps/node-status/src/components/validator-info.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { useLoaderData } from 'react-router-dom'; -import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { IndexLoaderResponse } from '../fetching/loader'; -import { PublicKey } from '@buf/tendermint_tendermint.bufbuild_es/tendermint/crypto/keys_pb'; -import { uint8ArrayToHex } from '@penumbra-zone/types/hex'; -import { uint8ArrayToString } from '@penumbra-zone/types/string'; - -const PublicKeyComponent = ({ publicKey }: { publicKey: PublicKey | undefined }) => { - if (!publicKey) return null; - - const publicKeyType = publicKey.sum.case; - const value = publicKey.sum.value ? uint8ArrayToHex(publicKey.sum.value) : undefined; - - return ( -
- Public Key - Type: {publicKeyType} - Value: {value} -
- ); -}; - -export const ValidatorInfo = () => { - const { - status: { validatorInfo }, - } = useLoaderData() as IndexLoaderResponse; - if (!validatorInfo) return <>; - - return ( - // Outer div used to shrink to size instead of expand to sibling's size -
- -
- Voting Power - {validatorInfo.votingPower.toString()} -
-
- Proposer Priority - {validatorInfo.proposerPriority.toString()} -
-
- Address - {uint8ArrayToString(validatorInfo.address)} -
- -
-
- ); -}; diff --git a/apps/node-status/src/constants.ts b/apps/node-status/src/constants.ts deleted file mode 100644 index 339a4ec9..00000000 --- a/apps/node-status/src/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const prodBaseUrl = '/'; -export const devBaseUrl = 'https://grpc.testnet.penumbra.zone'; - -export const prodFrontend = '/app/'; -export const devFrontend = 'https://app.testnet.penumbra.zone'; diff --git a/apps/node-status/src/fetching/loader.ts b/apps/node-status/src/fetching/loader.ts deleted file mode 100644 index 29ad738b..00000000 --- a/apps/node-status/src/fetching/loader.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { LoaderFunction } from 'react-router-dom'; -import { GetStatusResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/util/tendermint_proxy/v1/tendermint_proxy_pb'; -import { sha256HashStr } from '@penumbra-zone/crypto-web/sha256'; -import { tendermintClient } from '../clients/grpc'; - -export interface IndexLoaderResponse { - status: GetStatusResponse; - latestBlockHash: string | undefined; - latestAppHash: string | undefined; -} - -export const IndexLoader: LoaderFunction = async (): Promise => { - const status = await tendermintClient.getStatus({}); - const latestBlockHash = await getHash(status.syncInfo?.latestBlockHash); - const latestAppHash = await getHash(status.syncInfo?.latestAppHash); - - return { - status, - latestBlockHash, - latestAppHash, - }; -}; - -const getHash = async (uintArr?: Uint8Array): Promise => - uintArr ? sha256HashStr(uintArr) : undefined; diff --git a/apps/node-status/src/fetching/refetch-hook.ts b/apps/node-status/src/fetching/refetch-hook.ts deleted file mode 100644 index 00e35b4f..00000000 --- a/apps/node-status/src/fetching/refetch-hook.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { useRevalidator } from 'react-router-dom'; -import { useEffect, useState } from 'react'; - -const REFETCH_INTERVAL = 5000; - -export const useRefetchStatusOnInterval = () => { - const { revalidate } = useRevalidator(); - - useEffect(() => { - const interval = setInterval(revalidate, REFETCH_INTERVAL); - return () => clearInterval(interval); // Clear the interval when the component is unmounted - }, [revalidate]); -}; - -const VISIBILITY_STATE_CHANGE_DELAY = 800; - -// Meant to slow down the state transition from loading to idle so the UI can show a discernible spinner -export const useDelayedIsLoading = () => { - const { state } = useRevalidator(); - - const [isVisible, setIsVisible] = useState(false); - - useEffect(() => { - if (state === 'loading') { - setIsVisible(true); - } - - if (state === 'idle') { - setTimeout(() => setIsVisible(false), VISIBILITY_STATE_CHANGE_DELAY); - } - }, [state]); - - return isVisible; -}; diff --git a/apps/node-status/src/main.tsx b/apps/node-status/src/main.tsx deleted file mode 100644 index df941faf..00000000 --- a/apps/node-status/src/main.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import { RouterProvider } from 'react-router-dom'; -import { router } from './components/router'; - -import '@penumbra-zone/ui/styles/globals.css'; - -const Main = () => ( - - - -); - -const rootElement = document.getElementById('root') as HTMLDivElement; -createRoot(rootElement).render(
); diff --git a/apps/node-status/src/vite-env.d.ts b/apps/node-status/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2..00000000 --- a/apps/node-status/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/apps/node-status/tailwind.config.js b/apps/node-status/tailwind.config.js deleted file mode 100644 index c97ca2ed..00000000 --- a/apps/node-status/tailwind.config.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from '@penumbra-zone/tailwind-config'; diff --git a/apps/node-status/tsconfig.json b/apps/node-status/tsconfig.json deleted file mode 100644 index 12fa88f8..00000000 --- a/apps/node-status/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "tsconfig/vite.json", - "include": ["src", "vite.config.ts"], - "exclude": ["node_modules"] -} diff --git a/apps/node-status/vite.config.ts b/apps/node-status/vite.config.ts deleted file mode 100644 index 62d32341..00000000 --- a/apps/node-status/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - clearScreen: false, - plugins: [react()], -});