Skip to content

Commit

Permalink
feat: prax wallet support
Browse files Browse the repository at this point in the history
  • Loading branch information
codingki committed Jul 30, 2024
1 parent 81ae8c3 commit 21d4685
Show file tree
Hide file tree
Showing 10 changed files with 1,001 additions and 36 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ test-results/
tests/downloads/

!.env.example

certificates
2 changes: 1 addition & 1 deletion chain-registry
Submodule chain-registry updated 364 files
2 changes: 1 addition & 1 deletion initia-registry
Submodule initia-registry updated 40 files
+7 −15 .github/workflows/utility/chain_registry.mjs
+5 −9 .github/workflows/utility/sync_images.mjs
+49 −0 .github/workflows/utility/validate_data.py
+1 −1 .github/workflows/validate_data.yml
+1 −1 _packages/initia-registry/README.md
+2 −2 _packages/initia-registry/package-lock.json
+1 −1 _packages/initia-registry/package.json
+110 −2 _packages/initia-registry/src/testnet/blackwing/assets.ts
+39 −0 _packages/initia-registry/src/testnet/civitia/assets.ts
+110 −2 _packages/initia-registry/src/testnet/init_ai/assets.ts
+275 −1 _packages/initia-registry/src/testnet/initia/assets.ts
+0 −12 _packages/initia-registry/src/testnet/initia/chain.ts
+14 −0 _packages/initia-registry/src/testnet/milkyway/assets.ts
+110 −2 _packages/initia-registry/src/testnet/minimove/assets.ts
+15 −0 _packages/initia-registry/src/testnet/minimove/chain.ts
+110 −2 _packages/initia-registry/src/testnet/miniwasm/assets.ts
+15 −0 _packages/initia-registry/src/testnet/miniwasm/chain.ts
+110 −2 _packages/initia-registry/src/testnet/noon/assets.ts
+3 −3 _packages/initia-registry/src/testnet/noon/chain.ts
+81 −0 _packages/initia-registry/src/testnet/tucana/assets.ts
+1 −1 _packages/types/package.json
+20 −1 _packages/types/src/types/AssetList.ts
+2 −5 _packages/types/src/types/Chain.ts
+28 −0 _packages/types/src/zods/AssetList.ts
+40 −0 assetlist.schema.json
+6 −0 package-lock.json
+108 −2 testnets/blackwing/assetlist.json
+38 −0 testnets/civitia/assetlist.json
+108 −2 testnets/init_ai/assetlist.json
+238 −0 testnets/initia/assetlist.json
+3 −15 testnets/initia/chain.json
+ testnets/initia/images/milkINIT-INIT.png
+28 −0 testnets/initia/images/milkINIT-INIT.svg
+14 −0 testnets/milkyway/assetlist.json
+108 −2 testnets/minimove/assetlist.json
+15 −0 testnets/minimove/chain.json
+108 −2 testnets/miniwasm/assetlist.json
+15 −0 testnets/miniwasm/chain.json
+108 −2 testnets/noon/assetlist.json
+80 −0 testnets/tucana/assetlist.json
778 changes: 750 additions & 28 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,23 @@
"visdeps": "bash ./src/scripts/visdeps.sh"
},
"dependencies": {
"@connectrpc/connect": "1.4.0",
"@fontsource/jost": "^5.0.16",
"@graz-sh/types": "^0.0.14",
"@heroicons/react": "^2.1.1",
"@keplr-wallet/types": "^0.12.66",
"@penumbra-zone/bech32m": "6.1.1",
"@penumbra-zone/client": "11.0.1",
"@penumbra-zone/protobuf": "5.4.0",
"@penumbra-zone/transport-dom": "7.2.2",
"@radix-ui/colors": "^3.0.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@sentry/nextjs": "^7.99.0",
"@skip-go/widget": "^2.1.1",
"@skip-go/widget": "^2.3.0",
"@solana/spl-token": "^0.4.1",
"@solana/wallet-adapter-react": "^0.15.35",
"@solana/wallet-adapter-wallets": "^0.19.31",
Expand Down
5 changes: 3 additions & 2 deletions src/hooks/useURLQueryParams.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Chain } from "@skip-go/core";
import { useAssets, useChains } from "@skip-go/widget";
import { useQueryState } from "nuqs";
import { useEffect, useState } from "react";
Expand All @@ -24,7 +25,7 @@ export const useURLQueryParams = () => {
useEffect(() => {
if (!chains || !isReady) return;
if (srcChainQP) {
const findChain = chains.find((x) => x.chainID.toLowerCase() === decodeURI(srcChainQP).toLowerCase());
const findChain = chains.find((x: Chain) => x.chainID.toLowerCase() === decodeURI(srcChainQP).toLowerCase());
if (findChain) {
if (srcAssetQP) {
const assets = assetsByChainID(findChain.chainID);
Expand Down Expand Up @@ -68,7 +69,7 @@ export const useURLQueryParams = () => {
useEffect(() => {
if (!chains || !isReady) return;
if (destChainQP) {
const findChain = chains.find((x) => x.chainID.toLowerCase() === decodeURI(destChainQP).toLowerCase());
const findChain = chains.find((x: Chain) => x.chainID.toLowerCase() === decodeURI(destChainQP).toLowerCase());
if (findChain) {
if (destAssetQP) {
const assets = assetsByChainID(findChain.chainID);
Expand Down
184 changes: 184 additions & 0 deletions src/lib/prax.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { createPromiseClient, PromiseClient, Transport } from "@connectrpc/connect";
import { PenumbraProviderNotAvailableError } from "@penumbra-zone/client";
import { assertGlobalPresent, assertProviderConnected, getPenumbraPort } from "@penumbra-zone/client/create";
import { jsonOptions, PenumbraService } from "@penumbra-zone/protobuf";
import { ChannelTransportOptions, createChannelTransport } from "@penumbra-zone/transport-dom/create";

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

export const getPraxOrigin = () => prax_origin;

export const getPraxManifest = () => getPenumbraManifest(prax_origin);

export const isPraxConnected = () => {
try {
assertProviderConnected(prax_origin);
return true;
} catch {
return false;
}
};

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

export const throwIfPraxNotConnected = () => assertProviderConnected(prax_origin);

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

export const requestPraxAccess = () => assertProvider(prax_origin).then((p) => p.request());

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

let praxTransport: Transport | undefined;
export const createPraxClient = <T extends PenumbraService>(service: T): PromiseClient<T> =>
createPromiseClient(service, (praxTransport ??= createPraxTransport()));

export const getPenumbraManifest = async (penumbraOrigin: string, signal?: AbortSignal): Promise<PenumbraManifest> => {
const manifestJson = await assertProviderManifest(penumbraOrigin, signal);
if (!isPenumbraManifest(manifestJson)) {
throw new TypeError("Invalid manifest");
}
return manifestJson;
};

/** Currently, Penumbra manifests are chrome extension manifest v3. There's no type
* guard because manifest format is enforced by chrome. This type only describes
* fields we're interested in as a client.
*
* @see https://developer.chrome.com/docs/extensions/reference/manifest#keys
*/
export interface PenumbraManifest {
/**
* manifest id is present in production, but generally not in dev, because
* they are inserted by chrome store tooling. chrome extension id are simple
* hashes of the 'key' field, an extension-specific public key.
*
* developers may configure a public key in dev, and the extension id will
* match appropriately, but will not be present in the manifest.
*
* the extension id is also part of the extension's origin URI.
*
* @see https://developer.chrome.com/docs/extensions/reference/manifest/key
* @see https://web.archive.org/web/20120606044635/http://supercollider.dk/2010/01/calculating-chrome-extension-id-from-your-private-key-233
*/
id?: string;
key?: string;

// these are required
name: string;
version: string;
description: string;

// these are optional, but might be nice to have
homepage_url?: string;
options_ui?: { page: string };
options_page?: string;

// icons are not indexed by number, but by a stringified number. they may be
// any square size but the power-of-two sizes are typical. the chrome store
// requires a '128' icon.
icons: Record<`${number}`, string> & {
["128"]: string;
};
}

export const isPenumbraManifest = (mf: unknown): mf is PenumbraManifest =>
mf !== null &&
typeof mf === "object" &&
"name" in mf &&
typeof mf.name === "string" &&
"version" in mf &&
typeof mf.version === "string" &&
"description" in mf &&
typeof mf.description === "string" &&
"icons" in mf &&
typeof mf.icons === "object" &&
mf.icons !== null &&
"128" in mf.icons &&
mf.icons["128"] === "string";

/**
* Synchronously create a channel transport for the specified provider, or the
* first available provider if unspecified.
*
* Will always succeed, but the transport may fail if the provider is not
* present, or if the provider rejects the connection.
*
* Confirms presence of the provider's manifest. Will attempt to request
* approval if connection is not already active.
*
* @param requireProvider optional string identifying a provider origin
* @param transportOptions optional `ChannelTransportOptions` without `getPort`
*/
export const createPenumbraChannelTransportSync = (
requireProvider?: string,
transportOptions: Omit<ChannelTransportOptions, "getPort"> = { jsonOptions },
): Transport =>
createChannelTransport({
...transportOptions,
getPort: () => getPenumbraPort(requireProvider),
});

/**
* 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;
};

/**
* 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 assertStringIsOrigin = (s?: string) => {
if (!s || new URL(s).origin !== s) {
throw new TypeError("Invalid origin");
}
return s;
};

export const assertProvider = (providerOrigin?: string) =>
assertProviderManifest(providerOrigin).then(() => assertProviderRecord(providerOrigin));
47 changes: 46 additions & 1 deletion src/lib/skip-go-widget.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { SwapWidgetProviderProps } from "@skip-go/widget/build/provider";
import { bech32mAddress } from "@penumbra-zone/bech32m/penumbra";
import { ViewService } from "@penumbra-zone/protobuf";
import { MinimalWallet, SwapWidgetProviderProps } from "@skip-go/widget";
import toast from "react-hot-toast";

import { appUrl } from "@/constants/api";

import { createPraxClient, isPraxInstalled, requestPraxAccess } from "./prax";

export const endpointOptions: SwapWidgetProviderProps["endpointOptions"] = {
getRpcEndpointForChain: async (chainID) => {
return `${appUrl}/api/rpc/${chainID}`;
Expand All @@ -12,3 +17,43 @@ export const endpointOptions: SwapWidgetProviderProps["endpointOptions"] = {
};

export const apiURL = `${appUrl}/api/skip`;

export const praxWallet: MinimalWallet = {
walletName: "prax",
walletPrettyName: "Prax Wallet",
walletInfo: {
logo: "https://raw.githubusercontent.com/prax-wallet/web/e8b18f9b997708eab04f57e7a6c44f18b3cf13a8/apps/extension/public/prax-white-vertical.svg",
},
connect: async () => {
console.error("Prax wallet is not supported for connect");
toast.error("Prax wallet is not supported for connect");
},
getAddress: async (props) => {
const penumbraWalletIndex = props?.penumbraWalletIndex;
try {
const isInstalled = await isPraxInstalled();
if (!isInstalled) {
throw new Error("Prax Wallet is not installed");
}
await requestPraxAccess();
const praxClient = createPraxClient(ViewService);
const address = await praxClient.addressByIndex({
addressIndex: {
account: penumbraWalletIndex ? penumbraWalletIndex : 0,
},
});
if (!address.address) throw new Error("No address found");
const bech32Address = bech32mAddress(address.address);
return bech32Address;
} catch (error) {
console.error(error);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
toast.error(error?.message);
}
},
disconnect: async () => {
console.error("Prax wallet is not supported");
},
isWalletConnected: false,
};
8 changes: 7 additions & 1 deletion src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Analytics } from "@vercel/analytics/react";
import { AppProps } from "next/app";

import { DefaultSeo } from "@/components/DefaultSeo";
import { apiURL, endpointOptions } from "@/lib/skip-go-widget";
import { apiURL, endpointOptions, praxWallet } from "@/lib/skip-go-widget";

export default function App({ Component, pageProps }: AppProps) {
return (
Expand All @@ -15,6 +15,12 @@ export default function App({ Component, pageProps }: AppProps) {
<SwapWidgetProvider
endpointOptions={endpointOptions}
apiURL={apiURL}
makeDestinationWallets={(chainID) => {
if (chainID.includes("penumbra")) {
return [praxWallet];
}
return [];
}}
>
<Component {...pageProps} />
</SwapWidgetProvider>
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"noEmit": true,
"paths": {
"@/*": ["./src/*"]
Expand Down

0 comments on commit 21d4685

Please sign in to comment.