Skip to content

Commit

Permalink
disconnect api (#1307)
Browse files Browse the repository at this point in the history
* add disconnect method to page api
* minifront use disconnect api
  • Loading branch information
turbocrime committed Jun 27, 2024
1 parent e207faa commit 47c6bc0
Show file tree
Hide file tree
Showing 14 changed files with 446 additions and 69 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
23 changes: 17 additions & 6 deletions apps/minifront/src/components/header/menu/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import * as NavigationMenu from '@radix-ui/react-navigation-menu';
import { getChainId } from '../../../fetchers/chain-id';
import { useCallback, useEffect, useState } from 'react';
import { itemStyle, triggerStyle, dropdownStyle, linkStyle } from './nav-style';
import { Link1Icon } from '@radix-ui/react-icons';
import { Link1Icon, LinkBreak1Icon } from '@radix-ui/react-icons';
import { getPraxManifest, getPraxOrigin } from '../../../prax';
import { PenumbraSymbol } from '@penumbra-zone/client';

export const ProviderMenu = () => {
const [chainId, setChainId] = useState<string | undefined>();
Expand All @@ -14,8 +15,9 @@ export const ProviderMenu = () => {
const [manifestIconUnavailable, setManifestIconUnavailable] = useState<boolean>();

const disconnect = useCallback(() => {
console.log('unimplemented');
//window[Symbol.for('penumbra')][providerOrigin].disconnect(),
void window[PenumbraSymbol]?.[providerOrigin]
?.disconnect()
.then(() => window.location.reload());
}, [providerOrigin]);

useEffect(() => {
Expand Down Expand Up @@ -59,9 +61,18 @@ export const ProviderMenu = () => {
</div>
</NavigationMenu.Link>
</NavigationMenu.Item>
<NavigationMenu.Item className={cn(...itemStyle)}>
<NavigationMenu.Link hidden className={cn(...linkStyle)} onSelect={disconnect}>
Disconnect
<NavigationMenu.Item
hidden={
// hide if injection does not contain disconnect
!window[PenumbraSymbol]?.[providerOrigin]?.disconnect
}
className={cn(...itemStyle)}
>
<NavigationMenu.Link className={cn(...linkStyle)} onSelect={disconnect}>
<span>
<LinkBreak1Icon className={cn('size-[1em]', 'inline-block')} />
&nbsp;Disconnect
</span>
</NavigationMenu.Link>
</NavigationMenu.Item>
</NavigationMenu.Content>
Expand Down
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
111 changes: 111 additions & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# `@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. 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 47c6bc0

Please sign in to comment.