diff --git a/BBMTLib/tss/nostrtransport/client.go b/BBMTLib/tss/nostrtransport/client.go index 5b4c0f7..046e929 100644 --- a/BBMTLib/tss/nostrtransport/client.go +++ b/BBMTLib/tss/nostrtransport/client.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "runtime/debug" "strings" "sync" "time" @@ -285,14 +286,18 @@ func (c *Client) Publish(ctx context.Context, event *Event) error { fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - signed event, PubKey (hex)=%s, tags=%v\n", event.PubKey, event.Tags) - // Use all valid relays, not just initially connected ones + // Resiliency: Use ALL valid relays (not just initially connected ones) // The pool will handle connections - if a relay isn't connected yet, it will try to connect // This ensures we publish to all relays, including those that connected in background + // This is critical for resiliency across multiple relays relaysToUse := c.validRelays if len(relaysToUse) == 0 { // Fallback to urls if validRelays not set (backward compatibility) relaysToUse = c.urls } + if len(relaysToUse) == 0 { + return errors.New("no relays configured for publishing") + } results := c.pool.PublishMany(ctx, relaysToUse, *event) totalRelays := len(relaysToUse) @@ -301,6 +306,18 @@ func (c *Client) Publish(ctx context.Context, event *Event) error { errorCh := make(chan error, 1) go func() { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in Client.Publish goroutine: %v", r) + fmt.Fprintf(os.Stderr, "BBMTLog: %s\n", errMsg) + fmt.Fprintf(os.Stderr, "BBMTLog: Stack trace: %s\n", string(debug.Stack())) + select { + case errorCh <- fmt.Errorf("internal error (panic): %v", r): + default: + } + } + }() + var successCount int var failureCount int var allErrors []error @@ -353,13 +370,20 @@ func (c *Client) Publish(ctx context.Context, event *Event) error { } return } + // Safely extract relay URL to avoid nil pointer dereference + var relayURL string + if res.Relay != nil { + relayURL = res.Relay.URL + } else { + relayURL = "" + } if res.Error != nil { failureCount++ allErrors = append(allErrors, res.Error) - fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - relay %s error: %v (%d/%d failed)\n", res.Relay, res.Error, failureCount, totalRelays) + fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - relay %s error: %v (%d/%d failed)\n", relayURL, res.Error, failureCount, totalRelays) } else { successCount++ - fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - relay %s success (%d/%d succeeded)\n", res.Relay, successCount, totalRelays) + fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - relay %s success (%d/%d succeeded)\n", relayURL, successCount, totalRelays) // Return immediately on first success (non-blocking) if successCount == 1 { select { @@ -532,6 +556,9 @@ func (c *Client) Subscribe(ctx context.Context, filter Filter) (<-chan *Event, e } // PublishWrap publishes a pre-signed gift wrap event (kind:1059) +// Resiliency policy: Publishes to ALL valid relays in parallel, returns immediately on first success, +// continues publishing to other relays in background. Only fails if ALL relays fail. +// This ensures co-signing messages are delivered even if some relays are down or slow. func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { if wrap == nil { return errors.New("nil wrap event") @@ -553,22 +580,40 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { wrap.CreatedAt = nostr.Now() } - // Use all valid relays, not just initially connected ones + // Resiliency: Use ALL valid relays (not just initially connected ones) // The pool will handle connections - if a relay isn't connected yet, it will try to connect // This ensures we publish to all relays, including those that connected in background + // This is critical for co-signing resiliency across multiple relays relaysToUse := c.validRelays if len(relaysToUse) == 0 { // Fallback to urls if validRelays not set (backward compatibility) relaysToUse = c.urls } + if len(relaysToUse) == 0 { + return errors.New("no relays configured for publishing") + } results := c.pool.PublishMany(ctx, relaysToUse, *wrap) totalRelays := len(relaysToUse) - // Track results in background goroutine - return immediately on first success + // Resiliency: Track results in background goroutine - return immediately on first success + // This allows co-signing to proceed quickly while other relays continue publishing in background + // Only fails if ALL relays fail, ensuring maximum resiliency successCh := make(chan bool, 1) errorCh := make(chan error, 1) go func() { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in Client.PublishWrap goroutine: %v", r) + fmt.Fprintf(os.Stderr, "BBMTLog: %s\n", errMsg) + fmt.Fprintf(os.Stderr, "BBMTLog: Stack trace: %s\n", string(debug.Stack())) + select { + case errorCh <- fmt.Errorf("internal error (panic): %v", r): + default: + } + } + }() + var successCount int var failureCount int var allErrors []error @@ -576,9 +621,9 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { for { select { case <-ctx.Done(): - // Context cancelled - check if we had any successes + // Context cancelled - check if we had any successes (resilient: partial success is still success) if successCount > 0 { - fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - context cancelled but %d/%d relays succeeded\n", successCount, totalRelays) + fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - context cancelled but %d/%d relays succeeded (resilient)\n", successCount, totalRelays) select { case successCh <- true: default: @@ -601,6 +646,7 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { if !ok { // All relays have responded if successCount > 0 { + // Resilient: At least one relay succeeded, operation is successful if failureCount > 0 { fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - %d/%d relays succeeded, %d failed (resilient)\n", successCount, totalRelays, failureCount) } else { @@ -612,7 +658,7 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { default: } } else { - // All relays failed + // All relays failed - this is the only failure case if len(allErrors) > 0 { fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - all %d relays failed\n", totalRelays) select { @@ -628,14 +674,22 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { } return } + // Safely extract relay URL to avoid nil pointer dereference + var relayURL string + if res.Relay != nil { + relayURL = res.Relay.URL + } else { + relayURL = "" + } if res.Error != nil { failureCount++ allErrors = append(allErrors, res.Error) - fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - relay %s error: %v (%d/%d failed)\n", res.Relay, res.Error, failureCount, totalRelays) + fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - relay %s error: %v (%d/%d failed)\n", relayURL, res.Error, failureCount, totalRelays) } else { successCount++ - fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - relay %s success (%d/%d succeeded)\n", res.Relay, successCount, totalRelays) - // Return immediately on first success (non-blocking) + fmt.Fprintf(os.Stderr, "BBMTLog: Client.PublishWrap - relay %s success (%d/%d succeeded)\n", relayURL, successCount, totalRelays) + // Resilient: Return immediately on first success (non-blocking) + // Other relays continue publishing in background for redundancy if successCount == 1 { select { case successCh <- true: @@ -648,17 +702,17 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error { } }() - // Wait for first success or all failures + // Wait for first success or all failures (resiliency: succeed if ANY relay succeeds) select { case <-successCh: - // At least one relay succeeded - return immediately - // Other relays continue publishing in background + // Resilient: At least one relay succeeded - return immediately + // Other relays continue publishing in background for redundancy return nil case err := <-errorCh: - // All relays failed + // Only fails if ALL relays failed return err case <-ctx.Done(): - // Context cancelled - check if we got any success + // Context cancelled - check if we got any success (resilient: partial success is still success) select { case <-successCh: return nil diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be6081..bc801d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog +## [2.1.6] - 2025-12-31 + +### Added +- **Balance Card in Send Modal**: New prominent balance card displayed above amount input in Send Bitcoin modal + - Shows available balance in BTC and fiat currency + - Integrated "Max" button for quick balance selection + - Clean, professional UI with card styling +- **Smart Balance Check for Send Button**: Automatic balance refresh when clicking Send with zero balance + - Prevents modal from opening prematurely when balance hasn't loaded + - Shows loading spinner on Send button during balance check + - Automatically opens modal if balance is found, or shows alert if truly zero + - Prevents multiple rapid clicks with button disable state + - 5-second timeout with graceful error handling + +### Fixed +- **Co-signing Go Panic Recovery**: Fixed potential panic crashes in Nostr transport layer during co-signing + - Added panic recovery with stack trace logging in `Client.Publish` goroutine + - Improved nil pointer safety when extracting relay URLs + - Better error messages for debugging relay connection issues + - Enhanced resiliency for co-signing message delivery across multiple relays +- **Send Button Balance Race Condition**: Fixed issue where Send button would show "Insufficient Balance" alert even when balance was still loading + - Eliminates flickering and need to click Send button multiple times + - Better UX with immediate feedback during balance check + +### Changed +- **Send Modal UI Enhancement**: Improved balance visibility and Max button placement + - Balance card replaces inline "Max" text link + - More prominent balance display with better visual hierarchy + - Updated QR scanner icon to use scan-icon.png for consistency + +### Technical Details +- **WalletHome.tsx**: Added `checkBalanceForSend()` function for dedicated balance fetching +- **SendBitcoinModal.tsx**: New balance card component with integrated Max button +- **client.go**: Enhanced panic recovery and error handling in Nostr publish operations +- **Error Handling**: Improved timeout and error recovery for balance checks + ## [Unreleased] ### Added diff --git a/RELEASE_v2.1.5.md b/RELEASE_v2.1.5.md deleted file mode 100644 index ec02cfa..0000000 --- a/RELEASE_v2.1.5.md +++ /dev/null @@ -1,73 +0,0 @@ -### πŸš€ BoldWallet v2.1.5 – Address Stability, Legacy Wallet Support & Network State Improvements - -**✨ What's Changed** - -### πŸ”’ Address Stability & State Management -* **Fixed Address Flickering**: Completely resolved address changing/flickering after lock/unlock by making UserContext the single source of truth for addresses -* **Network-Specific Address Derivation**: UserContext now properly derives separate btcPub values for both mainnet and testnet, eliminating race conditions -* **Consistent Address Display**: WalletHome now prioritizes userActiveAddress from UserContext over local state, ensuring you always see the correct address -* **Eliminated Race Conditions**: Improved address derivation flow prevents address mismatches during state updates and network switches - -### 🎯 Legacy Wallet Migration Support -* **Migration Advisory Modal**: New modal appears for users with legacy wallets, providing friendly guidance on migrating to new wallet setup -* **Better PSBT Compatibility**: Advises users that new wallet setups offer improved PSBT compatibility and interoperability with modern wallets -* **User Preference**: "Do not remind me again" checkbox allows users to dismiss the modal while keeping the option to see it again on new wallet imports -* **Smart Reset Logic**: Modal flag automatically resets on wallet import if the imported wallet is legacy, ensuring users are always informed - -### 🌐 Network State Management -* **Clean State on Import**: Network always resets to mainnet when importing a keyshare, ensuring proper address derivation and clean wallet state -* **Synchronized Contexts**: All contexts and providers properly synchronized with network changes for consistent state across the app -* **Proper Address Derivation**: Network reset on import ensures addresses are correctly derived for the imported wallet's network - -### 🧹 Cache Management -* **Comprehensive Cache Clearing**: Automatic cache clearing on wallet setup and import screens for fresh, clean state -* **Stale Data Prevention**: Removes stale btcPub from EncryptedStorage and clears WalletService cache when setting up or importing wallets -* **Setup Mode Detection**: Cache clearing only occurs during wallet setup (duo/trio modes), not during signing operations, preserving existing wallet state - -### 🎨 UI/UX Improvements -* **Transparent Balance Display**: Balance rows (BTC and USD) now have transparent background while maintaining tap-to-hide functionality for a cleaner look -* **Button Alignment**: Send and Receive buttons now vertically align with Device and Address Type buttons above for consistent, professional spacing -* **Improved QR Scanner**: Updated subtitle text to "Point camera to Sending Device QR" for clearer user guidance -* **Visual Consistency**: Better alignment and spacing throughout the wallet home screen for a more polished appearance - -### πŸ”§ Reliability & Stability -* **Enhanced UserContext**: Improved refresh() function to derive network-specific btcPub values correctly for both networks -* **Better State Synchronization**: WalletHome now uses UserContext as primary address source with local state as fallback -* **Cache Management**: Added useEffect hooks to clear all cache on wallet setup/import screens for fresh state -* **Network Reset Integration**: ShowcaseScreen now properly resets network to mainnet on keyshare import using setActiveNetwork() - -### Technical -* Enhanced **`UserContext.refresh()`** to derive separate btcPub values for mainnet and testnet -* Updated **WalletHome** to prioritize userActiveAddress from UserContext over local state -* Added **network reset** to mainnet on keyshare import in ShowcaseScreen -* Implemented **comprehensive cache clearing** on wallet setup/import screens -* Updated **balanceRowWithMargin** style to use transparent background -* Applied **flexOneMinWidthZero and partyGap** styles to action buttons for consistent alignment -* Created standalone **`LegacyWalletModal`** component for reusability - -**_πŸ’› No servers. No seed phrases. Just sovereign sats._** - -βΈ» - -πŸ”— **Learn more** at [boldbitcoinwallet.com](https://boldbitcoinwallet.com/) - -πŸ” **PGP Public Key** at [boldwallet-publickey.asc](https://github.com/BoldBitcoinWallet/.github/blob/main/PGP/boldwallet-publickey.asc) - -**πŸ“Ž SHA256: app-release.apk.sha256** - -`[SHA256_HASH_WILL_BE_ADDED_AFTER_BUILD]` - -**πŸ”‘ SHA256-PGP-Signature: app-release.apk.sha256.asc** - -```text -[PGP_SIGNATURE_WILL_BE_ADDED_AFTER_SIGNING] -``` - -βΈ» - -⚠️ APK Signature - -This APK is signed with the official BoldWallet keystore. - -Do not mix it with the F-Droid build. Stick to one source for updates. - diff --git a/android/app/build.gradle b/android/app/build.gradle index a9486b0..c426f88 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId "com.boldwallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 38 - versionName "2.1.5" + versionCode 39 + versionName "2.1.6" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-arch', 'oldarch' diff --git a/android/app/libs/tss.aar b/android/app/libs/tss.aar index 136c95b..9785983 100644 Binary files a/android/app/libs/tss.aar and b/android/app/libs/tss.aar differ diff --git a/ios/BoldWallet.xcodeproj/project.pbxproj b/ios/BoldWallet.xcodeproj/project.pbxproj index d7fcda3..6ccac66 100644 --- a/ios/BoldWallet.xcodeproj/project.pbxproj +++ b/ios/BoldWallet.xcodeproj/project.pbxproj @@ -518,7 +518,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_BITCODE = NO; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.4; + MARKETING_VERSION = 2.1.6; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", @@ -556,7 +556,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 38; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_TESTABILITY = NO; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.4; + MARKETING_VERSION = 2.1.6; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/ios/Tss.xcframework/Info.plist b/ios/Tss.xcframework/Info.plist index 6ff0575..c81fd9d 100644 --- a/ios/Tss.xcframework/Info.plist +++ b/ios/Tss.xcframework/Info.plist @@ -8,24 +8,21 @@ BinaryPath Tss.framework/Tss LibraryIdentifier - ios-arm64_x86_64-simulator + ios-arm64 LibraryPath Tss.framework SupportedArchitectures arm64 - x86_64 SupportedPlatform ios - SupportedPlatformVariant - simulator BinaryPath - Tss.framework/Versions/A/Tss + Tss.framework/Tss LibraryIdentifier - macos-arm64_x86_64 + ios-arm64_x86_64-simulator LibraryPath Tss.framework SupportedArchitectures @@ -34,21 +31,24 @@ x86_64 SupportedPlatform - macos + ios + SupportedPlatformVariant + simulator BinaryPath - Tss.framework/Tss + Tss.framework/Versions/A/Tss LibraryIdentifier - ios-arm64 + macos-arm64_x86_64 LibraryPath Tss.framework SupportedArchitectures arm64 + x86_64 SupportedPlatform - ios + macos CFBundlePackageType diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist index 70b7423..4d2ed59 100644 --- a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist +++ b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist @@ -9,9 +9,9 @@ MinimumOSVersion 100.0 CFBundleShortVersionString - 0.0.1767079533 + 0.0.1767180858 CFBundleVersion - 0.0.1767079533 + 0.0.1767180858 CFBundlePackageType FMWK diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss index b6460f3..3f3e14a 100644 Binary files a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss and b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss differ diff --git a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist index 70b7423..4d2ed59 100644 --- a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist +++ b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist @@ -9,9 +9,9 @@ MinimumOSVersion 100.0 CFBundleShortVersionString - 0.0.1767079533 + 0.0.1767180858 CFBundleVersion - 0.0.1767079533 + 0.0.1767180858 CFBundlePackageType FMWK diff --git a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss index b12aec7..ac7259f 100644 Binary files a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss and b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss differ diff --git a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist index 70b7423..4d2ed59 100644 --- a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist +++ b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist @@ -9,9 +9,9 @@ MinimumOSVersion 100.0 CFBundleShortVersionString - 0.0.1767079533 + 0.0.1767180858 CFBundleVersion - 0.0.1767079533 + 0.0.1767180858 CFBundlePackageType FMWK diff --git a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss index 1cfbebc..aeea091 100644 Binary files a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss and b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss differ diff --git a/package.json b/package.json index 131ca2a..61c153b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boldwallet", - "version": "2.1.5", + "version": "2.1.6", "private": true, "scripts": { "android": "react-native run-android", diff --git a/screens/SendBitcoinModal.tsx b/screens/SendBitcoinModal.tsx index 550df83..2125004 100644 --- a/screens/SendBitcoinModal.tsx +++ b/screens/SendBitcoinModal.tsx @@ -204,6 +204,53 @@ const SendBitcoinModal: React.FC = ({ marginBottom: 10, textDecorationLine: 'underline', }, + balanceCard: { + backgroundColor: '#f8f9fa', + borderRadius: 10, + padding: 14, + marginBottom: 16, + borderWidth: 1, + borderColor: theme.colors.secondary || '#e0e0e0', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + balanceCardLeft: { + flex: 1, + }, + balanceCardLabel: { + fontSize: 12, + fontWeight: '600', + color: '#7f8c8d', + marginBottom: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + balanceCardBtc: { + fontSize: 18, + fontWeight: 'bold', + color: theme.colors.text, + marginBottom: 2, + }, + balanceCardFiat: { + fontSize: 13, + color: '#7f8c8d', + fontWeight: '500', + }, + balanceCardMaxButton: { + backgroundColor: theme.colors.accent, + paddingVertical: 8, + paddingHorizontal: 16, + borderRadius: 8, + minWidth: 70, + alignItems: 'center', + justifyContent: 'center', + }, + balanceCardMaxButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: 'bold', + }, inputContainer: { marginBottom: 0, }, @@ -717,22 +764,35 @@ const SendBitcoinModal: React.FC = ({ }} style={styles.qrIconContainer}> - - - Amount in BTC (β‚Ώ) - - Max - + {/* Balance Card */} + + + Available Balance + + {walletBalance.toFixed(8)} BTC + + + ~{selectedCurrency}{' '} + {formatUSD(walletBalance.times(btcToFiatRate).toNumber())} + + + Max + + + + + Amount in BTC (β‚Ώ) = ({navigation}) => { balance: 0, }); const [isRefreshing, setIsRefreshing] = useState(false); + const [isCheckingBalanceForSend, setIsCheckingBalanceForSend] = useState(false); const [isCurrencySelectorVisible, setIsCurrencySelectorVisible] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(''); @@ -408,6 +410,67 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { fetchDataRef.current = fetchData; }, [fetchData]); + // Function to check balance specifically for send button + const checkBalanceForSend = useCallback(async (): Promise => { + try { + dbg('checkBalanceForSend: Starting balance check...'); + + const addr = userActiveAddress || address || (await LocalCache.getItem('currentAddress')); + const baseApi = apiBase || (await LocalCache.getItem('api')); + + if (!addr || !baseApi) { + dbg('checkBalanceForSend: Missing wallet address or baseApi'); + return 0; + } + + // Set up API URL + const cleanBaseApi = baseApi.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + const apiUrl = `${cleanBaseApi}/api`; + + // Ensure native module has correct settings + await BBMTLibNativeModule.setAPI(network, apiUrl); + + // Set up timeout (5 seconds) + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Balance check timed out')); + }, 5000); + }); + + // Fetch balance only (force fresh fetch) + const balancePromise = WalletService.getInstance().getWalletBalance( + addr, + btcRate, + _pendingSent, + true, // force fresh fetch + ); + + const balanceResult = await Promise.race([ + balancePromise, + timeoutPromise, + ]); + + if (balanceResult && typeof balanceResult === 'object' && 'btc' in balanceResult) { + const newBalance = parseFloat((balanceResult as any).btc || '0'); + dbg('checkBalanceForSend: Balance fetched:', newBalance); + + // Update balance state + setBalanceBTC((balanceResult as any).btc || '0.00000000'); + if (btcRate > 0) { + const fiatBalance = Number((balanceResult as any).btc) * btcRate; + setBalanceFiat(fiatBalance.toFixed(2)); + } + + return newBalance; + } + + return 0; + } catch (error: any) { + dbg('checkBalanceForSend: Error checking balance:', error); + return 0; + } + }, [userActiveAddress, address, apiBase, network, btcRate, _pendingSent]); + // Function to update address type modal with new network addresses const updateAddressTypeModal = useCallback( async (newNetwork: string) => { @@ -1899,26 +1962,56 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { { + style={[ + styles.actionButton, + styles.sendButton, + styles.flexOneMinWidthZero, + styles.partyGap, + isCheckingBalanceForSend && {opacity: 0.6}, + ]} + onPress={async () => { HapticFeedback.medium(); // Check if balance is 0 or empty const balance = parseFloat(balanceBTC || '0'); if (balance <= 0) { - Alert.alert( - 'Insufficient Balance', - "You don't have any satoshis to send.", - ); + // Balance might not be loaded yet, check it + setIsCheckingBalanceForSend(true); + try { + const newBalance = await checkBalanceForSend(); + if (newBalance > 0) { + // Balance found, open modal + setIsSendModalVisible(true); + } else { + // Still zero, show alert + Alert.alert( + 'Insufficient Balance', + "You don't have any satoshis to send.", + ); + } + } catch (error) { + dbg('Error checking balance for send:', error); + // On error, just re-enable button and let user retry + } finally { + setIsCheckingBalanceForSend(false); + } return; } setIsSendModalVisible(true); - }}> - - Send + }} + disabled={isCheckingBalanceForSend} + activeOpacity={0.7}> + {isCheckingBalanceForSend ? ( + + ) : ( + <> + + Send + + )} {/* Scan QR button replaces lock button in action row */}