feat: add sign-with-wallet-adapter React example#29
feat: add sign-with-wallet-adapter React example#29klausundklaus wants to merge 4 commits intoLightprotocol:mainfrom
Conversation
Adds a Wallet Adapter equivalent of the existing Privy example. Swaps the signing layer from Privy SDK to @solana/wallet-adapter-react while keeping identical Light Token SDK usage (transfer, wrap, unwrap). Changes from Privy example: - ConnectionProvider + WalletProvider + WalletModalProvider (replaces PrivyProvider) - useWallet().signTransaction (replaces useSignTransaction from Privy) - signAndSendBatches takes signTransaction(Transaction) -> Transaction - WalletMultiButton for connect/disconnect (replaces Privy login/logout) - Removed wallet selector (single connected wallet) - Fixed hot/cold balance fetch order (cold before hot) so compressed-only mints also get hot balance lookup 26 unit tests passing, build clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- useWrap now awaits confirmTransaction after sendRawTransaction, matching the behavior of signAndSendBatches (used by transfer/unwrap) - Added confirmTransaction assertion to wrap test - README expanded to match Privy example quality: hook docs, component docs, setup helpers table, quick start guide Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| const getOrCreate = (mintStr: string) => { | ||
| let entry = mintMap.get(mintStr); | ||
| if (!entry) { | ||
| entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9 }; |
There was a problem hiding this comment.
🔴 Token decimals hardcoded to 9 — causes wrong display and wrong transfer amounts for non-9-decimal tokens
The getOrCreate helper in useUnifiedBalance always initialises decimals: 9 and the value is never updated from on-chain mint metadata. This decimals field propagates to TransferForm where it controls both the balance display (formatBigint) and the transfer amount calculation (useTransfer computes Math.floor(amount * Math.pow(10, decimals))).
Root cause and impact
At useUnifiedBalance.ts:39, every new mint entry is created with decimals: 9:
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9 };No code path ever writes a different value to entry.decimals. The field is then emitted in the TokenBalance at useUnifiedBalance.ts:132.
For a token with 6 decimals (e.g. USDC):
- Display: a raw balance of
1_000_000(= 1 USDC) is formatted as0.001instead of1.0. - Transfer: the user enters
1, the hook computesMath.floor(1 × 10⁹) = 1_000_000_000instead ofMath.floor(1 × 10⁶) = 1_000_000, attempting to send 1 000× more tokens than intended. This will either drain the account or fail with an insufficient-balance error.
Impact: Incorrect balances shown to users and potentially catastrophic over-sends for any token whose decimals ≠ 9.
Prompt for agents
In toolkits/sign-with-wallet-adapter/react/src/hooks/useUnifiedBalance.ts, the getOrCreate helper at line 39 hardcodes decimals to 9 and the value is never updated from on-chain data. You need to fetch the actual decimals for each mint. Options:
1. After collecting all mint keys (around line 99), fetch each mint's on-chain account (e.g. via getMint or getAccountInfo + parsing the MintLayout) and set entry.decimals to the real value.
2. Alternatively, when parsing SPL/T22 token accounts (steps 2-3), the token account data buffer contains the mint's decimals information if you also fetch the mint account.
3. For the hot-balance step, getAtaInterface may already return decimals in its parsed result — if so, use that to update entry.decimals.
The same bug exists in the Privy version at toolkits/sign-with-privy/react/src/hooks/useUnifiedBalance.ts:39 and should be fixed there as well.
Was this helpful? React with 👍 or 👎 to provide feedback.
Tests useUnifiedBalance, useTransfer, and useTransactionHistory against devnet using filesystem keypair signing. Creates a fresh mint with SPL interface, mints tokens, wraps to light-token, then exercises the full transfer + balance + history flow. Run: VITE_HELIUS_RPC_URL=<url> pnpm test:integration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Support both devnet and localnet testing: - VITE_HELIUS_RPC_URL=<url> for devnet (Helius bundles compression API) - VITE_LOCALNET=true for localnet (createRpc() uses default ports 8899/8784/3001) - Localnet mode airdrops SOL before running tests Note: localnet currently blocked by CLI/SDK version mismatch (CLI v0.27.0 programs don't match SDK v0.23.0-beta.9 instructions). Devnet tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| const owner = new PublicKey(ownerPublicKey); | ||
| const mintPubkey = new PublicKey(mint); | ||
| const recipient = new PublicKey(toAddress); | ||
| const tokenAmount = Math.floor(amount * Math.pow(10, decimals)); |
There was a problem hiding this comment.
🟡 Floating-point precision loss in token amount calculation silently sends fewer tokens than intended
When converting a human-readable amount (e.g. 1.001) to the smallest token unit, Math.floor(amount * Math.pow(10, decimals)) produces an off-by-one result for ~2.7% of input values.
Root Cause & Reproduction
At toolkits/sign-with-wallet-adapter/react/src/hooks/useTransfer.ts:37:
const tokenAmount = Math.floor(amount * Math.pow(10, decimals));IEEE-754 floating-point multiplication can produce a result just below the true integer value. Math.floor then truncates it down by 1 smallest unit.
Confirmed with Node.js:
Math.floor(1.001 * 1e9) → 1000999999 (expected: 1001000000)
Math.floor(1.003 * 1e9) → 1002999999 (expected: 1003000000)
Math.floor(1.005 * 1e9) → 1004999999 (expected: 1005000000)
Out of 9999 test values (0.001–9.999 with step 0.001), 271 (2.7%) produced a result 1 lamport less than expected.
The same pattern appears in useWrap.ts:39 and useUnwrap.ts:36.
Impact: Users occasionally send 1 smallest-unit less than intended. For a 9-decimal token, that's 0.000000001 tokens — practically negligible but semantically incorrect. A user typing 1.001 expects exactly 1001000000 base units but gets 1000999999.
Fix: Use Math.round instead of Math.floor, or parse the decimal string directly to avoid floating-point arithmetic altogether.
| const tokenAmount = Math.floor(amount * Math.pow(10, decimals)); | |
| const tokenAmount = Math.round(amount * Math.pow(10, decimals)); |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
toolkits/sign-with-wallet-adapter/react/— a Wallet Adapter equivalent of the existing Privy example@solana/wallet-adapter-reactwhile keeping identical Light Token SDK usage (transfer, wrap, unwrap)useUnifiedBalanceso mints discovered only via compressed accounts also get hot balance lookupWhat changed from Privy
PrivyProviderConnectionProvider+WalletProvider+WalletModalProvideruseSignTransaction→ serialize unsigned → sign buffer → deserializeuseWallet().signTransaction→ sign Transaction object → serializeWalletMultiButton(auto-detects wallet-standard wallets)@privy-io/react-auth,@solana-program/memo,@solana/kit@solana/wallet-adapter-react,@solana/wallet-adapter-react-uiFiles identical to Privy
useLightBalance,useUnifiedBalance,useTransactionHistory,CopyButton,Section,TransactionStatus,TransactionHistory,WalletInfo, config files (vite, tsconfig, vitest)Test plan
pnpm test)pnpm build)VITE_HELIUS_RPC_URL)🤖 Generated with Claude Code