Skip to content

feat: add sign-with-wallet-adapter React example#29

Open
klausundklaus wants to merge 4 commits intoLightprotocol:mainfrom
klausundklaus:feat/sign-with-wallet-adapter
Open

feat: add sign-with-wallet-adapter React example#29
klausundklaus wants to merge 4 commits intoLightprotocol:mainfrom
klausundklaus:feat/sign-with-wallet-adapter

Conversation

@klausundklaus
Copy link

@klausundklaus klausundklaus commented Feb 26, 2026

Summary

  • Adds toolkits/sign-with-wallet-adapter/react/ — 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)
  • Fixes hot/cold balance fetch order in useUnifiedBalance so mints discovered only via compressed accounts also get hot balance lookup

What changed from Privy

Layer Privy Wallet Adapter
Provider PrivyProvider ConnectionProvider + WalletProvider + WalletModalProvider
Sign useSignTransaction → serialize unsigned → sign buffer → deserialize useWallet().signTransaction → sign Transaction object → serialize
Connect UI Privy login/logout WalletMultiButton (auto-detects wallet-standard wallets)
Wallet selection Multi-wallet dropdown Single connected wallet
Dependencies removed @privy-io/react-auth, @solana-program/memo, @solana/kit
Dependencies added @solana/wallet-adapter-react, @solana/wallet-adapter-react-ui

Files identical to Privy

useLightBalance, useUnifiedBalance, useTransactionHistory, CopyButton, Section, TransactionStatus, TransactionHistory, WalletInfo, config files (vite, tsconfig, vitest)

Test plan

  • 26/26 unit tests passing (pnpm test)
  • Build succeeds with zero TS errors (pnpm build)
  • Integration tests skip cleanly without RPC URL
  • Manual test with Phantom on devnet (needs VITE_HELIUS_RPC_URL)

🤖 Generated with Claude Code


Open with Devin

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>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

- 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>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

const getOrCreate = (mintStr: string) => {
let entry = mintMap.get(mintStr);
if (!entry) {
entry = { spl: 0n, t22: 0n, hot: 0n, cold: 0n, decimals: 9 };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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 as 0.001 instead of 1.0.
  • Transfer: the user enters 1, the hook computes Math.floor(1 × 10⁹) = 1_000_000_000 instead of Math.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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

klausundklaus and others added 2 commits February 26, 2026 03:32
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>
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 8 additional findings in Devin Review.

Open in Devin Review

const owner = new PublicKey(ownerPublicKey);
const mintPubkey = new PublicKey(mint);
const recipient = new PublicKey(toAddress);
const tokenAmount = Math.floor(amount * Math.pow(10, decimals));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
const tokenAmount = Math.floor(amount * Math.pow(10, decimals));
const tokenAmount = Math.round(amount * Math.pow(10, decimals));
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant