Skip to content

Commit

Permalink
add disconnect method to page api
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Jun 25, 2024
1 parent 2eb69d2 commit 31209d4
Show file tree
Hide file tree
Showing 13 changed files with 430 additions and 63 deletions.
5 changes: 5 additions & 0 deletions .changeset/moody-bulldogs-mate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/client': major
---

add disconnect method, add `create` module
6 changes: 6 additions & 0 deletions .changeset/pink-starfishes-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@penumbra-zone/transport-chrome': minor
'@penumbra-zone/transport-dom': minor
---

support disconnection
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ lerna-debug.log*
*_pk.bin

# pack outputs
penumbra-zone-*.tgz
packages/*/penumbra-zone-*.tgz
packages/*/repo-*-*.tgz
packages/*/package

tsconfig.tsbuildinfo
9 changes: 6 additions & 3 deletions apps/minifront/src/components/shared/error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { isRouteErrorResponse, useRouteError } from 'react-router-dom';
import { PenumbraNotInstalledError, PenumbraNotConnectedError } from '@penumbra-zone/client';
import {
PenumbraProviderNotInstalledError,
PenumbraProviderNotConnectedError,
} from '@penumbra-zone/client';
import { ExtensionNotConnected } from '../extension-not-connected';
import { NotFound } from '../not-found';
import { ExtensionTransportDisconnected } from '../extension-transport-disconnected';
Expand All @@ -12,8 +15,8 @@ export const ErrorBoundary = () => {

if (error instanceof ConnectError && error.code === Code.Unavailable)
return <ExtensionTransportDisconnected />;
if (error instanceof PenumbraNotInstalledError) return <ExtensionNotInstalled />;
if (error instanceof PenumbraNotConnectedError) return <ExtensionNotConnected />;
if (error instanceof PenumbraProviderNotInstalledError) return <ExtensionNotInstalled />;
if (error instanceof PenumbraProviderNotConnectedError) return <ExtensionNotConnected />;
if (isRouteErrorResponse(error) && error.status === 404) return <NotFound />;

console.error('ErrorBoundary caught error:', error);
Expand Down
59 changes: 26 additions & 33 deletions apps/minifront/src/prax.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,55 @@
import type { PromiseClient, Transport } from '@connectrpc/connect';
import { createPromiseClient } from '@connectrpc/connect';
import { createPromiseClient, PromiseClient, Transport } from '@connectrpc/connect';
import {
PenumbraNotConnectedError,
PenumbraNotInstalledError,
PenumbraSymbol,
} from '@penumbra-zone/client';
import type { PenumbraService } from '@penumbra-zone/protobuf';
import { jsonOptions } from '@penumbra-zone/protobuf';
import { createChannelTransport } from '@penumbra-zone/transport-dom/create';
assertProviderConnected,
assertProviderManifest,
getPenumbraPort,
syncCreatePenumbraChannelTransport,
} from '@penumbra-zone/client/create';
import { jsonOptions, PenumbraService } from '@penumbra-zone/protobuf';

const prax_id = 'lkpmkhpnhknhmibgnmmhdhgdilepfghe';
const prax_origin = `chrome-extension://${prax_id}`;
const prax_manifest = `chrome-extension://${prax_id}/manifest.json`;

export const getPraxOrigin = () => prax_origin;

export const getPraxManifest = async () => {
const res = await fetch(prax_manifest);
return (await res.json()) as unknown;
const { manifest } = await assertProviderManifest(prax_origin);
const requestManifest = await fetch(manifest);
return (await requestManifest.json()) as unknown;
};

export const getPraxOrigin = () => prax_origin;

export const isPraxConnected = () => Boolean(window[PenumbraSymbol]?.[prax_origin]?.isConnected());
export const isPraxConnected = () => {
try {
assertProviderConnected(prax_origin);
return true;
} catch {
return false;
}
};

export const isPraxInstalled = async () => {
try {
await getPraxManifest();
await assertProviderManifest();
return true;
} catch {
return false;
}
};

export const throwIfPraxNotConnected = () => {
if (!isPraxConnected())
throw new PenumbraNotConnectedError('Prax not connected', { cause: prax_origin });
};
export const throwIfPraxNotConnected = () => assertProviderConnected(prax_origin);

export const throwIfPraxNotInstalled = async () => {
if (!(await isPraxInstalled()))
throw new PenumbraNotInstalledError('Prax not installed', { cause: prax_origin });
};
export const throwIfPraxNotInstalled = async () => assertProviderManifest(prax_origin);

export const getPraxPort = async () => {
await throwIfPraxNotInstalled();
return window[PenumbraSymbol]![prax_origin]!.connect();
};
export const getPraxPort = () => getPenumbraPort(prax_origin);

export const requestPraxAccess = async () => {
await throwIfPraxNotInstalled();
await window[PenumbraSymbol]?.[prax_origin]?.request();
};
export const requestPraxAccess = () => getPraxPort();

export const praxTransportOptions = {
jsonOptions,
getPort: getPraxPort,
};

export const createPraxTransport = () => createChannelTransport(praxTransportOptions);
export const createPraxTransport = () => syncCreatePenumbraChannelTransport(prax_origin);

let praxTransport: Transport | undefined;
export const createPraxClient = <T extends PenumbraService>(service: T): PromiseClient<T> =>
Expand Down
112 changes: 112 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# `@penumbra-zone/client`

This package contains interfaces, types, and some helpers for using the page API to Penumbra providers.

**To use this package, you need to [enable the Buf Schema Registry](https://buf.build/docs/bsr/generated-sdks/npm):**

```sh
echo "@buf:registry=https://buf.build/gen/npm/v1/" >> .npmrc
```

## A simple example

```ts
import { bech32mAddress } from '@penumbra-zone/bech32m';
import { createPenumbraClient } from '@penumbra-zone/client/create';
import { ViewService, SctService } from '@penumbra-zone/protobuf';

// This may connect to any available injected provider.
const viewClient = createPenumbraClient(ViewService);

// Or, you might prefer a specific provider.
const praxViewClient = createPenumbraClient(
ViewService,
'chrome-extension://lkpmkhpnhknhmibgnmmhdhgdilepfghe',
);

const { address } = await praxViewClient.addressByIndex({});
console.log(bech32mAddress(address));
```

## React use

It's likely you want to use this client in your webapp, and there's a good
chance you're using React or another tanstacklike. Penumbra providers use
`@connectrpc` tooling, so these clients are supported by `@connectrpc/query` and
`@tanstack/react-query`.

After using `createPenumbraChannelTransport` from `@penumbra-zone/client/create`
and `TransportProvider` from `@connectrpc/query` in a parent component, you can
use convenient React idioms.

You can see a full example of this at https://github.com/penumbra-zone/nextjs-penumbra-client-example

### A parent component

```ts
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { syncCreatePenumbraChannelTransport } from "@penumbra-zone/client/create";
import { TransportProvider } from "@connectrpc/connect-query";
import { useMemo } from "react";

const queryClient = new QueryClient();

export const PenumbraQueryProvider = ({
providerOrigin,
children,
}: {
providerOrigin: string;
children: React.ReactNode;
}) => {
const penumbraTransport = useMemo(
() => syncCreatePenumbraChannelTransport(providerOrigin),
[providerOrigin],
);
return (
<TransportProvider transport={penumbraTransport}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</TransportProvider>
);
};
```

### A querying component

```ts
"use client";
import { addressByIndex } from "@buf/penumbra-zone_penumbra.connectrpc_query-es/penumbra/view/v1/view-ViewService_connectquery";
import { bech32mAddress } from "@penumbra-zone/bech32m/penumbra";
import { useQuery } from "@connectrpc/connect-query";

export const PenumbraAddress = ({ account }: { account?: number }) => {
const { data } = useQuery(addressByIndex, { addressIndex: { account } });
return (
data?.address && (
<span className="address">{bech32mAddress(data.address)}</span>
)
);
};
```

## You could access the providers directly, without importing this package.

This example is javascript.

```js
import { createChannelTransport } from '@penumbra-zone/transport-dom';
import { createPromiseClient } from '@connectrpc/connect';
import { jsonOptions, ViewService } from '@penumbra-zone/protobuf';

// naively get first available provider
const provider = Object.values(window[Symbol.for('penumbra')])[0];
void provider.request();

// create a client
const viewClient = createPromiseClient(
ViewService,
createChannelTransport({ jsonOptions, getPort: provider.connect }),
);

const { catchingUp, fullSyncHeight } = viewClient.status({});
```
7 changes: 6 additions & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,22 @@
],
"exports": {
".": "./src/index.ts",
"./prax": "./src/prax.ts"
"./create": "./src/create.ts"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./create": {
"types": "./dist/create.d.ts",
"default": "./dist/create.js"
}
}
},
"peerDependencies": {
"@connectrpc/connect": "^1.4.0",
"@penumbra-zone/protobuf": "workspace:*",
"@penumbra-zone/transport-dom": "workspace:*"
}
Expand Down
Loading

0 comments on commit 31209d4

Please sign in to comment.