Giter Club home page Giter Club logo

wallet's Introduction

Penumbra dApp

Overview

Penumbra dApp is the canonical wallet functionality interface for the Penumbra network.

Getting Started

To use the Penumbra dApp, you must first install the Penumbra wallet extension and import/create a new wallet.
The Penumbra wallet extension is available for installation in the Chrome store.
You can also build the wallet extension locally by following the build instructions.
After you've installed the extension, navigate to the dApp: https://app.testnet.penumbra.zone

1.Building the site locally

  • Building the site locally

    npm install
    npm run dev

2. Remote packages

Add library to your app.

  • Configure registry

    npm config set @buf:registry https://buf.build/gen/npm/v1/
  • Packages

    • bufbuild/connect-es
    npm install @buf/penumbra-zone_penumbra.bufbuild_connect-es@latest
    • bufbuild/es
    npm install @buf/penumbra-zone_penumbra.bufbuild_es@latest
    • bufbuild/connect-web
    npm install @buf/penumbra-zone_penumbra.bufbuild_connect-web@latest

3. Types for window object penumbra

Add to global.d.ts

import {
	AddressByIndexRequest,
	AddressByIndexResponse,
	AssetsRequest,
	AssetsResponse,
	ChainParametersRequest,
	ChainParametersResponse,
	FMDParametersRequest,
	FMDParametersResponse,
	NotesRequest,
	StatusRequest,
	StatusResponse,
	TransactionInfoByHashRequest,
	TransactionInfoByHashResponse,
	TransactionPlannerRequest,
	TransactionPlannerResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb'

declare global {
	interface Window {
		penumbra: Penumbra.PenumbraApi
	}
}

export declare namespace Penumbra {
	type PenumbraApi = {
		requestAccounts: () => Promise<[string]>
		on(event: Events, cb: (state: any) => any, args?: any): object
		getChainParameters: (
			request?: ChainParametersRequest
		) => Promise<ChainParametersResponse>
		getStatus: (request?: StatusRequest) => Promise<StatusResponse>
		getFmdParameters: (
			request?: FMDParametersRequest
		) => Promise<FMDParametersResponse>
		signTransaction: (request: any) => Promise<TransactionResponse>
		getTransactionInfoByHash: (
			request: TransactionInfoByHashRequest
		) => Promise<TransactionInfoByHashResponse>
		getAddressByIndex: (
			request: AddressByIndexRequest
		) => Promise<AddressByIndexResponse>
		getTransactionPlanner: (
			request: TransactionPlannerRequest
		) => Promise<TransactionPlannerResponse>
	}
}

export type TransactionResponse = {
	id: number
	jsonrpc: string
	result: {
		code: 1 | 0
		codespace: string
		data: string
		hash: string
		log: string
	}
}

export type Events =
	| 'state'
	| 'status'
	| 'balance'
	| 'assets'
	| 'transactions'
	| 'notes'
	| 'accountsChanged'

Extension Buf Transport

import { createRouterTransport } from '@bufbuild/connect'
import { ViewProtocolService } from '@buf/penumbra-zone_penumbra.bufbuild_connect-es/penumbra/view/v1alpha1/view_connect'
import {
	AddressByIndexRequest,
	AssetsRequest,
	AssetsResponse,
	BalanceByAddressRequest,
	BalanceByAddressResponse,
	ChainParametersRequest,
	ChainParametersResponse,
	FMDParametersRequest,
	FMDParametersResponse,
	NotesRequest,
	NotesResponse,
	StatusRequest,
	StatusResponse,
	StatusStreamRequest,
	StatusStreamResponse,
	TransactionInfoByHashRequest,
	TransactionInfoByHashResponse,
	TransactionInfoRequest,
	TransactionInfoResponse,
	TransactionPlannerRequest,
	TransactionPlannerResponse,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb'

export const extensionTransport = (s: typeof ViewProtocolService) =>
	createRouterTransport(({ service }) => {
		let receiveMessage: (value: unknown) => void = function () {}
		function waitForNextMessage() {
			return new Promise(resolve => {
				receiveMessage = resolve
			})
		}
		async function* createMessageStream() {
			while (true) {
				yield waitForNextMessage()
			}
		}
		service(s, {
			status: async (message: StatusRequest) => {
				const response = await window.penumbra.getStatus()

				return new StatusResponse(response)
			},
			addressByIndex: async (request: AddressByIndexRequest) => {
				const response = await window.penumbra.getAddressByIndex(request)
				return response
			},

			transactionPlanner: async (message: TransactionPlannerRequest) => {
				const response = await window.penumbra.getTransactionPlanner(message)

				return new TransactionPlannerResponse(response)
			},
			transactionInfoByHash: async (message: TransactionInfoByHashRequest) => {
				const response = await window.penumbra.getTransactionInfoByHash(message)

				return new TransactionInfoByHashResponse(response)
			},
			fMDParameters: async (message: FMDParametersRequest) => {
				const response = await window.penumbra.getFmdParameters()

				return new FMDParametersResponse(response)
			},
			chainParameters: async (message: ChainParametersRequest) => {
				const response = await window.penumbra.getChainParameters()

				return new ChainParametersResponse(response)
			},
			async *statusStream(message: StatusStreamRequest) {
				window.penumbra.on('status', status => receiveMessage(status))

				for await (const res of createMessageStream()) {
					yield new StatusStreamResponse(res)
				}
			},
			async *assets(message: AssetsRequest) {
				window.penumbra.on('assets', asset => receiveMessage(asset))

				for await (const res of createMessageStream()) {
					yield new AssetsResponse(res)
				}
			},
			async *balanceByAddress(message: BalanceByAddressRequest) {
				window.penumbra.on('balance', balance => receiveMessage(balance))

				for await (const res of createMessageStream()) {
					yield new BalanceByAddressResponse(res)
				}
			},
			async *notes(message: NotesRequest) {
				window.penumbra.on('notes', note => receiveMessage(note))

				for await (const res of createMessageStream()) {
					yield new NotesResponse(res)
				}
			},
			async *transactionInfo(message: TransactionInfoRequest) {
				window.penumbra.on(
					'transactions',
					tx => receiveMessage(tx),
					message.toJson()
				)

				for await (const res of createMessageStream()) {
					yield new TransactionInfoResponse(res)
				}
			},
		})
	})

Create promise client

import { createPromiseClient } from '@bufbuild/connect'
import { ViewProtocolService } from '@buf/penumbra-zone_penumbra.bufbuild_connect-es/penumbra/view/v1alpha1/view_connect'

const client = createPromiseClient(
	ViewProtocolService,
	extensionTransport(ViewProtocolService)
)

Methods

User Info

login

Authenticates user with his/her account;

Usage:

const poll = (
	resolve: (result: boolean) => void,
	reject: (...args: unknown[]) => void,
	attempt = 0,
	retries = 30,
	interval = 100
) => {
	if (attempt > retries) return resolve(false)

	if (typeof window !== 'undefined' && 'undefined') {
		return resolve(true)
	} else setTimeout(() => poll(resolve, reject, ++attempt), interval)
}

const _isPenumbraInstalled = new Promise(poll)

export async function isPenumbraInstalled() {
	return _isPenumbraInstalled
}

const [walletAddress, setWalletAddress] = useState<string>('')
const [isPenumbra, setIsPenumbra] = useState<boolean>(false)

const checkIsPenumbraInstalled = async () => {
	const isInstalled = await isPenumbraInstalled()
	setIsPenumbra(isInstalled)
}

useEffect(() => {
	checkIsPenumbraInstalled()
}, [])

useEffect(() => {
	if (!isPenumbra) return
	addWalletListener(isPenumbra)
}, [isPenumbra])

const addWalletListener = async (isPenumbra: boolean) => {
	if (isPenumbra) {
		window.penumbra.on('accountsChanged', (accounts: [string]) => {
			setWalletAddress(accounts[0])
		})
	} else {
		/* Penumbra is not installed */
		setWalletAddress('')
		console.log('Please install Penumbra Wallet')
	}
}

const signin = async () => {
	if (isPenumbra) {
		try {
			/* Penumbra is installed */
			const accounts = await window.penumbra.requestAccounts()
			setWalletAddress(accounts[0])
		} catch (err) {
			console.error(err)
		}
	} else {
		/* Penumbra is not installed */
		console.log('Please install Penumbra Wallet')
	}
}

Output example:

;[
	'penumbrav2t13vh0fkf3qkqjacpm59g23ufea9n5us45e4p5h6hty8vg73r2t8g5l3kynad87uvn9eragf3hhkgkhqe5vhngq2cw493k48c9qg9ms4epllcmndd6ly4v4dwwjcnxaxzjqnlvnw',
]

service ViewProtocolService

The view protocol is used by a view client, who wants to do some transaction-related actions, to request data from a view service, which is responsible for synchronizing and scanning the public chain state with one or more full viewing keys.

View protocol requests optionally include the account group ID, used to identify which set of data to query.

Status

Get current status of chain sync

const request = new StatusRequest({})
const response = await client.status(request)

Parameters

Response

Status Stream

Queries for notes that have been accepted by the core.chain.v1alpha1.

const request = new StatusStreamRequest({})
for await (const status of client.statusStream(statusRequest)) {
	console.log(status)
}

Parameters

Response

Status Stream

Queries for notes that have been accepted by the core.chain.v1alpha1.

const request = new StatusStreamRequest({})
for await (const status of client.statusStream(statusRequest)) {
	console.log(status)
}

Parameters

Response

Notes

Queries for notes that have been accepted by the core.chain.v1alpha1.

const request = new NotesRequest({})
for await (const note of client.notes(statusRequest)) {
	console.log(note)
}

Parameters

Response

Assets

Queries for assets that have been accepted by the core.chain.v1alpha1.

const request = new AssetsRequest({})
for await (const asset of client.assets(request)) {
	console.log(asset)
}

Parameters

Response

ChainParameters

Query for the current chain parameters.

const request = new ChainParametersRequest({})
const response = await client.chainParameters(request)

Parameters

Response

FMDParameters

Query for the current FMD parameters.

const request = new FMDParametersRequest({})
const response = await client.fMDParameters(request)

Parameters

Response

AddressByIndex

Query for an address given an address index

const request = new AddressByIndexRequest({})
const response = await client.addressByIndex(request)

Parameters

Response

BalanceByAddress

Query for balance of a given address

const request = new BalanceByAddressRequest({})
for await (const balance of client.balanceByAddress(request)) {
	console.log(balance)
}

Parameters

Response

TransactionInfoByHash

Query for a given transaction by its hash.

const request = new BalanceByAddressRequest({})
const response = await client.transactionInfoByHash(request)

Parameters

Response

TransactionInfo

Query for the full transactions in the given range of blocks.

const request = new TransactionInfoRequest({})
for await (const balance of client.transactionInfo(request)) {
	console.log(balance)
}

Parameters

Response

TransactionPlanner

Query for a transaction plan

const request = new TransactionPlannerRequest({})
const response = await client.transactionPlanner(request)

Parameters

Response

Other

Send transaction

const client = createPromiseClient(ViewProtocolService,extensionTransport(ViewProtocolService))

const transactionPlan = (await client.transactionPlanner(new TransactionPlannerRequest({
	outputs: [
		{
			value: {
				amount: {
					lo: [amont * 10 ** exponents],
					hi: 0
				},
			assetId:
				{ inner: [assetId]
				},
			},
			address: {
				inner: [receiver as Uint8Array],
				altBech32m: [reciever],
			},
		},
	],
}))).plan


const tx = await window.penumbra.signTransaction(transactionPlan?.toJson())

Testing local wasm artifacts directory

1. Add the full path to the wasm artifacts directory to .env

 WASM_ARTIFACTS_DIRECTORY_PATH= <full_path>

2. Run command

npm run dev:local

Error Codes

Error's class Code Type Example

wallet's People

Contributors

valentine1898 avatar zbuc avatar conorsch avatar zpoken avatar aubrika avatar

Stargazers

Andrejs Agejevs avatar evalir avatar Erwan Or avatar  avatar

Watchers

 avatar  avatar redshiftzero avatar Vadim121197 avatar  avatar

Forkers

hitchhooker

wallet's Issues

SCT desynchronization bug

I experienced SCT desynchronization after clearing the cache and re-syncing. When making a spend transfer to myself, the window closed and the frontend's console reported

{code: 1, data: '', log: 'provided anchor 0092ebdef8e84daed9fb4b1ca47fc55d4c…f2c594117fd16f26443c62b06 is not a valid SCT root', codespace: '', hash: '94D53502F0B607D091AADC11B4FC0AE5155BA0060C3D2CABC0F4F548801145BB'}code: 1codespace: ""data: ""hash: "94D53502F0B607D091AADC11B4FC0AE5155BA0060C3D2CABC0F4F548801145BB"log: "provided anchor 0092ebdef8e84daed9fb4b1ca47fc55d4ccf692f2c594117fd16f26443c62b06 is not a valid SCT root"[[Prototype]]: Object

which indicates that the SCT instance in the browser extension has desynchronized with the on-chain SCT state.

It's hard to understand why the desynchronization happened or when. For pcli, the way we tracked down this type of bug was by adding an sct-divergence-check feature that requests the SCT root after each block it commits. This makes syncing >100x slower because it does many extra RPC requests, but it would be useful for debugging to have some way to configure the extension to have an option like "Sync Debug" that would enable divergence checking.

Transaction history concept

Now the user cannot view the transaction history on the static site. We need to add this feature.
The basic concept:

  1. Extension view service already scans blocks and saves FVK-affiliated transactions in indexedDB.

  2. Extension view service should pass already decrypted transactions to static site. To do this, you can use the rpc Transactions

  3. The list of transactions will be displayed with this design. It contains information about the height of the block in which the transaction was found, its hash, and its type

image

  1. The type of transaction is determined by a specific set of actions in a transaction. The type cannot always be defined, because in fact it is possible to create a transaction with any set of actions.

  2. By clicking on the transaction, you can see its details. In fact, this is a universal TransactionView -> HTML view. In addition, we can actually inherit the TransactionPlan -> HTML view from the extension

image

Transaction sender address is invalid

Using account group A on the web wallet and account group B on pcli, I sent funds from A to B, and viewed them in pcli:

~/c/p/penumbra2 ((v0.53.1)|✔) [101]$ cargo run --release --bin pcli -- view tx 0832ff091ab1ac228973b495460b8519bd2893fdd0d8aac41d87a88c7dd4d1a3
Finished release [optimized] target(s) in 0.33s
Running target/release/pcli view tx 0832ff091ab1ac228973b495460b8519bd2893fdd0d8aac41d87a88c7dd4d1a3
Scanning blocks from last sync height 77407 to latest height 77407
[0s] ██████████████████████████████████████████████████ 0/0 0/s ETA: 0s
Action Type Description
Spend [?] spent [?]
Output 100penumbra to [account 0]
Output [?] to [?]

Transaction Fee 0penumbra
Transaction Memo Sender penumbrav2t1rjzx23qh5fh6hh05gcaxljp8yl2gp5wh3vdvt9f40v4yjdzfhq3nu4dee8rtrtn065e4s5s46h0c0qdq8s3u5jgczur4yqwv36wrxkeu24tyu9ty3858qqfygn7vw26c2cgzcv
Transaction Memo Text
Transaction Expiration Height 0


But when attempting to send funds back to the sender address, I get an error:

~/c/p/penumbra2 ((v0.53.1)|✔) $ cargo run --release --bin pcli -- tx send 10pizza --to penumbrav2t1rjzx23qh5fh6hh05gcaxljp8yl2gp5wh3vdvt9f40v4yjdzfhq3nu4dee8rtrtn065e4s5s46h0c0qdq8s3u5jgczur4yqwv36wrxkeu24tyu9ty3858qqfygn7vw26c2cgzcv
Finished release [optimized] target(s) in 0.32s
Running target/release/pcli tx send 10pizza --to penumbrav2t1rjzx23qh5fh6hh05gcaxljp8yl2gp5wh3vdvt9f40v4yjdzfhq3nu4dee8rtrtn065e4s5s46h0c0qdq8s3u5jgczur4yqwv36wrxkeu24tyu9ty3858qqfygn7vw26c2cgzcv
Scanning blocks from last sync height 77419 to latest height 77419
[0s] ██████████████████████████████████████████████████ 0/0 0/s ETA: 0s
building transaction...
thread 'main' panicked at 'valid address: InvalidAddress', crates/core/transaction/src/plan/clue.rs:33:64
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

Wallet Extension + Static Site Implementation

Wallet Extension + Static Site

Currently, the browser extension provides a lot of functions that one would not find in a hardware wallet. These are desirable features to have a user interface for, but we would like the wallet extension to function more like a “virtual hardware wallet”, with deliberately limited functionality, and to move most of the existing functionality of the wallet to a static site.

The point of this is to make sure that it is possible to support third-party frontends, allowing the browser extension to function as a neutral, minimal piece of standalone software that many different websites can utilize. So, the extension itself should not obligate a user to use its particular user interface for anything more than basic key management and transaction approval wallet functions. Moving the other functions currently in the extension into a website will serve as a valuable proof of concept for this design and be an example that future developers can follow.

We propose moving most of the existing browser extension features into a website (app.testnet.penumbra.zone), leaving only transaction signing and key management in the browser extension itself. By having the website use the extension for these two functions, we will provide a means for other developers to use the wallet extension with their own software in the same way.

Browser Extension

  • Does two things: 1.) transaction signing with confirmation pop-up 2.) key management
  • Has a view service implementation
  • Provides a custody service, using a browser popup to review a human-readable form of the transaction plan and approve the transaction
  • Does syncing/scanning, but does not include send/balance/transaction history
  • Should include a settings page for key management & other settings
  • Should display a popup for reviewing submitted transaction plans and approving/denying transaction submission
  • Should work more like a virtual hardware wallet (very simple) compared to the implementation on the static site.

Website (app.testnet.penumbra.zone)

  • We will host the static site & we can coordinate to set up a deployment pipeline which will auto-deploy from the wallet repo
  • The app hosted here should support any currently-implemented or proposed user action that is not related to 1.) signing a transaction or 2.) key management (i.e. send transaction, balance, transaction history, all of that)
  • If this site needs keys to do anything, it should utilize the extension’s key management service.

Architecture

                  ┌───────────┐
                  │ Extension │
              ╭   │ ┌───────┐ │ custody
     spending │   │ │custody│ │ protocol
   capability │   │ │service│◀┼─────────────────┐
              ╰   │ └───────┘ │                 │
                  │           │                 │
                  │           │                 │
                  │           │                 │
              ╭   │ ┌───────┐ │ view            │
 full viewing │   │ │view   │ │ protocol        │
   capability │   │ │service│◀┼──────┐          │
              ╰   │ └───────┘ │      │          │
                  │   ▲       │      │      ┌───┼────────────┐
                  └───┼───────┘      │      │   │ Web Content│
                      │              │      │   ▼            │
              ╭       │              │      │ ┌───────┐      │
  transaction │       │              └──────┼▶│wallet │      │
 perspectives │       │                     │ │ logic │      │
              ╰       │                     │ └───────┘      │
                      │                     │   ▲ │          │
                      │ specific/oblivious  └───┼─┼──────────┘
                      │ client protocols        │ │
                      ├─────────────────────────┘ │ tx
                      │┌──────────────────────────┘ broadcast                     .───.
                      ││                                                        ,'     `.
              ╭   ┌───┼┼─────────────────────────────────────┐             .───;         :
       public │   │   ││                    Penumbra Fullnode│            ;              │
        chain │   │   ││ grpc/grpc-web                       │          .─┤              ├──.
         data │   │   ▼▼                                     │        ,'                     `.
              │   │ ┌────┐     tm rpc proxy     ┌──────────┐ │       ;               Penumbra  :
              │   │ │    │◀────────────────────▶│          │ │       : ┌───────────▶ Network   ;
              │   │ │ pd │◀────────────────────▶│tendermint│◀┼─────────┘                      ╱
              │   │ └────┘       abci app       └──────────┘ │         `.     `.     `.     ,'
              ╰   └──────────────────────────────────────────┘           `───'  `───'  `───'

Questions

  • How much refactoring work does this sound like?
  • Do you foresee any particular difficulties with the design changes proposed above?
  • How can we support you in this work?

Gabe's web review

Hey there! Just joined Penumbra Labs this week and spent the last few days collecting my thoughts on the web strategy thus far. Segmented my feedback by themes:

Type safety 🛡️

The repo has opted out of Typescript type safety. This makes development inherently difficult as compile-time checks cannot be trusted to protect the developer. I suspect this causes a number of runtime bugs to slip in unnoticed. Typescript originally intended the relaxed ruleset to encourage the migration of javascript codebases. However, for modern stacks, it’s dangerous allowing any's. When strict mode is enabled, there are 100+ typing issues that reveal themselves in this repo.

Another big challenge is the fact that wasm-bindgen does not export types to the frontend (it uses JSValue type). See penumbra_wasm.d.ts, all inputs/outputs are any's. This causes the consumers of these function calls to also be not typesafe, and the consumers of those, and on and on. It appears we are getting around this a bit by grabbing the global environment’s indexdb. Unfortunately, I think there’s an argument that this is even more unsafe as global dependencies are now shared between two systems.

Recommendation:

  1. Add the strictest-possible Typescript compiler options. My recommendation:

    {
      "compilerOptions": {
            // ...
            "strict": true, // most important
            "allowUnreachableCode": false,
            "allowUnusedLabels": false,
            "exactOptionalPropertyTypes": true,
            "noFallthroughCasesInSwitch": true,
            "noImplicitOverride": true,
            "noImplicitReturns": true,
            "noPropertyAccessFromIndexSignature": true,
            "noUncheckedIndexedAccess": true,
            "noUnusedLocals": true,
            "noUnusedParameters": true,
      },
    }

    The remaining rules are set via strict mode.

  2. Add Eslint with recommended ruleset

  3. Make wasm-bindgen typesafe

    1. Option A (preferred): There is a crate that specifically addresses the JSValue issue: Tsify. Adding attributes like below causes the generated code to be strongly typed in Typescript.

      #[derive(Tsify, Serialize, Deserialize)]
      #[tsify(into_wasm_abi, from_wasm_abi)]
      pub struct HealthComputer {
          pub positions: Positions,
      }
      
      #[derive(Tsify, Serialize, Deserialize)]
      #[tsify(into_wasm_abi, from_wasm_abi)]
      pub struct HealthValuesResponse {
          pub max_ltv_health_factor: Option<Decimal>,
          pub above_max_ltv: bool,
      }
      
      #[wasm_bindgen]
      pub fn compute_health_js(c: HealthComputer) -> HealthValuesResponse {
          c.compute_health().unwrap().into()
      }
      
      // penumbra_wasm.d.ts
      // Argument and return type is now strongly typed!
      export function compute_health_js(c: HealthComputer): HealthValuesResponse
    2. Option B: Generated typescript code (that is unsafe) gets wrapped by type-safe functions that get exported and shared. Not as ideal given the types need to be manually written and the Rust code could still break it silently. A solution to this is to have tests that runs zod schema validation.

  4. Fix the type errors that are now revealed. This is likely far harder to do retroactively in one go. Should consider a strategy of a slower types migration by turning on strict mode and adding // @tsignore's temporarily.

  5. Add CI/CD actions for checking types/linting. At the moment, I do not see any github actions. We should add these to ensure PRs cannot be merged if violating rules.

  6. Re-consider depending on globals between two different systems. Would be helpful to get more context on why this approach was preferred.

Modularization 🧱

We anticipate many interfaces will want to make use of the different abilities that at the moment live exclusively in the web wallet. For this reason, the current architecture is a bit too bundled. For example, it’s highly unlikely that a popular cosmos wallet like Keplr will do transaction planning/proving. This has high storage costs and is computationally expensive. In this case, it’s likely a frontend or trusted backed would want access to the proving part of the stack separately.

Recommendation:

  1. Modularize into these functions:

    1. UI library, shared components, & design assets
    2. Data fetching, type parsing
    3. Assembling plans & proving
    4. Signing & broadcasting
    5. Chrome extension
    6. Web app A (uses penumbra ext)
    7. Web app B (uses keplr ext)

    In theory, this can be done with multiple repos. However, the dev experience will make this burdensome. I'd recommend a frontend monorepo build tool like TurboRepo. Monorepos come with their own pros and cons, but in this case it would tightly couple the versioning between these systems and make development easier.

  2. Research more deeply the exact requirements to support metamask snaps & keplr. It is likely the majority of our users will want to use the wallet they already are using (with hardware support). For that reason, we should be modularize with that use-case in mind.

  3. Add github actions for automatically publishing npm packages upon merges.

UX Design 🎨

Font sizing, padding values, spinners, and UI components feel a bit out of date with modern UIs. I definitely think there is exploration that can be done comparing the work of some of the best wallet UIs out there.

Recommendation:

  1. Start utilizing a UI library that works with Tailwind out of the box. I’d suggest tailwind (already using this) + radix-ui (component primitives) + shadcn/ui (component starter registry). This combo is used in the epic stack. Alternatively, headless-ui could be considered too.
  2. Do not copy, but consult the best-in-class wallets as inspiration.
  3. Transaction status is not clearly communicated. It’s hard to tell exactly what is happening at the proving/broadcasting stages. We likely need a <TxStatus /> component that does proper error handling, loading indicators, status communicating, etc.
  4. Consult/hire a designer. UX design is so critical to the success of a web3 app and I think Penumbra is under-investing in this area. There are a number of design-related things that could use leveling up.
    1. Published design guidelines. Will help in building out a component library.
    2. Logo usage. At the moment, the current logo does not scale in small sizes. For example, the chrome extension icon loses its distinction at that size.
    3. General auditing of user-flows.

House Cleaning 🧹

Here’s some random things I found going through the repo:

  1. Should use Prettier for code formatting.
  2. There are a number of commented out code blocks. We should just delete these and not commit them in the future.
  3. Using this observable pattern is a bit confusing to me. I need to get ramped up on the lifecycle of an extension request, but it feels a bit complex the way it’s designed here.
    1. Another example is the RemoteConfigController. It’s written as an event emitter with an observable store. However, it’s only ever used synchronously for a single value and not an ongoing stream consumers subscribe to.
  4. React.FC<> is now discouraged to be used as a type now that the React 18 types no longer automatically include childrenas a prop.
  5. General documentation and code comments need to be significantly higher. It’s very difficult to know the context of why some decisions were made (example).
    1. A system design would be helpful as well to know the roles/responsibilities of the controllers, background service, etc. Something like this.
  6. Testing. At the moment there are none. We should start writing them. Silent errors and breaking changes will grow and grow if we don’t defend the code via tests.
  7. I wonder the pros/cons of injecting via the window object versus using chrome.runtime.sendMessage(). I have a feeling the latter is more common (?).
  8. If proving times are so long, we should consider streaming options where we can get progress indicators. I wonder if wasm supports a kind of observable pattern.
  9. Frontenders love to debate state management libraries. If we are doing a migration, would be interesting to consider Zustand (a simpler redux) or XState (state machine pattern) as an alternative.
  10. Isn’t multithreaded wasm available now? Here’s an example.

Migration 🛻

Taken together, all of these changes would merit starting a new repo (say penumbra/web). Starting with Turbo repo with the strictest linting rules + CI/CD will allow us to move things over and refactor as we go. This will boost code quality and modularity. It also has the dual purpose of getting me up to speed on the this part of the stack as well.

However, this isn’t without a cost. Engineering resources will have to be used for adapting to the new architecture instead of building new features. Depending on launch urgency, this should be weighed (proceeding with current design vs starting anew).

Thoughts welcome!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.