Full Example
The following example provides a complete wallet implementation structure to use as a starting point. This code assumes MyWallet extends the Unisat wallet.
src/mywallet/controller.ts
TypeScript
src/mywallet/controller.ts
import { networks } from '@btc-vision/bitcoin';
import { type UnisatChainInfo, UnisatChainType } from '@btc-vision/transaction';
import { AbstractRpcProvider, JSONRpcProvider } from 'opnet';
import { type WalletChainType, type WalletBase } from '../types';
import { type MyWallet } from './interface';
interface MyWalletWindow extends Window {
mywallet?: MyWallet;
}
const notInstalledError = 'MY_WALLET is not installed';
class MyWalletInstance implements WalletBase {
private walletBase: MyWalletWindow['mywallet'];
private accountsChangedHookWrapper?: (accounts: Array<string>) => void;
private chainChangedHookWrapper?: (network: UnisatChainInfo) => void;
private disconnectHookWrapper?: () => void;
private _isConnected: boolean = false;
isInstalled() {
// Check that we are in a browser environment
if (typeof window === 'undefined') {
return false;
}
this.walletBase = (window as unknown as MyWalletWindow).mywallet;
return !!this.walletBase;
}
isConnected() {
return !!this.walletBase && this._isConnected;
}
async canAutoConnect(): Promise<boolean> {
// Check that we can connect to MyWallet
// without showing the login window to the user
return this.walletBase?.canAutoConnect() || false
}
getChainId(): void {
throw new Error('Method not implemented.');
}
async connect(): Promise<string[]> {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
// Show login window (if needed)
// Retrieve the logged in account(s)
// set _isConnected to true
// return the accounts
const accounts: string[] = await this.walletBase.loginAndRetrieveAccounts()
this._isConnected = true;
return accounts;
}
async disconnect() {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
// If connected, disconnect
// set _isConnected to false
if (this._isConnected) this.walletBase.disconnect();
}
getWalletInstance(): MyWallet | null {
// Return the current wallet instance available
// in the global window object. Ensure to return
// complete interface (interface MyWallet in this case)
return (this._isConnected && this.walletBase) || null;
}
public async getProvider(): Promise<AbstractRpcProvider | null> {
if (!this._isConnected || !this.walletBase) return null;
// Up to now, this function is the same for all Wallet
// supporting AbstractRpcProvider.
// You will need to update the way to retrieve the chain below
const chain = await this.walletBase.getCurrentChain();
const unisatChain = this.convertChainToUnisat(chain);
switch (unisatChain) {
case UnisatChainType.BITCOIN_MAINNET:
return new JSONRpcProvider('https://mainnet.opnet.org', networks.bitcoin);
case UnisatChainType.BITCOIN_TESTNET:
return new JSONRpcProvider('https://testnet.opnet.org', networks.testnet);
case UnisatChainType.BITCOIN_REGTEST:
return new JSONRpcProvider('https://regtest.opnet.org', networks.regtest);
// TODO: Add Fractal Mainnet & Testnet when available
default:
return null;
}
}
async getSigner(): Promise<null> {
throw new Error('Method reserved for future use.');
return Promise.resolve(null);
}
async getPublicKey(): Promise<string> {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
// Return the string version of the current public key
return this.walletBase.getPublicKey();
}
async getBalance(): Promise<WalletBalance | null> {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
return (await this.walletBase.getBalance()) as WalletBalance | null;
}
unisatChainToWalletNetwork = (chainType: UnisatChainType): WalletChainType => {
switch (chainType) {
case UnisatChainType.BITCOIN_MAINNET: return WalletChainType.BITCOIN_MAINNET;
case UnisatChainType.BITCOIN_TESTNET: return WalletChainType.BITCOIN_TESTNET;
case UnisatChainType.BITCOIN_REGTEST: return WalletChainType.BITCOIN_REGTEST;
case UnisatChainType.BITCOIN_TESTNET4: return WalletChainType.BITCOIN_TESTNET4;
case UnisatChainType.FRACTAL_BITCOIN_MAINNET: return WalletChainType.FRACTAL_BITCOIN_MAINNET;
case UnisatChainType.FRACTAL_BITCOIN_TESTNET: return WalletChainType.FRACTAL_BITCOIN_TESTNET;
case UnisatChainType.BITCOIN_SIGNET: return WalletChainType.BITCOIN_SIGNET;
default: return WalletChainType.BITCOIN_REGTEST;
}
}
async getNetwork(): Promise<WalletChainType> {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
const chainInfo = await this.walletBase.getChain();
if (!chainInfo) {
throw new Error('Failed to retrieve chain information');
}
return this.unisatChainToWalletNetwork(chainInfo.enum);
}
setAccountsChangedHook(fn: (accounts: string[]) => void): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
// Keep all needed information to be able to remove
// this hook later. It may be the hook function,
// or it may be returned by the 'on', 'addListener' of
// similar function.
this.accountsChangedHookWrapper = (accounts: string[]) => {
fn(accounts);
};
// Connect the above account changed hooks to the wallet
this.walletBase.on('accountsChanged', this.accountsChangedHookWrapper);
}
removeAccountsChangedHook(): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
// Check that we have a hook connected to the wallet
// if so, disconnect it and unset the related variables.
if (this.accountsChangedHookWrapper) {
this.walletBase.off('accountsChanged', this.accountsChangedHookWrapper);
this.accountsChangedHookWrapper = undefined;
}
}
setDisconnectHook(fn: () => void): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
this.disconnectHookWrapper = () => {
fn();
};
this.walletBase.on('disconnect', this.disconnectHookWrapper);
}
removeDisconnectHook(): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
if (this.disconnectHookWrapper) {
this.walletBase.removeListener('disconnect', this.disconnectHookWrapper);
this.disconnectHookWrapper = undefined;
}
}
setChainChangedHook(fn: (chainType: WalletChainType) => void): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
this.chainChangedHookWrapper = (chainInfo: UnisatChainInfo) => {
fn(this.unisatChainToWalletNetwork(chainInfo.enum));
};
this.walletBase.on('chainChanged', this.chainChangedHookWrapper);
}
removeChainChangedHook(): void {
if (!this.isInstalled() || !this.walletBase) {
throw new Error(notInstalledError);
}
if (this.chainChangedHookWrapper) {
this.walletBase.removeListener('chainChanged', this.chainChangedHookWrapper);
this.chainChangedHookWrapper = undefined;
}
}
async getMLDSAPublicKey(): Promise<string | null> {
if (!this._isConnected || !this.walletBase?.web3) return null;
return this.walletBase.web3.getMLDSAPublicKey();
}
async getHashedMLDSAKey(): Promise<string | null> {
const mldsaPublicKey = await this.getMLDSAPublicKey();
if (!mldsaPublicKey) return null;
const publicKeyBuffer = Buffer.from(mldsaPublicKey, 'hex');
const hash = MessageSigner.sha256(publicKeyBuffer);
return hash.toString('hex');
}
unisatChainToWalletNetwork = (chainType: UnisatChainType): WalletChainType => {
switch (chainType) {
case UnisatChainType.BITCOIN_MAINNET: return WalletChainType.BITCOIN_MAINNET;
case UnisatChainType.BITCOIN_TESTNET: return WalletChainType.BITCOIN_TESTNET;
case UnisatChainType.BITCOIN_REGTEST: return WalletChainType.BITCOIN_REGTEST;
case UnisatChainType.BITCOIN_TESTNET4: return WalletChainType.BITCOIN_TESTNET4;
case UnisatChainType.FRACTAL_BITCOIN_MAINNET: return WalletChainType.FRACTAL_BITCOIN_MAINNET;
case UnisatChainType.FRACTAL_BITCOIN_TESTNET: return WalletChainType.FRACTAL_BITCOIN_TESTNET;
case UnisatChainType.BITCOIN_SIGNET: return WalletChainType.BITCOIN_SIGNET;
default: return WalletChainType.BITCOIN_REGTEST;
}
}
walletNetworkToUnisatChain = (network: WalletNetwork|WalletChainType): UnisatChainType => {
switch (network) {
case WalletNetwork.mainnet:
case WalletChainType.BITCOIN_MAINNET: return UnisatChainType.BITCOIN_MAINNET;
case WalletNetwork.testnet:
case WalletChainType.BITCOIN_TESTNET: return UnisatChainType.BITCOIN_TESTNET;
case WalletNetwork.regtest:
case WalletChainType.BITCOIN_REGTEST: return UnisatChainType.BITCOIN_REGTEST;
case WalletChainType.BITCOIN_TESTNET4: return UnisatChainType.BITCOIN_TESTNET4;
case WalletChainType.FRACTAL_BITCOIN_MAINNET: return UnisatChainType.FRACTAL_BITCOIN_MAINNET;
case WalletChainType.FRACTAL_BITCOIN_TESTNET: return UnisatChainType.FRACTAL_BITCOIN_TESTNET;
case WalletChainType.BITCOIN_SIGNET: return UnisatChainType.BITCOIN_SIGNET;
default: return UnisatChainType.BITCOIN_REGTEST;
}
}
async switchNetwork(network: WalletNetwork|WalletChainType): Promise<void> {
if (!this._isConnected || !this.walletBase) return;
const unisatChainType = this.walletNetworkToUnisatChain(network)
await this.walletBase.switchChain(unisatChainType);
}
async signMessage(message: string, messageType?: MessageType): Promise<string | null> {
if (!this._isConnected || !this.walletBase) return null;
return this.walletBase.signMessage(message, messageType);
}
async signMLDSAMessage(message: string): Promise<MLDSASignature | null> {
if (!this._isConnected || !this.walletBase?.web3) return null;
return this.walletBase.web3.signMLDSAMessage(message);
}
async verifyMLDSASignature(message: string, signature: MLDSASignature): Promise<boolean> {
if (!this._isConnected || !this.walletBase?.web3) return false;
return this.walletBase.web3.verifyMLDSASignature(message, signature);
}
}
export default MyWalletInstance;src/mywallet/interface.ts
Define an interface that matches the wallet's implementation. Method names, parameters, and return types must correspond to those exposed by the wallet extension.
TypeScript
src/mywallet/interface.ts
export interface MyWallet {
disconnect: () => Promise<void>;
connect: () => void;
myWalletVersion: number;
canAutoConnect: () => boolean;
loginAndRetrieveAccounts(): Promise<string[]>;
switchNetwork(network: string): Promise<void>;
getPublicKey(): Promise<string>;
getCurrentChain(): Promise<string>;
getAccounts(): Promise<string[]>;
getBalance(): Promise<number>;
//!!!! Add more functions here???
on(event: 'accountsChanged', listener: (accounts: string[]) => void): void;
on(event: 'chainChanged', listener: (chain: string) => void): void;
on(event: 'disconnect', listener: () => void): void;
off(event: string, listener: (accounts: string[]) => void): void;
off(event: 'chainChanged', listener: (chain: string) => void): void;
off(event: 'disconnect', listener: () => void): void;
}
export const logo =
'data:image/svg+xml;base64,' +
'PHN2ZyB3aWR0aD0iNDgxIiBoZWlnaHQ9IjExMiIgdmlld0JveD0iMCAwIDQ4MSAxMTIiIGZpbGw9' +
// ....... A lots of lines here .......
// Try to keep the image small,
// between 32x32 and 128x128 pixels.
'ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=';src/wallets/controller.ts
Add MyWallet to the return type of getWalletInstance(). Add the necessary imports as well.
TypeScript
src/wallets/controller.ts
import type { MyWallet } from './mywallet/interface';
...
static getWalletInstance(): OPWallet | MyWallet | null {
...
}
...
}src/wallets/types.ts
Add your new wallet interface to the getWalletInstance() signature in the src/wallets/types.ts file.
TypeScript
src/wallets/types.ts
import type { MyWallet } from './mywallet/interface';
...
getWalletInstance(): OPWallet | MyWallet | null;
...src/wallets/types.ts
Add the type guards for MyWallet.
TypeScript
src/wallets/types.ts
import { type MyWallet } from './wallets/mywallet/interface';
import { type OPWallet } from '@btc-vision/transaction';
...
export function isMyWallet(walletInstance: OPWallet|MyWallet|null): walletInstance is MyWallet {
return typeof walletInstance == 'object' && (walletInstance as MyWallet)?.myWalletVersion !== undefined;
}