Skip to content

Wallet Integration

Documentation for Tezos wallet integration using Beacon SDK and Taquito.

Overview

The wallet integration layer handles:

  • Wallet connection (Beacon SDK)
  • Transaction signing
  • Message authentication
  • Balance queries
  • Domain name resolution

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                    ClientsProvider                               │
│              (context/clients-context.tsx)                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌─────────────────────┐    ┌─────────────────────────────┐    │
│  │    TezosWallet      │    │    Global Atoms (Jotai)     │    │
│  │                     │    │                             │    │
│  │  - TezosToolkit     │    │  - walletStateAtom          │    │
│  │  - BeaconWallet     │    │  - walletLoadingAtom        │    │
│  │  - DomainsClient    │    │  - tezNetworkSelected       │    │
│  └─────────────────────┘    └─────────────────────────────┘    │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│                      Beacon SDK                                  │
│                   (@airgap/beacon-sdk)                          │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌───────────────┐    ┌───────────────┐    ┌───────────────┐   │
│  │ Temple Wallet │    │ Kukai Wallet  │    │ Other Wallets │   │
│  └───────────────┘    └───────────────┘    └───────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

TezosWallet Class

File: apps/emprops-studio/wallet.ts

Initialization

typescript
class TezosWallet implements WalletInterface {
  private tezos: TezosToolkit
  private wallet: BeaconWallet
  private domainsClient: DomainsClient

  async createClients(): Promise<void> {
    const rpcUrl = getRPCFromRegistry(getChainIdByEnvironment("TEZOS"))

    // Create Taquito toolkit
    this.tezos = new TezosToolkit(rpcUrl)

    // Create Beacon wallet
    this.wallet = new BeaconWallet({
      name: process.env.NEXT_PUBLIC_DAPP_NAME,
      preferredNetwork: process.env.NEXT_PUBLIC_NETWORK as NetworkType
    })

    // Set wallet as provider with timeout
    this.tezos.setWalletProvider(this.wallet)
    this.tezos.setProvider({
      config: { confirmationPollingTimeoutSecond: 120 }
    })

    // Create domains client for .tez resolution
    this.domainsClient = new DomainsClient(rpcUrl, chainId)
  }
}

Setup (Check Existing Connection)

typescript
async setup(): Promise<WalletState | null> {
  // Check for existing active account
  const activeAccount = await this.wallet.client.getActiveAccount()

  if (activeAccount) {
    const address = await this.wallet.getPKH()
    const balanceMutez = await this.tezos.tz.getBalance(address)
    const balance = simpleFixPrice(balanceMutez.toNumber(), "TEZOS")

    return {
      address,
      connected: true,
      blockchain: "TEZOS",
      network: activeAccount.network?.type,
      balance
    }
  }

  return null
}

Connect

typescript
async connect(): Promise<WalletState> {
  // Request wallet permissions
  const permissions = await this.wallet.requestPermissions({
    network: {
      type: process.env.NEXT_PUBLIC_NETWORK as NetworkType
    }
  })

  const address = await this.wallet.getPKH()
  const balanceMutez = await this.tezos.tz.getBalance(address)
  const balance = simpleFixPrice(balanceMutez.toNumber(), "TEZOS")

  return {
    address,
    connected: true,
    blockchain: "TEZOS",
    network: permissions.network?.type,
    balance
  }
}

Disconnect

typescript
async disconnect(): Promise<void> {
  await this.wallet.clearActiveAccount()
}

Authenticate (Sign Message)

typescript
async authenticate(message: string): Promise<string> {
  const dappUrl = process.env.NEXT_PUBLIC_DAPP_URL
  const timestamp = new Date().toISOString()

  // Format message per Tezos signing standard
  const formattedMessage = [
    "Tezos Signed Message:",
    dappUrl,
    timestamp,
    message
  ].join(" ")

  // Request signature (no on-chain transaction)
  const signature = await this.wallet.client.requestSignPayload({
    signingType: SigningType.MICHELINE,
    payload: formattedMessage
  })

  // Store in localStorage
  localStorage.setItem("tezos-auth", JSON.stringify({
    signature: signature.signature,
    timestamp,
    message
  }))

  return signature.signature
}

Get Network

typescript
async getNetwork(): Promise<string> {
  const activeAccount = await this.wallet.client.getActiveAccount()
  return activeAccount?.network?.type || ""
}

Wallet State Management

Jotai Atoms

File: apps/emprops-studio/globals/wallet.ts

typescript
import { atom } from 'jotai'
import { WalletState } from '@/types/wallet'

// Current wallet state
export const walletStateAtom = atom<WalletState | null>(null)

// Loading state during connection
export const walletLoadingAtom = atom<boolean>(true)

// Selected Tezos network
export const tezNetworkSelected = atom<string>("")

WalletState Interface

typescript
interface WalletState {
  address: string | undefined
  connected: boolean
  blockchain: "TEZOS" | "ETHEREUM" | "BASE"
  network?: string        // Chain ID or network name
  balance?: number        // In native units (XTZ, ETH)
}

ClientsProvider

File: apps/emprops-studio/context/clients-context.tsx

Context Definition

typescript
interface Clients {
  wallet: TezosWallet | null
}

const ClientsContext = createContext<Clients | null>(null)

export function useClients(): Clients | null {
  return useContext(ClientsContext)
}

Provider Implementation

typescript
export function ClientsProvider({ children }: { children: ReactNode }) {
  const [clients, setClients] = useState<Clients | null>(null)

  useEffect(() => {
    // Dynamic import to avoid SSR issues
    import('@/wallet').then(({ TezosWallet }) => {
      const wallet = new TezosWallet()
      wallet.createClients().then(() => {
        setClients({ wallet })
      })
    })
  }, [])

  return (
    <ClientsContext.Provider value={clients}>
      {children}
    </ClientsContext.Provider>
  )
}

WalletProviderLayout

File: apps/emprops-studio/layouts/WalletProviderLayout/index.tsx

Handles automatic wallet setup on app load:

typescript
const WalletProviderLayout: React.FC<Props> = ({ children }) => {
  const clients = useClients()
  const [, setWalletState] = useAtom(walletStateAtom)
  const [, setWalletLoading] = useAtom(walletLoadingAtom)
  const [, setTezNetworkSelected] = useAtom(tezNetworkSelected)

  const setupTezosWallet = useCallback(async () => {
    try {
      if (!clients?.wallet) return

      // Check for existing connection
      const state = await clients.wallet.setup()
      setWalletState(state)

      // Get network
      const chainId = await clients.wallet.getNetwork()
      setTezNetworkSelected(chainId)
    } catch (e) {
      console.error(e)
    } finally {
      setWalletLoading(false)
    }
  }, [clients, setWalletLoading, setWalletState, setTezNetworkSelected])

  useEffect(() => {
    setupTezosWallet()
  }, [setupTezosWallet])

  return <>{children}</>
}

Tezos Domain Resolution

File: apps/emprops-studio/clients/tezos-client.ts

DomainsClient Class

typescript
class DomainsClient {
  private client: TaquitoTezosDomainsClient

  constructor(rpcUrl: string, chainId: ChainId) {
    const toolkit = new TezosToolkit(rpcUrl)
    toolkit.addExtension(new Tzip16Module())

    // Determine network from chain ID
    const network = chainId === ChainId.TezosMainnet
      ? SupportedNetwork.Mainnet
      : SupportedNetwork.Ghostnet

    this.client = new TaquitoTezosDomainsClient({
      tezos: toolkit,
      network,
      caching: { enabled: true }
    })
  }

  async resolveDomain(address: string): Promise<string | null> {
    try {
      const domain = await this.client.resolver.resolveAddressToName(address)
      return domain || null
    } catch {
      return null
    }
  }
}

Usage:

typescript
// Resolve tz1... address to .tez domain
const domain = await domainsClient.resolveDomain("tz1abc...")
// Returns: "myname.tez" or null

Beacon SDK Integration

Wallet Types Supported

The Beacon SDK supports multiple Tezos wallets:

  • Temple Wallet (browser extension)
  • Kukai Wallet (web-based)
  • Umami Wallet
  • AirGap Wallet
  • Galleon Wallet
  • Spire Wallet

Connection Flow

1. User clicks "Connect Wallet"

   ├─▶ wallet.requestPermissions() called

   ├─▶ Beacon shows wallet selection modal

   ├─▶ User selects wallet (e.g., Temple)

   ├─▶ Wallet extension prompts for approval

   ├─▶ User approves network connection

   ├─▶ Beacon returns permissions object

   └─▶ App stores wallet state

Signing Types

typescript
enum SigningType {
  RAW = "raw",           // Plain bytes
  OPERATION = "operation", // Tezos operation
  MICHELINE = "micheline"  // Micheline-encoded data
}

MICHELINE is used for:

  • Message signing (authentication)
  • Structured data signing
  • Wert payment widget integration

Using Wallet in Components

Getting Wallet Instance

typescript
function MyComponent() {
  const clients = useClients()
  const [walletState] = useAtom(walletStateAtom)

  // Check if connected
  if (!walletState?.connected) {
    return <ConnectWalletButton />
  }

  // Use wallet for transactions
  const handleMint = async () => {
    const wallet = clients?.wallet
    if (!wallet) return

    const toolkit = wallet.getTezosToolkit()
    const contract = await toolkit.wallet.at(contractAddress)
    // ... execute transaction
  }
}

Getting TezosToolkit

typescript
// For transactions (requires wallet)
const toolkit = clients.wallet.getTezosToolkit()
const contract = await toolkit.wallet.at(address)
await contract.methods.mint(params).send()

// For queries (no wallet needed)
const queryToolkit = getQuerierClientTez()
const contract = await queryToolkit.contract.at(address)
const storage = await contract.storage()

Chain ID Reference

File: apps/emprops-studio/types/wallet.ts

typescript
export enum ChainId {
  // Tezos
  TezosMainnet = "NetXdQprcVkpaWU",
  TezosGhostnet = "NetXnHfVqm9iesp",

  // Ethereum
  EthereumMainnet = "1",
  EthereumSepolia = "11155111",

  // Base
  BaseMainnet = "8453",
  BaseSepolia = "84532"
}

Environment Variables

bash
# Dapp Identity
NEXT_PUBLIC_DAPP_NAME="EmProps Studio"
NEXT_PUBLIC_DAPP_URL="https://emprops.io"

# Network Configuration
NEXT_PUBLIC_NETWORK=mainnet           # or ghostnet
NEXT_PUBLIC_RPC_URL=https://mainnet.tezos.marigold.dev
NEXT_PUBLIC_NETWORKS_REGISTRY='{
  "NetXdQprcVkpaWU": ["https://mainnet.tezos.marigold.dev"],
  "NetXnHfVqm9iesp": ["https://ghostnet.tezos.marigold.dev"]
}'

Error Handling

Common Errors

ErrorCauseResolution
WALLET_NOT_CONNECTEDNo active accountCall connect() first
USER_REJECTEDUser cancelledShow retry option
NETWORK_MISMATCHWrong network selectedPrompt network switch
RPC_ERRORRPC node issueRetry with different RPC

Example Error Handling

typescript
async function connectWallet() {
  try {
    const state = await wallet.connect()
    setWalletState(state)
  } catch (error) {
    if (error.message.includes('USER_REJECTED')) {
      toast.error('Connection cancelled')
    } else if (error.message.includes('NETWORK')) {
      toast.error('Please switch to mainnet')
    } else {
      toast.error('Failed to connect wallet')
    }
  }
}

Dependencies

json
{
  "@taquito/taquito": "16.0.1",
  "@taquito/beacon-wallet": "16.2.0",
  "@taquito/tzip16": "16.1.2",
  "@tezos-domains/taquito-client": "1.24.0",
  "@airgap/beacon-sdk": "4.6.2"
}

Released under the MIT License.