Skip to content

feat(rn_cli_wallet): add Canton blockchain support#469

Merged
ignaciosantise merged 3 commits intomainfrom
feat/luxembourg-v1
Apr 10, 2026
Merged

feat(rn_cli_wallet): add Canton blockchain support#469
ignaciosantise merged 3 commits intomainfrom
feat/luxembourg-v1

Conversation

@ignaciosantise
Copy link
Copy Markdown
Collaborator

@ignaciosantise ignaciosantise commented Apr 9, 2026

Summary

  • Add Canton namespace (canton:mainnet, canton:devnet) to the RN CLI wallet with Ed25519 signing via tweetnacl
  • Implement all 7 Canton RPC methods: 5 auto-approve read-only methods (canton_listAccounts, canton_getPrimaryAccount, canton_getActiveNetwork, canton_status, canton_ledgerApi) and 2 manual-approve methods (canton_signMessage, canton_prepareSignExecute)
  • Wire Canton into session proposals, event routing, wallet initialization, import wallet, and secret phrase screens
  • Fix pre-existing bug where TON/TRON wallet objects weren't stored in SettingsStore (causing "not initialized" on Secret Phrase screen)

Context

Screenshots

iOS

Screenshot 2026-04-09 at 3 02 04 PM Screenshot 2026-04-09 at 3 02 12 PM

Android

Screenshot 2026-04-09 at 3 06 25 PM Screenshot 2026-04-09 at 3 06 32 PM

New files

File Purpose
src/constants/Canton.ts Chain definitions, signing methods, events
src/lib/CantonLib.ts Ed25519 keypair via tweetnacl
src/utils/CantonWalletUtil.ts Wallet create/restore/import
src/utils/CantonRequestHandlerUtil.ts RPC method handlers (mock + real signing)
src/modals/SessionSignCantonModal.tsx Approval modal for sensitive methods

Test plan

  • Build: npx tsc --noEmit passes (verified)
  • Connect to react-dapp-v2 with Canton chains selected
  • Session proposal shows Canton mainnet/devnet chains
  • Read-only methods auto-respond without modal
  • canton_signMessage opens approval modal and returns Ed25519 signature
  • canton_prepareSignExecute opens approval modal and returns mock response
  • Canton secret key visible on Secret Phrase screen
  • Canton wallet importable via Import Wallet modal

🤖 Generated with Claude Code

Add Canton namespace integration with Ed25519 signing via tweetnacl.
Implements all 7 Canton RPC methods with auto-approve for read-only
methods and manual approval modal for sensitive operations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 9, 2026 18:10
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 9, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pos-demo Ready Ready Preview, Comment Apr 10, 2026 1:29pm

Request Review

@claude
Copy link
Copy Markdown

claude bot commented Apr 9, 2026

Claude finished @ignaciosantise's task in 1m 40s —— View job


Code Review

  • Gather context and read changed files
  • Run license compliance check — ✅ tweetnacl@1.0.3 (Unlicense/public domain), no issues
  • Run breaking changes check — ✅ No breaking changes; all additions are purely additive
  • Run data classification check
  • Compile and post review findings

Found 6 issue(s)

Issue 1: React hooks called after conditional return

ID: canton-modal-hooks-violation-a7c1
File: wallets/rn_cli_wallet/src/modals/SessionSignCantonModal.tsx:1
Severity: CRITICAL
Category: code_quality

Context:

  • Pattern: /* eslint-disable react-hooks/rules-of-hooks */ at line 1 suppresses a real violation. useState and useSnapshot hooks (lines 23–28) are called before the conditional return at line 32, but useCallback hooks (onApprove at line 43, onReject at line 70) are called after the conditional early return. This is a React Rules of Hooks violation.
  • Risk: If requestEvent or requestSession become falsy on a re-render after being truthy, React will call fewer hooks than the previous render, causing an invariant violation and potential runtime crash.
  • Impact: Modal crashes in strict mode or when state changes mid-render; hooks called with inconsistent order across renders.
  • Trigger: Any re-render where data becomes unavailable (e.g., modal store reset race condition).

Recommendation: Move all hooks before the conditional return and remove the eslint-disable:

export default function SessionSignCantonModal() {
  const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state);
  const [isLoadingApprove, setIsLoadingApprove] = useState(false);
  const [isLoadingReject, setIsLoadingReject] = useState(false);

  const requestEvent = ModalStore.state.data?.requestEvent;
  const requestSession = ModalStore.state.data?.requestSession;
  const { topic, params } = requestEvent ?? { topic: '', params: { request: {} } };

  const onApprove = useCallback(async () => { ... }, [requestEvent, topic]);
  const onReject = useCallback(async () => { ... }, [requestEvent, topic]);

  if (!requestEvent || !requestSession) {
    return <Text variant="md-400" color="text-error">Missing request data</Text>;
  }
  // rest of render
}

Fix this →


Issue 2: Unhandled async error in Canton auto-approve path

ID: canton-events-unhandled-async-b3d2
File: wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts:130
Severity: HIGH
Category: code_quality

Context:

  • Pattern: The Canton read-only method auto-approve block (lines 130–138) directly awaits approveCantonRequest(requestEvent) inside the onSessionRequest async callback with no try/catch:
    case CANTON_SIGNING_METHODS.LIST_ACCOUNTS:
    ...
      return walletKit.respondSessionRequest({
        topic,
        response: await approveCantonRequest(requestEvent),
      });
  • Risk: approveCantonRequest explicitly throws (line 121 of CantonRequestHandlerUtil.ts) when the wallet is uninitialized or on unexpected errors. The outer onSessionRequest function has no try/catch, so these become unhandled promise rejections.
  • Impact: Silent failures — the dApp request hangs indefinitely with no error feedback to the user or WalletConnect session.
  • Trigger: Any Canton read-only request arriving before wallet initialization completes, or any unexpected error in the handler.

Recommendation:

case CANTON_SIGNING_METHODS.LIST_ACCOUNTS:
case CANTON_SIGNING_METHODS.GET_PRIMARY_ACCOUNT:
case CANTON_SIGNING_METHODS.GET_ACTIVE_NETWORK:
case CANTON_SIGNING_METHODS.STATUS:
case CANTON_SIGNING_METHODS.LEDGER_API:
  try {
    const response = await approveCantonRequest(requestEvent);
    return walletKit.respondSessionRequest({ topic, response });
  } catch (e) {
    LogStore.error((e as Error).message, 'WalletKitEvents', 'onSessionRequest');
    Toast.show({ type: 'error', text1: 'Canton request failed', text2: (e as Error).message });
  }
  return;

Issue 3: Canton wallet not in reactive SettingsStore state

ID: canton-wallet-nonreactive-state-c5e3
File: wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx:136
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: SecretPhrase reads the Canton wallet via getCantonWallet()?.getSecretKey?.() — a module-level function call outside Valtio's reactive graph. All other chains (TON, TRON, SUI) store their wallet objects in SettingsStore and read them via useSnapshot.
  • Risk: When a user imports a new Canton wallet via ImportWalletModal, the module-level wallet1 updates but SettingsStore has no cantonWallet field — so the SecretPhrase screen won't re-render reactively to show the new key.
  • Impact: After importing a Canton wallet, the Secret Phrase screen may show a stale key until manually remounted.
  • Trigger: Import Canton wallet → navigate to Secret Phrase screen without closing/reopening it.

Recommendation: Add cantonWallet: CantonLib | null to SettingsStore.state, add setCantonWallet(wallet: CantonLib) action, call it in useInitializeWalletKit.ts (and in loadCantonWallet), then read via useSnapshot in SecretPhrase:

const { cantonWallet } = useSnapshot(SettingsStore.state);
const cantonSecretKey = cantonWallet?.getSecretKey?.() ?? null;

Issue 4: Missing Android versionCode increment

ID: canton-missing-versioncode-d4f4
File: wallets/rn_cli_wallet/android/app/build.gradle
Severity: MEDIUM
Category: code_quality

Context:

  • Pattern: Per AGENTS.md: "Always increment the versionCode when creating a new feature, fix, or PR." This PR adds Canton blockchain support (527 additions across 17 files) but android/app/build.gradle is not in the changed files list.
  • Risk: Duplicate versionCode across builds can cause update failures on Android devices and CI build distribution conflicts.
  • Impact: Play Store / internal distribution may reject or silently fail to update the app.
  • Trigger: Any Android build from this branch.

Recommendation: Increment versionCode in android/app/build.gradle by 1.


Issue 5: Ed25519 secret key stored and displayed as plaintext (dcl-cantonlib-secret-key-exposed-ui-a3f1 / dcl-cantonwalletutil-plaintext-key-storage-b7e2)

ID: dcl-cantonlib-secret-key-exposed-ui-a3f1
File: wallets/rn_cli_wallet/src/utils/CantonWalletUtil.ts:20 / wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx:136
Severity: MEDIUM
Category: security

Context:

  • Pattern: The Canton secret key is stored via storage.setItem('CANTON_SECRET_KEY_1', wallet1.getSecretKey()) and rendered to the UI in SecretPhrase with a clipboard copy button — following the same pattern as TON/TRON in this codebase.
  • Risk: On a rooted/jailbroken device or via ADB on a debug build, the plaintext key in storage is readable. The Secret Phrase screen provides direct clipboard access to the raw 64-byte private key.
  • Impact: Full wallet compromise if device storage is accessible.
  • Trigger: Any debug build on an accessible device.
  • Note: This follows the existing established pattern for all other chains in this reference wallet (TON, TRON, EVM). The warning banner ("for development purposes only") is present. Flagging for awareness — not unique to this PR.

Recommendation: Document explicitly in code comments that the storage utility uses MMKV (verify it's using an encrypted instance) and not plain AsyncStorage. For the UI, consider gating the Secret Phrase screen behind biometric auth, consistent with how production wallets protect key export.


Issue 6: getWallet() return type masks potential undefined

ID: canton-walletutil-return-type-e6f5
File: wallets/rn_cli_wallet/src/utils/CantonWalletUtil.ts:37
Severity: LOW
Category: code_quality

Context:

  • Pattern: export let wallet1: CantonLib; is declared without initialization, meaning it's undefined at runtime until createOrRestoreCantonWallet() completes. The getWallet() function returns wallet1 typed as CantonLib (not CantonLib | undefined), masking the potential undefined state from TypeScript callers.
  • Risk: TypeScript callers trust the return type and may omit null checks; only the manual runtime check if (!wallet) in approveCantonRequest catches this.

Recommendation:

export let wallet1: CantonLib | undefined;
export const getWallet = (): CantonLib | undefined => wallet1;

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class Canton namespace support to the RN CLI wallet, including key management (Ed25519), WalletConnect session proposal wiring, request routing, and UI affordances for manual approvals and secret display.

Changes:

  • Introduce Canton chain/constants, wallet utilities (create/restore/import), and Ed25519 signing via tweetnacl.
  • Add Canton WalletConnect RPC handling (auto-approve read-only + modal-driven approval for sensitive methods).
  • Fix initialization flow by persisting TON/TRON wallet objects into SettingsStore and integrate Canton into initialization/import/secret phrase screens.

Reviewed changes

Copilot reviewed 15 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
wallets/rn_cli_wallet/package.json Adds tweetnacl dependency for Ed25519 signing.
wallets/rn_cli_wallet/yarn.lock Locks tweetnacl dependency.
wallets/rn_cli_wallet/src/constants/Canton.ts Defines Canton chains, methods, events, and network icons.
wallets/rn_cli_wallet/src/lib/CantonLib.ts Implements Ed25519 keypair creation and message signing.
wallets/rn_cli_wallet/src/utils/CantonWalletUtil.ts Canton wallet create/restore and import via secret key.
wallets/rn_cli_wallet/src/utils/CantonRequestHandlerUtil.ts Implements Canton RPC method handlers (read-only + signing/mock execution).
wallets/rn_cli_wallet/src/utils/PresetsUtil.ts Registers Canton chains/icons into presets lookup.
wallets/rn_cli_wallet/src/store/SettingsStore.ts Adds cantonAddress state + setter; fixes TON/TRON wallet object storage.
wallets/rn_cli_wallet/src/store/ModalStore.ts Adds SessionSignCantonModal to modal type union.
wallets/rn_cli_wallet/src/modals/SessionSignCantonModal.tsx New approval modal for Canton sensitive requests.
wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx Advertises Canton namespace/chains/methods/events during session approval.
wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts Routes Canton requests (auto-approve vs manual modal).
wallets/rn_cli_wallet/src/hooks/useInitializeWalletKit.ts Initializes Canton wallet and stores Canton address; fixes TON/TRON wallet storage in SettingsStore.
wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx Adds Canton option for importing a wallet via secret key.
wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx Displays Canton secret key on the Secret Phrase screen.
wallets/rn_cli_wallet/src/components/Modal.tsx Wires SessionSignCantonModal into modal switch.
wallets/rn_cli_wallet/src/assets/chains/canton.png Adds Canton chain icon asset.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Fix React hooks rules violation: move useCallback before conditional
  return in SessionSignCantonModal and remove eslint-disable
- Add try/catch around Canton auto-approve path in event manager to
  prevent unhandled rejections and respond with JSON-RPC error
- Send JSON-RPC error response on approve failure in Canton modal so
  dapp doesn't hang
- Add cantonWallet to SettingsStore for reactive SecretPhrase screen
- Add __DEV__ warning when storing Canton secret key unencrypted
- Make getWallet() return type CantonLib | undefined for type safety
- Increment Android versionCode to 62

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The downloaded canton.png was actually a WebP image with a .png extension,
causing AAPT2 compilation failure on Android builds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ignaciosantise ignaciosantise merged commit 00f3cf1 into main Apr 10, 2026
8 checks passed
@ignaciosantise ignaciosantise deleted the feat/luxembourg-v1 branch April 10, 2026 13:54
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.

3 participants