Skip to content

Commit

Permalink
Merge pull request #14 from babylonlabs-io/293-bwc-implement-widget-api
Browse files Browse the repository at this point in the history
feat: widget API
  • Loading branch information
totraev authored Nov 21, 2024
2 parents 8c3e697 + dfd0704 commit c6c78e8
Show file tree
Hide file tree
Showing 38 changed files with 853 additions and 264 deletions.
63 changes: 63 additions & 0 deletions src/context/Chain.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { createContext, PropsWithChildren, useEffect, useState, useCallback, useContext } from "react";
import { useAppState } from "@/state/state";
import { WalletConnector } from "@/core/WalletConnector";
import type { NetworkConfig } from "@/core/types";

import metadata from "@/core/wallets";
import { BTCProvider } from "@/core/wallets/btc/BTCProvider";
import { BBNProvider } from "@/core/wallets/bbn/BBNProvider";

interface ProviderProps {
context: any;
config: NetworkConfig;
}

export interface Connectors {
BTC: WalletConnector<"BTC", BTCProvider> | null;
BBN: WalletConnector<"BBN", BBNProvider> | null;
}

export type SupportedChains = keyof Connectors;

const defaultState: Connectors = {
BTC: null,
BBN: null,
};

const Context = createContext<Connectors>(defaultState);

export function ChainProvider({ children, context, config }: PropsWithChildren<ProviderProps>) {
const [connectors, setConnectors] = useState(defaultState);
const { addChain, displayLoader, displayTermsOfService } = useAppState();

const init = useCallback(async () => {
displayLoader?.();

const metadataArr = Object.values(metadata);
const connectorArr = await Promise.all(metadataArr.map((data) => WalletConnector.create(data, context, config)));

return connectorArr.reduce((acc, connector) => ({ ...acc, [connector.id]: connector }), {} as Connectors);
}, []);

useEffect(() => {
if (!displayLoader || !addChain || !setConnectors || !displayTermsOfService) return;

displayLoader();

init().then((connectors) => {
setConnectors(connectors);

Object.values(connectors).forEach((connector) => {
addChain(connector);
});

displayTermsOfService();
});
}, [displayLoader, addChain, setConnectors, init, displayTermsOfService]);

return <Context.Provider value={connectors}>{children}</Context.Provider>;
}

export const useChainProviders = () => {
return useContext(Context);
};
35 changes: 35 additions & 0 deletions src/context/Inscriptions.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createContext, PropsWithChildren, useContext, useMemo } from "react";

import { usePersistState } from "@/hooks/usePersistState";

interface InscriptionContext {
lockInscriptions: boolean;
showAgain: boolean;
toggleLockInscriptions?: (value: boolean) => void;
toggleShowAgain?: (value: boolean) => void;
}

const Context = createContext<InscriptionContext>({ lockInscriptions: true, showAgain: true });

export function InscriptionProvider({ children, context }: PropsWithChildren<{ context: any }>) {
const [showAgain, toggleShowAgain] = usePersistState("bwc-inscription-modal-show-again", context.localStorage, true);
const [lockInscriptions, toggleLockInscriptions] = usePersistState(
"bwc-inscription-modal-lock",
context.localStorage,
true,
);

const inscriptionContext = useMemo(
() => ({
showAgain,
lockInscriptions,
toggleLockInscriptions,
toggleShowAgain,
}),
[showAgain, lockInscriptions, toggleLockInscriptions, toggleShowAgain],
);

return <Context.Provider value={inscriptionContext}>{children}</Context.Provider>;
}

export const useInscriptionProvider = () => useContext(Context);
18 changes: 17 additions & 1 deletion src/core/Wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IWallet, Network, IProvider, type NetworkConfig, type WalletMetadata } from "@/core/types";
import { IWallet, Network, IProvider, type NetworkConfig, type WalletMetadata, Account } from "@/core/types";

interface Options<P extends IProvider> {
id: string;
Expand All @@ -20,6 +20,7 @@ export class Wallet<P extends IProvider> implements IWallet {
readonly docs: string;
readonly networkds: Network[];
readonly provider: P | null = null;
account: Account | null = null;

static create = async <P extends IProvider>(metadata: WalletMetadata<P>, context: any, config: NetworkConfig) => {
const {
Expand Down Expand Up @@ -87,7 +88,22 @@ export class Wallet<P extends IProvider> implements IWallet {
}

await this.provider.connectWallet();
const [address, publicKeyHex] = await Promise.all([this.provider.getAddress(), this.provider.getPublicKeyHex()]);

this.account = { address, publicKeyHex };

return this;
}

clone() {
return new Wallet({
id: this.id,
origin: this.origin,
name: this.name,
icon: this.icon,
docs: this.docs,
networks: this.networkds,
provider: this.provider,
});
}
}
41 changes: 29 additions & 12 deletions src/core/WalletConnector.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,51 @@
import { Wallet } from "@/core/Wallet";
import type { NetworkConfig, IProvider, IChain, ConnectMetadata } from "@/core/types";
import type { NetworkConfig, IProvider, IChain, ChainMetadata } from "@/core/types";

export class WalletConnector<P extends IProvider> implements IChain {
connectedWallet: Wallet<P> | null = null;
export class WalletConnector<N extends string, P extends IProvider> implements IChain {
private _connectedWallet: Wallet<P> | null = null;

static async create<P extends IProvider>(
metadata: ConnectMetadata<P>,
config: NetworkConfig,
static async create<N extends string, P extends IProvider>(
metadata: ChainMetadata<N, P>,
context: any,
): Promise<WalletConnector<P>> {
config: NetworkConfig,
): Promise<WalletConnector<N, P>> {
const wallets: Wallet<P>[] = [];

for (const walletMetadata of metadata.wallets) {
wallets.push(await Wallet.create(walletMetadata, context, config));
}

return new WalletConnector(metadata.chain, metadata.icon, wallets);
return new WalletConnector(metadata.chain, metadata.name, metadata.icon, wallets);
}

constructor(
public readonly chain: string,
public readonly id: N,
public readonly name: string,
public readonly icon: string,
public readonly wallets: Wallet<P>[],
) {}

async connect(name: string) {
const wallet = this.wallets.find((wallet) => wallet.name.toLowerCase() === name.toLowerCase());
get connectedWallet() {
return this._connectedWallet;
}

async connect(walletId: string) {
const wallet = this.wallets.find((wallet) => wallet.id === walletId);

if (!wallet) {
throw new Error("Wallet not found");
}

this.connectedWallet = (await wallet?.connect()) ?? null;
this._connectedWallet = await wallet.connect();

return this.connectedWallet;
}

disconnect() {
this._connectedWallet = null;
}

clone() {
return new WalletConnector(this.id, this.name, this.icon, this.wallets);
}
}
15 changes: 12 additions & 3 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface NetworkConfig {

export interface IProvider {
connectWallet: () => Promise<this>;
getAddress: () => Promise<string>;
getPublicKeyHex: () => Promise<string>;
}

export interface IWallet {
Expand All @@ -61,14 +63,20 @@ export interface IWallet {
docs: string;
installed: boolean;
provider: IProvider | null;
account: Account | null;
}

export interface IChain {
chain: string;
id: string;
name: string;
icon: string;
wallets: IWallet[];
}

export interface Account {
address: string;
publicKeyHex: string;
}
export interface WalletMetadata<P extends IProvider> {
id: string;
wallet?: string | ((context: any, config: NetworkConfig) => any);
Expand All @@ -79,8 +87,9 @@ export interface WalletMetadata<P extends IProvider> {
createProvider: (wallet: any, config: NetworkConfig) => P;
}

export interface ConnectMetadata<P extends IProvider> {
chain: string;
export interface ChainMetadata<N extends string, P extends IProvider> {
chain: N;
name: string;
icon: string;
wallets: WalletMetadata<P>[];
}
11 changes: 11 additions & 0 deletions src/core/wallets/bbn/BBNProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IProvider } from "@/core/types";

export abstract class BBNProvider implements IProvider {
async connectWallet() {
return this;
}

abstract getAddress(): Promise<string>;

abstract getPublicKeyHex(): Promise<string>;
}
Binary file added src/core/wallets/bbn/babylon.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/core/wallets/bbn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BBNProvider } from "./BBNProvider";
import icon from "./babylon.jpeg";

import { ChainMetadata } from "@/core/types";

const metadata: ChainMetadata<"BBN", BBNProvider> = {
chain: "BBN",
name: "Babylon Chain",
icon,
wallets: [],
};

export default metadata;
Binary file added src/core/wallets/btc/bitcoin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/core/wallets/btc/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { BTCProvider } from "./BTCProvider";
import icon from "./bitcoin.png";

import injectable from "./injectable";
import okx from "./okx";

import { ConnectMetadata } from "@/core/types";
import { ChainMetadata } from "@/core/types";

const metadata: ConnectMetadata<BTCProvider> = {
const metadata: ChainMetadata<"BTC", BTCProvider> = {
chain: "BTC",
icon: "test",
name: "Bitcoin",
icon,
wallets: [injectable, okx],
};

Expand Down
4 changes: 4 additions & 0 deletions src/core/wallets/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import btc from "./btc";
import bbn from "./bbn";

export default { btc, bbn } as const;
18 changes: 18 additions & 0 deletions src/hocs/withAppState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-empty-object-type */
import { ComponentType, memo, useMemo } from "react";
import { useAppState } from "@/state/state";
import type { Actions, State } from "@/state/state.d";

export const withAppState =
<OP, IP = {}, P = {}>(stateMapper: (state: State & Actions) => IP) =>
(Component: ComponentType<P>) => {
const Container = (props: OP) => {
const appState = useAppState();
const outerProps = useMemo(() => stateMapper(appState), [appState, stateMapper]);
const PureComponent = memo(Component);

return <PureComponent {...(props as any)} {...outerProps} />;
};

return Container;
};
7 changes: 7 additions & 0 deletions src/hooks/useChainConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type SupportedChains, useChainProviders } from "@/context/Chain.context";

export function useChainConnector<K extends SupportedChains>(chainId: K) {
const connectors = useChainProviders();

return connectors?.[chainId] ?? null;
}
24 changes: 24 additions & 0 deletions src/hooks/usePersistState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { type SetStateAction, type Dispatch, useState, useEffect } from "react";

export function usePersistState<S>(key: string, storage: Storage, initialState?: S): [S, Dispatch<SetStateAction<S>>] {
function getDefaultState() {
const defaultValue = typeof initialState === "function" ? (initialState as () => S)() : initialState;
const persistValue = storage.getItem(key);
const defaultState = persistValue ? (JSON.parse(persistValue) as S) : null;

return (defaultState ?? defaultValue) as S;
}

const [state, setState] = useState<S>(getDefaultState);

useEffect(
function updateLocalStorage() {
storage.setItem(key, JSON.stringify(state ?? ""));
},
[key, storage, state],
);

return [state, setState];
}
6 changes: 6 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import "./index.css";

export { useChainConnector } from "@/hooks/useChainConnector";
export { useAppState } from "@/state/state";
export { WalletProvider } from "@/widgets/WalletProvider";
export * from "@/state/state.d";

// core-ui
export * from "./components/Text";
export * from "./components/Heading";
export * from "./components/Button";
Expand Down
29 changes: 21 additions & 8 deletions src/state/state.d.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
import type { IChain, IWalllet } from "@/core/types";

type Step = "loading" | "acceptTermsOfService" | "selectChain" | "selectWallet" | "lockInscriptions";
type Screen<T extends string = string> = {
type: T;
params?: Record<string, string | number>;
};

type Screens =
| Screen<"LOADER">
| Screen<"TERMS_OF_SERVICE">
| Screen<"CHAINS">
| Screen<"WALLETS">
| Screen<"INSCRIPTIONS">;

export interface State {
visible: boolean;
loading: boolean;
step: Step;
screen: Screens;
selectedWallets: Record<string, IWalllet>;
visibleWallets: string;
chains: Record<string, IChain>;
displayTermsOfService?: () => void;
}

export interface Actions {
open?: () => void;
close?: () => void;
displayLoader?: () => void;
displayChains?: () => void;
displayWallets?: (chain: string) => void;
displayInscriptions?: () => void;
displayTermsOfService?: () => void;
selectWallet?: (chain: string, wallet: IWalllet) => void;
removeWallet?: (chain: string) => void;
addChain?: (chain: IChain) => void;
setLoading?: (value: boolean) => void;
open?: () => void;
close?: () => void;
}
Loading

0 comments on commit c6c78e8

Please sign in to comment.