penumbra-zone / web

Apache License 2.0
12 stars 15 forks source link

Reactivity of wallet state #1093

Closed grod220 closed 4 weeks ago

grod220 commented 4 months ago

In the client package we expose methods that frontends will use to connect to our wallet. After working with the cosmos-kit, think we should consider some enhancements.

cosmos-kit library

Cosmology's cosmos-kit is a standard in cosmos for connecting to wallets. They have a library managing the core logic and packages (such as @cosmos-kit/react) that provide helpers for various frontend frameworks. For our case, we are using the react package to access react hooks such as useWallet which gives us access to the user's wallet state:

import { useWallet } from '@cosmos-kit/react';
const { status } = useWallet()

switch (status) {
  case WalletStatus.Disconnected:
    return <WalletButtonBase buttonText='Connect Wallet' onClick={onClickConnect} />;
  case WalletStatus.Connecting:
    return ...
  case WalletStatus.Connected:
   return ...
  case WalletStatus.Error:
   return ...
}

The hook is reactive. So whenever the user's wallet state changes, the UI automatically reflects the correct state.

Their API that they expose in WalletContext is helpful to review as well. It has wallet and chain state:

export interface ChainWalletContext {
  chain: Chain;
  assets: AssetList | undefined;
  wallet: Wallet;
  logoUrl: string | undefined;
   ...
}

Wallet connection status:

export interface ChainWalletContext {
   ...
  isWalletDisconnected: boolean;
  isWalletConnecting: boolean;
  isWalletConnected: boolean;
  isWalletRejected: boolean;
  isWalletNotExist: boolean;
  isWalletError: boolean;
   ...
}

and actions the user can take

export interface ChainWalletContext {
   ...
  connect: () => Promise<void>;
  disconnect: (options?: DisconnectOptions) => Promise<void>;
   ...
}

API change recommendations for us

  1. Enclose our separately exported client functions into a single object (that implements an interface) that gets exported. This was also discussed here: https://github.com/penumbra-zone/web/issues/825.
  2. Create a new WalletContext interface that exposes a wallet state field akin to cosmos-kit plus the functions from the interface above.
  3. Build a new method that allows to subscribe to wallet state and get live updates.
  4. Write a usePenumbraWallet() hook that makes use of this new possibility for reactivity
export const usePenumbraWallet = () => {
  const [walletState, setWalletState] = useState();

  useEffect(() => {
    PenumbraClient.subscribe(state => setWalletState(state)); 
  });

  // And other methods from the client
  return { walletState };
};
VanishMax commented 3 months ago

Hey @turbocrime and @grod220, wanted to discuss the API of the client library and touch both #1093, #825, #340 and #341 – all issues describe the ways to connect to the extensions and consume data from the blockchain. I read the related issues and did my research with a goal to decompose the issue. So, the following read aims to solve this user pain:

I, as a user of Penumbra, want to connect my dApp to my third-party wallet and perform transactions and query the blockchain.

It does not explain how users should create their wallets. I'll assume their wallets are Prax's twins.

API inspiration

I am highly inspired by the Viem library for Ethereum. In my opinion, it has one of the best DX in the field by having a notion of clients:

import { createPublicClient, createWalletClient, http, custom } from 'viem';
import { mainnet } from 'viem/chains';

// Public client – provides the access to RPC for Ethereum blockchain
const publicClient = createPublicClient({ 
  chain: mainnet,
  transport: http()
});

const balance = await publicClient.getBalance({ 
  address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
});

// Wallet client – allows sending transactions on behalf on the connected user
const client = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum!)
});

const hash = await client.sendTransaction({ 
  account: address,
  to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC',
  value: parseEther('0.001')
})

I am sure we could make connection to Penumbra and its wallets similar. The only difference is that Penumbra won't have a Public client – all actions and even queries must be made by the user connected to wallet.

Next API concepts

Here are the main concepts that I think we need for the comfortable dApp development by third-parties:

  1. Penumbra wallets should follow the EIP-6963 Ethereum standard for wallets discovery (demo here). Right now we are writing into the window[Symbol('penumbra')] but this creates race conditions between wallets. The EIP-6963 would solve this problem.
  2. All services like ViewService, StakeService, DexService, etc., will be unified into one interface within this API. A field supportedServices should be added to the wallet configuration, so it would instantly throw errors in case of any non-supported apis.

With this in mind, I propose the following API:

import { createClient } from '@penumbra-zone/client';

// Calling `createClient` starts listening to "eip6963:announceProvider" event 
// but we should rename it to not confuse with Ethereum
const client = createClient();

// Returns the list of discovered wallets. Probably a function `onWalletDiscovered` should also be
// created to subscribe to new wallets sending events – this way users won't have to reload the page to see them
const wallets = client.getInjectedWallets();

// Connect to any of discovered injected wallets
// Additionally, `client.onConnectionChange` could be called to monitor for the connection
const wallet = client.connect(wallets[0]);

// Consume the `ViewService`
const { address } = await client.view.addressByIndex({});

I believe such API would enhance Penumbra adoption and bring many projects connected to Penumbra wallets and even blockchain newcomers could easily create their first app in the private blockchain!

What do you think about this API? Maybe I misunderstood some theory behind the wallets or it is just irrelevant – please hit me with your opinion!

turbocrime commented 3 months ago

glancing over this. interesting it was published around last year when i was initially working on the wallet injection, would have been cool to read then!

some of the conflict conditions described in the article don't exist in our implementation and ive prototyped another design https://github.com/penumbra-zone/web/pull/1145 with some other benefits/considerations

i think specifically adopting an eth standard is not something that works. penumbra is expected to integrate more with the cosmos ecosystem

will look at this more soon but i think significant changes have been declined by @grod220 as not a current priority

turbocrime commented 3 months ago

the transport is capable of type-agnostic transmission but currently the developer is expected to init with the message types they expect to handle, and connect to an extension that is aware of those types

grod220 commented 3 months ago

Helpful writeup @VanishMax. Interesting to see references in the industry for a similar problem space. Timing-wise, for now it's best to be forward-compatible (ish) to the current injected provider interface. However, it may be good to capture this in another issue so it doesn't get lost.

I'd say the main effort of this issue is to ensure consumers are able to create reactive interfaces. At the moment, dapps cannot for example create a "Connect" button that listens to the current state of the wallet. It has to re-request/poll to get the latest state.