Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disconnect api #1307

Merged
merged 3 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading