Previously, after keygen completed, the Session page would just update the
status to 'completed' but not navigate away. Users had to manually click
the "Return Home" button. This could result in a white screen if the button
wasn't visible or clickable.
Now the page auto-navigates to home after 2 seconds, giving users time to
see the completion status and public key before redirecting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a party re-subscribes (e.g., Android reconnects), the old gRPC stream's
defer Unsubscribe() was accidentally removing the NEW subscription from the
subscribers map, causing the party to miss session_started events.
Fix:
- Subscribe() now returns the channel to the caller
- Unsubscribe() now takes the channel and only removes if it matches
- This prevents older streams from removing newer subscriptions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add AppSettingEntity and AppSettingDao to Database.kt for key-value storage
- Add database migration (version 1 → 2) to create app_settings table
- Modify TssRepository.registerParty() to load/create partyId from database
- PartyId is now persisted across app restarts, matching Electron's getOrCreatePartyId()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add warning log when parties miss session event broadcast (message-router)
- Add logging for subscribeSessionEvents to detect null asyncStub
- Add sessionStatusPollingJob field for future fallback polling mechanism
This helps diagnose why Android parties are not receiving session_started events.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problem:
- Android initiator/joiner could miss session_started events due to race condition
- Events arriving between joinSession() and _currentSession.value assignment were ignored
- This caused keygen timeout because parties never started the TSS protocol
Solution:
- Add pendingSessionId field set BEFORE joinSession() call
- Modify startSessionEventSubscription() to match events against both activeSession and pendingSessionId
- Clear pendingSessionId on session completion, failure, or cancellation
This ensures session_started events are correctly processed even if they arrive
before _currentSession is fully initialized.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add getPartyId() method to TssRepository
- Call getSessionStatus after createKeygenSession to fetch all participants
including server-party-co-managed that have already auto-joined
- This matches Electron's behavior of calling getSessionStatus on session page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend changes (session-coordinator):
- Add PublishParticipantJoined method to JoinSessionMessageRouterClient interface
- Implement PublishParticipantJoined in MessageRouterClient to broadcast events
- Call PublishParticipantJoined in join_session.go after participant joins
- Add detailed logging for debugging event broadcast
Android changes (service-party-android):
- Add detailed logging in TssRepository for session event handling
- Add detailed logging in MainViewModel for participant_joined processing
- Log activeSession state, event matching, and participant updates
This enables the initiator's waiting screen to receive real-time updates
when participants join the session, matching the expected behavior.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Created PortraitCaptureActivity that extends CaptureActivity
- Registered it in AndroidManifest.xml with screenOrientation="portrait"
- Updated JoinKeygenScreen and CoSignJoinScreen to use the portrait activity
- Also simplified keygen join logic to match Electron exactly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Removed polling fallback and simplified to match Electron's design:
- If joinSession returns sessionStatus="in_progress", trigger keygen immediately
- Otherwise wait for session_started gRPC event
Added debug log to show sessionStatus value for troubleshooting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed from single 2-second delay to 5 attempts at 500ms intervals.
This provides faster detection while covering a longer window (2.5 seconds total).
The polling loop:
- Checks every 500ms for up to 5 times
- Stops immediately if keygen is already triggered
- Stops if session context changes (user cancelled/navigated away)
This handles the case where the last joiner triggers session_started
but cannot receive the event themselves.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When multiple Android devices join a keygen session nearly simultaneously,
the last joiner may miss the session_started gRPC event because it's sent
before the device has fully set up its event subscription.
This fix adds a 2-second delayed polling check after join to detect if
the session has already started. If the session is in_progress and we
haven't started keygen yet, trigger it via polling instead of relying
solely on the session_started event.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
- Change address derivation from deriveKavaAddress to deriveEvmAddress
in TssRepository.kt (3 locations)
- Add AddressUtils.isEvmAddress() and getEvmAddress() helper methods
to handle both old Cosmos and new EVM address formats
- Fix balance query for old wallets by deriving EVM address from
public key when needed (MainViewModel.fetchBalanceForShare)
- Add retry logic for optimistic lock conflicts in join_session.go
to prevent party_index collision during concurrent joins
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major improvements to Android gRPC client:
- Add automatic reconnection with exponential backoff (1s to 30s)
- Add heartbeat mechanism with failure detection (30s interval, 3 failures trigger reconnect)
- Add stream version tracking to filter stale callbacks
- Add channel state monitoring (every 5s)
- Add per-call deadline instead of one-time deadline for stubs
- Add SharedFlow for connection events (Connected, Disconnected, Reconnecting, Reconnected, PendingMessages)
- Add callback exception handling for robustness
- Add stream recovery after reconnection via callback mechanism
TssRepository changes:
- Save message routing params for recovery after reconnect
- Expose grpcConnectionEvents SharedFlow for UI notifications
- Auto-restore event subscriptions after reconnection
Other changes:
- Add QR code to Electron Create page for mobile scanning
- Auto version increment from version.properties
- SettingsScreen shows BuildConfig version info
- CreateWalletScreen tracks hasEnteredSession state
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The server validates device_type and only accepts specific values.
Electron doesn't send device_info at all, which passes validation.
Match that behavior for consistency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The app was crashing with FRAME_SIZE_ERROR because the gRPC client
was using plaintext mode when connecting to port 443 (TLS endpoint).
This caused the client to receive encrypted data that it couldn't parse.
Fix: Use useTransportSecurity() for port 443, usePlaintext() for other ports.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Theme changes:
- Replace green theme with dark gray & gold color scheme
- Primary color: Gold (#D4AF37)
- Background: Dark gray (#1A1A1A)
- Surface: Medium gray (#2D2D2D)
- Disable dynamic colors to enforce custom theme
- Default to dark theme for best visual impact
- Update success indicators from green to gold across screens
JoinKeygen flow fixes (100% Electron compatible):
- Add onResetState callback for proper state reset
- Cancel in confirm/joining/progress resets to input state (stays on page)
- Two-step flow: joinKeygenSessionViaGrpc + executeKeygenAsJoiner
- Wait for session_started event before executing keygen
CoSign flow fixes (100% Electron compatible):
- Add onResetState callback and QR scanner support
- Add three-button layout (Cancel, Back, Join) in select_share step
- Two-step flow: joinSignSessionViaGrpc + executeSignAsJoiner
- If session already in_progress, trigger sign immediately (Solution B)
- Wait for session_started event otherwise
Repository changes:
- Add joinKeygenSessionViaGrpc and executeKeygenAsJoiner methods
- Add joinSignSessionViaGrpc and executeSignAsJoiner methods
- Add JoinKeygenViaGrpcResult and JoinSignViaGrpcResult data classes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Matching Electron app functionality:
1. Copy address button:
- Shows "✓ 已复制" feedback after copying
- Auto-resets after 2 seconds
2. Explorer link button (new):
- Opens address in Kava block explorer
- Uses correct URL based on network type:
- Mainnet: kavascan.com
- Testnet: testnet.kavascan.com
Changes:
- WalletsScreen: Added networkType parameter
- WalletDetailDialog: Added copy feedback state and explorer button
- MainActivity: Pass networkType to WalletsScreen
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. Copy address button:
- Changed from alert() to visual feedback (shows "✓ 已复制")
- Feedback auto-hides after 2 seconds
2. Explorer link button:
- Was hardcoded to testnet (true)
- Now uses getCurrentNetwork() to determine correct explorer URL
- Links to kavascan.com for mainnet, testnet.kavascan.com for testnet
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, the network badge (testnet/mainnet) in Layout sidebar only
loaded once on mount and didn't update when user changed network in
Settings page.
Changes:
- Layout.tsx: Read network from localStorage first (consistent with
transaction.ts), then fallback to Electron API
- Layout.tsx: Listen for 'storage' event (cross-tab) and custom
'kava-network-change' event (same-tab) to update display
- Settings.tsx: Dispatch custom event when switching networks so
Layout can update immediately
Android app doesn't have this issue - it uses StateFlow which
automatically triggers re-renders when settings change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Both Electron and Android apps now calculate the maximum transferable
amount by subtracting estimated gas fees from the balance:
Electron (Home.tsx):
- Added calculateMaxAmount() async function that fetches gas price
- Uses 21000 gas limit for simple transfers
- Shows loading state while calculating
Android (TransferScreen.kt):
- Added calculateMaxTransferAmount() in TransactionUtils
- Uses coroutine to fetch gas price asynchronously
- Shows "..." while calculating, falls back to balance on error
Both implementations:
- Add 10% buffer to gas price for safety
- Round down to 6 decimal places
- Show error if balance insufficient for gas
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The network toggle in Settings was saving to database via electron API
but getCurrentNetwork() in transaction.ts reads from localStorage.
This caused the balance display to use wrong RPC endpoint after switching.
Now syncs to localStorage when switching networks to ensure consistency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously Home.tsx hardcoded testnet RPC for balance queries.
Now uses getCurrentRpcUrl() to respect user's network setting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Key changes:
- Add `go get -d golang.org/x/mobile/cmd/gomobile` step (official recommended)
- This adds golang.org/x/mobile dependency to go.mod, fixing "unable to import bind" error
- Remove complex Go 1.22 version detection logic (no longer needed)
- Simplify gomobile installation flow
- Update tsslib/go.mod with proper golang.org/x/mobile dependency
The fix follows the official Go Mobile documentation:
https://go.dev/wiki/Mobile🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This ensures compatibility with modern NDK versions that don't
support older Android API levels. API 21 (Android 5.0) is the
minimum supported by current NDK versions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tsslib source code is located in service-party-android/tsslib/,
not in libs/tsslib/. Updated the path and output location accordingly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changes:
- Get GOPATH using 'go env GOPATH' command
- Add GOPATH/bin to PATH if not already present
- Check for gomobile.exe directly in GOBIN directory
- Use full path to gomobile.exe for init and bind commands
- Add verification that gomobile was installed correctly
This fixes the issue where gomobile is installed but not found
because GOPATH/bin is not in the system PATH.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When tsslib.aar is not found, the build script now automatically:
1. Checks if Go is installed
2. Installs gomobile if not present (go install golang.org/x/mobile/cmd/gomobile@latest)
3. Initializes gomobile if needed
4. Runs go mod tidy in the tsslib directory
5. Builds tsslib.aar using gomobile bind
This allows building APKs on any machine with Go installed, without
needing to manually compile the TSS library first.
Requirements:
- Go installed and in PATH
- Android NDK (installed via Android SDK)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When ANDROID_HOME environment variable contains quotes (e.g., set with
quotes in system settings), the generated local.properties file would
have an invalid path like 'sdk.dir=C:/Android"'.
This fix strips any surrounding quotes from ANDROID_HOME before using
it to create local.properties, ensuring valid SDK path format.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When local.properties is missing, the build script now automatically:
- Checks ANDROID_HOME environment variable first
- Scans common Windows SDK locations:
- %LOCALAPPDATA%\Android\Sdk
- %USERPROFILE%\AppData\Local\Android\Sdk
- C:\Android\Sdk
- C:\Android
- Creates local.properties with the detected SDK path
- Displays helpful error message if SDK is not found
This allows the build script to work on machines without manual
configuration, making it easier to build APKs on different systems.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add Windows batch script for building Android APKs:
- build-apk.bat debug - Build debug APK only
- build-apk.bat release - Build release APK only
- build-apk.bat - Build both debug and release APKs
- build-apk.bat clean - Clean build files
- build-apk.bat help - Show usage help
Output locations:
- Debug: app/build/outputs/apk/debug/app-debug.apk
- Release: app/build/outputs/apk/release/app-release.apk
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Major changes:
- Add complete Android app (service-party-android) with Jetpack Compose UI
- Implement real account-service API calls for keygen and sign sessions:
- POST /api/v1/co-managed/sessions (create keygen session)
- GET /api/v1/co-managed/sessions/by-invite-code/{code} (validate invite)
- POST /api/v1/co-managed/sessions/{id}/join (join keygen session)
- POST /api/v1/co-managed/sign (create sign session)
- GET /api/v1/co-managed/sign/by-invite-code/{code} (validate sign invite)
- POST /api/v1/co-managed/sign/{id}/join (join sign session)
- Add QR code generation and scanning for session invites
- Remove password requirement (use empty string)
- Add floating action button for wallet creation
- Add network type aware explorer links (mainnet/testnet)
Network configuration:
- Change default network to Kava mainnet for both Electron and Android apps
- Electron: main.ts, transaction.ts, Settings.tsx, Layout.tsx
- Android: Models.kt (NetworkType.MAINNET default)
Features:
- Full TSS keygen and sign protocol via gomobile bindings
- gRPC message routing for multi-party communication
- Cross-platform compatibility with service-party-app (Electron)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Additional safeguards to prevent "CANCELLED: Cancelled on client" error:
1. Add `this.connected` check at the start of subscribeMessages()
2. Set messageStream to null after canceling old stream
3. Wrap new stream creation in try-catch to handle creation errors
4. Add logging for ignored cancel errors
These changes ensure that:
- subscribeMessages won't proceed if connection is lost
- Old stream is fully cleaned up before creating new one
- Errors during stream creation are properly caught and logged
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: After keygen/sign completion, the gRPC message stream was not
unsubscribed. On the second operation, prepareForSign/prepareForKeygen
would try to cancel the stale stream, causing "CANCELLED: Cancelled on client".
Changes in tss-handler.ts:
- Add grpcClient.unsubscribeMessages() in all cleanup paths:
- participateKeygen close handler
- participateKeygen error handler
- participateSign close handler
- participateSign error handler
- cancel() method
- Reset sessionId and partyId in all cleanup paths
Changes in main.ts:
- Add reconnection logic in app 'activate' event for macOS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The app was crashing with "CANCELLED: Cancelled on client" error when
opening the app a second time. This happened because:
1. When window was reopened, old gRPC streams were in cancelled state
2. prepareForSign/prepareForKeygen tried to subscribe on cancelled streams
3. The error was unhandled and crashed the app
Changes:
- Add isConnected() check in prepareForSign() and prepareForKeygen()
- Throw meaningful error when gRPC client is not connected
- Wrap all prepareFor* calls in try-catch in main.ts
- Return user-friendly error message instead of crashing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed getGasPrice() to use eth_gasPrice RPC method instead of calculating
from baseFeePerGas (which is for EIP-1559 transactions)
- Added 10% buffer to gas price to ensure transaction gets included
- Updated Home.tsx to use gasPrice instead of maxFeePerGas for display
KAVA doesn't support EIP-1559, so we must use Legacy (Type 0) transactions
with gasPrice from eth_gasPrice RPC.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
KAVA EVM does not support EIP-1559 dynamic fee transactions.
Changed from EIP-1559 (Type 2) to Legacy (Type 0) format:
- prepareTransaction: Use [nonce, gasPrice, gasLimit, to, value, data, chainId, 0, 0]
- finalizeTransaction: Use EIP-155 v calculation (chainId * 2 + 35 + recoveryId)
- Remove type prefix (0x02) as Legacy transactions don't need it
- Update Home.tsx and CoSignSession.tsx to use gasPrice instead of maxFeePerGas
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The signature was 64 bytes (r + s) but EVM transactions need 65 bytes (r + s + v).
Now the recovery ID is appended to the signature so the frontend can correctly
parse and broadcast the transaction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
TSS-lib convention: threshold=t means (t+1) signers required.
User expectation: "2-of-3" means 2 signers needed.
Before this fix:
- Keygen used thresholdT directly (e.g., 2)
- TSS-lib interpreted as needing 3 signers (2+1)
- 2-of-3 wallet was actually 3-of-3!
After this fix:
- Both keygen and signing use (thresholdT-1)
- For 2-of-3: tss-lib threshold=1, needs 1+1=2 signers ✓
Files changed:
- tss-party/main.go: keygen and signing both use thresholdT-1
- tss-wasm/main.go: keygen and signing both use thresholdT-1
- pkg/tss/keygen.go: uses config.Threshold-1
- pkg/tss/signing.go: uses config.Threshold-1
BREAKING CHANGE: Existing wallets created before this fix used wrong
threshold and need to be regenerated. New wallets will work correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The signing code was using thresholdT-1 while keygen was using thresholdT,
causing Lagrange coefficient mismatch and "U doesn't equal T" error in round 9.
Root cause: commit d0c504dc added -1 to signing threshold to "match user expectation",
but this broke the keygen/sign consistency that TSS-lib requires.
Changes:
- tss-party/main.go: Sign now uses thresholdT (same as keygen)
- pkg/tss/signing.go: Add logging, emphasize threshold must match keygen
- tss-wasm/main.go: Add comment about threshold consistency
NOTE: This fix maintains backward compatibility with existing wallets.
No wallet regeneration is needed.
ROLLBACK: If this causes issues, revert to commit before this one.
Previous signing threshold was thresholdT-1 (commit d0c504dc).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When signing with fewer parties than keygen (e.g., 2-of-3 signing with only 2 parties),
the TSS-lib requires filtered save data containing only the participating parties.
Without this fix, signing fails with "U doesn't equal T" error because:
- Keygen creates save data for all N parties (e.g., 3 parties with indices 0, 1, 2)
- Sign uses only T parties (e.g., 2 parties with indices 1, 2)
- TSS-lib internal index validation fails due to mismatch
Changes:
- pkg/tss/signing.go: Use len(sortedPartyIDs) for partyCount and call BuildLocalSaveDataSubset
- tss-party/main.go: Add BuildLocalSaveDataSubset call for Electron app
- tss-wasm/main.go: Add BuildLocalSaveDataSubset call for WASM builds
This fix is backward compatible - when all parties participate, the subset equals the original data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
User says "3-of-5" meaning 3 signers needed.
tss-lib threshold t means t+1 signers required.
Now we store t-1 at session creation (like persistent-only does).
Changes:
- co_managed_handler.go: tssThresholdT = req.ThresholdT - 1
- tss-party/main.go: remove -1 from sign (now consistent with keygen)
BREAKING: Existing co-managed wallets must be regenerated.
ROLLBACK: Revert this commit if signing still fails.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The tss.NewParameters() expects the party count to match the number of
parties in peerCtx. For signing, this should be len(sortedPartyIDs)
(actual signing participants), not thresholdN (original keygen parties).
This fixes the "U doesn't equal T" error in round 9 when doing 3-of-5
co-managed signing with parties at indices 2,3,4.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When switching message/event streams, the old stream's 'end' or 'error'
events could fire after the new stream was created. Since activeMessageSubscription
was already updated to the new session, the old stream's events would
incorrectly trigger reconnection, causing TSS message routing to fail.
Fix:
- Remove event listeners from old stream before canceling
- Use closure to capture current stream reference
- Check if event is from current active stream before triggering reconnect
This fixes the "Not connected" error during co-sign TSS message routing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Keygen/co-keygen: must have exactly N participants joined
- Sign (co-sign/persistent): only check all registered participants joined
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
User says 3-of-5 meaning 3 signers needed, but tss-lib threshold t means t+1 signers.
Pass thresholdT-1 so tss-lib needs (t-1)+1 = t signers, matching user expectation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reverts e81757ad - the threshold conversion was wrong.
Keygen works with original thresholdT/thresholdN parameters.
The signing issue needs a different fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>