diff --git a/App.tsx b/App.tsx
index 2df1b0c..ccdc159 100644
--- a/App.tsx
+++ b/App.tsx
@@ -23,6 +23,8 @@ import {
NativeEventEmitter,
Platform,
DeviceEventEmitter,
+ View,
+ StyleSheet,
} from 'react-native';
import WalletSettings from './screens/WalletSettings';
import {NativeModules} from 'react-native';
@@ -313,58 +315,67 @@ const App = () => {
-
-
- null,
- }}
- />
- null,
- }}
- />
-
-
-
-
-
-
+
+
+
+ null,
+ contentStyle: {backgroundColor: '#ffffff'},
+ }}
+ />
+ null,
+ contentStyle: {backgroundColor: '#ffffff'},
+ }}
+ />
+
+
+
+
+
+
+
@@ -373,4 +384,11 @@ const App = () => {
);
};
+const styles = StyleSheet.create({
+ navigationContainer: {
+ flex: 1,
+ backgroundColor: '#ffffff',
+ },
+});
+
export default App;
diff --git a/BBMTLib/tss/mpc_nostr.go b/BBMTLib/tss/mpc_nostr.go
index a4d9e0c..07b126a 100644
--- a/BBMTLib/tss/mpc_nostr.go
+++ b/BBMTLib/tss/mpc_nostr.go
@@ -436,11 +436,9 @@ func runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionF
Logf("runNostrPreAgreementSendBTC: sending message: %s", localMessage)
// Context for the pre-agreement phase
- // Timeout: 2 minutes (120 seconds) to allow for:
- // - Network delays
- // - Retroactive message processing (messages sent before we started listening)
- // - Relay synchronization delays
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ // Timeout: 16 seconds - fail fast if peer doesn't respond quickly
+ // With resilient relays, messages should arrive quickly if peer is online
+ ctx, cancel := context.WithTimeout(context.Background(), 16*time.Second)
defer cancel()
// Channel to receive peer's message
@@ -483,8 +481,10 @@ func runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionF
}
}()
- // Small delay to ensure subscription is active before sending
- time.Sleep(1 * time.Second)
+ // Wait for subscription to be ready before sending
+ // Give the MessagePump time to establish subscriptions to all relays
+ // The subscription needs to be active to receive the peer's response
+ time.Sleep(2 * time.Second)
// Send our message to peer
err = messenger.SendMessage(ctx, localNpub, peerNpub, localMessage)
diff --git a/BBMTLib/tss/nostrtransport/client.go b/BBMTLib/tss/nostrtransport/client.go
index a685850..5b4c0f7 100644
--- a/BBMTLib/tss/nostrtransport/client.go
+++ b/BBMTLib/tss/nostrtransport/client.go
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"strings"
+ "sync"
"time"
nostr "github.com/nbd-wtf/go-nostr"
@@ -284,8 +285,16 @@ 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)
- results := c.pool.PublishMany(ctx, c.urls, *event)
- totalRelays := len(c.urls)
+ // 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
+ relaysToUse := c.validRelays
+ if len(relaysToUse) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToUse = c.urls
+ }
+ results := c.pool.PublishMany(ctx, relaysToUse, *event)
+ totalRelays := len(relaysToUse)
// Track results in background goroutine - return immediately on first success
successCh := make(chan bool, 1)
@@ -385,42 +394,64 @@ func (c *Client) Publish(ctx context.Context, event *Event) error {
}
func (c *Client) Subscribe(ctx context.Context, filter Filter) (<-chan *Event, error) {
- if len(c.urls) == 0 {
- return nil, errors.New("no relays configured")
- }
- events := make(chan *Event)
-
// 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 subscribe to all relays, including those that connected in background
relaysToUse := c.validRelays
if len(relaysToUse) == 0 {
// Fallback to urls if validRelays not set (backward compatibility)
relaysToUse = c.urls
}
- relayCh := c.pool.SubscribeMany(ctx, relaysToUse, filter)
+ if len(relaysToUse) == 0 {
+ return nil, errors.New("no relays configured")
+ }
+
+ events := make(chan *Event, 100) // Buffered to prevent blocking
+ totalRelays := len(relaysToUse)
// Track relay connection status
connectedRelays := make(map[string]bool)
- totalRelays := len(relaysToUse)
- var connectionCheckDone bool
+ connectedRelaysMu := sync.Mutex{}
+ firstConnectionCh := make(chan bool, 1) // Signal when first connection succeeds
+ connectionCheckDone := false
+
+ // Ensure at least one relay is connected before subscribing (in parallel, non-blocking)
+ // This ensures we have working connections before attempting subscription
+ // Connect to all relays in parallel, wait for at least one success
+ connectDone := make(chan string, totalRelays) // Relay URL when connected
+ connectTimeout := time.NewTimer(3 * time.Second)
+ defer connectTimeout.Stop()
+
+ // Try connecting to all relays in parallel
+ for _, url := range relaysToUse {
+ go func(relayURL string) {
+ relay, err := c.pool.EnsureRelay(relayURL)
+ if err == nil && relay != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected for subscription\n", relayURL)
+ select {
+ case connectDone <- relayURL:
+ default:
+ }
+ }
+ }(url)
+ }
- // Start a goroutine to monitor connection status
- connectionCtx, connectionCancel := context.WithTimeout(ctx, 5*time.Second)
- defer connectionCancel()
+ // Wait for at least one connection or timeout
+ connectedRelay := ""
+ select {
+ case connectedRelay = <-connectDone:
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - at least one relay connected (%s), proceeding with subscription\n", connectedRelay)
+ // Continue connecting others in background (non-blocking)
+ case <-connectTimeout.C:
+ // Timeout - proceed anyway, pool will handle connections
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - connection timeout, proceeding (pool will handle connections)\n")
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
- go func() {
- <-connectionCtx.Done()
- if !connectionCheckDone {
- connectionCheckDone = true
- if len(connectedRelays) == 0 {
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - WARNING: No relays connected after 5 seconds (all %d relays may have failed)\n", totalRelays)
- } else if len(connectedRelays) < totalRelays {
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - %d/%d relays connected\n", len(connectedRelays), totalRelays)
- }
- }
- }()
+ // Start subscription to all relays in parallel (non-blocking)
+ // SubscribeMany connects to all relays in parallel and merges their events
+ relayCh := c.pool.SubscribeMany(ctx, relaysToUse, filter)
+ // Process events and track connections in background
go func() {
defer close(events)
for {
@@ -430,47 +461,74 @@ func (c *Client) Subscribe(ctx context.Context, filter Filter) (<-chan *Event, e
case relayEvent, ok := <-relayCh:
if !ok {
// Channel closed - check if we ever got any connections
+ connectedRelaysMu.Lock()
connectionCheckDone = true
if len(connectedRelays) == 0 {
fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - ERROR: All %d relays failed to connect or disconnected\n", totalRelays)
} else {
fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription closed (%d/%d relays were connected)\n", len(connectedRelays), totalRelays)
}
+ connectedRelaysMu.Unlock()
return
}
+
// Get relay URL for tracking
var relayURL string
if relayEvent.Relay != nil {
relayURL = relayEvent.Relay.URL
}
- if relayEvent.Event == nil {
- // Track relay connection (even if no event yet, the relay is responding)
- if relayURL != "" {
- if !connectedRelays[relayURL] {
- connectedRelays[relayURL] = true
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected (%d/%d)\n", relayURL, len(connectedRelays), totalRelays)
- }
- }
- continue
- }
- // Track relay connection when we receive an event
+ // Track connection when we receive events (relay is working)
if relayURL != "" {
+ connectedRelaysMu.Lock()
if !connectedRelays[relayURL] {
connectedRelays[relayURL] = true
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected (%d/%d)\n", relayURL, len(connectedRelays), totalRelays)
+ isFirst := len(connectedRelays) == 1
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s active (%d/%d) - received event\n", relayURL, len(connectedRelays), totalRelays)
+
+ // Signal first connection success (non-blocking)
+ if isFirst && !connectionCheckDone {
+ connectionCheckDone = true
+ select {
+ case firstConnectionCh <- true:
+ default:
+ }
+ }
}
+ connectedRelaysMu.Unlock()
}
- select {
- case events <- relayEvent.Event:
- case <-ctx.Done():
- return
+
+ // Forward events to the output channel
+ if relayEvent.Event != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - forwarding event from relay %s: kind=%d, pubkey=%s, content_len=%d, tags_count=%d\n", relayURL, relayEvent.Event.Kind, relayEvent.Event.PubKey, len(relayEvent.Event.Content), len(relayEvent.Event.Tags))
+ select {
+ case events <- relayEvent.Event:
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - event forwarded to subscription channel\n")
+ case <-ctx.Done():
+ return
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relayEvent.Event is nil from relay %s\n", relayURL)
}
}
}
}()
- return events, nil
+ // Wait for subscription to be ready - give it time to establish
+ // We've already ensured at least one relay is connected, so subscription should work
+ select {
+ case <-firstConnectionCh:
+ // Subscription ready - at least one relay is receiving events
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription ready, active\n")
+ return events, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(1 * time.Second):
+ // After 1 second, assume subscription is established
+ // We've already connected to at least one relay, so subscription should work
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription established (at least one relay connected)\n")
+ return events, nil
+ }
}
// PublishWrap publishes a pre-signed gift wrap event (kind:1059)
@@ -495,8 +553,16 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error {
wrap.CreatedAt = nostr.Now()
}
- results := c.pool.PublishMany(ctx, c.urls, *wrap)
- totalRelays := len(c.urls)
+ // 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
+ relaysToUse := c.validRelays
+ if len(relaysToUse) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToUse = c.urls
+ }
+ results := c.pool.PublishMany(ctx, relaysToUse, *wrap)
+ totalRelays := len(relaysToUse)
// Track results in background goroutine - return immediately on first success
successCh := make(chan bool, 1)
diff --git a/BBMTLib/tss/nostrtransport/pump.go b/BBMTLib/tss/nostrtransport/pump.go
index a984e22..1ed16d4 100644
--- a/BBMTLib/tss/nostrtransport/pump.go
+++ b/BBMTLib/tss/nostrtransport/pump.go
@@ -87,6 +87,210 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) error
retryTicker := time.NewTicker(1 * time.Second)
defer retryTicker.Stop()
+ // Helper function to process an event (unwrap, verify, and call handler)
+ processEvent := func(event *nostr.Event) error {
+ if event == nil {
+ return nil
+ }
+
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from %s (hex), kind=%d, content_len=%d, tags_count=%d\n", event.PubKey, event.Kind, len(event.Content), len(event.Tags))
+
+ // Verify it's a gift wrap (kind:1059)
+ if event.Kind != 1059 {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump skipping non-wrap event (kind=%d)\n", event.Kind)
+ return nil
+ }
+
+ // Step 1: Unwrap the gift wrap to get the seal
+ seal, err := unwrapGift(event, p.cfg.LocalNsec)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unwrap gift: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unwrapped gift, got seal from %s\n", seal.PubKey)
+
+ // Verify seal is from an expected peer
+ sealSenderNpub := seal.PubKey
+ isFromExpectedPeer := false
+ for _, expectedNpub := range p.cfg.PeersNpub {
+ expectedHex, err := npubToHex(expectedNpub)
+ if err != nil {
+ continue
+ }
+ if sealSenderNpub == expectedHex {
+ isFromExpectedPeer = true
+ break
+ }
+ }
+ if !isFromExpectedPeer {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump seal from unexpected sender (hex: %s)\n", sealSenderNpub)
+ return nil
+ }
+
+ // Step 2: Unseal to get the rumor
+ // Convert seal sender npub to bech32 format for unseal (it expects npub format)
+ sealSenderNpubBech32 := sealSenderNpub
+ for _, npub := range p.cfg.PeersNpub {
+ npubHex, err := npubToHex(npub)
+ if err == nil && npubHex == sealSenderNpub {
+ sealSenderNpubBech32 = npub
+ break
+ }
+ }
+
+ rumor, err := unseal(seal, p.cfg.LocalNsec, sealSenderNpubBech32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unseal: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unsealed, got rumor\n")
+
+ // Step 3: Extract chunk data from rumor
+ var chunkMessage map[string]interface{}
+ if err := json.Unmarshal([]byte(rumor.Content), &chunkMessage); err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse rumor content: %v\n", err)
+ return nil
+ }
+
+ sessionIDValue, ok := chunkMessage["session_id"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing session_id\n")
+ return nil
+ }
+ if sessionIDValue != p.cfg.SessionID {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump session mismatch (got %s, expected %s)\n", sessionIDValue, p.cfg.SessionID)
+ return nil
+ }
+
+ // Check if this is a ready/complete message (handled by SessionCoordinator, not MessagePump)
+ if _, ok := chunkMessage["phase"].(string); ok {
+ // This is a ready/complete message, skip it (handled by SessionCoordinator)
+ return nil
+ }
+
+ // Extract chunk metadata
+ chunkTagValue, ok := chunkMessage["chunk"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk metadata\n")
+ return nil
+ }
+
+ meta, err := ParseChunkTag(chunkTagValue)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse chunk tag '%s': %v\n", chunkTagValue, err)
+ return nil
+ }
+ meta.SessionID = p.cfg.SessionID
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump parsed chunk metadata: hash=%s, index=%d/%d\n", meta.Hash, meta.Index, meta.Total)
+
+ // Extract chunk data
+ chunkDataB64, ok := chunkMessage["data"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk data\n")
+ return nil
+ }
+
+ chunkData, err := base64.StdEncoding.DecodeString(chunkDataB64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to decode chunk data: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump decoded chunk data: %d bytes\n", len(chunkData))
+
+ // Check if already processed
+ p.processedMu.Lock()
+ if p.processed[meta.Hash] {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump message %s already processed, skipping\n", meta.Hash)
+ p.processedMu.Unlock()
+ return nil
+ }
+ p.processedMu.Unlock()
+
+ // Add chunk to assembler
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump adding chunk %d/%d to assembler\n", meta.Index+1, meta.Total)
+ reassembled, complete := p.assembler.Add(meta, chunkData)
+ if !complete {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk %d/%d added, waiting for more chunks\n", meta.Index+1, meta.Total)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump all chunks received, reassembled %d bytes\n", len(reassembled))
+
+ hashBytes := sha256.Sum256(reassembled)
+ calculatedHash := hex.EncodeToString(hashBytes[:])
+ if !strings.EqualFold(calculatedHash, meta.Hash) {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk hash mismatch (calc=%s, expected=%s)\n", calculatedHash, meta.Hash)
+ return nil
+ }
+
+ // Reassemble the full message from chunks (chunks are plaintext now, not encrypted)
+ // The reassembled data is the full message body
+ plaintext := reassembled
+
+ // Mark as processed
+ p.processedMu.Lock()
+ p.processed[meta.Hash] = true
+ p.processedMu.Unlock()
+
+ // Call handler with plaintext payload
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump calling handler with %d bytes\n", len(plaintext))
+ if err := handler(plaintext); err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler error: %v\n", err)
+ return fmt.Errorf("handler error: %w", err)
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler completed successfully\n")
+ return nil
+ }
+
+ // First, query for existing events BEFORE starting subscription
+ // This ensures we catch events that were published before we started listening
+ // Query in parallel to all relays (resilient - if one fails, others continue)
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump querying for existing events for session %s (from last 1 minute)\n", p.cfg.SessionID)
+ queryCtx, queryCancel := context.WithTimeout(ctx, 3*time.Second)
+ defer queryCancel()
+
+ // Use all valid relays, not just initially connected ones
+ relaysToQuery := p.client.validRelays
+ if len(relaysToQuery) == 0 {
+ relaysToQuery = p.client.urls
+ }
+
+ queryDone := make(chan bool, 1)
+ go func() {
+ defer func() { queryDone <- true }()
+ // Query all relays in parallel
+ for _, url := range relaysToQuery {
+ go func(relayURL string) {
+ relay, err := p.client.GetPool().EnsureRelay(relayURL)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to ensure relay %s for query: %v\n", relayURL, err)
+ return
+ }
+ existingEvents, err := relay.QuerySync(queryCtx, filter)
+ if err == nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump query on relay %s returned %d events for session %s\n", relayURL, len(existingEvents), p.cfg.SessionID)
+ for _, event := range existingEvents {
+ if event != nil {
+ // Process the event (this will call handler if it's a valid message)
+ processEvent(event)
+ }
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump query on relay %s failed (non-fatal): %v\n", relayURL, err)
+ }
+ }(url)
+ }
+ // Give queries time to complete
+ time.Sleep(2 * time.Second)
+ }()
+
+ // Wait for initial query to complete (with timeout) before starting subscription
+ select {
+ case <-queryDone:
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump initial query completed\n")
+ case <-time.After(3 * time.Second):
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump initial query timeout, proceeding with subscription\n")
+ }
+
// Retry loop: resubscribe when channel closes (e.g., network disconnection)
for {
// Check if context is cancelled before attempting subscription
@@ -132,155 +336,18 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) error
}
break
}
- if event == nil {
- continue
- }
-
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from %s (hex), kind=%d, content_len=%d, tags_count=%d\n", event.PubKey, event.Kind, len(event.Content), len(event.Tags))
-
- // Verify it's a gift wrap (kind:1059)
- if event.Kind != 1059 {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump skipping non-wrap event (kind=%d)\n", event.Kind)
- continue
- }
-
- // Step 1: Unwrap the gift wrap to get the seal
- seal, err := unwrapGift(event, p.cfg.LocalNsec)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unwrap gift: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unwrapped gift, got seal from %s\n", seal.PubKey)
-
- // Verify seal is from an expected peer
- sealSenderNpub := seal.PubKey
- isFromExpectedPeer := false
- for _, expectedNpub := range p.cfg.PeersNpub {
- expectedHex, err := npubToHex(expectedNpub)
- if err != nil {
- continue
- }
- if sealSenderNpub == expectedHex {
- isFromExpectedPeer = true
- break
- }
- }
- if !isFromExpectedPeer {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump seal from unexpected sender (hex: %s)\n", sealSenderNpub)
- continue
- }
-
- // Step 2: Unseal to get the rumor
- // Convert seal sender npub to bech32 format for unseal (it expects npub format)
- sealSenderNpubBech32 := sealSenderNpub
- for _, npub := range p.cfg.PeersNpub {
- npubHex, err := npubToHex(npub)
- if err == nil && npubHex == sealSenderNpub {
- sealSenderNpubBech32 = npub
- break
- }
- }
-
- rumor, err := unseal(seal, p.cfg.LocalNsec, sealSenderNpubBech32)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unseal: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unsealed, got rumor\n")
-
- // Step 3: Extract chunk data from rumor
- var chunkMessage map[string]interface{}
- if err := json.Unmarshal([]byte(rumor.Content), &chunkMessage); err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse rumor content: %v\n", err)
- continue
- }
-
- sessionIDValue, ok := chunkMessage["session_id"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing session_id\n")
- continue
- }
- if sessionIDValue != p.cfg.SessionID {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump session mismatch (got %s, expected %s)\n", sessionIDValue, p.cfg.SessionID)
- continue
- }
-
- // Check if this is a ready/complete message (handled by SessionCoordinator, not MessagePump)
- if _, ok := chunkMessage["phase"].(string); ok {
- // This is a ready/complete message, skip it (handled by SessionCoordinator)
- continue
- }
-
- // Extract chunk metadata
- chunkTagValue, ok := chunkMessage["chunk"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk metadata\n")
- continue
- }
-
- meta, err := ParseChunkTag(chunkTagValue)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse chunk tag '%s': %v\n", chunkTagValue, err)
- continue
- }
- meta.SessionID = p.cfg.SessionID
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump parsed chunk metadata: hash=%s, index=%d/%d\n", meta.Hash, meta.Index, meta.Total)
-
- // Extract chunk data
- chunkDataB64, ok := chunkMessage["data"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk data\n")
- continue
- }
-
- chunkData, err := base64.StdEncoding.DecodeString(chunkDataB64)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to decode chunk data: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump decoded chunk data: %d bytes\n", len(chunkData))
-
- // Check if already processed
- p.processedMu.Lock()
- if p.processed[meta.Hash] {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump message %s already processed, skipping\n", meta.Hash)
- p.processedMu.Unlock()
- continue
- }
- p.processedMu.Unlock()
-
- // Add chunk to assembler
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump adding chunk %d/%d to assembler\n", meta.Index+1, meta.Total)
- reassembled, complete := p.assembler.Add(meta, chunkData)
- if !complete {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk %d/%d added, waiting for more chunks\n", meta.Index+1, meta.Total)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump all chunks received, reassembled %d bytes\n", len(reassembled))
-
- hashBytes := sha256.Sum256(reassembled)
- calculatedHash := hex.EncodeToString(hashBytes[:])
- if !strings.EqualFold(calculatedHash, meta.Hash) {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk hash mismatch (calc=%s, expected=%s)\n", calculatedHash, meta.Hash)
+ // Log that we received an event from the subscription channel
+ if event != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from subscription channel: kind=%d, pubkey=%s, content_len=%d\n", event.Kind, event.PubKey, len(event.Content))
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received nil event from subscription channel\n")
continue
}
-
- // Reassemble the full message from chunks (chunks are plaintext now, not encrypted)
- // The reassembled data is the full message body
- plaintext := reassembled
-
- // Mark as processed
- p.processedMu.Lock()
- p.processed[meta.Hash] = true
- p.processedMu.Unlock()
-
- // Call handler with plaintext payload
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump calling handler with %d bytes\n", len(plaintext))
- if err := handler(plaintext); err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler error: %v\n", err)
- return fmt.Errorf("handler error: %w", err)
+ // Process event using the helper function
+ if err := processEvent(event); err != nil {
+ // Handler error - return to stop processing
+ return err
}
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler completed successfully\n")
}
}
// If we break out of the inner loop, we'll retry subscribing in the outer loop
diff --git a/BBMTLib/tss/nostrtransport/session.go b/BBMTLib/tss/nostrtransport/session.go
index eb57727..2e4d672 100644
--- a/BBMTLib/tss/nostrtransport/session.go
+++ b/BBMTLib/tss/nostrtransport/session.go
@@ -130,10 +130,16 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
defer queryCancel()
// Query all relays in parallel and wait for results
+ // Use all valid relays, not just initially connected ones
+ relaysToQuery := s.client.validRelays
+ if len(relaysToQuery) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToQuery = s.client.urls
+ }
queryDone := make(chan bool, 1)
go func() {
defer func() { queryDone <- true }()
- for _, url := range s.client.urls {
+ for _, url := range relaysToQuery {
relay, err := s.client.GetPool().EnsureRelay(url)
if err != nil {
fmt.Fprintf(os.Stderr, "BBMTLog: Failed to ensure relay %s: %v\n", url, err)
@@ -205,10 +211,36 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
}
// Now start subscription to catch new events
- fmt.Fprintf(os.Stderr, "BBMTLog: Starting subscription for ready wraps for session %s\n", s.cfg.SessionID)
- eventsCh, err := s.client.Subscribe(ctx, filter)
- if err != nil {
- return fmt.Errorf("subscribe to ready wraps: %w", err)
+ // Retry subscription if it fails or channel closes (resilient to relay failures)
+ retryTicker := time.NewTicker(1 * time.Second)
+ defer retryTicker.Stop()
+
+ var eventsCh <-chan *Event
+ subscriptionActive := false
+
+ // Retry loop for subscription
+ for !subscriptionActive {
+ select {
+ case <-ctx.Done():
+ fmt.Fprintf(os.Stderr, "BBMTLog: AwaitPeers timed out during subscription (seen: %d/%d)\n", s.countSeen(&seen), len(expected))
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ default:
+ }
+
+ fmt.Fprintf(os.Stderr, "BBMTLog: Starting subscription for ready wraps for session %s\n", s.cfg.SessionID)
+ var err error
+ eventsCh, err = s.client.Subscribe(ctx, filter)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe failed: %v, retrying in 1 second...\n", err)
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ continue // Retry subscription
+ }
+ }
+ subscriptionActive = true
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription active for session %s\n", s.cfg.SessionID)
}
ticker := time.NewTicker(5 * time.Second)
@@ -222,7 +254,37 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
case evt, ok := <-eventsCh:
if !ok {
- return fmt.Errorf("relay subscription closed")
+ // Channel closed (e.g., relay disconnection) - retry subscription
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription channel closed, retrying subscription in 1 second...\n")
+ subscriptionActive = false
+ // Wait before retrying
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ // Retry subscription
+ for !subscriptionActive {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ default:
+ }
+ var err error
+ eventsCh, err = s.client.Subscribe(ctx, filter)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe retry failed: %v, retrying in 1 second...\n", err)
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ continue // Retry subscription
+ }
+ }
+ subscriptionActive = true
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription re-established for session %s\n", s.cfg.SessionID)
+ }
+ continue // Continue processing events
+ }
}
if evt == nil || evt.Kind != 1059 {
continue
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 80b0c75..c6ea984 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,23 @@
## [Unreleased]
+## [2.1.4] - 2025-12-30
+
+### Added
+- **Resilient Nostr relay connections**: BTC sends now work reliably even if some Nostr relays are down
+ - Automatically connects to multiple relays in parallel
+ - Continues working if relays fail during the signing process
+ - Faster connection establishment and better error recovery
+
+### Changed
+- **Faster pre-agreement timeout**: Reduced from 2 minutes to 16 seconds for quicker failure detection
+
+### Fixed
+- **Android navigation bar overlap**: Fixed bottom navigation bar overlapping system navigation on Android devices (e.g., Samsung)
+- **Message delivery reliability**: Improved handling of messages sent just before subscription starts
+
+---
+
## [2.1.3] - 2025-12-20
### Added
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5f604d0..49a3c14 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 36
- versionName "2.1.3"
+ versionCode 37
+ versionName "2.1.4"
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 40837d9..136c95b 100644
Binary files a/android/app/libs/tss.aar and b/android/app/libs/tss.aar differ
diff --git a/components/TransportModeSelector.tsx b/components/TransportModeSelector.tsx
index 0348485..0933e00 100644
--- a/components/TransportModeSelector.tsx
+++ b/components/TransportModeSelector.tsx
@@ -313,7 +313,7 @@ const TransportModeSelector: React.FC = ({
{/* Modal Body */}
- {description && (
+ {description && description.length > 0 && (
{description}
)}
@@ -431,7 +431,7 @@ const TransportModeSelector: React.FC = ({
{/* Selected Transport Hint */}
- {selectedTransport && (
+ {selectedTransport && description && description.length > 0 && (
BinaryPath
Tss.framework/Tss
LibraryIdentifier
- ios-arm64
+ ios-arm64_x86_64-simulator
LibraryPath
Tss.framework
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
+ SupportedPlatformVariant
+ simulator
BinaryPath
@@ -37,18 +40,15 @@
BinaryPath
Tss.framework/Tss
LibraryIdentifier
- ios-arm64_x86_64-simulator
+ ios-arm64
LibraryPath
Tss.framework
SupportedArchitectures
arm64
- x86_64
SupportedPlatform
ios
- SupportedPlatformVariant
- simulator
CFBundlePackageType
diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist
index f058436..70b7423 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.1767027095
+ 0.0.1767079533
CFBundleVersion
- 0.0.1767027095
+ 0.0.1767079533
CFBundlePackageType
FMWK
diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss
index 120ab64..b6460f3 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 f058436..70b7423 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.1767027095
+ 0.0.1767079533
CFBundleVersion
- 0.0.1767027095
+ 0.0.1767079533
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 593ebdb..b12aec7 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 f058436..70b7423 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.1767027095
+ 0.0.1767079533
CFBundleVersion
- 0.0.1767027095
+ 0.0.1767079533
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 b8bfa23..1cfbebc 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/screens/WalletHome.tsx b/screens/WalletHome.tsx
index c81eacc..d18d165 100644
--- a/screens/WalletHome.tsx
+++ b/screens/WalletHome.tsx
@@ -1646,7 +1646,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}
return (
-
+
@@ -2095,7 +2095,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
setIsTransportModalVisible(false);
}}
title="Select Signing Method"
- description="Choose how to sign your transaction"
+ description=""
sendBitcoinData={
pendingSendParams
? {