Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 70 additions & 16 deletions BBMTLib/tss/nostrtransport/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"os"
"runtime/debug"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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 = "<unknown>"
}
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 {
Expand Down Expand Up @@ -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")
Expand All @@ -553,32 +580,50 @@ 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

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:
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 = "<unknown>"
}
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:
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
73 changes: 0 additions & 73 deletions RELEASE_v2.1.5.md

This file was deleted.

4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Binary file modified android/app/libs/tss.aar
Binary file not shown.
8 changes: 4 additions & 4 deletions ios/BoldWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)",
Expand All @@ -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;
Expand All @@ -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)",
Expand Down
Loading