diff --git a/skills/use-wallet/SKILL.md b/skills/use-wallet/SKILL.md index 4a055b5..2429d4c 100644 --- a/skills/use-wallet/SKILL.md +++ b/skills/use-wallet/SKILL.md @@ -4,9 +4,10 @@ description: >- Integrate Algorand/AVM wallet connections using @txnlab/use-wallet v4.x. Use when building dApps that need wallet connectivity (Pera, Defly, Lute, Kibisis, Exodus, WalletConnect, etc.), transaction signing, data signing - (ARC-60/SIWA), network switching, or wallet state management in React, Vue, - SolidJS, or Svelte applications. Also covers the companion UI component - library @txnlab/use-wallet-ui-react for drop-in wallet buttons and menus. + (ARC-60/SIWA), network switching, or wallet state management in vanilla + JS/TS, React, Vue, SolidJS, or Svelte applications. Also covers the + companion UI component library @txnlab/use-wallet-ui-react for drop-in + wallet buttons and menus. --- # @txnlab/use-wallet @@ -34,6 +35,7 @@ Read the reference file that matches the developer's task: ### Framework-Specific Integration +- **Vanilla JS/TS (no framework)**: [references/vanilla.md](references/vanilla.md) — WalletManager direct usage, BaseWallet subscribe, no provider wrapper - **React**: [references/react.md](references/react.md) — WalletProvider, useWallet, useNetwork hooks - **Vue**: [references/vue.md](references/vue.md) — WalletManagerPlugin, useWallet/useNetwork composables - **SolidJS**: [references/solid.md](references/solid.md) — WalletProvider, useWallet/useNetwork primitives (signals) diff --git a/skills/use-wallet/references/signing-transactions.md b/skills/use-wallet/references/signing-transactions.md index 1d4b6d9..9560806 100644 --- a/skills/use-wallet/references/signing-transactions.md +++ b/skills/use-wallet/references/signing-transactions.md @@ -11,6 +11,31 @@ The `useWallet` hook/composable/primitive provides two methods for signing trans Accepts an array of `algosdk.Transaction` objects, encoded transactions (`Uint8Array`), or arrays of either (for groups). Returns `Promise<(Uint8Array | null)[]>` where `null` indicates an unsigned transaction. +### Vanilla JS/TS + +```typescript +import { WalletManager } from '@txnlab/use-wallet' +import algosdk from 'algosdk' + +async function sendTransaction(manager: WalletManager) { + const activeAddress = manager.activeAddress + if (!activeAddress) return + + const suggestedParams = await manager.algodClient.getTransactionParams().do() + const transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + sender: activeAddress, + receiver: activeAddress, + amount: 0, + suggestedParams, + }) + + const signedTxns = await manager.signTransactions([transaction]) + const { txid } = await manager.algodClient.sendRawTransaction(signedTxns).do() + const result = await algosdk.waitForConfirmation(manager.algodClient, txid, 4) + console.log(`Confirmed at round ${result['confirmed-round']}`) +} +``` + ### React ```tsx @@ -205,11 +230,12 @@ function CallWithAlgoKit() { ## Framework Access Patterns Summary -| Framework | activeAddress | algodClient | transactionSigner | -| --------- | ----------------------- | --------------------- | ------------------- | -| React | `activeAddress` | `algodClient` | `transactionSigner` | -| Vue | `activeAddress.value` | `algodClient.value` | `transactionSigner` | -| SolidJS | `activeAddress()` | `algodClient()` | `transactionSigner` | -| Svelte | `activeAddress.current` | `algodClient.current` | `transactionSigner` | +| Framework | activeAddress | algodClient | transactionSigner | +| --------- | ----------------------- | --------------------- | ---------------------------- | +| Vanilla | `manager.activeAddress` | `manager.algodClient` | `manager.transactionSigner` | +| React | `activeAddress` | `algodClient` | `transactionSigner` | +| Vue | `activeAddress.value` | `algodClient.value` | `transactionSigner` | +| SolidJS | `activeAddress()` | `algodClient()` | `transactionSigner` | +| Svelte | `activeAddress.current` | `algodClient.current` | `transactionSigner` | `transactionSigner` is the same across all frameworks — not wrapped in reactive state. diff --git a/skills/use-wallet/references/vanilla.md b/skills/use-wallet/references/vanilla.md new file mode 100644 index 0000000..d6afa0f --- /dev/null +++ b/skills/use-wallet/references/vanilla.md @@ -0,0 +1,298 @@ +# Vanilla JS/TS (No Framework) + +Package: `@txnlab/use-wallet` + +Use the core library directly when building without a framework (plain TypeScript, vanilla JS, or a non-supported framework). No provider wrapper is needed — you work with `WalletManager` and `BaseWallet` instances directly. + +## Installation + +```bash +npm install @txnlab/use-wallet algosdk +``` + +Then install wallet provider packages for the wallets you want to support (see [getting-started.md](getting-started.md)). + +## Setup + +Create a `WalletManager` instance and call `resumeSessions()` when the page loads to restore previously connected wallets: + +```typescript +import { WalletManager, WalletId, NetworkId } from '@txnlab/use-wallet' + +const walletManager = new WalletManager({ + wallets: [WalletId.PERA, WalletId.DEFLY, WalletId.LUTE], + defaultNetwork: NetworkId.TESTNET, +}) + +// Restore previous wallet sessions +document.addEventListener('DOMContentLoaded', async () => { + try { + await walletManager.resumeSessions() + } catch (error) { + console.error('Error resuming sessions:', error) + } +}) +``` + +## Accessing Wallets + +`walletManager.wallets` returns a `BaseWallet[]` array of all configured wallet providers: + +```typescript +interface BaseWallet { + id: WalletId + metadata: { name: string; icon: string } + accounts: WalletAccount[] + activeAccount: WalletAccount | null + isConnected: boolean + isActive: boolean + canSignData: boolean + connect(args?: Record): Promise + disconnect(): Promise + setActive(): void + setActiveAccount(address: string): void + transactionSigner: algosdk.TransactionSigner + signTransactions(txnGroup: T | T[], indexesToSign?: number[]): Promise<(Uint8Array | null)[]> + subscribe(callback: (state: State) => void): () => void +} +``` + +Iterate wallets to build UI: + +```typescript +for (const wallet of walletManager.wallets) { + console.log(wallet.metadata.name, wallet.isConnected) +} +``` + +## Subscribing to State + +Each wallet exposes a `subscribe()` method that calls your callback whenever the wallet's state changes. It returns an unsubscribe function for cleanup: + +```typescript +const wallet = walletManager.wallets[0] + +const unsubscribe = wallet.subscribe((state) => { + console.log('State changed:', state) + // Re-render your UI here +}) + +// Clean up when done +window.addEventListener('beforeunload', () => { + unsubscribe() +}) +``` + +You can also subscribe to the manager for global state changes (across all wallets): + +```typescript +const unsubscribe = walletManager.subscribe((state) => { + console.log('Active wallet:', state.activeWallet) + console.log('Accounts:', state.wallets) +}) +``` + +## Connecting and Disconnecting + +```typescript +const wallet = walletManager.wallets[0] + +// Connect +try { + const accounts = await wallet.connect() + console.log('Connected accounts:', accounts) +} catch (error) { + console.error('Connection failed:', error) +} + +// Disconnect +await wallet.disconnect() + +// Set as active wallet (when multiple wallets are connected) +wallet.setActive() + +// Switch active account (when wallet has multiple accounts) +wallet.setActiveAccount(someAddress) +``` + +## Network Switching + +Access and change the active network directly on the manager: + +```typescript +// Read current network +console.log(walletManager.activeNetwork) // 'testnet' + +// Switch network +await walletManager.setActiveNetwork(NetworkId.MAINNET) + +// Update algod config at runtime +walletManager.updateAlgodConfig('mainnet', { + baseServer: 'https://my-node.example.com', + token: 'my-token', +}) + +// Reset to initial config +walletManager.resetNetworkConfig('mainnet') +``` + +## Sign & Send Transaction + +```typescript +import { WalletManager } from '@txnlab/use-wallet' +import algosdk from 'algosdk' + +async function sendTransaction(manager: WalletManager) { + const activeAddress = manager.activeAddress + if (!activeAddress) return + + const suggestedParams = await manager.algodClient.getTransactionParams().do() + const transaction = algosdk.makePaymentTxnWithSuggestedParamsFromObject({ + sender: activeAddress, + receiver: activeAddress, + amount: 0, + suggestedParams, + }) + + const signedTxns = await manager.signTransactions([transaction]) + const { txid } = await manager.algodClient.sendRawTransaction(signedTxns).do() + const result = await algosdk.waitForConfirmation(manager.algodClient, txid, 4) + console.log(`Confirmed at round ${result['confirmed-round']}`) +} +``` + +## Using with ATC (transactionSigner) + +The `transactionSigner` on both `WalletManager` and individual `BaseWallet` instances is a typed `algosdk.TransactionSigner` for use with `AtomicTransactionComposer`: + +```typescript +import { WalletManager } from '@txnlab/use-wallet' +import algosdk from 'algosdk' + +async function callContract(manager: WalletManager) { + const activeAddress = manager.activeAddress + if (!activeAddress) return + + const atc = new algosdk.AtomicTransactionComposer() + const suggestedParams = await manager.algodClient.getTransactionParams().do() + + const method = algosdk.ABIMethod.fromSignature('hello(string)string') + atc.addMethodCall({ + sender: activeAddress, + signer: manager.transactionSigner, + appID: 123, + method, + methodArgs: ['World'], + suggestedParams, + }) + + const result = await atc.execute(manager.algodClient, 4) + console.log('Return value:', result.methodResults[0].returnValue) +} +``` + +## Using with AlgoKit Utils + +```typescript +import { WalletManager } from '@txnlab/use-wallet' +import { AlgorandClient } from '@algorandfoundation/algokit-utils' +import { HelloWorldClient } from './artifacts/HelloWorld/client' + +async function callWithAlgoKit(manager: WalletManager) { + const activeAddress = manager.activeAddress + if (!activeAddress) return + + const algorand = AlgorandClient.fromClients({ + algod: manager.algodClient, + }).setSigner(activeAddress, manager.transactionSigner) + + const client = algorand.client.getTypedAppClientById(HelloWorldClient, { + appId: 123n, + }) + + const result = await client + .newGroup() + .hello({ args: { name: 'World' } }) + .execute() + + console.log('Return value:', result.return) +} +``` + +## Connect Wallet Menu Example + +A minimal vanilla TypeScript wallet menu with reactive state: + +```typescript +import { WalletManager, WalletId, NetworkId, BaseWallet } from '@txnlab/use-wallet' + +const manager = new WalletManager({ + wallets: [WalletId.PERA, WalletId.DEFLY, WalletId.LUTE], + defaultNetwork: NetworkId.TESTNET, +}) + +const container = document.querySelector('#app')! +const unsubscribers: (() => void)[] = [] + +function render() { + container.innerHTML = '' + + for (const wallet of manager.wallets) { + const div = document.createElement('div') + + if (wallet.isActive && wallet.activeAccount) { + div.innerHTML = ` +

${wallet.metadata.name} [active]

+

${wallet.activeAccount.address}

+ + ` + } else if (wallet.isConnected) { + div.innerHTML = ` +

${wallet.metadata.name} [connected]

+ + + ` + } else { + div.innerHTML = ` +

${wallet.metadata.name}

+ + ` + } + + container.appendChild(div) + } +} + +// Subscribe to each wallet's state changes +for (const wallet of manager.wallets) { + unsubscribers.push(wallet.subscribe(() => render())) +} + +// Handle button clicks +container.addEventListener('click', async (e) => { + const target = e.target as HTMLElement + const action = target.dataset.action + const walletId = target.dataset.wallet + if (!action || !walletId) return + + const wallet = manager.wallets.find((w) => w.id === walletId) + if (!wallet) return + + try { + if (action === 'connect') await wallet.connect() + else if (action === 'disconnect') await wallet.disconnect() + else if (action === 'set-active') wallet.setActive() + } catch (error) { + console.error(`${action} failed:`, error) + } +}) + +// Initial render + restore sessions +render() +manager.resumeSessions() + +// Cleanup +window.addEventListener('beforeunload', () => { + unsubscribers.forEach((fn) => fn()) +}) +```