From a19b911202748b48b9244365a74fdadf771df8db Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 12 Jul 2024 16:30:48 +0200 Subject: [PATCH 1/5] feat: add ADR-006 for web APIs and general description --- docs/adrs/006-web-apis.md | 190 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 docs/adrs/006-web-apis.md diff --git a/docs/adrs/006-web-apis.md b/docs/adrs/006-web-apis.md new file mode 100644 index 0000000000..242e9429a5 --- /dev/null +++ b/docs/adrs/006-web-apis.md @@ -0,0 +1,190 @@ +# ADR 006: Client package API + +Penumbra's web repository is growing fast. As more applications use its packages, introducing new changes will become increasingly difficult. This document aims to define what and how the packages should export and set the path for growth. However, the implementation of the APIs is not within the scope of this document. + +Benefits for the ecosystem: A decreased entry level in Penumbra development leads to more created applications and faster mass adoption. + +Benefits for Penumbra developers: API specifications aim to align the vision for package development and simplify the decision-making process when creating new features for external use. + +## Design for dApp makers + +**dApp makers** are developers of applications that connect to Penumbra account: transaction explorers, DEXes, payment systems, etc. They are interested in rapid development based on existing solutions and are often new not only to Penumbra but to blockchain as a whole. These developers need client-side libraries for wallet connection, data requests and data rendering. + +When developing a web application for communicating with the Penumbra blockchain, dApp makers might need the following features: + +- Get a list of injected wallets +- Connect to account +- Disconnect from account +- Monitor the connection +- Get and display private information about the account +- Get and display public information about the blockchain +- Get real-time updates about the account and new blocks +- Sign and send transactions within Penumbra +- Wait for transactions to be complete +- Send IBC transactions +- Estimate transaction costs +- Stake and swap assets +- Participate in protocol governance + +This list is supposed to be covered by the proposed API, documented and presented with examples to users. + +## Creating the client + +In other blockchains, JavaScript SDKs usually split the interfaces into at least two parts: one that manages the wallet connection and private transactions, and one that reads the public blockchain data. Examples include [Viem](https://viem.sh/docs/clients/intro) and [Thirdweb](https://portal.thirdweb.com/typescript/v5/client) on Ethereum. + +In Penumbra, any blockchain query requires a connection with the wallet provider, therefore all actions and +queries can be called 'private'. It is reasonable to construct the notion of a **client** – the interface +that manages the injected connections and provides methods for interacting with the blockchain. + +Creating the `client` should be the starting point for any application working with Penumbra: + +```ts +import { createPenumbraClient } from '@penumbra-zone/client'; + +export const client = createPenumbraClient(); +``` + +The flow of work with the `client` would be as follows: + +- User creates an instance of the `client` +- They use the `client` to establish the injected connection, and the `client` saves the connection in its state +- To fetch information from the wallet, the application passes the `client` instance to the fetch methods + +The idea of a single shared `client` is inspired by these popular TypeScript libraries: + +- [Prisma ORM](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/instantiate-prisma-client) +- [Supabase](https://supabase.com/docs/reference/javascript/initializing) +- [Viem](https://viem.sh/docs/clients/intro) + +## Injected connection + +The `client` must be able to connect to injected wallets. To make the API possible, the interface injected into the `window` object must be standardized: + +```ts +/** The callback type for the `penumbrastate` event */ +export type PenumbraStateEventHandler = ( + value: CustomEvent<{ origin: string; connected: boolean; state: PenumbraState }>, +) => void; + +/** + * Describes the type of `addListener` and `removeListener` functions. + * If there will more event types in the future, this function should be overloaded + */ +export type PenumbraListener = (type: 'penumbrastate', value: PenumbraStateListener) => void; + +export interface PenumbraProvider extends Readonly { + /** Should contain a URI at the provider's origin, + * serving a manifest describing this provider */ + readonly manifest: string; + + /** Call to gain approval. Returns `MessagePort` to this provider. + * Might throw descriptive errors if user denies the connection */ + readonly connect: () => Promise; + + /** Call to indicate the provider should discard approval of this origin. + * Successfull if doesn't throw errors */ + readonly disconnect: () => Promise; + + /** Should synchronously return the present connection state. + * - `true` indicates active connection. + * - `false` indicates connection is closed, rejected, or not attempted. + */ + readonly isConnected: () => boolean; + + /** Synchronously return present injection state */ + readonly state: () => PenumbraState; + + /** Fires a callback with CustomEvent each time the state changes */ + readonly addEventListener: PenumbraListener; + /** Unsubscribes from the state change events. Provide the same callback as in `addListener` */ + readonly removeEventListener: PenumbraListener; +} +``` + +## Client API + +A globally shared `client` instance should be able to establish the connection with +injected wallet providers and use the connection to query the blockchain. The API in this case would be: + +```ts +import type { PenumbraService } from '@penumbra-zone/protobuf'; +import type { PromiseClient } from '@connectrpc/connect'; + +interface PenumbraClient { + /** + * Asks users to approve the connection to a specific browser manifest URL. + * If `manifest` argument is not provided, tries to connect to the first injected provider. + * Returns the manifest URL of the connected provider or error otherwise + */ + readonly connect: (providerUrl?: string) => Promise; + + /** Reexports the `disconnect` function from injected provider */ + readonly disconnect: () => Promise; + /** Reexports the `isConnected` function from injected provider */ + readonly isConnected: () => boolean | undefined; + /** Reexports the `state` function from injected provider */ + readonly getState: () => PenumbraState; + /** Reexports the `onConnectionChange` listener from injected provider*/ + readonly onConnectionChange: ( + cb: (connection: { origin: string; connected: boolean; state: PenumbraState }) => void, + ) => void; + + /** + * Needed for custom service connections if `getService` is not enough. + * For example, might be useful for React wrapper of the `client` package + */ + readonly getMessagePort: () => MessagePort; +} +``` + +Moreover, the `client` package might export but not limited to the following useful functions and types: + +```ts +export type PenumbraManifest = Partial & + Required>; + +export type getInjectedProvider = (penumbraOrigin: string) => Promise; + +export type getAllInjectedProviders = () => string[]; + +export type getPenumbraManifest = ( + penumbraOrigin: string, + signal?: AbortSignal, +) => Promise; + +export type getAllPenumbraManifests = () => Record< + keyof (typeof window)[typeof PenumbraSymbol], + Promise +>; +``` + +## Requests + +The client library should separately export the creation of service clients. It is not going to be integrated into the `client` instance to save the initial bundle size. Instead, it should be exported from `@penumbra-zone/client/service`: + +```ts +/** Synchronously creates a connectrpc `PromiseClient` instance to a given Penumbra service */ +export type createServiceClient = ( + client: PenumbraClient, + service: T, +) => PromiseClient; +``` + +Under the hood, `createServiceClient` might save the resulting PromiseClient in the `client` instance to avoid creating multiple instances of the same service. + +Requesting data example: + +```ts +import { createPenumbraClient } from '@penumbra-zone/client'; +import { createServiceClient } from '@penumbra-zone/client/servce'; +import { ViewService } from '@penumbra-zone/protobuf'; + +export const client = createPenumbraClient(); + +const viewService = createServiceClient(client, ViewService); + +const address = await viewService.getAddressByIndex({ account: 0 }); +const balances = await viewService.getBalances({ account: 0 }); +``` + +Each call of the services function should check for a connection and throw an error if the connection is not established. From ea08652da2d20dc8a14d2c30c3c74461dcb26456 Mon Sep 17 00:00:00 2001 From: the letter L <134443988+turbocrime@users.noreply.github.com> Date: Tue, 6 Aug 2024 04:10:21 -0700 Subject: [PATCH 2/5] adr 006 changes (#1647) --- docs/adrs/006-web-apis.md | 141 +++++++++++++++----------------------- 1 file changed, 57 insertions(+), 84 deletions(-) diff --git a/docs/adrs/006-web-apis.md b/docs/adrs/006-web-apis.md index 242e9429a5..4dd5b619eb 100644 --- a/docs/adrs/006-web-apis.md +++ b/docs/adrs/006-web-apis.md @@ -39,9 +39,13 @@ that manages the injected connections and provides methods for interacting with Creating the `client` should be the starting point for any application working with Penumbra: ```ts -import { createPenumbraClient } from '@penumbra-zone/client'; +import { PenumbraClient } from '@penumbra-zone/client'; -export const client = createPenumbraClient(); +const providers: Record = PenumbraClient.providers(); +const someProviderOrigin: keyof providers = '....'; + +// connect will fetch and verify manifest before initiating connection, then return an active client +const someProviderClient = await PenumbraClient.connect(someProviderOrigin); ``` The flow of work with the `client` would be as follows: @@ -61,43 +65,28 @@ The idea of a single shared `client` is inspired by these popular TypeScript lib The `client` must be able to connect to injected wallets. To make the API possible, the interface injected into the `window` object must be standardized: ```ts -/** The callback type for the `penumbrastate` event */ -export type PenumbraStateEventHandler = ( - value: CustomEvent<{ origin: string; connected: boolean; state: PenumbraState }>, -) => void; - -/** - * Describes the type of `addListener` and `removeListener` functions. - * If there will more event types in the future, this function should be overloaded - */ -export type PenumbraListener = (type: 'penumbrastate', value: PenumbraStateListener) => void; - -export interface PenumbraProvider extends Readonly { - /** Should contain a URI at the provider's origin, - * serving a manifest describing this provider */ +export interface PenumbraProvider extends Readonly { + /** Should contain a URI at the provider's origin, serving a manifest + * describing this provider */ readonly manifest: string; - /** Call to gain approval. Returns `MessagePort` to this provider. - * Might throw descriptive errors if user denies the connection */ + /** Call to acquire a `MessagePort` to this provider, subject to approval. */ readonly connect: () => Promise; - /** Call to indicate the provider should discard approval of this origin. - * Successfull if doesn't throw errors */ + /** Call to indicate the provider should discard approval of this origin. */ readonly disconnect: () => Promise; /** Should synchronously return the present connection state. * - `true` indicates active connection. - * - `false` indicates connection is closed, rejected, or not attempted. - */ + * - `false` indicates connection is inactive. */ readonly isConnected: () => boolean; /** Synchronously return present injection state */ readonly state: () => PenumbraState; - /** Fires a callback with CustomEvent each time the state changes */ - readonly addEventListener: PenumbraListener; - /** Unsubscribes from the state change events. Provide the same callback as in `addListener` */ - readonly removeEventListener: PenumbraListener; + /** Standard EventTarget methods emitting PenumbraStateEvent upon state changes */ + readonly addEventListener: PenumbraEventTarget['addEventListener']; + readonly removeEventListener: PenumbraEventTarget['removeEvenListener']; } ``` @@ -110,81 +99,65 @@ injected wallet providers and use the connection to query the blockchain. The AP import type { PenumbraService } from '@penumbra-zone/protobuf'; import type { PromiseClient } from '@connectrpc/connect'; -interface PenumbraClient { - /** - * Asks users to approve the connection to a specific browser manifest URL. - * If `manifest` argument is not provided, tries to connect to the first injected provider. - * Returns the manifest URL of the connected provider or error otherwise - */ - readonly connect: (providerUrl?: string) => Promise; +interface PenumbraClientConstructor { + // static features + providers(): string[] | undefined; + providerManifests(): Record>; - /** Reexports the `disconnect` function from injected provider */ - readonly disconnect: () => Promise; - /** Reexports the `isConnected` function from injected provider */ - readonly isConnected: () => boolean | undefined; - /** Reexports the `state` function from injected provider */ - readonly getState: () => PenumbraState; - /** Reexports the `onConnectionChange` listener from injected provider*/ - readonly onConnectionChange: ( - cb: (connection: { origin: string; connected: boolean; state: PenumbraState }) => void, - ) => void; - - /** - * Needed for custom service connections if `getService` is not enough. - * For example, might be useful for React wrapper of the `client` package - */ - readonly getMessagePort: () => MessagePort; -} -``` + // provider-specific static features + providerManifest(providerOrigin: string): Promise; + providerIsConnected(providerOrigin: string): boolean; + providerState(providerOrigin: string): PenumbraState | undefined; -Moreover, the `client` package might export but not limited to the following useful functions and types: + // Initiates connection and returns a connected client instance. + providerConnect(providerOrigin: C): Promise>; -```ts -export type PenumbraManifest = Partial & - Required>; + // constructor for an instance bound to a specific provider + new (providerOrigin: O): PenumbraClient; +} -export type getInjectedProvider = (penumbraOrigin: string) => Promise; +interface PenumbraClient { + // constructor input + origin: O; -export type getAllInjectedProviders = () => string[]; + // populated when client is in appropriate state. + transport?: Transport; + port?: MessagePort; -export type getPenumbraManifest = ( - penumbraOrigin: string, - signal?: AbortSignal, -) => Promise; + // direct re-exports from the selected provider + disconnect(): Promise; + isConnected: () => boolean; + state: () => PenumbraState; + addEventListener: PenumbraProvider['addEventListener']; + removeEventListener: PenumbraProvider['removeEvenListener']; -export type getAllPenumbraManifests = () => Record< - keyof (typeof window)[typeof PenumbraSymbol], - Promise ->; -``` + /** Initiates connection request and then connection. A transport is + * constructed, maintained internally and also returned to the caller. */ + connect: () => Promise; -## Requests + /** Fetches manifest for the provider of this instance */ + manifest: () => Promise; -The client library should separately export the creation of service clients. It is not going to be integrated into the `client` instance to save the initial bundle size. Instead, it should be exported from `@penumbra-zone/client/service`: + // Returns a new or re-used `PromiseClient` for a specific `PenumbraService` + service = (service: T): PromiseClient; -```ts -/** Synchronously creates a connectrpc `PromiseClient` instance to a given Penumbra service */ -export type createServiceClient = ( - client: PenumbraClient, - service: T, -) => PromiseClient; + onConnectionStateChange(listener: (detail: PenumbraStateEventDetail) => void, removeListener?: AbortSignal): void; +} ``` -Under the hood, `createServiceClient` might save the resulting PromiseClient in the `client` instance to avoid creating multiple instances of the same service. - Requesting data example: ```ts -import { createPenumbraClient } from '@penumbra-zone/client'; -import { createServiceClient } from '@penumbra-zone/client/servce'; +import { PenumbraClient } from '@penumbra-zone/client'; import { ViewService } from '@penumbra-zone/protobuf'; -export const client = createPenumbraClient(); +const providers: Record = PenumbraClient.providers(); +const someProviderOrigin: keyof providers = '....'; -const viewService = createServiceClient(client, ViewService); +const penumbraClient = await PenumbraClient.connect(someProviderOrigin); -const address = await viewService.getAddressByIndex({ account: 0 }); -const balances = await viewService.getBalances({ account: 0 }); -``` +const viewClient = penumbraClient.service(ViewService); -Each call of the services function should check for a connection and throw an error if the connection is not established. +const address = await viewClient.getAddressByIndex({ account: 0 }); +const balances = await viewClient.getBalances({ account: 0 }); +``` From 54664aeb119fe117ed51fbd26d1e7c474d0eb5d5 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 9 Aug 2024 08:29:07 +0200 Subject: [PATCH 3/5] feat: updates from #1648 Co-Authored-By: the letter L <134443988+turbocrime@users.noreply.github.com> --- docs/adrs/006-web-apis.md | 279 ++++++++++++++++++++++++++------------ 1 file changed, 190 insertions(+), 89 deletions(-) diff --git a/docs/adrs/006-web-apis.md b/docs/adrs/006-web-apis.md index 4dd5b619eb..e8b4e8e1a3 100644 --- a/docs/adrs/006-web-apis.md +++ b/docs/adrs/006-web-apis.md @@ -12,152 +12,253 @@ Benefits for Penumbra developers: API specifications aim to align the vision for When developing a web application for communicating with the Penumbra blockchain, dApp makers might need the following features: -- Get a list of injected wallets -- Connect to account -- Disconnect from account +- Identify available Penumbra connections +- Connect to Penumbra +- Disconnect from Penumbra - Monitor the connection -- Get and display private information about the account -- Get and display public information about the blockchain +- Fetch private information about the user +- Fetch public information about the chain - Get real-time updates about the account and new blocks -- Sign and send transactions within Penumbra -- Wait for transactions to be complete -- Send IBC transactions +- Create, authorize, and publish Penumbra transactions +- Create, authorize, and publish IBC transactions +- Verify transaction appearance on the chain - Estimate transaction costs -- Stake and swap assets -- Participate in protocol governance +- Trade and swap assets, provide liquidity +- Stake assets with validators +- Participate in governance -This list is supposed to be covered by the proposed API, documented and presented with examples to users. +These features are available through API described and presented here. -## Creating the client +## Client concepts -In other blockchains, JavaScript SDKs usually split the interfaces into at least two parts: one that manages the wallet connection and private transactions, and one that reads the public blockchain data. Examples include [Viem](https://viem.sh/docs/clients/intro) and [Thirdweb](https://portal.thirdweb.com/typescript/v5/client) on Ethereum. +In public-state blockchains, web toolkits usually split the interface into at least two parts: -In Penumbra, any blockchain query requires a connection with the wallet provider, therefore all actions and -queries can be called 'private'. It is reasonable to construct the notion of a **client** – the interface -that manages the injected connections and provides methods for interacting with the blockchain. +1. the wallet, keys, metadata (small, local, private) +2. the chain, viewing transactions (large, remote, public) -Creating the `client` should be the starting point for any application working with Penumbra: +Examples include [Viem](https://viem.sh/docs/clients/intro) and [Thirdweb](https://portal.thirdweb.com/typescript/v5/client) on Ethereum. + +This distinction works well when there is a clear separation between the client and the server. + +But in Penumbra, the user is running a local node with a local copy of the chain. Instead of speaking to a remote server, a server is running directly in the user's web browser. This local 'light node' on the webpage is queryable with the same API as a remote 'full node', except the protocol is not `https`. + +An additional pair of services (the `ViewService` and the `CustodyService`) are available on the local node, and represent the API to the private chain state. + +## Client brief + +It is reasonable to construct the notion of a **client** – the interface that manages connections and provides methods for interacting with the blockchain. + +Creating the `PenumbraClient` should be the starting point for any application working with Penumbra. A simple example: ```ts import { PenumbraClient } from '@penumbra-zone/client'; +import { ViewService } from '@penumbra-zone/protobuf'; -const providers: Record = PenumbraClient.providers(); -const someProviderOrigin: keyof providers = '....'; +const providers: Record = PenumbraClient.getProviders(); +const someProviderOrigin: keyof providers = /* choose a provider */ -// connect will fetch and verify manifest before initiating connection, then return an active client -const someProviderClient = await PenumbraClient.connect(someProviderOrigin); +const penumbra = createPenumbraClient(someProviderOrigin); +await penumbra.connect(); // the user must approve a connection + +const address0 = penumbra.service(ViewService).getAddressByIndex({ account: 0 }) ``` -The flow of work with the `client` would be as follows: +The flow of work with the `PenumbraClient` would be as follows: + +- Developer uses static methods of `PenumbraClient` to identify and choose a provider. +- Developer creates an instance of `PenumbraClient` to encapsulate configuration and connection state. +- The `PenumbraClient` instance establishes and manages the connection. + +At this point, the developer may begin interacting with the public chain or the user's private state. + +- The developer creates service-specific clients with the `service` method +- The service clients may query the service API endpoints to fetch information -- User creates an instance of the `client` -- They use the `client` to establish the injected connection, and the `client` saves the connection in its state -- To fetch information from the wallet, the application passes the `client` instance to the fetch methods +These steps are evident above. -The idea of a single shared `client` is inspired by these popular TypeScript libraries: +## `PenumbraProvider` interface -- [Prisma ORM](https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/instantiate-prisma-client) -- [Supabase](https://supabase.com/docs/reference/javascript/initializing) -- [Viem](https://viem.sh/docs/clients/intro) +Any user may have one or multiple tools present that independently offer some kind of Penumbra service. These independent software are called "providers". -## Injected connection +You can interact with providers directly, but it is recommended to use `PenumbraClient`. -The `client` must be able to connect to injected wallets. To make the API possible, the interface injected into the `window` object must be standardized: +Providers should identify themselves by origin URI, typically a chrome extension URI, and expose a simple `PenumbraProvider` API to initate connection. + +Available providers may be discovered by a record on the document at `window[Symbol.for('penumbra')]`, of the type `Record` where the key is a URI origin at which the provider's manifest is hosted. ```ts -export interface PenumbraProvider extends Readonly { +export interface PenumbraProvider { /** Should contain a URI at the provider's origin, serving a manifest - * describing this provider */ + * describing this provider. */ readonly manifest: string; /** Call to acquire a `MessagePort` to this provider, subject to approval. */ readonly connect: () => Promise; - /** Call to indicate the provider should discard approval of this origin. */ + /** Call to indicate the provider should discard approval of this dapp. */ readonly disconnect: () => Promise; - /** Should synchronously return the present connection state. - * - `true` indicates active connection. - * - `false` indicates connection is inactive. */ + /** `true` indicates active connection, `false` indicates inactive connection. */ readonly isConnected: () => boolean; - /** Synchronously return present injection state */ + /** Synchronously return present state. */ readonly state: () => PenumbraState; - /** Standard EventTarget methods emitting PenumbraStateEvent upon state changes */ + /** + * Like a standard `EventTarget.addEventListener`, but providers should only + * emit `PenumbraEvent`s (currently only `PenumbraStateEvent` with typename + * `'penumbrastate'`.) Event types and type guards are available from + * `@penumbra-zone/client/event` or the root export. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener + */ readonly addEventListener: PenumbraEventTarget['addEventListener']; - readonly removeEventListener: PenumbraEventTarget['removeEvenListener']; + readonly removeEventListener: PenumbraEventTarget['addEventListener']; } ``` -## Client API +## `PenumbraClient` interface -A globally shared `client` instance should be able to establish the connection with -injected wallet providers and use the connection to query the blockchain. The API in this case would be: +`PenumbraClient` is intended to be the 'entry' to the collection of service APIs for dapp developers inspecting or interacting with a user's Penumbra chain state. -```ts -import type { PenumbraService } from '@penumbra-zone/protobuf'; -import type { PromiseClient } from '@connectrpc/connect'; +PenumbraClient static methods will allow you to -interface PenumbraClientConstructor { - // static features - providers(): string[] | undefined; - providerManifests(): Record>; +- inspect available providers +- verify the provider is present +- choose a provider to connect - // provider-specific static features - providerManifest(providerOrigin: string): Promise; - providerIsConnected(providerOrigin: string): boolean; - providerState(providerOrigin: string): PenumbraState | undefined; +If you're developing a dapp using penumbra, you should likely: - // Initiates connection and returns a connected client instance. - providerConnect(providerOrigin: C): Promise>; +- gate penumbra features, if no providers are installed +- display a button to initiate connection, if no providers are connected +- display a modal choice, if multiple providers are present - // constructor for an instance bound to a specific provider - new (providerOrigin: O): PenumbraClient; -} +When you've selected a provider, you can provide its origin URI to `createPenumbraClient` or `new PenumbraClient`. This will create a client attached to that provider, and you can then: -interface PenumbraClient { - // constructor input - origin: O; +- request permission to connect and create an active connection with `connect` +- access the provider's services with `service` and a `ServiceType` parameter +- release your permissions with `disconnect` - // populated when client is in appropriate state. - transport?: Transport; - port?: MessagePort; +### Static features - // direct re-exports from the selected provider - disconnect(): Promise; - isConnected: () => boolean; - state: () => PenumbraState; - addEventListener: PenumbraProvider['addEventListener']; - removeEventListener: PenumbraProvider['removeEvenListener']; +Methods for inspecting providers without interaction are provided as static class members. None of these static methods will modify any provider state. + +```ts +export declare class PenumbraClient { + /** Return the record of all present providers available in the page. */ + static getProviders(): Record; - /** Initiates connection request and then connection. A transport is - * constructed, maintained internally and also returned to the caller. */ - connect: () => Promise; + /** Return a record of all present providers, and fetch their manifests. */ + static getAllProviderManifests(): Record>; - /** Fetches manifest for the provider of this instance */ - manifest: () => Promise; + /** Fetch manifest of a specific provider, or return `undefined` if the + * provider is not present. */ + static getProviderManifest(providerOrigin: string): Promise | undefined; - // Returns a new or re-used `PromiseClient` for a specific `PenumbraService` - service = (service: T): PromiseClient; + /** Return boolean connection state of a specific provider, or `undefined` if + * the provider is not present. */ + static isProviderConnected(providerOrigin: string): boolean | undefined; - onConnectionStateChange(listener: (detail: PenumbraStateEventDetail) => void, removeListener?: AbortSignal): void; + /** Return connection state enum of a specific provider, or `undefined` if the + * provider is not present. */ + static getProviderState(providerOrigin: string): PenumbraState | undefined; } ``` -Requesting data example: +### Instance features + +After selecting a provider, you can use `createPenumbraClient` or the constructor to create an instance of `PenumbraClient` attached to your selected provider. The instance allows you to engage in more detail and begin state manipulation. ```ts -import { PenumbraClient } from '@penumbra-zone/client'; -import { ViewService } from '@penumbra-zone/protobuf'; +export declare class PenumbraClient { + /** Construct a client instance but take no specific action. Will immediately + * attach to a specified provider, or remain unconfigured. */ + constructor(providerOrigin?: string | undefined, options?: PenumbraClientOptions); + + /** Attempt to connect to the attached provider, or attach and then connect to + * the provider specified by parameter. + * + * Presence of the public `connected` field can confirm the client is + * connected or can connect. The public `transport` field can confirm the + * client possesses an active connection. + * + * May reject with an enumerated `PenumbraRequestFailure`. + */ + connect(providerOrigin?: string): Promise; + + /** Call `disconnect` on any associated provider to release connection + * approval, and destroy any present connection. */ + disconnect(): Promise; -const providers: Record = PenumbraClient.providers(); -const someProviderOrigin: keyof providers = '....'; + /** Return a `PromiseClient` for some `T extends ServiceType`, using this + * client's internal `Transport`. If you call this method before this client + * is attached, this method will throw. + * + * You should also prefer to call this method *after* this client's connection + * has succeeded. + * + * If you call this method before connection success is resolved, a connection + * will be initiated if necessary but will not be awaited (as this is a + * synchronous method). If a connection initated this way is rejected, or does + * not resolve within the `defaultTimeoutMs` of this client's + * `options.transport`, requests made with the returned `PromiseClient` + * will throw. + */ + service(service: T): PromiseClient; + + /** Simplified callback interface to the `EventTarget` interface of the + * associated provider. */ + onConnectionStateChange( + listener: (detail: PenumbraEventDetail<'penumbrastate'>) => void, + removeListener?: AbortSignal, + ): void; + + /** It is recommended to construct clients with a specific provider origin. If + * you didn't do that, and you're working with an unconfigured client, you can + * configure it with `attach`. + * + * A client may only be attached once. A client must be attached to connect. + * + * Presence of the public `origin` field can confirm a client instance is + * attached. + * + * If called repeatedly with a matching provider, `attach` is a no-op. If + * called repeatedly with a different provider, `attach` will throw. + */ + attach(providerOrigin: string): Promise; + + /** The parsed `PenumbraManifest` associated with this provider, fetched at + * time of provider attach. This will be `undefined` if this client is not + * attached to a provider, or if the manifest fetch has not yet resolved. + * + * If you have awaited the return of `attach` or `connect`, this should be + * present. + */ + get manifest(): PenumbraManifest | undefined; + + /** The provider origin URI, or `undefined` if this client is not attached. */ + get origin(): string | undefined; + /** The attached provider, or `undefined` if this client is not attached. */ + get provider(): PenumbraProvider | undefined; + /** The boolean provider connection status, or `undefined` if this client is + * not attached to a provider. */ + get connected(): boolean | undefined; + /** The `PenumbraState` enumerated provider connection state, or `undefined` if + * this client is not attached to a provider. */ + get state(): PenumbraState | undefined; +} +``` -const penumbraClient = await PenumbraClient.connect(someProviderOrigin); +### Service client features -const viewClient = penumbraClient.service(ViewService); +The service-specific clients returned by the `service` method are generated from Protobuf specifications which are compiled into `ServiceType` definitions that may be imported from `@penumbra-zone/protobuf`. Your IDE should provide type introspection on the `ServiceType`, and on the returned `PromiseClient`. -const address = await viewClient.getAddressByIndex({ account: 0 }); -const balances = await viewClient.getBalances({ account: 0 }); -``` +It's recommended to read [ConnectRPC Web](https://connectrpc.com/docs/web/) documentation for general details of client use. + +Penumbra's proto specs are published to the Buf Schema Registry at [buf.build/penumbra-zone/penumbra](https://buf.build/penumbra-zone/penumbra). You are likely interested in the [View service](https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.view.v1) and the [Custody service](https://buf.build/penumbra-zone/penumbra/docs/main:penumbra.custody.v1) API docs. + +More detailed and objective-specific documentation is available from [guide.penumbra.zone](https://guide.penumbra.zone/) and the [Penumbra web monorepo](https://github.com/penumbra-zone/web/). Web developers will be ineterested in the documentation of `@penumbra-zone/client` (discussed in this ADR) and `@penumbra-zone/react`. + +The `@penumbra-zone/protobuf` package exports several services, but technically, the `PenumbraClient` interface is flexible enough that a provider could implement and provide any service they wish. Documentation on any uniquely available services should be sought from the developers of the provider. + +The detailed Penumbra documentation is available at [protocol.penumbra.zone](https://protocol.penumbra.zone). From 5d2e3ebac51f279a2dbb79a4ae49cdc4dff22cd4 Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Fri, 9 Aug 2024 08:46:50 +0200 Subject: [PATCH 4/5] fix: add some info --- docs/adrs/006-web-apis.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/adrs/006-web-apis.md b/docs/adrs/006-web-apis.md index e8b4e8e1a3..dc822901c0 100644 --- a/docs/adrs/006-web-apis.md +++ b/docs/adrs/006-web-apis.md @@ -78,11 +78,11 @@ These steps are evident above. ## `PenumbraProvider` interface -Any user may have one or multiple tools present that independently offer some kind of Penumbra service. These independent software are called "providers". +Any user may have one or multiple tools present that independently offer some kind of Penumbra service. These independent software are called "providers". For example, Prax browser extension is a provider. You can interact with providers directly, but it is recommended to use `PenumbraClient`. -Providers should identify themselves by origin URI, typically a chrome extension URI, and expose a simple `PenumbraProvider` API to initate connection. +Providers should identify themselves by origin URI, typically a chrome extension URI, and expose a simple `PenumbraProvider` API to initiate connection. Available providers may be discovered by a record on the document at `window[Symbol.for('penumbra')]`, of the type `Record` where the key is a URI origin at which the provider's manifest is hosted. @@ -129,16 +129,18 @@ PenumbraClient static methods will allow you to If you're developing a dapp using penumbra, you should likely: -- gate penumbra features, if no providers are installed - display a button to initiate connection, if no providers are connected - display a modal choice, if multiple providers are present +- gate penumbra features, if no providers are installed -When you've selected a provider, you can provide its origin URI to `createPenumbraClient` or `new PenumbraClient`. This will create a client attached to that provider, and you can then: +When you've selected a provider, you can provide its origin URI to `createPenumbraClient`, or `new PenumbraClient`. This will create a client attached to that provider, and you can then: - request permission to connect and create an active connection with `connect` - access the provider's services with `service` and a `ServiceType` parameter - release your permissions with `disconnect` +As an alternative, you can create an unconfigured client with empty state by calling `createPenumbraClient` with no arguments and then provide the origin URI to the `connect` method. + ### Static features Methods for inspecting providers without interaction are provided as static class members. None of these static methods will modify any provider state. From a6b6b76129445393b8ec1587404bf2dc7b52c28f Mon Sep 17 00:00:00 2001 From: turbocrime Date: Thu, 8 Aug 2024 23:53:17 -0700 Subject: [PATCH 5/5] docstrings merge --- docs/adrs/006-web-apis.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/docs/adrs/006-web-apis.md b/docs/adrs/006-web-apis.md index dc822901c0..631a61b9f4 100644 --- a/docs/adrs/006-web-apis.md +++ b/docs/adrs/006-web-apis.md @@ -177,33 +177,27 @@ export declare class PenumbraClient { * attach to a specified provider, or remain unconfigured. */ constructor(providerOrigin?: string | undefined, options?: PenumbraClientOptions); - /** Attempt to connect to the attached provider, or attach and then connect to - * the provider specified by parameter. - * - * Presence of the public `connected` field can confirm the client is - * connected or can connect. The public `transport` field can confirm the - * client possesses an active connection. + /** Attempt to connect to the attached provider. If this client is unattached, + * a provider may be specified at this moment. * * May reject with an enumerated `PenumbraRequestFailure`. + * + * The public `connected` field will report the provider's connected state, or + * `undefined` if this client is not attached to a provider. The public + * `transport` field can confirm the client possesses an active connection. + * + * If called again while already connected, `connect` is a no-op. */ connect(providerOrigin?: string): Promise; - /** Call `disconnect` on any associated provider to release connection + /** Call `disconnect` on the associated provider to release connection * approval, and destroy any present connection. */ disconnect(): Promise; /** Return a `PromiseClient` for some `T extends ServiceType`, using this - * client's internal `Transport`. If you call this method before this client - * is attached, this method will throw. + * client's internal `Transport`. * - * You should also prefer to call this method *after* this client's connection - * has succeeded. - * - * If you call this method before connection success is resolved, a connection - * will be initiated if necessary but will not be awaited (as this is a - * synchronous method). If a connection initated this way is rejected, or does - * not resolve within the `defaultTimeoutMs` of this client's - * `options.transport`, requests made with the returned `PromiseClient` + * If you call this method while this client is not `Connected`, this method * will throw. */ service(service: T): PromiseClient; @@ -215,17 +209,20 @@ export declare class PenumbraClient { removeListener?: AbortSignal, ): void; - /** It is recommended to construct clients with a specific provider origin. If + /** + * It is recommended to construct clients with a specific provider origin. If * you didn't do that, and you're working with an unconfigured client, you can * configure it with `attach`. * * A client may only be attached once. A client must be attached to connect. * * Presence of the public `origin` field can confirm a client instance is - * attached. + * attached to a provider, and presence of the public `manifest` field can + * confirm the attached provider served an appropriate manifest. You may await + * manifest confirmation by awaiting the return of `attach`. * - * If called repeatedly with a matching provider, `attach` is a no-op. If - * called repeatedly with a different provider, `attach` will throw. + * If called again with a matching provider, `attach` is a no-op. If called + * again with a different provider, `attach` will throw. */ attach(providerOrigin: string): Promise;