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 ? {