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

react provider 3 #1450

Merged
merged 5 commits into from
Jul 15, 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/dirty-mice-help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/react': major
---

initial react wallet
5 changes: 5 additions & 0 deletions .changeset/tidy-spiders-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/client': minor
---

provide type guards for event, and better types for event interface
27 changes: 9 additions & 18 deletions apps/minifront/src/prax.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { createPromiseClient, PromiseClient, Transport } from '@connectrpc/connect';
import { getPenumbraManifest } from '@penumbra-zone/client';
import {
assertProvider,
assertProviderConnected,
assertProviderManifest,
getPenumbraPort,
syncCreatePenumbraChannelTransport,
} from '@penumbra-zone/client/create';
} from '@penumbra-zone/client/assert';
import { createPenumbraChannelTransportSync } from '@penumbra-zone/client/create';
import { jsonOptions, PenumbraService } from '@penumbra-zone/protobuf';

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

export const getPraxOrigin = () => prax_origin;

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

export const isPraxConnected = () => {
try {
Expand All @@ -29,7 +26,7 @@ export const isPraxConnected = () => {

export const isPraxInstalled = async () => {
try {
await assertProviderManifest();
await assertProviderManifest(prax_origin);
return true;
} catch {
return false;
Expand All @@ -40,16 +37,10 @@ export const throwIfPraxNotConnected = () => assertProviderConnected(prax_origin

export const throwIfPraxNotInstalled = async () => assertProviderManifest(prax_origin);

export const getPraxPort = () => getPenumbraPort(prax_origin);

export const requestPraxAccess = () => getPraxPort();

export const praxTransportOptions = {
jsonOptions,
getPort: getPraxPort,
};
export const requestPraxAccess = () => assertProvider(prax_origin).then(p => p.request());

export const createPraxTransport = () => syncCreatePenumbraChannelTransport(prax_origin);
export const createPraxTransport = () =>
createPenumbraChannelTransportSync(prax_origin, { jsonOptions });

let praxTransport: Transport | undefined;
export const createPraxClient = <T extends PenumbraService>(service: T): PromiseClient<T> =>
Expand Down
8 changes: 4 additions & 4 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@
],
"exports": {
".": "./src/index.ts",
"./create": "./src/create.ts"
"./*": "./src/*.ts"
},
"publishConfig": {
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./create": {
"types": "./dist/create.d.ts",
"default": "./dist/create.js"
"./*": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
}
}
},
Expand Down
82 changes: 82 additions & 0 deletions packages/client/src/assert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
PenumbraNotInstalledError,
PenumbraProviderNotAvailableError,
PenumbraProviderNotConnectedError,
} from './error.js';
import { PenumbraSymbol } from './symbol.js';

export const assertStringIsOrigin = (s?: string) => {
if (!s || new URL(s).origin !== s) {
throw new TypeError('Invalid origin');
}
return s;
};

export const assertGlobalPresent = () => {
if (!window[PenumbraSymbol]) {
throw new PenumbraNotInstalledError();
}
return window[PenumbraSymbol];
};

/**
* Given a specific origin, identify the relevant injection or throw. An
* `undefined` origin is accepted but will throw.
*/
export const assertProviderRecord = (providerOrigin?: string) => {
const provider = providerOrigin && assertGlobalPresent()[assertStringIsOrigin(providerOrigin)];
if (!provider) {
throw new PenumbraProviderNotAvailableError(providerOrigin);
}
return provider;
};

export const assertProvider = (providerOrigin?: string) =>
assertProviderManifest(providerOrigin).then(() => assertProviderRecord(providerOrigin));

/**
* Given a specific origin, identify the relevant injection, and confirm
* provider is connected or throw. An `undefined` origin is accepted but will
* throw.
*/
export const assertProviderConnected = (providerOrigin?: string) => {
const provider = assertProviderRecord(providerOrigin);
if (!provider.isConnected()) {
throw new PenumbraProviderNotConnectedError(providerOrigin);
}
return provider;
};

/**
* Given a specific origin, identify the relevant injection, and confirm its
* manifest is actually present or throw. An `undefined` origin is accepted but
* will throw.
*/
export const assertProviderManifest = async (providerOrigin?: string, signal?: AbortSignal) => {
// confirm the provider injection is present
const provider = assertProviderRecord(providerOrigin);

let manifest: unknown;

try {
// confirm the provider manifest is located at the expected origin
if (new URL(provider.manifest).origin !== providerOrigin) {
throw new Error('Manifest located at unexpected origin');
}

// confirm the provider manifest can be fetched, and is json
const req = await fetch(provider.manifest, { signal });
manifest = await req.json();

if (!manifest) {
throw new Error(`Cannot confirm ${providerOrigin} is real.`);
}
} catch (e) {
if (signal?.aborted !== true) {
console.warn(e);
throw new PenumbraProviderNotAvailableError(providerOrigin);
}
}

return manifest;
};
82 changes: 8 additions & 74 deletions packages/client/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,80 +4,12 @@ import {
createChannelTransport,
type ChannelTransportOptions,
} from '@penumbra-zone/transport-dom/create';
import { PenumbraSymbol, type PenumbraInjection } from './index.js';
import {
PenumbraNotInstalledError,
PenumbraProviderNotAvailableError,
PenumbraProviderNotConnectedError,
} from './error.js';
import { assertProviderManifest, assertProviderRecord } from './assert.js';
import { PenumbraSymbol } from './symbol.js';

// Naively return the first available provider origin, or `undefined`.
const availableOrigin = () => Object.keys(window[PenumbraSymbol] ?? {})[0];

export const assertGlobalPresent = () => {
if (!window[PenumbraSymbol]) {
throw new PenumbraNotInstalledError();
}
return window[PenumbraSymbol];
};

/**
* Given a specific origin, identify the relevant injection or throw. An
* `undefined` origin is accepted but will throw.
*/
export const assertProvider = (providerOrigin?: string): PenumbraInjection => {
const provider = providerOrigin && assertGlobalPresent()[providerOrigin];
if (!provider) {
throw new PenumbraProviderNotAvailableError(providerOrigin);
}
return provider;
};

/**
* Given a specific origin, identify the relevant injection, and confirm
* provider is connected or throw. An `undefined` origin is accepted but will
* throw.
*/
export const assertProviderConnected = (providerOrigin?: string) => {
const provider = assertProvider(providerOrigin);
if (!provider.isConnected()) {
throw new PenumbraProviderNotConnectedError(providerOrigin);
}
return provider;
};

/**
* Given a specific origin, identify the relevant injection, and confirm its
* manifest is actually present or throw. An `undefined` origin is accepted but
* will throw.
*/
export const assertProviderManifest = async (
providerOrigin?: string,
): Promise<PenumbraInjection> => {
// confirm the provider injection is present
const provider = assertProvider(providerOrigin);

try {
// confirm the provider manifest is located at the expected origin
if (new URL(provider.manifest).origin !== providerOrigin) {
throw new Error('Manifest located at unexpected origin');
}

// confirm the provider manifest can be fetched, and is json
const req = await fetch(provider.manifest);
const manifest: unknown = await req.json();

if (!manifest) {
throw new Error(`Cannot confirm ${providerOrigin} is real.`);
}
} catch (e) {
console.warn(e);
throw new PenumbraProviderNotAvailableError(providerOrigin);
}

return provider;
};

/**
* Asynchronously get a connection to the specified provider, or the first
* available provider if unspecified.
Expand All @@ -88,7 +20,9 @@ export const assertProviderManifest = async (
* @param requireProvider optional string identifying a provider origin
*/
export const getPenumbraPort = async (requireProvider?: string) => {
const provider = await assertProviderManifest(requireProvider ?? availableOrigin());
const penumbraOrigin = requireProvider ?? availableOrigin();
await assertProviderManifest(penumbraOrigin);
const provider = assertProviderRecord(penumbraOrigin);
if (!provider.isConnected()) {
await provider.request();
}
Expand All @@ -108,7 +42,7 @@ export const getPenumbraPort = async (requireProvider?: string) => {
* @param requireProvider optional string identifying a provider origin
* @param transportOptions optional `ChannelTransportOptions` without `getPort`
*/
export const syncCreatePenumbraChannelTransport = (
export const createPenumbraChannelTransportSync = (
requireProvider?: string,
transportOptions: Omit<ChannelTransportOptions, 'getPort'> = { jsonOptions },
): Transport =>
Expand Down Expand Up @@ -140,10 +74,10 @@ export const createPenumbraChannelTransport = async (
*
* If the provider is unavailable, the client will fail to make requests.
*/
export const syncCreatePenumbraClient = <P extends PenumbraService>(
export const createPenumbraClientSync = <P extends PenumbraService>(
service: P,
requireProvider?: string,
) => createPromiseClient(service, syncCreatePenumbraChannelTransport(requireProvider));
) => createPromiseClient(service, createPenumbraChannelTransportSync(requireProvider));

/**
* Asynchronously create a client for `service` from the specified provider, or
Expand Down
50 changes: 43 additions & 7 deletions packages/client/src/event.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
import { PenumbraInjectionState, PenumbraSymbol } from './index.js';
import { PenumbraState } from './state.js';
import { PenumbraSymbol } from './symbol.js';

export class PenumbraInjectionStateEvent extends CustomEvent<{
export interface PenumbraStateEventDetail {
origin: string;
state?: PenumbraInjectionState;
}> {
constructor(injectionProviderOrigin: string, injectionState?: PenumbraInjectionState) {
state?: PenumbraState;
}

export class PenumbraStateEvent extends CustomEvent<PenumbraStateEventDetail> {
constructor(penumbraOrigin: string, penumbraState?: PenumbraState) {
super('penumbrastate', {
detail: {
state: injectionState ?? window[PenumbraSymbol]?.[injectionProviderOrigin]?.state(),
origin: injectionProviderOrigin,
origin: penumbraOrigin,
state: penumbraState ?? window[PenumbraSymbol]?.[penumbraOrigin]?.state(),
},
});
}
}

export const isPenumbraStateEvent = (evt: Event): evt is PenumbraStateEvent =>
evt instanceof PenumbraStateEvent || ('detail' in evt && isPenumbraStateEventDetail(evt.detail));

export const isPenumbraStateEventDetail = (detail: unknown): detail is PenumbraStateEventDetail =>
typeof detail === 'object' &&
detail !== null &&
'origin' in detail &&
typeof detail.origin === 'string';

// utility type for SpecificEventTarget. any is required for type inference
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ParametersTail<T extends (...args: any[]) => any> =
Parameters<T> extends [unknown, ...infer TailParams] ? TailParams : never;

// like EventTarget, but restricts possible event types
interface SpecificEventTarget<SpecificTypeName extends string, SpecificEvent extends Event = Event>
extends EventTarget {
addEventListener: (
type: SpecificTypeName,
...rest: ParametersTail<EventTarget['addEventListener']>
) => void;
removeEventListener: (
type: SpecificTypeName,
...rest: ParametersTail<EventTarget['removeEventListener']>
) => void;
dispatchEvent: (event: SpecificEvent) => boolean;
}

export type PenumbraStateEventTarget = Omit<
SpecificEventTarget<'penumbrastate', never>,
'dispatchEvent'
>;
Loading
Loading