diff --git a/.gitignore b/.gitignore index 802156a..c7c9160 100644 --- a/.gitignore +++ b/.gitignore @@ -380,3 +380,6 @@ vendor/bundle/ # PR documentation (local only, not for git) PR_SUMMARY.md PR_README.md + +# third_party +third_party/ \ No newline at end of file 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/build-bold-spend.sh b/BBMTLib/bold-spend-builder.sh similarity index 100% rename from BBMTLib/build-bold-spend.sh rename to BBMTLib/bold-spend-builder.sh diff --git a/BBMTLib/tss/btc.go b/BBMTLib/tss/btc.go index 49bbc35..a6ad3ec 100644 --- a/BBMTLib/tss/btc.go +++ b/BBMTLib/tss/btc.go @@ -27,9 +27,13 @@ import ( // UTXO represents an unspent transaction output type UTXO struct { - TxID string `json:"txid"` - Vout uint32 `json:"vout"` - Value int64 `json:"value"` // Value in satoshis + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Value int64 `json:"value"` // Value in satoshis + Status struct { + Confirmed bool `json:"confirmed"` + BlockHeight int64 `json:"block_height"` + } `json:"status,omitempty"` // Status is optional, includes both confirmed and unconfirmed UTXOs } var _btc_net = "testnet3" // default to testnet @@ -79,18 +83,76 @@ func GetNetwork() (string, error) { } // FetchUTXOs fetches UTXOs for a given address +// The mempool.space API returns both confirmed and unconfirmed UTXOs by default func FetchUTXOs(address string) ([]UTXO, error) { url := fmt.Sprintf("%s/address/%s/utxo", _api_url, address) + Logf("Fetching UTXOs from endpoint: %s", url) resp, err := http.Get(url) if err != nil { + Logf("Error fetching UTXOs from %s: %v", url, err) return nil, fmt.Errorf("failed to fetch UTXOs: %w", err) } defer resp.Body.Close() + // Check response status + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + Logf("Error response from %s: HTTP %d - %s", url, resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to fetch UTXOs: HTTP %d - %s", resp.StatusCode, string(body)) + } + Logf("Successfully fetched UTXOs from %s (HTTP %d)", url, resp.StatusCode) + + // Read the response body to log it + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + Logf("Error reading response body from %s: %v", url, err) + return nil, fmt.Errorf("failed to read UTXO response: %w", err) + } + + // Log the raw response (first 500 chars to avoid huge logs) + bodyStr := string(bodyBytes) + if len(bodyStr) > 500 { + Logf("Raw API response (first 500 chars): %s...", bodyStr[:500]) + } else { + Logf("Raw API response: %s", bodyStr) + } + + // Decode JSON from the body bytes var utxos []UTXO - if err := json.NewDecoder(resp.Body).Decode(&utxos); err != nil { + if err := json.Unmarshal(bodyBytes, &utxos); err != nil { + Logf("Error parsing JSON response from %s: %v. Response was: %s", url, err, bodyStr) return nil, fmt.Errorf("failed to parse UTXO response: %w", err) } + + // Log UTXO status for debugging + if len(utxos) > 0 { + confirmedCount := 0 + unconfirmedCount := 0 + totalValue := int64(0) + for _, utxo := range utxos { + totalValue += utxo.Value + if utxo.Status.Confirmed { + confirmedCount++ + } else { + unconfirmedCount++ + } + } + Logf("Fetched %d UTXOs: %d confirmed, %d unconfirmed, total value: %d satoshis", len(utxos), confirmedCount, unconfirmedCount, totalValue) + // Log first few UTXOs for debugging + for i, utxo := range utxos { + if i < 3 { // Log first 3 UTXOs + Logf("UTXO[%d]: txid=%s, vout=%d, value=%d, confirmed=%v", i, utxo.TxID, utxo.Vout, utxo.Value, utxo.Status.Confirmed) + } + } + } else { + Logf("No UTXOs found for address %s (this includes both confirmed and unconfirmed)", address) + Logf("API returned empty array. This could mean:") + Logf(" - Address has no UTXOs (all spent)") + Logf(" - Address format mismatch") + Logf(" - Network mismatch (checking testnet vs mainnet)") + Logf(" - API endpoint issue") + } + return utxos, nil } @@ -119,8 +181,10 @@ func TotalUTXO(address string) (result string, err error) { func FetchUTXODetails(txID string, vout uint32) (*wire.TxOut, bool, error) { url := fmt.Sprintf("%s/tx/%s", _api_url, txID) + Logf("Fetching UTXO details from endpoint: %s", url) resp, err := http.Get(url) if err != nil { + Logf("Error fetching UTXO details from %s: %v", url, err) return nil, false, fmt.Errorf("failed to fetch transaction details: %w", err) } defer resp.Body.Close() @@ -151,8 +215,10 @@ func RecommendedFees(feeType string) (int, error) { for _, url := range _api_urls { fee_url := strings.TrimSuffix(url, "/") url := fmt.Sprintf("%s/v1/fees/recommended", fee_url) + Logf("Fetching recommended fees from endpoint: %s (fee type: %s)", url, feeType) resp, err := http.Get(url) if err != nil { + Logf("Error fetching fees from %s: %v, trying next endpoint", url, err) continue } defer resp.Body.Close() @@ -200,10 +266,12 @@ func PostTx(rawTxHex string) (string, error) { func postTxOnce(rawTxHex string) (string, error) { // Define the Blockstream API endpoint for broadcasting transactions url := fmt.Sprintf("%s/tx", _api_url) + Logf("Broadcasting transaction to endpoint: %s", url) // Create a POST request with the raw transaction hex as the body req, err := http.NewRequest("POST", url, bytes.NewBufferString(rawTxHex)) if err != nil { + Logf("Error creating POST request to %s: %v", url, err) return "", fmt.Errorf("failed to create request: %w", err) } @@ -350,23 +418,55 @@ func EstimateFees(senderAddress, receiverAddress string, amountSatoshi int64) (r } }() - Logln("BBMTLog", "invoking SendBitcoin...") + Logln("BBMTLog", "invoking EstimateFees...") utxos, err := FetchUTXOs(senderAddress) if err != nil { return "", fmt.Errorf("failed to fetch UTXOs: %w", err) } - // select the utxos + // Check if we have any UTXOs + if len(utxos) == 0 { + Logf("No UTXOs found for address %s during fee estimation", senderAddress) + return "", fmt.Errorf("no UTXOs available for address %s. Please ensure you have confirmed transactions before sending", senderAddress) + } + + // Use iterative approach to match MpcSendBTC behavior: + // MpcSendBTC selects UTXOs for amountSatoshi+estimatedFee, so we need to do the same + // 1. First estimate: select UTXOs for amount only, calculate fee + // 2. Second estimate: re-select UTXOs for amount+fee (matching actual send), re-calculate fee + // This ensures we select the same UTXOs that will be used in the actual send + + // First iteration: select UTXOs for amount only selectedUTXOs, _, err := SelectUTXOs(utxos, amountSatoshi, "smallest") if err != nil { return "", err } + // First fee estimate with UTXOs selected for amount only _fee, _err := calculateFees(senderAddress, selectedUTXOs, amountSatoshi, receiverAddress) if _err != nil { return "", _err } + + // Second iteration: re-select UTXOs for amount + fee (matching MpcSendBTC behavior) + // This ensures the fee estimation uses the same UTXOs that will be used in actual send + Logf("Re-selecting UTXOs for amount+fee (%d + %d = %d) to match MpcSendBTC behavior", amountSatoshi, _fee, amountSatoshi+_fee) + selectedUTXOs, _, err = SelectUTXOs(utxos, amountSatoshi+_fee, "smallest") + if err != nil { + // If we can't select enough UTXOs for amount+fee, return the original fee estimate + // This can happen if the wallet doesn't have enough funds + Logf("Could not select UTXOs for amount+fee, using original estimate: %v", err) + return strconv.FormatInt(_fee, 10), nil + } + + // Re-calculate fee with the new UTXOs (which match what will be used in actual send) + _fee, _err = calculateFees(senderAddress, selectedUTXOs, amountSatoshi, receiverAddress) + if _err != nil { + return "", _err + } + Logf("Final fee estimate with UTXOs selected for amount+fee: %d", _fee) + return strconv.FormatInt(_fee, 10), nil } @@ -647,11 +747,25 @@ func MpcSendBTC( } Logf("Fetched UTXOs: %+v", utxos) + // Check if we have any UTXOs + if len(utxos) == 0 { + Logf("No UTXOs found for address %s. This may be because:", senderAddress) + Logf("1. The address has no confirmed transactions") + Logf("2. All transactions are still pending (unconfirmed)") + Logf("3. The address has been fully spent") + return "", fmt.Errorf("no UTXOs available for address %s. Please ensure you have confirmed transactions before sending", senderAddress) + } + mpcHook("selecting utxos", session, "", 0, 0, false) selectedUTXOs, totalAmount, err := SelectUTXOs(utxos, amountSatoshi+estimatedFee, "smallest") if err != nil { Logf("Error selecting UTXOs: %v", err) - return "", err + // Provide more context in the error message + totalAvailable := int64(0) + for _, utxo := range utxos { + totalAvailable += utxo.Value + } + return "", fmt.Errorf("insufficient funds: needed %d (amount: %d + fee: %d), available: %d. Note: Only confirmed UTXOs are available for spending", amountSatoshi+estimatedFee, amountSatoshi, estimatedFee, totalAvailable) } Logf("Selected UTXOs: %+v, Total Amount: %d", selectedUTXOs, totalAmount) @@ -1440,6 +1554,7 @@ func ReplaceTransaction( // Fetch the original transaction details url := fmt.Sprintf("%s/tx/%s", _api_url, originalTxID) + Logf("Fetching transaction details from endpoint: %s", url) resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to fetch original transaction: %w", err) 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/BBMTLib/tss/psbt.go b/BBMTLib/tss/psbt.go index 8daed7d..ae43c9c 100644 --- a/BBMTLib/tss/psbt.go +++ b/BBMTLib/tss/psbt.go @@ -197,6 +197,10 @@ func MpcSignPSBT( inputDerivePath = extractedPath Logf("Input %d: Using derivation path from PSBT Bip32Derivation: %s", i, inputDerivePath) + // Get the pubkey from PSBT's Bip32Derivation (this is what the PSBT expects) + psbtPubKeyBytes := input.Bip32Derivation[0].PubKey + Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) + // Derive the public key from keyshare using GetDerivedPubKey with the path from PSBT // Use the keyshare's pub_key and chaincode to derive the corresponding public key Logf("Input %d: Deriving public key from keyshare - xpub: %s, chaincode: %s, path: %s", i, truncateHex(keyshareData.PubKey), truncateHex(keyshareData.ChainCodeHex), inputDerivePath) @@ -210,7 +214,15 @@ func MpcSignPSBT( } Logf("Input %d: Derived public key (hex): %s, bytes: %x (length: %d)", i, truncateHex(derivedPubKeyHex), derivedPubKeyBytes, len(derivedPubKeyBytes)) - // Use the derived key for signing + // Check if this input belongs to us by comparing derived pubkey with PSBT's pubkey + if !bytes.Equal(derivedPubKeyBytes, psbtPubKeyBytes) { + Logf("Input %d: ⚠️ SKIPPING SIGNATURE - Pubkey mismatch: derived %x != PSBT %x (path: %s)", i, derivedPubKeyBytes[:min(8, len(derivedPubKeyBytes))], psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], inputDerivePath) + Logf("Input %d: This input does not belong to our wallet, skipping signature", i) + continue // Skip signing this input, but continue with other inputs + } + + Logf("Input %d: ✅ Pubkey matches - this input belongs to our wallet, will sign", i) + // Use the derived key for signing (which matches PSBT's key) inputPubKeyBytes = derivedPubKeyBytes // Create session for this input @@ -246,17 +258,7 @@ func MpcSignPSBT( Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash) Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes) - // Check PSBT's Bip32Derivation key if available - if len(input.Bip32Derivation) > 0 { - psbtPubKeyBytes := input.Bip32Derivation[0].PubKey - psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash) - if bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) { - Logf("Input %d: PSBT key matches script, but derived key doesn't - keyshare mismatch?", i) - } - } - + // Verify the public key hash matches the script (we already verified pubkey ownership above) if !bytes.Equal(scriptPubKeyHash, pubKeyHash) { return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath) } @@ -321,37 +323,7 @@ func MpcSignPSBT( Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash) Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes) - // Check PSBT's Bip32Derivation key if available - if len(input.Bip32Derivation) > 0 { - psbtPubKeyBytes := input.Bip32Derivation[0].PubKey - psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash) - - // If derived key doesn't match script, but PSBT key does, verify we can derive PSBT key - if !bytes.Equal(scriptPubKeyHash, pubKeyHash) && bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) { - Logf("Input %d: PSBT key matches script, but derived key doesn't - verifying if PSBT key can be derived from keyshare", i) - // Try to derive the PSBT key from our keyshare - verifyPubKeyHex, err := GetDerivedPubKey(keyshareData.PubKey, keyshareData.ChainCodeHex, inputDerivePath, false) - if err != nil { - return "", fmt.Errorf("input %d: failed to verify PSBT key derivation: %w", i, err) - } - verifyPubKeyBytes, err := hex.DecodeString(verifyPubKeyHex) - if err != nil { - return "", fmt.Errorf("input %d: failed to decode verified public key: %w", i, err) - } - if bytes.Equal(verifyPubKeyBytes, psbtPubKeyBytes) { - // We can derive it - use the PSBT key - Logf("Input %d: Verified PSBT key can be derived from keyshare - using PSBT key", i) - inputPubKeyBytes = psbtPubKeyBytes - pubKeyHash = psbtPubKeyHash - } else { - // Cannot derive PSBT key - keyshare mismatch - return "", fmt.Errorf("input %d: keyshare mismatch - PSBT key matches script but cannot be derived from keyshare (PSBT key: %x, derived key: %x, path: %s)", i, psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], verifyPubKeyBytes[:min(8, len(verifyPubKeyBytes))], inputDerivePath) - } - } - } - + // Verify the public key hash matches the script (we already verified pubkey ownership above) if !bytes.Equal(scriptPubKeyHash, pubKeyHash) { return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath) } @@ -900,6 +872,10 @@ func runNostrMpcSignPSBTInternal( inputDerivePath = extractedPath Logf("Input %d: Using derivation path from PSBT Bip32Derivation: %s", i, inputDerivePath) + // Get the pubkey from PSBT's Bip32Derivation (this is what the PSBT expects) + psbtPubKeyBytes := input.Bip32Derivation[0].PubKey + Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) + // Derive the public key from keyshare using GetDerivedPubKey with the path from PSBT // Use the keyshare's pub_key and chaincode to derive the corresponding public key Logf("Input %d: Deriving public key from keyshare - xpub: %s, chaincode: %s, path: %s", i, truncateHex(keyshareData.PubKey), truncateHex(keyshareData.ChainCodeHex), inputDerivePath) @@ -913,7 +889,15 @@ func runNostrMpcSignPSBTInternal( } Logf("Input %d: Derived public key (hex): %s, bytes: %x (length: %d)", i, truncateHex(derivedPubKeyHex), derivedPubKeyBytes, len(derivedPubKeyBytes)) - // Use the derived key for signing + // Check if this input belongs to us by comparing derived pubkey with PSBT's pubkey + if !bytes.Equal(derivedPubKeyBytes, psbtPubKeyBytes) { + Logf("Input %d: ⚠️ SKIPPING SIGNATURE - Pubkey mismatch: derived %x != PSBT %x (path: %s)", i, derivedPubKeyBytes[:min(8, len(derivedPubKeyBytes))], psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], inputDerivePath) + Logf("Input %d: This input does not belong to our wallet, skipping signature", i) + continue // Skip signing this input, but continue with other inputs + } + + Logf("Input %d: ✅ Pubkey matches - this input belongs to our wallet, will sign", i) + // Use the derived key for signing (which matches PSBT's key) inputPubKeyBytes = derivedPubKeyBytes // Create session for this input @@ -949,17 +933,7 @@ func runNostrMpcSignPSBTInternal( Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash) Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes) - // Check PSBT's Bip32Derivation key if available - if len(input.Bip32Derivation) > 0 { - psbtPubKeyBytes := input.Bip32Derivation[0].PubKey - psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash) - if bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) { - Logf("Input %d: PSBT key matches script, but derived key doesn't - keyshare mismatch?", i) - } - } - + // Verify the public key hash matches the script (we already verified pubkey ownership above) if !bytes.Equal(scriptPubKeyHash, pubKeyHash) { return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath) } @@ -1024,37 +998,7 @@ func runNostrMpcSignPSBTInternal( Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash) Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes) - // Check PSBT's Bip32Derivation key if available - if len(input.Bip32Derivation) > 0 { - psbtPubKeyBytes := input.Bip32Derivation[0].PubKey - psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes) - Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash) - - // If derived key doesn't match script, but PSBT key does, verify we can derive PSBT key - if !bytes.Equal(scriptPubKeyHash, pubKeyHash) && bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) { - Logf("Input %d: PSBT key matches script, but derived key doesn't - verifying if PSBT key can be derived from keyshare", i) - // Try to derive the PSBT key from our keyshare - verifyPubKeyHex, err := GetDerivedPubKey(keyshareData.PubKey, keyshareData.ChainCodeHex, inputDerivePath, false) - if err != nil { - return "", fmt.Errorf("input %d: failed to verify PSBT key derivation: %w", i, err) - } - verifyPubKeyBytes, err := hex.DecodeString(verifyPubKeyHex) - if err != nil { - return "", fmt.Errorf("input %d: failed to decode verified public key: %w", i, err) - } - if bytes.Equal(verifyPubKeyBytes, psbtPubKeyBytes) { - // We can derive it - use the PSBT key - Logf("Input %d: Verified PSBT key can be derived from keyshare - using PSBT key", i) - inputPubKeyBytes = psbtPubKeyBytes - pubKeyHash = psbtPubKeyHash - } else { - // Cannot derive PSBT key - keyshare mismatch - return "", fmt.Errorf("input %d: keyshare mismatch - PSBT key matches script but cannot be derived from keyshare (PSBT key: %x, derived key: %x, path: %s)", i, psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], verifyPubKeyBytes[:min(8, len(verifyPubKeyBytes))], inputDerivePath) - } - } - } - + // Verify the public key hash matches the script (we already verified pubkey ownership above) if !bytes.Equal(scriptPubKeyHash, pubKeyHash) { return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath) } diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ec4ca..c6ea984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,72 @@ ## [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 +- **Hide/show balance user preference**: Tap balance to toggle visibility, preference persists across app sessions +- **Unified QR Scanner component** (`components/QRScanner.tsx` and `components/QRScanner.foss.tsx`): + - Platform-specific implementations: iOS uses `react-native-vision-camera`, Android uses `rn-barcode-zxing-scan` + - Support for both single and continuous scanning modes + - Progress indicator for animated QR codes + - Customizable titles, subtitles, and close button text +- **New `useQRScanner` hook** for reusable QR scanning logic +- **Enhanced pubkey matching logic** in `BBMTLib/tss/psbt.go`: + - Better validation of input ownership before signing + - Improved logging for debugging signature issues + - Skips signing inputs that don't belong to the wallet (prevents errors) + - More reliable pubkey derivation verification +- **PSBT sign filter**: Automatically filters and only signs inputs that belong to the wallet, preventing errors from unrelated inputs +- **QR Scan shortcut for Send (Second Device)**: Added QR code option in transport mode selector to share transaction details (address, amount, fee) from one phone to another for quick entry + +### Changed +- **Wallet mode terminology**: Updated from "basic/flexi" to "duo/trio" for clearer wallet type indication (2/2 vs 2/3 multisig) +- **Keyshare backup naming**: Keyshare backup files now use xpub-based naming with index (e.g., `a6tr2-1.ks`, `a6tr2-2.ks`, `a6tr2-3.ks`) for better organization and identification +- **Default address type**: New wallets default to SegWit Native (BIP84) instead of Legacy addresses for better efficiency and lower fees +- **Refactored screens** to use new QR scanner: + - `MobileNostrPairing.tsx` - Simplified QR scanning integration + - `SendBitcoinModal.tsx` - Improved QR code scanning UX + - `PSBTModal.tsx` - Enhanced QR code handling + - `WalletHome.tsx` - Updated QR scanning flow +- **Code cleanup**: Removed large FOSS variant files (reduced codebase by ~8,500 lines): + - `screens/MobileNostrPairing.foss.tsx` (6,032 lines removed) + - `screens/PSBTModal.foss.tsx` (1,661 lines removed) + - `screens/SendBitcoinModal.foss.tsx` (782 lines removed) + +### Fixed +- **Wallet Context & Address Handling**: Improved address type handling with legacy wallet detection +- **Derive path management**: Better derive path management for different network types +- **Address type caching**: Enhanced address type caching and retrieval +- **Build & Dependencies**: Updated Android build configuration and TSS library binaries (iOS and Android) +- **Release script**: Improved release script (`android/release.sh`) + +### Technical Details +- **Components**: Updated `KeyshareModal.tsx`, `TransportModeSelector.tsx`, and `Styles.tsx` +- **Screens**: Improvements to `WalletSettings.tsx`, `ShowcaseScreen.tsx`, and `PSBTScreen.tsx` +- **Utilities**: New utility functions in `utils.js` (53 lines added) with improved error handling and logging +- **Native Libraries**: Updated TSS framework binaries for iOS (all architectures) and TSS AAR library for Android +- **Native headers**: Updated native headers and Info.plist files +- **Statistics**: 32 files changed, 1,570 insertions, 9,089 deletions (net reduction of ~7,500 lines) + +--- + ## [2.1.2] - 2025-12-19 ### Added diff --git a/Dockerfile b/Dockerfile index 36a619c..03dcacb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -52,9 +52,7 @@ WORKDIR /BoldWallet # conditional F-Droid build switch RUN if [ "$fdroid" = "true" ]; then \ sed -i '/react-native-vision-camera/d' package.json; \ - mv /BoldWallet/screens/SendBitcoinModal.foss.tsx /BoldWallet/screens/SendBitcoinModal.tsx; \ - mv /BoldWallet/screens/MobileNostrPairing.foss.tsx /BoldWallet/screens/MobileNostrPairing.tsx; \ - mv /BoldWallet/screens/PSBTModal.foss.tsx /BoldWallet/screens/PSBTModal.tsx; \ + mv /BoldWallet/components/QRScanner.foss.tsx /BoldWallet/components/QRScanner.tsx; \ fi # npm install diff --git a/android/app/build.gradle b/android/app/build.gradle index d2df81c..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 35 - versionName "2.1.2" + versionCode 37 + versionName "2.1.4" missingDimensionStrategy 'react-native-camera', 'general' missingDimensionStrategy 'react-native-arch', 'oldarch' diff --git a/android/app/libs/tss-sources.jar b/android/app/libs/tss-sources.jar index f2aeb24..7d1ec39 100644 Binary files a/android/app/libs/tss-sources.jar and b/android/app/libs/tss-sources.jar differ diff --git a/android/app/libs/tss.aar b/android/app/libs/tss.aar index f273922..136c95b 100644 Binary files a/android/app/libs/tss.aar and b/android/app/libs/tss.aar differ diff --git a/android/release.sh b/android/release.sh index 3fc3c87..f72dd09 100755 --- a/android/release.sh +++ b/android/release.sh @@ -1,12 +1,15 @@ #!/bin/bash # Script to automate generating a release APK for React Native +# This script generates a DEV keystore for local development builds. +# For production releases, use signed-release.sh with production credentials. -# Keystore details (modify these with your own values) -KEYSTORE_FILE="my-release-key.jks" -KEY_ALIAS="my-key" -KEYSTORE_PASSWORD="your_actual_password_here" -KEY_PASSWORD="your_actual_password_here" +# Dev keystore details (for local development only) +# Note: Modern Java uses PKCS12 format which requires same password for store and key +KEYSTORE_FILE="dev-release-key.jks" +KEY_ALIAS="dev-key" +KEYSTORE_PASSWORD="dev-keystore-password" +KEY_PASSWORD="dev-keystore-password" # Must match KEYSTORE_PASSWORD for PKCS12 # Paths KEYSTORE_PATH="app/$KEYSTORE_FILE" @@ -14,62 +17,78 @@ GRADLE_PROPERTIES_PATH="release.properties" echo -e "--- Starting React Native APK Release Build Automation ---" -# Step 1: Generate Keystore if it doesn't exist -if [ ! -f "$KEYSTORE_PATH" ]; then - echo -e "Generating new Keystore..." +# Step 1: Generate or verify Dev Keystore +KEYSTORE_VALID=false + +if [ -f "$KEYSTORE_PATH" ]; then + echo -e "Dev keystore exists. Verifying it works with current credentials..." + # Try to list the keystore to verify password works + if keytool -list -keystore "$KEYSTORE_PATH" -storepass "$KEYSTORE_PASSWORD" -alias "$KEY_ALIAS" >/dev/null 2>&1; then + KEYSTORE_VALID=true + echo -e "✅ Existing dev keystore is valid." + else + echo -e "⚠️ Existing keystore doesn't work with current credentials." + echo -e "🗑️ Removing old keystore to regenerate..." + rm -f "$KEYSTORE_PATH" + KEYSTORE_VALID=false + fi +fi + +if [ "$KEYSTORE_VALID" = false ]; then + echo -e "Generating new DEV keystore for local development..." + echo -e "⚠️ This is a DEV keystore - NOT for production releases!" keytool -genkey -v -keystore "$KEYSTORE_PATH" \ -keyalg RSA -keysize 2048 -validity 10000 -alias "$KEY_ALIAS" \ - -dname "CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown" \ - -storepass "$KEY_PASSWORD" -keypass "$KEY_PASSWORD" + -dname "CN=Dev, OU=Dev, O=Dev, L=Dev, ST=Dev, C=US" \ + -storepass "$KEYSTORE_PASSWORD" -keypass "$KEY_PASSWORD" 2>&1 - echo -e "Keystore generated at: $KEYSTORE_PATH" -else - echo -e "Keystore already exists. Skipping generation." + if [ $? -eq 0 ]; then + echo -e "✅ Dev keystore generated at: $KEYSTORE_PATH" + echo -e "⚠️ This keystore is for development/testing only!" + else + echo -e "❌ Failed to generate keystore!" + exit 1 + fi fi -# Step 2: Update *.properties with Keystore credentials +# Step 2: Update release.properties with Keystore credentials if [ ! -f "$GRADLE_PROPERTIES_PATH" ]; then echo -e "Creating release.properties file..." touch "$GRADLE_PROPERTIES_PATH" fi -# Check if the store file path is correct, update if needed -if grep -q "MYAPP_UPLOAD_STORE_FILE" "$GRADLE_PROPERTIES_PATH"; then - CURRENT_PATH=$(grep "MYAPP_UPLOAD_STORE_FILE" "$GRADLE_PROPERTIES_PATH" | cut -d'=' -f2) - if [ "$CURRENT_PATH" != "$KEYSTORE_PATH" ]; then - echo -e "Updating incorrect keystore path in release.properties..." - # Update the path using sed +# Function to update or add property +update_property() { + local key=$1 + local value=$2 + if grep -q "^${key}=" "$GRADLE_PROPERTIES_PATH"; then + # Update existing property if [[ "$OSTYPE" == "darwin"* ]]; then # macOS - sed -i '' "s|MYAPP_UPLOAD_STORE_FILE=.*|MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH|" "$GRADLE_PROPERTIES_PATH" + sed -i '' "s|^${key}=.*|${key}=${value}|" "$GRADLE_PROPERTIES_PATH" else # Linux - sed -i "s|MYAPP_UPLOAD_STORE_FILE=.*|MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH|" "$GRADLE_PROPERTIES_PATH" + sed -i "s|^${key}=.*|${key}=${value}|" "$GRADLE_PROPERTIES_PATH" fi - echo -e "Updated MYAPP_UPLOAD_STORE_FILE to: $KEYSTORE_PATH" + echo -e "Updated ${key} in release.properties" else - echo -e "Keystore path in release.properties is correct." + # Add new property + echo "${key}=${value}" >> "$GRADLE_PROPERTIES_PATH" + echo -e "Added ${key} to release.properties" fi - - # Update other properties if they don't exist or are incorrect - if ! grep -q "MYAPP_UPLOAD_KEY_ALIAS" "$GRADLE_PROPERTIES_PATH"; then - echo "MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS" >> "$GRADLE_PROPERTIES_PATH" - fi - if ! grep -q "MYAPP_UPLOAD_STORE_PASSWORD" "$GRADLE_PROPERTIES_PATH"; then - echo "MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD" >> "$GRADLE_PROPERTIES_PATH" - fi - if ! grep -q "MYAPP_UPLOAD_KEY_PASSWORD" "$GRADLE_PROPERTIES_PATH"; then - echo "MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD" >> "$GRADLE_PROPERTIES_PATH" - fi -else - echo -e "Adding Keystore configuration to release.properties..." - cat <> $GRADLE_PROPERTIES_PATH -MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH -MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS -MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD -MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD -EOL -fi +} + +# Update all keystore properties +echo -e "Updating DEV keystore configuration in release.properties..." +update_property "MYAPP_UPLOAD_STORE_FILE" "$KEYSTORE_PATH" +update_property "MYAPP_UPLOAD_KEY_ALIAS" "$KEY_ALIAS" +update_property "MYAPP_UPLOAD_STORE_PASSWORD" "$KEYSTORE_PASSWORD" +update_property "MYAPP_UPLOAD_KEY_PASSWORD" "$KEY_PASSWORD" + +echo -e "" +echo -e "⚠️ NOTE: This build uses a DEV keystore for local development." +echo -e "⚠️ For production releases, use signed-release.sh instead." +echo -e "" # Step 3: Build the Release APK echo -e "Building the Release APK..." diff --git a/components/KeyshareModal.tsx b/components/KeyshareModal.tsx index ae6bf40..b246c85 100644 --- a/components/KeyshareModal.tsx +++ b/components/KeyshareModal.tsx @@ -22,7 +22,7 @@ interface KeyshareInfo { label: string; supportsLocal: boolean; supportsNostr: boolean; - type: 'basic' | 'flexi'; + type: 'duo' | 'trio'; pubKey: string; chainCode: string; xpub: string; @@ -282,14 +282,14 @@ const KeyshareModal: React.FC = ({ - {keyshareInfo.type === 'flexi' - ? 'Flexi • 3 devices' - : 'Basic • 2 devices'} + {keyshareInfo.type === 'trio' + ? 'Trio • 3 devices' + : 'Duo • 2 devices'} diff --git a/components/QRScanner.foss.tsx b/components/QRScanner.foss.tsx new file mode 100644 index 0000000..72b66de --- /dev/null +++ b/components/QRScanner.foss.tsx @@ -0,0 +1,298 @@ +import React, {useState, useEffect, useRef, useCallback} from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Modal, + Platform, +} from 'react-native'; +import BarcodeZxingScan from 'rn-barcode-zxing-scan'; +import {useTheme} from '../theme'; +import {dbg} from '../utils'; + +export interface QRProgress { + received: number; + total: number; + percentage?: number; +} + +export interface QRScannerProps { + visible: boolean; + onClose: () => void; + onScan: (data: string) => void; + mode?: 'single' | 'continuous'; + title?: string; + subtitle?: string; + showProgress?: boolean; + progress?: QRProgress; + closeButtonText?: string; +} + +const QRScanner: React.FC = ({ + visible, + onClose, + onScan, + mode = 'single', + title, + subtitle, + showProgress = false, + progress, + closeButtonText = 'Close', +}) => { + const {theme} = useTheme(); + const [isScanning, setIsScanning] = useState(false); + const scanSubscriptionRef = useRef(null); + + const styles = StyleSheet.create({ + modalOverlay: { + flex: 1, + backgroundColor: 'black', + }, + scannerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'black', + }, + qrFrame: { + width: 250, + height: 250, + borderWidth: 2, + borderColor: theme.colors.primary, + borderRadius: 12, + position: 'absolute', + alignSelf: 'center', + top: '25%', + }, + scannerHeader: { + position: 'absolute', + top: 80, + left: 0, + right: 0, + alignItems: 'center', + }, + scannerTitle: { + fontSize: 20, + fontWeight: '700', + color: '#FFFFFF', + marginBottom: 8, + }, + scannerSubtitle: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.7)', + textAlign: 'center', + paddingHorizontal: 20, + }, + progressBarContainer: { + marginTop: 16, + width: 200, + height: 6, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 3, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + backgroundColor: '#F7931A', + borderRadius: 3, + }, + closeScannerButton: { + position: 'absolute', + bottom: 60, + alignSelf: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 12, + }, + closeScannerButtonText: { + color: theme.colors.background, + fontSize: 16, + fontWeight: '600', + }, + cameraNotFoundContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'black', + }, + cameraNotFoundText: { + color: '#FFFFFF', + fontSize: 16, + marginBottom: 8, + }, + cameraNotFoundSubtext: { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: 14, + textAlign: 'center', + paddingHorizontal: 20, + }, + }); + + // Handle continuous scanning for Android + useEffect(() => { + if (visible && mode === 'continuous' && Platform.OS === 'android') { + // Set up event listener for continuous scanning + const {DeviceEventEmitter} = require('react-native'); + scanSubscriptionRef.current = DeviceEventEmitter.addListener( + 'BarcodeZxingScanContinuous', + (event: {data?: string; error?: string}) => { + if (event.error) { + dbg('FOSS: Continuous scan error:', event.error); + setIsScanning(false); + return; + } + if (event.data) { + dbg('FOSS: Continuous scan result:', event.data.substring(0, 50)); + onScan(event.data); + } + }, + ); + + // Start continuous scan + BarcodeZxingScan.showQrReaderContinuous((error: any, data: any) => { + if (error) { + dbg('FOSS: Continuous scan error:', error); + setIsScanning(false); + return; + } + if (data === 'SCANNER_STARTED') { + setIsScanning(true); + if (showProgress) { + setTimeout(() => { + BarcodeZxingScan.updateProgressText( + title || 'Scanning QR Code...', + ); + }, 100); + } + } + }); + } + + return () => { + if (scanSubscriptionRef.current) { + scanSubscriptionRef.current.remove(); + scanSubscriptionRef.current = null; + } + if (mode === 'continuous' && Platform.OS === 'android' && isScanning) { + BarcodeZxingScan.stopContinuousScan(); + setIsScanning(false); + } + }; + }, [visible, mode, onScan, showProgress, title, isScanning]); + + // Handle single scan + const handleSingleScan = useCallback(() => { + // Set custom status message before opening scanner (if supported) + if (subtitle && BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(subtitle); + } + + if (Platform.OS === 'android') { + BarcodeZxingScan.showQrReader((error: any, data: any) => { + // Clear custom status message + if (BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(''); + } + if (error) { + dbg('FOSS: Single scan error:', error); + return; + } + if (data) { + onScan(data); + onClose(); + } + }); + } else { + // iOS - use single scan + BarcodeZxingScan.showQrReader((error: any, data: any) => { + // Clear custom status message + if (BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(''); + } + if (error) { + dbg('FOSS: iOS scan error:', error); + return; + } + if (data) { + onScan(data); + onClose(); + } + }); + } + }, [subtitle, onScan, onClose]); + + // Auto-start single scan when modal opens + useEffect(() => { + if (visible && mode === 'single' && !isScanning) { + handleSingleScan(); + } + }, [visible, mode, isScanning, handleSingleScan]); + + const isAnimatedQR = showProgress && progress && progress.total > 1; + const progressPercent = isAnimatedQR + ? Math.min( + 100, + progress.percentage || + Math.round((progress.received / progress.total) * 100), + ) + : 0; + const isComplete = isAnimatedQR && progress.received >= progress.total; + + const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code'); + const displaySubtitle = + subtitle || + (isAnimatedQR + ? isComplete + ? 'Processing...' + : `Keep scanning animated QR: ${progressPercent}%` + : 'Point camera at the QR code to scan'); + + // For continuous mode on Android, show the scanner UI + if (mode === 'continuous' && Platform.OS === 'android' && visible) { + return ( + + + + {(title || subtitle || showProgress) && ( + + {displayTitle} + {displaySubtitle} + {isAnimatedQR && ( + + + + )} + + )} + { + if (isScanning) { + BarcodeZxingScan.stopContinuousScan(); + setIsScanning(false); + } + onClose(); + }} + activeOpacity={0.7}> + {closeButtonText} + + + + ); + } + + // For single mode, the native scanner handles UI, but we show a placeholder + // In practice, single mode opens native scanner which handles its own UI + return null; +}; + +export default QRScanner; + diff --git a/components/QRScanner.tsx b/components/QRScanner.tsx new file mode 100644 index 0000000..752ddd9 --- /dev/null +++ b/components/QRScanner.tsx @@ -0,0 +1,524 @@ +import React, {useState, useEffect, useRef} from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Modal, + Platform, +} from 'react-native'; +import {useTheme} from '../theme'; +import {dbg} from '../utils'; + +// iOS-specific imports (only imported when needed) +let Camera: any = null; +let useCameraDevice: any = null; +let useCodeScanner: any = null; + +if (Platform.OS === 'ios') { + try { + const visionCamera = require('react-native-vision-camera'); + Camera = visionCamera.Camera; + useCameraDevice = visionCamera.useCameraDevice; + useCodeScanner = visionCamera.useCodeScanner; + } catch (e) { + console.warn('Vision camera not available:', e); + } +} + +// Android-specific import +let BarcodeZxingScan: any = null; +if (Platform.OS === 'android') { + try { + BarcodeZxingScan = require('rn-barcode-zxing-scan').default; + } catch (e) { + console.warn('BarcodeZxingScan not available:', e); + } +} + +export interface QRProgress { + received: number; + total: number; + percentage?: number; +} + +export interface QRScannerProps { + visible: boolean; + onClose: () => void; + onScan: (data: string) => void; + mode?: 'single' | 'continuous'; + title?: string; + subtitle?: string; + showProgress?: boolean; + progress?: QRProgress; + closeButtonText?: string; +} + +// iOS QR Scanner Component (uses vision-camera) +const IOSQRScanner: React.FC = ({ + visible, + onClose, + onScan, + mode = 'single', + title, + subtitle, + showProgress = false, + progress, + closeButtonText = 'Close', +}) => { + const {theme} = useTheme(); + const device = useCameraDevice('back'); + const codeScanner = useCodeScanner({ + codeTypes: ['qr'], + onCodeScanned: codes => { + if (codes.length > 0 && codes[0].value) { + onScan(codes[0].value); + if (mode === 'single') { + onClose(); + } + } + }, + }); + + const styles = StyleSheet.create({ + scannerContainer: { + flex: 1, + backgroundColor: 'black', + }, + qrFrame: { + position: 'absolute', + borderWidth: 2, + borderColor: theme.colors.primary, + width: 250, + height: 250, + alignSelf: 'center', + top: '25%', + borderRadius: 12, + }, + scannerHeader: { + position: 'absolute', + top: 80, + left: 0, + right: 0, + alignItems: 'center', + }, + scannerTitle: { + fontSize: 20, + fontWeight: '700', + color: '#FFFFFF', + marginBottom: 8, + }, + scannerSubtitle: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.7)', + textAlign: 'center', + paddingHorizontal: 20, + }, + progressBarContainer: { + marginTop: 16, + width: 200, + height: 6, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 3, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + backgroundColor: '#F7931A', + borderRadius: 3, + }, + closeScannerButton: { + position: 'absolute', + bottom: 60, + alignSelf: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 12, + }, + closeScannerButtonText: { + color: theme.colors.background, + fontSize: 16, + fontWeight: '600', + }, + cameraNotFoundContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'black', + }, + cameraNotFoundText: { + color: '#FFFFFF', + fontSize: 16, + marginBottom: 8, + }, + cameraNotFoundSubtext: { + color: 'rgba(255, 255, 255, 0.7)', + fontSize: 14, + textAlign: 'center', + paddingHorizontal: 20, + }, + }); + + if (!device || !codeScanner) { + return ( + + + Camera Not Available + + Please check camera permissions in Settings + + + {closeButtonText} + + + + ); + } + + const isAnimatedQR = showProgress && progress && progress.total > 1; + const progressPercent = isAnimatedQR + ? Math.min( + 100, + progress.percentage || + Math.round((progress.received / progress.total) * 100), + ) + : 0; + const isComplete = isAnimatedQR && progress.received >= progress.total; + + const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code'); + const displaySubtitle = + subtitle || + (isAnimatedQR + ? isComplete + ? 'Processing...' + : `Keep scanning animated QR: ${progressPercent}%` + : 'Point camera at the QR code to scan'); + + return ( + + + + + {(title || subtitle || showProgress) && ( + + {displayTitle} + {displaySubtitle} + {isAnimatedQR && ( + + + + )} + + )} + + {closeButtonText} + + + + ); +}; + +// Android QR Scanner Component (uses BarcodeZxingScan) +const AndroidQRScanner: React.FC = ({ + visible, + onClose, + onScan, + mode = 'single', + title, + subtitle, + showProgress = false, + progress, + closeButtonText = 'Close', +}) => { + const {theme} = useTheme(); + const [isScanning, setIsScanning] = useState(false); + const scanSubscriptionRef = useRef(null); + const isScanningRef = useRef(false); + + const styles = StyleSheet.create({ + scannerContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'black', + }, + qrFrame: { + width: 250, + height: 250, + borderWidth: 2, + borderColor: theme.colors.primary, + borderRadius: 12, + position: 'absolute', + alignSelf: 'center', + top: '25%', + }, + scannerHeader: { + position: 'absolute', + top: 80, + left: 0, + right: 0, + alignItems: 'center', + }, + scannerTitle: { + fontSize: 20, + fontWeight: '700', + color: '#FFFFFF', + marginBottom: 8, + }, + scannerSubtitle: { + fontSize: 14, + color: 'rgba(255, 255, 255, 0.7)', + textAlign: 'center', + paddingHorizontal: 20, + }, + progressBarContainer: { + marginTop: 16, + width: 200, + height: 6, + backgroundColor: 'rgba(255, 255, 255, 0.2)', + borderRadius: 3, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + backgroundColor: '#F7931A', + borderRadius: 3, + }, + closeScannerButton: { + position: 'absolute', + bottom: 60, + alignSelf: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 12, + }, + closeScannerButtonText: { + color: theme.colors.background, + fontSize: 16, + fontWeight: '600', + }, + }); + + // Handle scanner close - properly stop scanner and cleanup + const handleClose = () => { + if (mode === 'continuous' && isScanningRef.current && BarcodeZxingScan) { + dbg('Android: Stopping continuous scan (close requested)'); + BarcodeZxingScan.stopContinuousScan(); + BarcodeZxingScan.updateProgressText(''); + isScanningRef.current = false; + setIsScanning(false); + } + if (scanSubscriptionRef.current) { + scanSubscriptionRef.current.remove(); + scanSubscriptionRef.current = null; + } + onClose(); + }; + + // Handle continuous scanning for Android + useEffect(() => { + if (visible && mode === 'continuous' && BarcodeZxingScan) { + // Only start if not already scanning + if (!isScanningRef.current) { + const {DeviceEventEmitter} = require('react-native'); + scanSubscriptionRef.current = DeviceEventEmitter.addListener( + 'BarcodeZxingScanContinuous', + (event: {data?: string; error?: string}) => { + if (event.error) { + dbg('Android: Continuous scan error:', event.error); + isScanningRef.current = false; + setIsScanning(false); + return; + } + if (event.data) { + dbg('Android: Continuous scan result:', event.data.substring(0, 50)); + onScan(event.data); + } + }, + ); + + BarcodeZxingScan.showQrReaderContinuous((error: any, data: any) => { + if (error) { + dbg('Android: Continuous scan error:', error); + isScanningRef.current = false; + setIsScanning(false); + return; + } + if (data === 'SCANNER_STARTED') { + isScanningRef.current = true; + setIsScanning(true); + if (showProgress) { + setTimeout(() => { + BarcodeZxingScan.updateProgressText( + title || 'Scanning QR Code...', + ); + }, 100); + } + } + }); + } + } else if (!visible && mode === 'continuous' && BarcodeZxingScan) { + // Stop scanner when modal becomes invisible + if (isScanningRef.current) { + dbg('Android: Stopping continuous scan (modal closed)'); + BarcodeZxingScan.stopContinuousScan(); + BarcodeZxingScan.updateProgressText(''); + isScanningRef.current = false; + setIsScanning(false); + } + if (scanSubscriptionRef.current) { + scanSubscriptionRef.current.remove(); + scanSubscriptionRef.current = null; + } + } + + return () => { + // Cleanup on unmount or when dependencies change + if (scanSubscriptionRef.current) { + scanSubscriptionRef.current.remove(); + scanSubscriptionRef.current = null; + } + if (mode === 'continuous' && isScanningRef.current && BarcodeZxingScan) { + dbg('Android: Cleanup - stopping continuous scan'); + BarcodeZxingScan.stopContinuousScan(); + BarcodeZxingScan.updateProgressText(''); + isScanningRef.current = false; + setIsScanning(false); + } + }; + }, [visible, mode, onScan, showProgress, title]); + + // Handle single scan - native scanner handles its own UI and back button + useEffect(() => { + if (visible && mode === 'single' && BarcodeZxingScan && !isScanningRef.current) { + // For single mode, native scanner opens its own activity + // Set custom status message before opening scanner (if supported) + if (subtitle && BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(subtitle); + } + + // It handles back button itself, we just need to handle the result + BarcodeZxingScan.showQrReader((error: any, data: any) => { + // Scanner closed (either via back button or scan completed) + isScanningRef.current = false; + // Clear custom status message + if (BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(''); + } + if (error) { + dbg('Android: Single scan error:', error); + // User pressed back or cancelled - close our modal too + onClose(); + return; + } + if (data) { + onScan(data); + onClose(); + } else { + // No data but no error - user likely pressed back + onClose(); + } + }); + isScanningRef.current = true; + } else if (!visible && mode === 'single') { + // Reset scanning state when modal closes + isScanningRef.current = false; + // Clear custom status message + if (BarcodeZxingScan && BarcodeZxingScan.setStatusMessage) { + BarcodeZxingScan.setStatusMessage(''); + } + } + }, [visible, mode, onScan, onClose, subtitle]); + + const isAnimatedQR = showProgress && progress && progress.total > 1; + const progressPercent = isAnimatedQR + ? Math.min( + 100, + progress.percentage || + Math.round((progress.received / progress.total) * 100), + ) + : 0; + const isComplete = isAnimatedQR && progress.received >= progress.total; + + const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code'); + const displaySubtitle = + subtitle || + (isAnimatedQR + ? isComplete + ? 'Processing...' + : `Keep scanning animated QR: ${progressPercent}%` + : 'Point camera at the QR code to scan'); + + // For continuous mode, show UI overlay + if (mode === 'continuous' && visible) { + return ( + + + + {(title || subtitle || showProgress) && ( + + {displayTitle} + {displaySubtitle} + {isAnimatedQR && ( + + + + )} + + )} + + {closeButtonText} + + + + ); + } + + // For single mode, native scanner handles UI - return null (modal is just a container) + // The native activity will handle its own UI and back button + return null; +}; + +// Main QRScanner component - routes to platform-specific implementation +const QRScanner: React.FC = (props) => { + if (Platform.OS === 'ios') { + return ; + } else { + return ; + } +}; + +export default QRScanner; diff --git a/components/Styles.tsx b/components/Styles.tsx index f50ee88..1c49c2e 100644 --- a/components/Styles.tsx +++ b/components/Styles.tsx @@ -195,8 +195,8 @@ export interface Styles { keyshareDetailValue: TextStyle; keyshareBadge: ViewStyle; keyshareBadgeText: TextStyle; - keyshareBadgeFlexi: ViewStyle; - keyshareBadgeBasic: ViewStyle; + keyshareBadgeTrio: ViewStyle; + keyshareBadgeDuo: ViewStyle; keyshareStatusBadge: ViewStyle; keyshareStatusBadgeText: TextStyle; keyshareStatusBadgeSuccess: ViewStyle; @@ -1285,10 +1285,10 @@ export const createStyles = (theme: Theme): Styles => ({ fontWeight: '600' as const, color: '#FFFFFF', }, - keyshareBadgeFlexi: { + keyshareBadgeTrio: { backgroundColor: theme.colors.primary, }, - keyshareBadgeBasic: { + keyshareBadgeDuo: { backgroundColor: theme.colors.secondary , }, keyshareStatusBadge: { diff --git a/components/TransportModeSelector.tsx b/components/TransportModeSelector.tsx index 7ec4e3b..0933e00 100644 --- a/components/TransportModeSelector.tsx +++ b/components/TransportModeSelector.tsx @@ -6,9 +6,11 @@ import { TouchableOpacity, Modal, Image, + ScrollView, } from 'react-native'; +import QRCode from 'react-native-qrcode-svg'; import {useTheme} from '../theme'; -import {HapticFeedback} from '../utils'; +import {HapticFeedback, encodeSendBitcoinQR} from '../utils'; interface TransportModeSelectorProps { visible: boolean; @@ -16,6 +18,14 @@ interface TransportModeSelectorProps { onSelect: (transport: 'local' | 'nostr') => void; title?: string; description?: string; + // Optional: Show QR code for send bitcoin data (only on device 1, not when scanned) + sendBitcoinData?: { + toAddress: string; + amountSats: string; + feeSats: string; + spendingHash?: string; + } | null; + showQRCode?: boolean; // Whether to show QR code (false when data came from scan) } const TransportModeSelector: React.FC = ({ @@ -24,6 +34,8 @@ const TransportModeSelector: React.FC = ({ onSelect, title = 'Transport Method', description = 'Choose how to connect with other devices', + sendBitcoinData = null, + showQRCode = true, }) => { const {theme} = useTheme(); const [selectedTransport, setSelectedTransport] = useState<'local' | 'nostr' | null>(null); @@ -107,7 +119,7 @@ const TransportModeSelector: React.FC = ({ modalDescription: { fontSize: 13, color: theme.colors.textSecondary, - marginBottom: 16, + marginBottom: 12, textAlign: 'left', fontWeight: '500', }, @@ -118,9 +130,9 @@ const TransportModeSelector: React.FC = ({ }, transportOptionCard: { borderRadius: 12, - paddingTop: 16, - paddingBottom: 12, - paddingHorizontal: 12, + paddingTop: 12, + paddingBottom: 10, + paddingHorizontal: 10, borderWidth: 1.5, borderColor: theme.colors.border + '40', backgroundColor: theme.colors.white, @@ -141,7 +153,7 @@ const TransportModeSelector: React.FC = ({ backgroundColor: 'transparent', }, transportOptionIconWrapper: { - marginBottom: 8, + marginBottom: 6, alignItems: 'center', justifyContent: 'center', backgroundColor: 'transparent', @@ -194,9 +206,9 @@ const TransportModeSelector: React.FC = ({ color: theme.colors.textSecondary, }, transportSelectedHint: { - marginTop: 14, + marginTop: 12, backgroundColor: theme.colors.white, - padding: 12, + padding: 10, borderRadius: 12, borderWidth: 1, borderColor: theme.colors.border, @@ -225,7 +237,7 @@ const TransportModeSelector: React.FC = ({ fontWeight: '700', }, continueButton: { - marginTop: 16, + marginTop: 12, borderRadius: 12, paddingVertical: 14, alignItems: 'center', @@ -239,6 +251,37 @@ const TransportModeSelector: React.FC = ({ fontSize: 16, fontWeight: '600', }, + qrCodeSection: { + marginTop: 12, + marginBottom: 24, + padding: 16, + backgroundColor: theme.colors.white, + borderRadius: 12, + borderWidth: 1, + borderColor: theme.colors.border, + alignItems: 'center', + }, + qrCodeLabel: { + fontSize: 13, + fontWeight: '600', + color: theme.colors.text, + marginBottom: 12, + textAlign: 'center', + lineHeight: 18, + }, + qrCodeSubLabel: { + fontSize: 11, + fontWeight: '400', + color: theme.colors.textSecondary, + marginTop: 4, + textAlign: 'center', + lineHeight: 15, + }, + qrCodeContainer: { + backgroundColor: 'white', + padding: 12, + borderRadius: 8, + }, }); return ( @@ -269,11 +312,39 @@ const TransportModeSelector: React.FC = ({ {/* Modal Body */} - - {description && ( + + {description && description.length > 0 && ( {description} )} + {/* QR Code Section - Only show if sendBitcoinData exists and showQRCode is true */} + {sendBitcoinData && showQRCode && (() => { + const qrData = encodeSendBitcoinQR( + sendBitcoinData.toAddress, + sendBitcoinData.amountSats, + sendBitcoinData.feeSats, + sendBitcoinData.spendingHash || '', + ); + return ( + + + Quick Shortcut for Other Devices + + + Scan this QR code on other devices to{'\n'}automatically enter address and amount + + + + + + ); + })()} + {/* Local WiFi/Hotspot Option */} = ({ {/* Selected Transport Hint */} - {selectedTransport && ( + {selectedTransport && description && description.length > 0 && ( = ({ Continue → - + diff --git a/components/useQRScanner.ts b/components/useQRScanner.ts new file mode 100644 index 0000000..c689f20 --- /dev/null +++ b/components/useQRScanner.ts @@ -0,0 +1,80 @@ +import {useCallback, useState} from 'react'; +import {Platform} from 'react-native'; +import BarcodeZxingScan from 'rn-barcode-zxing-scan'; +import {dbg} from '../utils'; + +export interface UseQRScannerOptions { + onScan: (data: string) => void; + mode?: 'single' | 'continuous'; + onError?: (error: any) => void; +} + +export interface UseQRScannerReturn { + isScanning: boolean; + startScan: () => void; + stopScan: () => void; +} + +/** + * Hook for handling QR scanning logic + * Works with both vision-camera (iOS in .tsx) and BarcodeZxingScan (Android/FOSS) + */ +export const useQRScanner = ( + options: UseQRScannerOptions, +): UseQRScannerReturn => { + const {onScan, mode = 'single', onError} = options; + const [isScanning, setIsScanning] = useState(false); + + const handleScanResult = useCallback( + (data: string) => { + if (data) { + dbg('QR Scanner: Scanned data:', data.substring(0, 50)); + onScan(data); + if (mode === 'single') { + setIsScanning(false); + } + } + }, + [onScan, mode], + ); + + const startScan = useCallback(() => { + if (isScanning) { + dbg('QR Scanner: Already scanning, ignoring'); + return; + } + + setIsScanning(true); + + if (Platform.OS === 'android' && mode === 'continuous') { + // Continuous scanning for Android (handled by component) + // This is mainly for reference - actual implementation in QRScanner.foss.tsx + dbg('QR Scanner: Starting continuous scan (Android)'); + } else { + // Single scan - handled by native scanner + BarcodeZxingScan.showQrReader((error: any, data: any) => { + setIsScanning(false); + if (error) { + dbg('QR Scanner: Scan error:', error); + onError?.(error); + return; + } + handleScanResult(data); + }); + } + }, [isScanning, mode, handleScanResult, onError]); + + const stopScan = useCallback(() => { + if (Platform.OS === 'android' && mode === 'continuous') { + BarcodeZxingScan.stopQrReader(); + } + setIsScanning(false); + }, [mode]); + + return { + isScanning, + startScan, + stopScan, + }; +}; + diff --git a/context/UserContext.tsx b/context/UserContext.tsx index b9f4fdc..e80bbf0 100644 --- a/context/UserContext.tsx +++ b/context/UserContext.tsx @@ -117,7 +117,7 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({children}) try { // Load address type const storedType = (await LocalCache.getItem('addressType')) as AddressType | null; - const currentAddressType = (storedType as AddressType) || 'legacy'; + const currentAddressType = (storedType as AddressType) || 'segwit-native'; setActiveAddressTypeState(currentAddressType); // Load/derive btcPub diff --git a/context/WalletContext.tsx b/context/WalletContext.tsx index fa93818..a46cc37 100644 --- a/context/WalletContext.tsx +++ b/context/WalletContext.tsx @@ -59,7 +59,7 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({ // Get current address type for path calculation const storedAddressType = await LocalCache.getItem('addressType'); - const currentAddressType = (storedAddressType as string) || 'legacy'; + const currentAddressType = (storedAddressType as string) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); const path = getDerivePathForNetwork(net, currentAddressType, useLegacyPath); diff --git a/ios/BoldWallet.xcodeproj/project.pbxproj b/ios/BoldWallet.xcodeproj/project.pbxproj index 2bd2b1e..d7fcda3 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 = 35; + CURRENT_PROJECT_VERSION = 37; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_BITCODE = NO; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.4; 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 = 35; + CURRENT_PROJECT_VERSION = 37; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_TESTABILITY = NO; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.1.2; + MARKETING_VERSION = 2.1.4; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", diff --git a/ios/Tss.xcframework/Info.plist b/ios/Tss.xcframework/Info.plist index c81fd9d..6ff0575 100644 --- a/ios/Tss.xcframework/Info.plist +++ b/ios/Tss.xcframework/Info.plist @@ -8,21 +8,24 @@ BinaryPath Tss.framework/Tss LibraryIdentifier - ios-arm64 + ios-arm64_x86_64-simulator LibraryPath Tss.framework SupportedArchitectures arm64 + x86_64 SupportedPlatform ios + SupportedPlatformVariant + simulator BinaryPath - Tss.framework/Tss + Tss.framework/Versions/A/Tss LibraryIdentifier - ios-arm64_x86_64-simulator + macos-arm64_x86_64 LibraryPath Tss.framework SupportedArchitectures @@ -31,24 +34,21 @@ x86_64 SupportedPlatform - ios - SupportedPlatformVariant - simulator + macos BinaryPath - Tss.framework/Versions/A/Tss + Tss.framework/Tss LibraryIdentifier - macos-arm64_x86_64 + ios-arm64 LibraryPath Tss.framework SupportedArchitectures arm64 - x86_64 SupportedPlatform - macos + ios CFBundlePackageType diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h b/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h index 0a50558..723ad0f 100644 --- a/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h +++ b/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h @@ -270,6 +270,8 @@ // skipped field UTXO.Vout with unsupported type: uint32 @property (nonatomic) int64_t value; +// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""} + @end // skipped const MaxUint32 with unsupported type: uint32 diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist index c08298e..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.1765909426 + 0.0.1767079533 CFBundleVersion - 0.0.1765909426 + 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 dade8fb..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/Headers/Tss.objc.h b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h index 0a50558..723ad0f 100644 --- a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h +++ b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h @@ -270,6 +270,8 @@ // skipped field UTXO.Vout with unsupported type: uint32 @property (nonatomic) int64_t value; +// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""} + @end // skipped const MaxUint32 with unsupported type: uint32 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 c08298e..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.1765909426 + 0.0.1767079533 CFBundleVersion - 0.0.1765909426 + 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 127c3ca..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/Headers/Tss.objc.h b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h index 0a50558..723ad0f 100644 --- a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h +++ b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h @@ -270,6 +270,8 @@ // skipped field UTXO.Vout with unsupported type: uint32 @property (nonatomic) int64_t value; +// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""} + @end // skipped const MaxUint32 with unsupported type: uint32 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 c08298e..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.1765909426 + 0.0.1767079533 CFBundleVersion - 0.0.1765909426 + 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 89faaee..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/package.json b/package.json index 43b988f..04d064f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boldwallet", - "version": "2.1.2", + "version": "2.1.3", "private": true, "scripts": { "android": "react-native run-android", diff --git a/patches/rn-barcode-zxing-scan+1.0.4.patch b/patches/rn-barcode-zxing-scan+1.0.4.patch index 39070d4..800b381 100644 --- a/patches/rn-barcode-zxing-scan+1.0.4.patch +++ b/patches/rn-barcode-zxing-scan+1.0.4.patch @@ -167,7 +167,7 @@ index 0000000..53a174a \ No newline at end of file diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json new file mode 100644 -index 0000000..c03fd85 +index 0000000..99bfe68 --- /dev/null +++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json @@ -0,0 +1,18 @@ @@ -178,54 +178,7 @@ index 0000000..c03fd85 + "kind": "Directory" + }, + "applicationId": "com.reactlibrary", -+ "variantName": "release", -+ "elements": [ -+ { -+ "type": "SINGLE", -+ "filters": [], -+ "attributes": [], -+ "outputFile": "AndroidManifest.xml" -+ } -+ ], -+ "elementType": "File" -+} -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml -new file mode 100644 -index 0000000..53a174a ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml -@@ -0,0 +1,15 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json -new file mode 100644 -index 0000000..c03fd85 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json -@@ -0,0 +1,18 @@ -+{ -+ "version": 3, -+ "artifactType": { -+ "type": "AAPT_FRIENDLY_MERGED_MANIFESTS", -+ "kind": "Directory" -+ }, -+ "applicationId": "com.reactlibrary", -+ "variantName": "release", ++ "variantName": "debug", + "elements": [ + { + "type": "SINGLE", @@ -236,1691 +189,434 @@ index 0000000..c03fd85 + ], + "elementType": "File" +} -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar -new file mode 100644 -index 0000000..dea64bd -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties -new file mode 100644 -index 0000000..1211b1e ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties -@@ -0,0 +1,6 @@ -+aarFormatVersion=1.0 -+aarMetadataVersion=1.0 -+minCompileSdk=1 -+minCompileSdkExtension=0 -+minAndroidGradlePluginVersion=1.0.0 -+coreLibraryDesugaringEnabled=false -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties -new file mode 100644 -index 0000000..1211b1e ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties -@@ -0,0 +1,6 @@ -+aarFormatVersion=1.0 -+aarMetadataVersion=1.0 -+minCompileSdk=1 -+minCompileSdkExtension=0 -+minAndroidGradlePluginVersion=1.0.0 -+coreLibraryDesugaringEnabled=false -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json -new file mode 100644 -index 0000000..9e26dfe ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json -@@ -0,0 +1 @@ -+{} -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json -new file mode 100644 -index 0000000..9e26dfe ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json -@@ -0,0 +1 @@ -+{} -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt -new file mode 100644 -index 0000000..e69de29 -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar -new file mode 100644 -index 0000000..7e41b57 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar -new file mode 100644 -index 0000000..15a3ae0 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar -new file mode 100644 -index 0000000..89780f8 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar -new file mode 100644 -index 0000000..89780f8 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt -new file mode 100644 -index 0000000..e69de29 -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt -new file mode 100644 -index 0000000..e69de29 -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 -new file mode 100644 -index 0000000..5a3e3a5 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 -@@ -0,0 +1,89 @@ -+# This is a configuration file for ProGuard. -+# http://proguard.sourceforge.net/index.html#manual/usage.html -+# -+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with -+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and -+# will be ignored by new version of the Android plugin for Gradle. -+ -+# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file -+# instead of this one, which turns off the optimization flags. -+-allowaccessmodification -+ -+# Preserve some attributes that may be required for reflection. -+-keepattributes AnnotationDefault, -+ EnclosingMethod, -+ InnerClasses, -+ RuntimeVisibleAnnotations, -+ RuntimeVisibleParameterAnnotations, -+ RuntimeVisibleTypeAnnotations, -+ Signature -+ -+-keep public class com.google.vending.licensing.ILicensingService -+-keep public class com.android.vending.licensing.ILicensingService -+-keep public class com.google.android.vending.licensing.ILicensingService -+-dontnote com.android.vending.licensing.ILicensingService -+-dontnote com.google.vending.licensing.ILicensingService -+-dontnote com.google.android.vending.licensing.ILicensingService -+ -+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native -+-keepclasseswithmembernames,includedescriptorclasses class * { -+ native ; -+} -+ -+# Keep setters in Views so that animations can still work. -+-keepclassmembers public class * extends android.view.View { -+ void set*(***); -+ *** get*(); -+} -+ -+# We want to keep methods in Activity that could be used in the XML attribute onClick. -+-keepclassmembers class * extends android.app.Activity { -+ public void *(android.view.View); -+} -+ -+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations -+-keepclassmembers enum * { -+ public static **[] values(); -+ public static ** valueOf(java.lang.String); -+} -+ -+-keepclassmembers class * implements android.os.Parcelable { -+ public static final ** CREATOR; -+} -+ -+# Preserve annotated Javascript interface methods. -+-keepclassmembers class * { -+ @android.webkit.JavascriptInterface ; -+} -+ -+# The support libraries contains references to newer platform versions. -+# Don't warn about those in case this app is linking against an older -+# platform version. We know about them, and they are safe. -+-dontnote android.support.** -+-dontnote androidx.** -+-dontwarn android.support.** -+-dontwarn androidx.** -+ -+# Understand the @Keep support annotation. -+-keep class android.support.annotation.Keep -+ -+-keep @android.support.annotation.Keep class * {*;} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep (...); -+} -+ -+# These classes are duplicated between android.jar and org.apache.http.legacy.jar. -+-dontnote org.apache.http.** -+-dontnote android.net.http.** -+ -+# These classes are duplicated between android.jar and core-lambda-stubs.jar. -+-dontnote java.lang.invoke.** -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 -new file mode 100644 -index 0000000..6f7e4ef ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 -@@ -0,0 +1,95 @@ -+# This is a configuration file for ProGuard. -+# http://proguard.sourceforge.net/index.html#manual/usage.html -+# -+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with -+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and -+# will be ignored by new version of the Android plugin for Gradle. -+ -+# Optimization is turned off by default. Dex does not like code run -+# through the ProGuard optimize steps (and performs some -+# of these optimizations on its own). -+# Note that if you want to enable optimization, you cannot just -+# include optimization flags in your own project configuration file; -+# instead you will need to point to the -+# "proguard-android-optimize.txt" file instead of this one from your -+# project.properties file. -+-dontoptimize -+ -+# Preserve some attributes that may be required for reflection. -+-keepattributes AnnotationDefault, -+ EnclosingMethod, -+ InnerClasses, -+ RuntimeVisibleAnnotations, -+ RuntimeVisibleParameterAnnotations, -+ RuntimeVisibleTypeAnnotations, -+ Signature -+ -+-keep public class com.google.vending.licensing.ILicensingService -+-keep public class com.android.vending.licensing.ILicensingService -+-keep public class com.google.android.vending.licensing.ILicensingService -+-dontnote com.android.vending.licensing.ILicensingService -+-dontnote com.google.vending.licensing.ILicensingService -+-dontnote com.google.android.vending.licensing.ILicensingService -+ -+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native -+-keepclasseswithmembernames,includedescriptorclasses class * { -+ native ; -+} -+ -+# Keep setters in Views so that animations can still work. -+-keepclassmembers public class * extends android.view.View { -+ void set*(***); -+ *** get*(); -+} -+ -+# We want to keep methods in Activity that could be used in the XML attribute onClick. -+-keepclassmembers class * extends android.app.Activity { -+ public void *(android.view.View); -+} -+ -+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations -+-keepclassmembers enum * { -+ public static **[] values(); -+ public static ** valueOf(java.lang.String); -+} -+ -+-keepclassmembers class * implements android.os.Parcelable { -+ public static final ** CREATOR; -+} -+ -+# Preserve annotated Javascript interface methods. -+-keepclassmembers class * { -+ @android.webkit.JavascriptInterface ; -+} -+ -+# The support libraries contains references to newer platform versions. -+# Don't warn about those in case this app is linking against an older -+# platform version. We know about them, and they are safe. -+-dontnote android.support.** -+-dontnote androidx.** -+-dontwarn android.support.** -+-dontwarn androidx.** -+ -+# Understand the @Keep support annotation. -+-keep class android.support.annotation.Keep -+ -+-keep @android.support.annotation.Keep class * {*;} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep (...); -+} -+ -+# These classes are duplicated between android.jar and org.apache.http.legacy.jar. -+-dontnote org.apache.http.** -+-dontnote android.net.http.** -+ -+# These classes are duplicated between android.jar and core-lambda-stubs.jar. -+-dontnote java.lang.invoke.** -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 -new file mode 100644 -index 0000000..7bbb228 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 -@@ -0,0 +1,89 @@ -+# This is a configuration file for ProGuard. -+# http://proguard.sourceforge.net/index.html#manual/usage.html -+# -+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with -+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and -+# will be ignored by new version of the Android plugin for Gradle. -+ -+# Optimizations can be turned on and off in the 'postProcessing' DSL block. -+# The configuration below is applied if optimizations are enabled. -+-allowaccessmodification -+ -+# Preserve some attributes that may be required for reflection. -+-keepattributes AnnotationDefault, -+ EnclosingMethod, -+ InnerClasses, -+ RuntimeVisibleAnnotations, -+ RuntimeVisibleParameterAnnotations, -+ RuntimeVisibleTypeAnnotations, -+ Signature -+ -+-keep public class com.google.vending.licensing.ILicensingService -+-keep public class com.android.vending.licensing.ILicensingService -+-keep public class com.google.android.vending.licensing.ILicensingService -+-dontnote com.android.vending.licensing.ILicensingService -+-dontnote com.google.vending.licensing.ILicensingService -+-dontnote com.google.android.vending.licensing.ILicensingService -+ -+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native -+-keepclasseswithmembernames,includedescriptorclasses class * { -+ native ; -+} -+ -+# Keep setters in Views so that animations can still work. -+-keepclassmembers public class * extends android.view.View { -+ void set*(***); -+ *** get*(); -+} -+ -+# We want to keep methods in Activity that could be used in the XML attribute onClick. -+-keepclassmembers class * extends android.app.Activity { -+ public void *(android.view.View); -+} -+ -+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations -+-keepclassmembers enum * { -+ public static **[] values(); -+ public static ** valueOf(java.lang.String); -+} -+ -+-keepclassmembers class * implements android.os.Parcelable { -+ public static final ** CREATOR; -+} -+ -+# Preserve annotated Javascript interface methods. -+-keepclassmembers class * { -+ @android.webkit.JavascriptInterface ; -+} -+ -+# The support libraries contains references to newer platform versions. -+# Don't warn about those in case this app is linking against an older -+# platform version. We know about them, and they are safe. -+-dontnote android.support.** -+-dontnote androidx.** -+-dontwarn android.support.** -+-dontwarn androidx.** -+ -+# Understand the @Keep support annotation. -+-keep class android.support.annotation.Keep -+ -+-keep @android.support.annotation.Keep class * {*;} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep ; -+} -+ -+-keepclasseswithmembers class * { -+ @android.support.annotation.Keep (...); -+} -+ -+# These classes are duplicated between android.jar and org.apache.http.legacy.jar. -+-dontnote org.apache.http.** -+-dontnote android.net.http.** -+ -+# These classes are duplicated between android.jar and core-lambda-stubs.jar. -+-dontnote java.lang.invoke.** -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar -new file mode 100644 -index 0000000..86eed0b -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties -new file mode 100644 -index 0000000..02d5733 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties -@@ -0,0 +1 @@ -+#Mon Dec 08 17:57:07 GST 2025 -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml -new file mode 100644 -index 0000000..9ac068c ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml -@@ -0,0 +1,2 @@ -+ -+ -\ No newline at end of file -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml -new file mode 100644 -index 0000000..385fb17 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml -@@ -0,0 +1,18 @@ -+ -+ -+ -+ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml -new file mode 100644 -index 0000000..b01967a ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml -@@ -0,0 +1,440 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml -new file mode 100644 -index 0000000..fa93e7f ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml -@@ -0,0 +1,791 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml +\ No newline at end of file +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml new file mode 100644 -index 0000000..a69c168 +index 0000000..53a174a --- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml -@@ -0,0 +1,31 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml +@@ -0,0 +1,15 @@ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ +\ No newline at end of file +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json +new file mode 100644 +index 0000000..c03fd85 +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json +@@ -0,0 +1,18 @@ ++{ ++ "version": 3, ++ "artifactType": { ++ "type": "AAPT_FRIENDLY_MERGED_MANIFESTS", ++ "kind": "Directory" ++ }, ++ "applicationId": "com.reactlibrary", ++ "variantName": "release", ++ "elements": [ ++ { ++ "type": "SINGLE", ++ "filters": [], ++ "attributes": [], ++ "outputFile": "AndroidManifest.xml" ++ } ++ ], ++ "elementType": "File" ++} +\ No newline at end of file +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar +new file mode 100644 +index 0000000..dea64bd +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +new file mode 100644 +index 0000000..1211b1e +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties +@@ -0,0 +1,6 @@ ++aarFormatVersion=1.0 ++aarMetadataVersion=1.0 ++minCompileSdk=1 ++minCompileSdkExtension=0 ++minAndroidGradlePluginVersion=1.0.0 ++coreLibraryDesugaringEnabled=false +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties +new file mode 100644 +index 0000000..1211b1e +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties +@@ -0,0 +1,6 @@ ++aarFormatVersion=1.0 ++aarMetadataVersion=1.0 ++minCompileSdk=1 ++minCompileSdkExtension=0 ++minAndroidGradlePluginVersion=1.0.0 ++coreLibraryDesugaringEnabled=false +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +new file mode 100644 +index 0000000..9e26dfe +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json +@@ -0,0 +1 @@ ++{} +\ No newline at end of file +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json +new file mode 100644 +index 0000000..9e26dfe +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json +@@ -0,0 +1 @@ ++{} +\ No newline at end of file +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt +new file mode 100644 +index 0000000..e69de29 +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +new file mode 100644 +index 0000000..7e41b57 +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar +new file mode 100644 +index 0000000..15a3ae0 +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar +new file mode 100644 +index 0000000..89780f8 +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar +new file mode 100644 +index 0000000..89780f8 +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt +new file mode 100644 +index 0000000..e69de29 +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt +new file mode 100644 +index 0000000..e69de29 +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 +new file mode 100644 +index 0000000..5a3e3a5 +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 +@@ -0,0 +1,89 @@ ++# This is a configuration file for ProGuard. ++# http://proguard.sourceforge.net/index.html#manual/usage.html ++# ++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with ++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and ++# will be ignored by new version of the Android plugin for Gradle. ++ ++# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file ++# instead of this one, which turns off the optimization flags. ++-allowaccessmodification ++ ++# Preserve some attributes that may be required for reflection. ++-keepattributes AnnotationDefault, ++ EnclosingMethod, ++ InnerClasses, ++ RuntimeVisibleAnnotations, ++ RuntimeVisibleParameterAnnotations, ++ RuntimeVisibleTypeAnnotations, ++ Signature ++ ++-keep public class com.google.vending.licensing.ILicensingService ++-keep public class com.android.vending.licensing.ILicensingService ++-keep public class com.google.android.vending.licensing.ILicensingService ++-dontnote com.android.vending.licensing.ILicensingService ++-dontnote com.google.vending.licensing.ILicensingService ++-dontnote com.google.android.vending.licensing.ILicensingService ++ ++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native ++-keepclasseswithmembernames,includedescriptorclasses class * { ++ native ; ++} ++ ++# Keep setters in Views so that animations can still work. ++-keepclassmembers public class * extends android.view.View { ++ void set*(***); ++ *** get*(); ++} ++ ++# We want to keep methods in Activity that could be used in the XML attribute onClick. ++-keepclassmembers class * extends android.app.Activity { ++ public void *(android.view.View); ++} ++ ++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations ++-keepclassmembers enum * { ++ public static **[] values(); ++ public static ** valueOf(java.lang.String); ++} ++ ++-keepclassmembers class * implements android.os.Parcelable { ++ public static final ** CREATOR; ++} ++ ++# Preserve annotated Javascript interface methods. ++-keepclassmembers class * { ++ @android.webkit.JavascriptInterface ; ++} ++ ++# The support libraries contains references to newer platform versions. ++# Don't warn about those in case this app is linking against an older ++# platform version. We know about them, and they are safe. ++-dontnote android.support.** ++-dontnote androidx.** ++-dontwarn android.support.** ++-dontwarn androidx.** ++ ++# Understand the @Keep support annotation. ++-keep class android.support.annotation.Keep ++ ++-keep @android.support.annotation.Keep class * {*;} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep (...); ++} ++ ++# These classes are duplicated between android.jar and org.apache.http.legacy.jar. ++-dontnote org.apache.http.** ++-dontnote android.net.http.** ++ ++# These classes are duplicated between android.jar and core-lambda-stubs.jar. ++-dontnote java.lang.invoke.** +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 +new file mode 100644 +index 0000000..6f7e4ef +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 +@@ -0,0 +1,95 @@ ++# This is a configuration file for ProGuard. ++# http://proguard.sourceforge.net/index.html#manual/usage.html ++# ++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with ++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and ++# will be ignored by new version of the Android plugin for Gradle. ++ ++# Optimization is turned off by default. Dex does not like code run ++# through the ProGuard optimize steps (and performs some ++# of these optimizations on its own). ++# Note that if you want to enable optimization, you cannot just ++# include optimization flags in your own project configuration file; ++# instead you will need to point to the ++# "proguard-android-optimize.txt" file instead of this one from your ++# project.properties file. ++-dontoptimize ++ ++# Preserve some attributes that may be required for reflection. ++-keepattributes AnnotationDefault, ++ EnclosingMethod, ++ InnerClasses, ++ RuntimeVisibleAnnotations, ++ RuntimeVisibleParameterAnnotations, ++ RuntimeVisibleTypeAnnotations, ++ Signature ++ ++-keep public class com.google.vending.licensing.ILicensingService ++-keep public class com.android.vending.licensing.ILicensingService ++-keep public class com.google.android.vending.licensing.ILicensingService ++-dontnote com.android.vending.licensing.ILicensingService ++-dontnote com.google.vending.licensing.ILicensingService ++-dontnote com.google.android.vending.licensing.ILicensingService ++ ++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native ++-keepclasseswithmembernames,includedescriptorclasses class * { ++ native ; ++} ++ ++# Keep setters in Views so that animations can still work. ++-keepclassmembers public class * extends android.view.View { ++ void set*(***); ++ *** get*(); ++} ++ ++# We want to keep methods in Activity that could be used in the XML attribute onClick. ++-keepclassmembers class * extends android.app.Activity { ++ public void *(android.view.View); ++} ++ ++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations ++-keepclassmembers enum * { ++ public static **[] values(); ++ public static ** valueOf(java.lang.String); ++} ++ ++-keepclassmembers class * implements android.os.Parcelable { ++ public static final ** CREATOR; ++} ++ ++# Preserve annotated Javascript interface methods. ++-keepclassmembers class * { ++ @android.webkit.JavascriptInterface ; ++} ++ ++# The support libraries contains references to newer platform versions. ++# Don't warn about those in case this app is linking against an older ++# platform version. We know about them, and they are safe. ++-dontnote android.support.** ++-dontnote androidx.** ++-dontwarn android.support.** ++-dontwarn androidx.** ++ ++# Understand the @Keep support annotation. ++-keep class android.support.annotation.Keep ++ ++-keep @android.support.annotation.Keep class * {*;} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep (...); ++} ++ ++# These classes are duplicated between android.jar and org.apache.http.legacy.jar. ++-dontnote org.apache.http.** ++-dontnote android.net.http.** ++ ++# These classes are duplicated between android.jar and core-lambda-stubs.jar. ++-dontnote java.lang.invoke.** +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 +new file mode 100644 +index 0000000..7bbb228 +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 +@@ -0,0 +1,89 @@ ++# This is a configuration file for ProGuard. ++# http://proguard.sourceforge.net/index.html#manual/usage.html ++# ++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with ++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and ++# will be ignored by new version of the Android plugin for Gradle. ++ ++# Optimizations can be turned on and off in the 'postProcessing' DSL block. ++# The configuration below is applied if optimizations are enabled. ++-allowaccessmodification ++ ++# Preserve some attributes that may be required for reflection. ++-keepattributes AnnotationDefault, ++ EnclosingMethod, ++ InnerClasses, ++ RuntimeVisibleAnnotations, ++ RuntimeVisibleParameterAnnotations, ++ RuntimeVisibleTypeAnnotations, ++ Signature ++ ++-keep public class com.google.vending.licensing.ILicensingService ++-keep public class com.android.vending.licensing.ILicensingService ++-keep public class com.google.android.vending.licensing.ILicensingService ++-dontnote com.android.vending.licensing.ILicensingService ++-dontnote com.google.vending.licensing.ILicensingService ++-dontnote com.google.android.vending.licensing.ILicensingService ++ ++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native ++-keepclasseswithmembernames,includedescriptorclasses class * { ++ native ; ++} ++ ++# Keep setters in Views so that animations can still work. ++-keepclassmembers public class * extends android.view.View { ++ void set*(***); ++ *** get*(); ++} ++ ++# We want to keep methods in Activity that could be used in the XML attribute onClick. ++-keepclassmembers class * extends android.app.Activity { ++ public void *(android.view.View); ++} ++ ++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations ++-keepclassmembers enum * { ++ public static **[] values(); ++ public static ** valueOf(java.lang.String); ++} ++ ++-keepclassmembers class * implements android.os.Parcelable { ++ public static final ** CREATOR; ++} ++ ++# Preserve annotated Javascript interface methods. ++-keepclassmembers class * { ++ @android.webkit.JavascriptInterface ; ++} ++ ++# The support libraries contains references to newer platform versions. ++# Don't warn about those in case this app is linking against an older ++# platform version. We know about them, and they are safe. ++-dontnote android.support.** ++-dontnote androidx.** ++-dontwarn android.support.** ++-dontwarn androidx.** ++ ++# Understand the @Keep support annotation. ++-keep class android.support.annotation.Keep ++ ++-keep @android.support.annotation.Keep class * {*;} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep ; ++} ++ ++-keepclasseswithmembers class * { ++ @android.support.annotation.Keep (...); ++} ++ ++# These classes are duplicated between android.jar and org.apache.http.legacy.jar. ++-dontnote org.apache.http.** ++-dontnote android.net.http.** ++ ++# These classes are duplicated between android.jar and core-lambda-stubs.jar. ++-dontnote java.lang.invoke.** +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar +new file mode 100644 +index 0000000..86eed0b +Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar differ +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +new file mode 100644 +index 0000000..8822d04 +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties +@@ -0,0 +1 @@ ++#Mon Dec 29 15:53:58 EET 2025 +diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +new file mode 100644 +index 0000000..9ac068c +--- /dev/null ++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml +@@ -0,0 +1,2 @@ ++ ++ +\ No newline at end of file diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/mergeDebugAssets/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/mergeDebugAssets/merger.xml new file mode 100644 index 0000000..8168c4d @@ -16841,376 +15537,6 @@ diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/java new file mode 100644 index 0000000..a140876 Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes/com/reactlibrary/ContinuousCaptureActivity.class differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt -new file mode 100644 -index 0000000..b6ac95c ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt -@@ -0,0 +1 @@ -+Cache for Android Lint31.12.0 -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml -new file mode 100644 -index 0000000..fcd42f5 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml -@@ -0,0 +1,22 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml -new file mode 100644 -index 0000000..cc5b531 ---- /dev/null -+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml -@@ -0,0 +1,321 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin -new file mode 100644 -index 0000000..0d21700 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin differ -diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz -new file mode 100644 -index 0000000..9bfdca2 -Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz differ diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint_model/release/generateReleaseLintModel/module.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint_model/release/generateReleaseLintModel/module.xml new file mode 100644 index 0000000..385fb17 @@ -54655,7 +52981,7 @@ new file mode 100644 index 0000000..b33df0f Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/tmp/compileReleaseJavaWithJavac/previous-compilation-data.bin differ diff --git a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java -index fe94d5b..23dcf0d 100644 +index fe94d5b..23f1314 100644 --- a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java +++ b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java @@ -4,11 +4,14 @@ import android.app.Activity; @@ -54673,13 +52999,14 @@ index fe94d5b..23dcf0d 100644 import com.google.zxing.integration.android.IntentIntegrator; import com.google.zxing.integration.android.IntentResult; -@@ -16,11 +19,17 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement +@@ -16,11 +19,18 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement private final ReactApplicationContext reactContext; private Callback mCallback; + private static final Object callbackLock = new Object(); + private static String lastProcessedText = null; + private static ReactApplicationContext staticReactContext = null; ++ private static String customStatusMessage = null; public BarcodeZxingScanModule(ReactApplicationContext reactContext) { super(reactContext); @@ -54691,7 +53018,20 @@ index fe94d5b..23dcf0d 100644 } @Override -@@ -54,6 +63,81 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement +@@ -47,13 +57,106 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement + integrator.setOrientationLocked(true); + integrator.setBeepEnabled(false); // Disable beep sound + integrator.setCaptureActivity(ContinuousCaptureActivity.class); +- integrator.initiateScan(); ++ // Pass custom status message via Intent ++ Intent intent = integrator.createScanIntent(); ++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) { ++ intent.putExtra("CUSTOM_STATUS_MESSAGE", customStatusMessage); ++ } ++ currentActivity.startActivityForResult(intent, IntentIntegrator.REQUEST_CODE); + reactContext.addActivityEventListener(this); // Ensure we handle the result + } else { + callback.invoke("Error: Activity is null"); } } @@ -54713,6 +53053,10 @@ index fe94d5b..23dcf0d 100644 + // Add extra to indicate continuous mode + Intent intent = integrator.createScanIntent(); + intent.putExtra("CONTINUOUS_SCAN", true); ++ // Pass custom status message via Intent if set ++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) { ++ intent.putExtra("CUSTOM_STATUS_MESSAGE", customStatusMessage); ++ } + // Use the REQUEST_CODE constant from IntentIntegrator + currentActivity.startActivityForResult(intent, IntentIntegrator.REQUEST_CODE); + // Invoke callback once to acknowledge scanner is starting @@ -54769,15 +53113,24 @@ index fe94d5b..23dcf0d 100644 + // Update the progress text overlay in the continuous scan activity + ContinuousCaptureActivity.updateProgressText(text); + } ++ ++ @ReactMethod ++ public void setStatusMessage(String message) { ++ // Set custom status message for the scanner ++ // This will be used when opening the scanner activity ++ synchronized (callbackLock) { ++ customStatusMessage = message; ++ } ++ } + @Override public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { if (mCallback != null) { diff --git a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java -index 8898813..82d7b72 100644 +index 8898813..166a9c8 100644 --- a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java +++ b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java -@@ -1,12 +1,288 @@ +@@ -1,12 +1,387 @@ package com.reactlibrary; +import android.content.Intent; @@ -54815,11 +53168,16 @@ index 8898813..82d7b72 100644 + private boolean continuousScanningSetup = false; + private CaptureManager captureManager; + private TextView progressTextView; ++ private String customStatusMessage = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Intent intent = getIntent(); + isContinuousMode = intent != null && intent.getBooleanExtra(CONTINUOUS_SCAN_EXTRA, false); ++ // Read custom status message from Intent ++ if (intent != null && intent.hasExtra("CUSTOM_STATUS_MESSAGE")) { ++ customStatusMessage = intent.getStringExtra("CUSTOM_STATUS_MESSAGE"); ++ } + + if (isContinuousMode) { + // Store static reference to this activity instance @@ -54867,6 +53225,11 @@ index 8898813..82d7b72 100644 + + super.onResume(); + ++ // Set custom status message if provided (for both single and continuous mode) ++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) { ++ setCustomStatusMessage(customStatusMessage); ++ } ++ + if (isContinuousMode && !continuousScanningSetup && barcodeView != null) { + // Set up continuous scanning after parent onResume completes + try { @@ -55036,7 +53399,7 @@ index 8898813..82d7b72 100644 + e.printStackTrace(); + } + } - ++ + @Override + protected void onStop() { + // Clear static reference when activity stops (before destroy) @@ -55050,7 +53413,7 @@ index 8898813..82d7b72 100644 + } + super.onStop(); + } - ++ + // Helper method to find DecoratedBarcodeView in view hierarchy + private DecoratedBarcodeView findBarcodeView(View view) { + if (view instanceof DecoratedBarcodeView) { @@ -55066,5 +53429,94 @@ index 8898813..82d7b72 100644 + } + } + return null; ++ } ++ ++ /** ++ * Set custom status message on the ViewfinderView ++ * Uses reflection to access the status TextView in the ZXing library ++ */ ++ private void setCustomStatusMessage(String message) { ++ try { ++ // Find the DecoratedBarcodeView ++ View rootView = findViewById(android.R.id.content); ++ if (rootView == null) { ++ return; ++ } ++ ++ DecoratedBarcodeView barcodeView = findBarcodeView(rootView); ++ if (barcodeView == null) { ++ return; ++ } ++ ++ // Use reflection to access the ViewfinderView and its status TextView ++ // The ViewfinderView is typically a child of DecoratedBarcodeView ++ ViewGroup barcodeViewGroup = (ViewGroup) barcodeView; ++ for (int i = 0; i < barcodeViewGroup.getChildCount(); i++) { ++ View child = barcodeViewGroup.getChildAt(i); ++ // Look for ViewfinderView (com.journeyapps.barcodescanner.ViewfinderView) ++ if (child.getClass().getName().contains("ViewfinderView")) { ++ // Try to find TextView inside ViewfinderView ++ if (child instanceof ViewGroup) { ++ ViewGroup viewfinderGroup = (ViewGroup) child; ++ for (int j = 0; j < viewfinderGroup.getChildCount(); j++) { ++ View viewfinderChild = viewfinderGroup.getChildAt(j); ++ if (viewfinderChild instanceof TextView) { ++ TextView statusTextView = (TextView) viewfinderChild; ++ // Check if this is the status text view (usually has specific ID or is the only TextView) ++ // Set the custom message ++ statusTextView.setText(message); ++ return; ++ } ++ } ++ } ++ // Alternative: Try to use reflection to access statusView field ++ try { ++ Field statusViewField = child.getClass().getDeclaredField("statusView"); ++ statusViewField.setAccessible(true); ++ TextView statusView = (TextView) statusViewField.get(child); ++ if (statusView != null) { ++ statusView.setText(message); ++ return; ++ } ++ } catch (NoSuchFieldException e) { ++ // Field doesn't exist, try other approaches ++ } ++ } ++ } + ++ // Fallback: Try to find any TextView that might be the status view ++ // by checking if it contains the default status message ++ TextView statusTextView = findStatusTextView(rootView); ++ if (statusTextView != null) { ++ statusTextView.setText(message); ++ } ++ } catch (Exception e) { ++ // If we can't set the custom message, that's okay - scanner will still work ++ e.printStackTrace(); ++ } ++ } + ++ /** ++ * Helper method to find the status TextView in the view hierarchy ++ */ ++ private TextView findStatusTextView(View view) { ++ if (view instanceof TextView) { ++ TextView textView = (TextView) view; ++ String text = textView.getText().toString(); ++ // Check if this TextView contains the default status message ++ if (text.contains("barcode") || text.contains("viewfinder") || text.contains("scan")) { ++ return textView; ++ } ++ } ++ if (view instanceof ViewGroup) { ++ ViewGroup group = (ViewGroup) view; ++ for (int i = 0; i < group.getChildCount(); i++) { ++ TextView found = findStatusTextView(group.getChildAt(i)); ++ if (found != null) { ++ return found; ++ } ++ } ++ } ++ return null; + } } diff --git a/screens/MobileNostrPairing.foss.tsx b/screens/MobileNostrPairing.foss.tsx deleted file mode 100644 index d9e4bf7..0000000 --- a/screens/MobileNostrPairing.foss.tsx +++ /dev/null @@ -1,6032 +0,0 @@ -/* eslint-disable react-native/no-inline-styles */ -import React, {useState, useEffect, useRef} from 'react'; -import { - View, - Text, - StyleSheet, - Alert, - Image, - TouchableOpacity, - Modal, - TextInput, - ScrollView, - Platform, - KeyboardAvoidingView, - NativeEventEmitter, - EmitterSubscription, - Animated, - Keyboard, -} from 'react-native'; -import Share from 'react-native-share'; -import {NativeModules} from 'react-native'; -import DeviceInfo from 'react-native-device-info'; -import EncryptedStorage from 'react-native-encrypted-storage'; -import QRCode from 'react-native-qrcode-svg'; -import Clipboard from '@react-native-clipboard/clipboard'; -import BarcodeZxingScan from 'rn-barcode-zxing-scan'; -import * as Progress from 'react-native-progress'; -import {CommonActions, RouteProp, useRoute} from '@react-navigation/native'; -import {SafeAreaView} from 'react-native-safe-area-context'; -import Big from 'big.js'; -import {dbg, HapticFeedback, getNostrRelays, getKeyshareLabel, hexToString, getDerivePathForNetwork} from '../utils'; -import {useTheme} from '../theme'; -import {useUser} from '../context/UserContext'; -import LocalCache from '../services/LocalCache'; -import {WalletService} from '../services/WalletService'; -import RNFS from 'react-native-fs'; - -const {BBMTLibNativeModule} = NativeModules; - -type RouteParams = { - mode?: string; // 'duo' | 'trio' | 'send_btc' | 'sign_psbt' - addressType?: string; - toAddress?: string; - satoshiAmount?: string; - fiatAmount?: string; - satoshiFees?: string; - fiatFees?: string; - selectedCurrency?: string; - spendingHash?: string; - psbtBase64?: string; // For PSBT signing mode - derivePath?: string; // BIP32 derivation path for PSBT -}; - -const MobileNostrPairing = ({navigation}: any) => { - const route = useRoute>(); - const isSendBitcoin = route.params?.mode === 'send_btc'; - const isSignPSBT = route.params?.mode === 'sign_psbt'; - const setupMode = route.params?.mode; - const addressType = route.params?.addressType; - // In send mode, determine isTrio from keyshare (3 devices = trio, 2 devices = duo) - // In keygen mode, use setupMode - const [isTrio, setIsTrio] = useState(setupMode === 'trio'); - const {theme} = useTheme(); - const {activeAddress} = useUser(); - const ppmFile = `${RNFS.DocumentDirectoryPath}/ppm.json`; - - // Nostr Identity - const [localNsec, setLocalNsec] = useState(''); - const [localNpub, setLocalNpub] = useState(''); - const [deviceName, setDeviceName] = useState(''); - - // Relays - Load from cache or use defaults - const [relaysInput, setRelaysInput] = useState(''); - const [relays, setRelays] = useState([]); - - // Partial nonce (random UUID/number generated on each device) - const [partialNonce, setPartialNonce] = useState(''); - - // Peer Connections (for duo: 1 peer, for trio: 2 peers) - const [peerConnectionDetails1, setPeerConnectionDetails1] = - useState(''); - const [peerNpub1, setPeerNpub1] = useState(''); - const [peerDeviceName1, setPeerDeviceName1] = useState(''); - const [peerNonce1, setPeerNonce1] = useState(''); - const [peerConnectionDetails2, setPeerConnectionDetails2] = - useState(''); - const [peerNpub2, setPeerNpub2] = useState(''); - const [peerDeviceName2, setPeerDeviceName2] = useState(''); - const [peerNonce2, setPeerNonce2] = useState(''); - const [peerInputError1, setPeerInputError1] = useState(''); - const [peerInputError2, setPeerInputError2] = useState(''); - const [peerInputValidating1, setPeerInputValidating1] = - useState(false); - const [peerInputValidating2, setPeerInputValidating2] = - useState(false); - - // Session (generated deterministically) - const [sessionID, setSessionID] = useState(''); - const [sessionKey, setSessionKey] = useState(''); - const [chaincode, setChaincode] = useState(''); - - // Progress - const [progress, setProgress] = useState(0); - const [status, setStatus] = useState(''); - const [isPairing, setIsPairing] = useState(false); - const [isKeygenReady, setIsKeygenReady] = useState(false); // Manual toggle for "other devices ready" - const [isKeysignReady, setIsKeysignReady] = useState(false); // Manual toggle for PSBT/send signing readiness - const [canStartKeygen, setCanStartKeygen] = useState(false); // Auto-calculated: all conditions met - const [mpcDone, setMpcDone] = useState(false); - const [psbtDetails, setPsbtDetails] = useState<{ - inputs: Array<{txid: string; vout: number; amount: number}>; - outputs: Array<{address: string; amount: number}>; - fee: number; - totalInput: number; - totalOutput: number; - derivePath?: string; - derivePaths?: string[]; - } | null>(null); - const [isPreParamsReady, setIsPreParamsReady] = useState(false); - const [isPreparing, setIsPreparing] = useState(false); - const [isPrepared, setIsPrepared] = useState(false); - const [prepCounter, setPrepCounter] = useState(0); - const progressAnimation = useRef(new Animated.Value(0)).current; - const progressAnimationLoop = useRef( - null, - ); - - // Backup state - const [isBackupModalVisible, setIsBackupModalVisible] = useState(false); - const [password, setPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [passwordVisible, setPasswordVisible] = useState(false); - const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false); - const [passwordStrength, setPasswordStrength] = useState(0); - const [passwordErrors, setPasswordErrors] = useState([]); - const [backupChecks, setBackupChecks] = useState({ - deviceOne: false, - deviceTwo: false, - deviceThree: false, - }); - - // Keyshare mapping (based on sorted npubs) - const [keyshareMapping, setKeyshareMapping] = useState<{ - keyshare1?: {npub: string; deviceName: string; isLocal: boolean}; - keyshare2?: {npub: string; deviceName: string; isLocal: boolean}; - keyshare3?: {npub: string; deviceName: string; isLocal: boolean}; - }>({}); - - // Send mode: device selection (for trio mode) - const [selectedPeerNpub, setSelectedPeerNpub] = useState(''); - const [sendModeDevices, setSendModeDevices] = useState< - Array<{ - keyshareLabel: string; - npub: string; - isLocal: boolean; - }> - >([]); - - // QR Scanner / QR Share - // FOSS version: Only use ref, no state needed since we don't use iOS modal - const scanningForPeerRef = useRef<1 | 2>(1); - const [isQRModalVisible, setIsQRModalVisible] = useState(false); - const [showRelayConfig, setShowRelayConfig] = useState(false); - const [showHelpModal, setShowHelpModal] = useState(false); - const connectionQrRef = useRef(null); - - // Connection details for sharing (hex encoded) - const connectionDetails = React.useMemo(() => { - if (!localNpub || !deviceName || !partialNonce) { - return ''; - } - const plaintext = `${localNpub}:${deviceName}:${partialNonce}`; - // Convert to hex encoding - let hex = ''; - for (let i = 0; i < plaintext.length; i++) { - const charCode = plaintext.charCodeAt(i); - hex += charCode.toString(16).padStart(2, '0'); - } - return hex; - }, [localNpub, deviceName, partialNonce]); - - // Load default relays on mount (from cache if available, otherwise fetch dynamically) - useEffect(() => { - const loadRelays = async () => { - try { - // Use getNostrRelays which handles cache and fetching - const fetchedRelays = await getNostrRelays(false); - const relaysCSV = fetchedRelays.join(','); - // Convert CSV to newline-separated for multiline display - const relaysForDisplay = relaysCSV.split(',').join('\n'); - setRelaysInput(relaysForDisplay); - setRelays(fetchedRelays); - } catch (error) { - dbg('Error loading relays:', error); - // Fallback to defaults on error - const defaults = [ - 'wss://bbw-nostr.xyz', - 'wss://nostr.hifish.org', - 'wss://nostr.xxi.quest', - ]; - const defaultsCSV = defaults.join(','); - const defaultsForDisplay = defaultsCSV.split(',').join('\n'); - setRelaysInput(defaultsForDisplay); - setRelays(defaults); - } - }; - loadRelays(); - }, []); - - // Update relays when input changes (support both comma and newline separation) - useEffect(() => { - const parsed = relaysInput - .split(/[,\n]/) - .map(r => r.trim()) - .filter(Boolean); - setRelays(parsed); - }, [relaysInput]); - - // Initialize device name and generate keypair on mount (only for keygen mode) - useEffect(() => { - const initialize = async () => { - try { - const name = await DeviceInfo.getDeviceName(); - setDeviceName(name); - // Generate random partial nonce (UUID or random number) - // Using a combination of timestamp and random for uniqueness - const randomNonce = await BBMTLibNativeModule.sha256( - `${Date.now()}-${Math.random()}`, - ); - setPartialNonce(randomNonce); - dbg('Generated partialNonce:', randomNonce); - // Only generate new keypair if not in send/sign mode (send/sign mode loads from keyshare) - if (!isSendBitcoin && !isSignPSBT) { - await generateLocalKeypair(); - } - } catch (error) { - dbg('Error initializing:', error); - Alert.alert('Error', 'Failed to initialize device'); - } - }; - initialize(); - }, [isSendBitcoin, isSignPSBT]); - - // Generate session params when peer connections are ready - useEffect(() => { - if (localNpub && deviceName && partialNonce) { - if (isSendBitcoin || isSignPSBT) { - // For send BTC / sign PSBT, we need balance - will be generated when starting - return; - } - // For keygen, generate when we have peer(s) with nonces - if (isTrio) { - if ( - peerNpub1 && - peerDeviceName1 && - peerNonce1 && - peerNpub2 && - peerDeviceName2 && - peerNonce2 - ) { - generateKeygenSessionParams(); - } - } else { - if (peerNpub1 && peerDeviceName1 && peerNonce1) { - generateKeygenSessionParams(); - } - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - localNpub, - deviceName, - partialNonce, - peerNpub1, - peerDeviceName1, - peerNonce1, - peerNpub2, - peerDeviceName2, - peerNonce2, - isTrio, - isSendBitcoin, - ]); - - // Check if all conditions are met to enable start keygen button - // (requires preparams ready AND manual confirmation that other devices are ready) - useEffect(() => { - const ready = - localNpub && - deviceName && - relays.length > 0 && - sessionID && - sessionKey && - chaincode && - isPreParamsReady && - isKeygenReady && // Manual confirmation that other devices are ready - ((isTrio && - peerNpub1 && - peerDeviceName1 && - peerNpub2 && - peerDeviceName2) || - (!isTrio && peerNpub1 && peerDeviceName1)); - setCanStartKeygen(!!ready); - }, [ - localNpub, - deviceName, - relays, - sessionID, - sessionKey, - chaincode, - isPreParamsReady, - isKeygenReady, // Include manual toggle - peerNpub1, - peerDeviceName1, - peerNpub2, - peerDeviceName2, - isTrio, - ]); - - // Toggle function for manual "other devices ready" confirmation - const toggleKeygenReady = () => { - setIsKeygenReady(!isKeygenReady); - }; - - const toggleKeysignReady = () => { - HapticFeedback.medium(); - setIsKeysignReady(!isKeysignReady); - }; - - // Parse PSBT details when PSBT is available - useEffect(() => { - const parsePSBT = async () => { - if (isSignPSBT && route.params?.psbtBase64) { - try { - dbg('Parsing PSBT details for summary...'); - const detailsJson = await BBMTLibNativeModule.parsePSBTDetails( - route.params.psbtBase64, - ); - - if (detailsJson.startsWith('error') || detailsJson.includes('failed')) { - dbg('Failed to parse PSBT details:', detailsJson); - setPsbtDetails(null); - return; - } - - const details = JSON.parse(detailsJson); - setPsbtDetails({ - inputs: details.inputs || [], - outputs: details.outputs || [], - fee: details.fee || 0, - totalInput: details.totalInput || 0, - totalOutput: details.totalOutput || 0, - derivePath: details.derivePath, - derivePaths: details.derivePaths || [], - }); - dbg('PSBT details parsed:', { - inputs: details.inputs?.length || 0, - outputs: details.outputs?.length || 0, - fee: details.fee, - }); - } catch (error) { - dbg('Error parsing PSBT details:', error); - setPsbtDetails(null); - } - } else { - setPsbtDetails(null); - } - }; - - parsePSBT(); - }, [isSignPSBT, route.params?.psbtBase64]); - - // Listen to native module events for progress tracking - useEffect(() => { - const eventEmitter = new NativeEventEmitter(BBMTLibNativeModule); - const keygenSteps = isTrio ? 25 : 18; - const keysignSteps = 36; - let utxoRange = 0; - let utxoIndex = 0; - let utxoCount = 0; - const processHook = (message: string) => { - try { - const msg = JSON.parse(message); - if (msg.type === 'keygen') { - if (msg.done) { - dbg('progress - keygen done'); - setProgress(100); - setMpcDone(true); - // Don't navigate away, let the backup UI handle it - } else { - dbg( - 'progress - keygen: ', - Math.round((100 * msg.step) / keygenSteps), - 'step', - msg.step, - 'time', - new Date(msg.time), - ); - setProgress(Math.round((100 * msg.step) / keygenSteps)); - dbg('keygen_hook_info:', msg.info); - const statusDot = - msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...'; - setStatus('Processing cryptographic operations' + statusDot); - } - } else if (msg.type === 'btc_send') { - if (msg.done) { - setProgress(100); - } - if (msg.utxo_total > 0) { - utxoCount = msg.utxo_total; - utxoIndex = msg.utxo_current; - utxoRange = 100 / utxoCount; - dbg('progress send_btc', { - utxoCount, - utxoIndex, - utxoRange, - }); - } - dbg('btc_send_hook_info:', msg.info); - const statusDot = - msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...'; - setStatus('Processing cryptographic operations' + statusDot); - } else if (msg.type === 'keysign') { - const prgUTXO = (utxoIndex - 1) * utxoRange; - const progressValue = - utxoCount > 0 - ? Math.round(prgUTXO + (utxoRange * msg.step) / keysignSteps) - : Math.round((100 * msg.step) / keysignSteps); - dbg( - 'progress - keysign: ', - progressValue, - 'prgUTXO', - prgUTXO, - 'step', - msg.step, - 'range', - utxoRange, - 'time', - new Date(msg.time), - ); - setProgress(progressValue); - dbg('keysign_hook_info:', msg.info); - const statusDot = - msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...'; - setStatus('Processing cryptographic operations' + statusDot); - if (msg.done) { - setProgress(100); - setMpcDone(true); - } - } - } catch { - // If parsing fails, it might be a log message, just log it - dbg('TSS log:', message); - } - }; - - const subscription: EmitterSubscription = eventEmitter.addListener( - Platform.OS === 'android' ? 'BBMT_DROID' : 'BBMT_APPLE', - (event: any) => { - if (event.tag === 'TssHook') { - processHook(event.message); - } else if (event.tag === 'GoLog') { - dbg('TSS:', event.message); - } - }, - ); - - return () => { - subscription.remove(); - }; - }, [isTrio]); - - // Load keyshare and derive device info in send/sign mode - useEffect(() => { - if (!isSendBitcoin && !isSignPSBT) return; - - const loadKeyshareData = async () => { - try { - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (!keyshareJSON) { - dbg('No keyshare found in send/sign mode'); - setSendModeDevices([]); - return; - } - - const keyshare = JSON.parse(keyshareJSON); - if (!keyshare.keygen_committee_keys || !keyshare.local_party_key) { - dbg('Keyshare missing required fields'); - setSendModeDevices([]); - return; - } - - // Determine if trio mode based on number of devices in keyshare - const numDevices = keyshare.keygen_committee_keys?.length || 0; - const isTrioMode = numDevices === 3; - setIsTrio(isTrioMode); - dbg( - 'Send mode - detected', - isTrioMode ? 'trio' : 'duo', - 'mode from keyshare (', - numDevices, - 'devices)', - ); - - // Get local npub from keyshare - const localNpubFromKeyshare = keyshare.nostr_npub || ''; - - // Get local nsec from keyshare - // The nsec might be stored as hex-encoded bytes OR already in bech32 format - const nsecFromKeyshare = keyshare.nsec || ''; - - if (nsecFromKeyshare) { - // Check if it's already in bech32 format - if (nsecFromKeyshare.startsWith('nsec1')) { - // Already in correct format - setLocalNsec(nsecFromKeyshare); - dbg( - 'Nsec from keyshare (already bech32):', - nsecFromKeyshare.substring(0, 20) + '...', - ); - } else { - // Try to decode from hex - try { - const decodedNsec = hexToString(nsecFromKeyshare); - dbg( - 'Decoded nsec from hex:', - decodedNsec.substring(0, 20) + '...', - ); - - // Verify it's a valid nsec format - if (decodedNsec.startsWith('nsec1')) { - setLocalNsec(decodedNsec); - dbg('Nsec set successfully'); - } else { - dbg( - 'Warning: Decoded nsec does not start with nsec1:', - decodedNsec.substring(0, 50), - ); - } - } catch (error) { - dbg('Error decoding nsec from hex:', error); - } - } - } else { - dbg('Warning: No nsec found in keyshare'); - } - - // Set local npub if available - if (localNpubFromKeyshare) { - setLocalNpub(localNpubFromKeyshare); - } - - // Sort keygen_committee_keys to match the order used for keyshare labels - const sortedKeys = [...keyshare.keygen_committee_keys].sort(); - - // Build device list IMMEDIATELY with available data - const devices: Array<{ - keyshareLabel: string; - npub: string; - isLocal: boolean; - }> = []; - - for (let i = 0; i < sortedKeys.length; i++) { - const hexKey = sortedKeys[i]; - const isLocal = hexKey === keyshare.local_party_key; - const keyshareLabel = `KeyShare${i + 1}`; - - let npub = ''; - if (isLocal) { - // Use local npub if available, otherwise use shortened hex - npub = - localNpubFromKeyshare || - hexKey.substring(0, 12) + - '...' + - hexKey.substring(hexKey.length - 8); - } else { - // For other devices, use shortened hex as placeholder, will update async - npub = - hexKey.substring(0, 12) + - '...' + - hexKey.substring(hexKey.length - 8); - } - - devices.push({ - keyshareLabel, - npub, - isLocal, - }); - } - - // Set devices immediately so UI can render - setSendModeDevices(devices); - dbg('Send mode devices loaded (initial):', devices); - - // Now update npubs for other devices asynchronously - const updatedDevices = [...devices]; - for (let i = 0; i < sortedKeys.length; i++) { - const hexKey = sortedKeys[i]; - const isLocal = hexKey === keyshare.local_party_key; - - if (!isLocal) { - try { - // Validate hex key format before calling hexToNpub - const hexPattern = /^[0-9a-fA-F]+$/; - if (!hexPattern.test(hexKey)) { - dbg( - 'Invalid hex key format, skipping npub conversion:', - hexKey.substring(0, 20) + '...', - ); - continue; - } - - const result = await BBMTLibNativeModule.hexToNpub(hexKey); - if ( - result && - typeof result === 'string' && - result.startsWith('npub1') - ) { - const oldNpub = updatedDevices[i].npub; - updatedDevices[i].npub = result; - dbg( - 'Updated npub for device:', - result.substring(0, 20) + '...', - ); - // Update state with new npub - setSendModeDevices([...updatedDevices]); - - // If this device was selected (by placeholder), update selectedPeerNpub to full npub - // Use a callback to access current selectedPeerNpub state - setSelectedPeerNpub(current => { - if ( - current === oldNpub || - (oldNpub && result.startsWith(oldNpub.substring(0, 20))) - ) { - dbg( - 'Updated selectedPeerNpub to full npub:', - result.substring(0, 20) + '...', - ); - return result; - } - return current; - }); - } - } catch (error) { - dbg('Error converting hex to npub:', error); - // Keep the shortened hex as fallback - } - } - } - } catch (error: any) { - dbg('Error loading keyshare data:', error); - setSendModeDevices([]); - } - }; - - loadKeyshareData(); - }, [isSendBitcoin, isSignPSBT]); - - // Auto-select peer in duo mode, or first peer in trio mode (deterministic) - // Only auto-selects if no selection exists - never overrides user's manual selection - useEffect(() => { - if ((isSendBitcoin || isSignPSBT) && sendModeDevices.length > 0) { - const otherDevices = sendModeDevices.filter(d => !d.isLocal); - - // Only auto-select if no peer is currently selected - if (!selectedPeerNpub) { - if (isTrio && otherDevices.length >= 2) { - // In trio mode, deterministically select the first peer (sorted by npub) - // This ensures both devices select the same peer by default - // User can still manually change the selection - const sortedOtherDevices = [...otherDevices].sort((a, b) => { - // Sort by npub (handle both full and shortened npubs) - const npubA = a.npub || ''; - const npubB = b.npub || ''; - return npubA.localeCompare(npubB); - }); - const firstPeer = sortedOtherDevices[0]; - if (firstPeer && firstPeer.npub) { - setSelectedPeerNpub(firstPeer.npub); - dbg( - 'Auto-selected first peer in trio mode (deterministic, user can change):', - firstPeer.npub.substring(0, 20) + '...', - ); - } - } else if (!isTrio && otherDevices.length >= 1) { - // In duo mode, auto-select the only other device - const otherDevice = otherDevices[0]; - if (otherDevice && otherDevice.npub) { - setSelectedPeerNpub(otherDevice.npub); - dbg( - 'Auto-selected peer in duo mode:', - otherDevice.npub.substring(0, 20) + '...', - ); - } - } - } - } - }, [isSendBitcoin, isSignPSBT, isTrio, sendModeDevices, selectedPeerNpub]); - - const generateLocalKeypair = async () => { - try { - const keypairJSON = await BBMTLibNativeModule.nostrKeypair(); - const keypair = JSON.parse(keypairJSON); - setLocalNsec(keypair.nsec); - setLocalNpub(keypair.npub); - dbg( - 'Generated Nostr keypair - nsec:', - keypair.nsec?.substring(0, 10) + '...', - ); - dbg('Generated Nostr keypair - npub:', keypair.npub); - dbg( - 'Generated Nostr keypair - npub format check:', - keypair.npub?.startsWith('npub1') - ? 'bech32 (correct)' - : 'NOT bech32 (incorrect - should start with npub1)', - ); - } catch (error: any) { - dbg('Error generating keypair:', error); - Alert.alert('Error', 'Failed to generate Nostr keypair'); - } - }; - - // Helper function to shorten npub for display - const shortenNpub = ( - npub: string, - startLen: number = 8, - endLen: number = 4, - ): string => { - if (!npub || npub.length <= startLen + endLen) { - return npub; - } - return `${npub.substring(0, startLen)}...${npub.substring( - npub.length - endLen, - )}`; - }; - - // Helper function to format connection details for display - const formatConnectionDisplay = ( - npub: string, - deviceNameValue: string, - ): string => { - if (!npub || !deviceNameValue) { - return ''; - } - return `${deviceNameValue}@${shortenNpub(npub)}`; - }; - - const parseConnectionDetails = async ( - input: string, - ): Promise<{ - npub: string; - deviceName: string; - partialNonce: string; - } | null> => { - const trimmed = input.trim(); - dbg('parseConnectionDetails: input =', trimmed.substring(0, 50) + '...'); - - if (!trimmed) { - dbg('parseConnectionDetails: empty input'); - return null; - } - - // Try to decode as hex first - let decoded = ''; - try { - // Check if it looks like hex (even length, only hex chars) - const hexPattern = /^[0-9a-fA-F]+$/; - if (hexPattern.test(trimmed) && trimmed.length % 2 === 0) { - // Decode hex to string - for (let i = 0; i < trimmed.length; i += 2) { - const hexByte = trimmed.substr(i, 2); - const charCode = parseInt(hexByte, 16); - decoded += String.fromCharCode(charCode); - } - dbg( - 'parseConnectionDetails: decoded hex to:', - decoded.substring(0, 50) + '...', - ); - } else { - // Not hex, try as plaintext (backward compatibility) - decoded = trimmed; - dbg('parseConnectionDetails: treating as plaintext'); - } - } catch (error) { - dbg('parseConnectionDetails: error decoding hex:', error); - return null; - } - - const parts = decoded.split(':'); - dbg('parseConnectionDetails: split parts count =', parts.length); - - if (parts.length !== 3) { - dbg( - 'parseConnectionDetails: invalid format - expected 3 parts (npub:deviceName:partialNonce), got', - parts.length, - ); - return null; - } - - let [npub, peerDeviceName, peerPartialNonce] = parts; - let trimmedNpub = npub.trim(); - const trimmedDeviceName = peerDeviceName.trim(); - const trimmedNonce = peerPartialNonce.trim(); - - dbg('parseConnectionDetails: npub =', trimmedNpub.substring(0, 20) + '...'); - dbg('parseConnectionDetails: deviceName =', trimmedDeviceName); - dbg('parseConnectionDetails: partialNonce =', trimmedNonce); - - // Check if it's a hex string (64 hex characters) and try to convert to npub - if (!trimmedNpub.startsWith('npub1')) { - // Check if it's a hex string - const hexPattern = /^[0-9a-fA-F]{64}$/; - if (hexPattern.test(trimmedNpub)) { - dbg( - 'parseConnectionDetails: detected hex string, attempting to convert to npub', - ); - try { - // Try to convert hex to npub using native module - // First, we need to check if there's a conversion function - // For now, we'll show a helpful error message - dbg( - 'parseConnectionDetails: hex string detected but conversion not available', - ); - return null; // Will show error message below - } catch (error) { - dbg('parseConnectionDetails: error converting hex to npub:', error); - return null; - } - } else { - dbg( - 'parseConnectionDetails: invalid npub - does not start with npub1 and is not valid hex', - ); - return null; - } - } - - if (trimmedNpub.length < 10) { - dbg('parseConnectionDetails: invalid npub - too short'); - return null; - } - - if (trimmedDeviceName.length === 0) { - dbg('parseConnectionDetails: invalid device name - empty'); - return null; - } - - if (trimmedNonce.length === 0) { - dbg('parseConnectionDetails: invalid partialNonce - empty'); - return null; - } - - dbg( - 'parseConnectionDetails: valid! npub =', - trimmedNpub.substring(0, 20) + '...', - 'deviceName =', - trimmedDeviceName, - 'partialNonce =', - trimmedNonce, - ); - return { - npub: trimmedNpub, - deviceName: trimmedDeviceName, - partialNonce: trimmedNonce, - }; - }; - - const handlePeerConnectionInput = async (input: string, peerNum: 1 | 2) => { - dbg(`handlePeerConnectionInput: peerNum=${peerNum}, input="${input}"`); - - const setValidating = - peerNum === 1 ? setPeerInputValidating1 : setPeerInputValidating2; - const setError = peerNum === 1 ? setPeerInputError1 : setPeerInputError2; - - // Clear previous error - setError(''); - - // If input is empty, clear everything - if (!input.trim()) { - dbg( - `handlePeerConnectionInput: peerNum=${peerNum}, clearing (empty input)`, - ); - if (peerNum === 1) { - setPeerNpub1(''); - setPeerDeviceName1(''); - setPeerNonce1(''); - setPeerConnectionDetails1(''); - } else { - setPeerNpub2(''); - setPeerDeviceName2(''); - setPeerNonce2(''); - setPeerConnectionDetails2(''); - } - return; - } - - // Set validating state - setValidating(true); - - // Small delay to show validation state - await new Promise(resolve => setTimeout(resolve, 300)); - - const parsed = await parseConnectionDetails(input); - - if (parsed) { - dbg( - `handlePeerConnectionInput: peerNum=${peerNum}, VALID - npub=${parsed.npub.substring( - 0, - 20, - )}..., deviceName=${parsed.deviceName}`, - ); - - // Check for duplicate npubs - const isDuplicateLocal = parsed.npub === localNpub; - const isDuplicatePeer1 = peerNum !== 1 && parsed.npub === peerNpub1; - const isDuplicatePeer2 = peerNum !== 2 && parsed.npub === peerNpub2; - - if (isDuplicateLocal || isDuplicatePeer1 || isDuplicatePeer2) { - let duplicateMsg = 'This device is already connected.'; - if (isDuplicateLocal) { - duplicateMsg = 'Cannot connect to your own device.'; - } - dbg( - `handlePeerConnectionInput: peerNum=${peerNum}, DUPLICATE - ${duplicateMsg}`, - ); - setError(duplicateMsg); - setValidating(false); - // Clear the input text - if (peerNum === 1) { - setPeerConnectionDetails1(''); - setPeerNonce1(''); - } else { - setPeerConnectionDetails2(''); - setPeerNonce2(''); - } - return; - } - - if (peerNum === 1) { - setPeerNpub1(parsed.npub); - setPeerDeviceName1(parsed.deviceName); - setPeerNonce1(parsed.partialNonce); - setPeerConnectionDetails1(input.trim()); - setPeerInputError1(''); - } else { - setPeerNpub2(parsed.npub); - setPeerDeviceName2(parsed.deviceName); - setPeerNonce2(parsed.partialNonce); - setPeerConnectionDetails2(input.trim()); - setPeerInputError2(''); - } - - HapticFeedback.light(); - } else { - dbg(`handlePeerConnectionInput: peerNum=${peerNum}, INVALID`); - - // Check if it's a hex string - const hexPattern = /^[0-9a-fA-F]{64}$/; - const parts = input.trim().split(':'); - const firstPart = parts[0]?.trim() || ''; - - let errorMsg = ''; - if (hexPattern.test(firstPart)) { - errorMsg = - 'Hex string detected. Please use npub format (npub1...). The connection details should show the npub, not a hex string.'; - } else if (!input.includes(':')) { - errorMsg = 'Missing colon separator. Format: npub1...:DeviceName'; - } else if (!firstPart.startsWith('npub1')) { - errorMsg = - 'Invalid format. Expected: npub1...:DeviceName (npub must start with "npub1")'; - } else { - errorMsg = 'Invalid format. Expected: npub1...:DeviceName'; - } - - dbg(`handlePeerConnectionInput: peerNum=${peerNum}, error="${errorMsg}"`); - setError(errorMsg); - - // Clear the input text and peer data - if (peerNum === 1) { - setPeerNpub1(''); - setPeerDeviceName1(''); - setPeerNonce1(''); - setPeerConnectionDetails1(''); - } else { - setPeerNpub2(''); - setPeerDeviceName2(''); - setPeerNonce2(''); - setPeerConnectionDetails2(''); - } - } - - setValidating(false); - }; - - const generateKeygenSessionParams = async () => { - try { - // Collect all npubs and device names - // IMPORTANT: Trim whitespace and ensure consistent format - const allNpubs: string[] = []; - const allDeviceNames: string[] = []; - - // Add local npub (trimmed) - if (localNpub && localNpub.trim()) { - allNpubs.push(localNpub.trim()); - } - if (deviceName && deviceName.trim()) { - allDeviceNames.push(deviceName.trim()); - } - - // Add peer 1 (trimmed) - if (peerNpub1 && peerNpub1.trim()) { - allNpubs.push(peerNpub1.trim()); - } - if (peerDeviceName1 && peerDeviceName1.trim()) { - allDeviceNames.push(peerDeviceName1.trim()); - } - - // Add peer 2 for trio (trimmed) - if (isTrio && peerNpub2 && peerNpub2.trim()) { - allNpubs.push(peerNpub2.trim()); - } - if (isTrio && peerDeviceName2 && peerDeviceName2.trim()) { - allDeviceNames.push(peerDeviceName2.trim()); - } - - // Validate we have the correct number of npubs - const expectedNpubs = isTrio ? 3 : 2; - if (allNpubs.length !== expectedNpubs) { - dbg( - `ERROR: Expected ${expectedNpubs} npubs for ${ - isTrio ? 'trio' : 'duo' - } mode, but got ${allNpubs.length}`, - ); - dbg( - 'allNpubs:', - allNpubs.map(n => n.substring(0, 20) + '...'), - ); - dbg( - 'localNpub:', - localNpub ? localNpub.substring(0, 20) + '...' : 'MISSING', - ); - dbg( - 'peerNpub1:', - peerNpub1 ? peerNpub1.substring(0, 20) + '...' : 'MISSING', - ); - if (isTrio) { - dbg( - 'peerNpub2:', - peerNpub2 ? peerNpub2.substring(0, 20) + '...' : 'MISSING', - ); - dbg( - 'peerDeviceName2:', - peerDeviceName2 ? peerDeviceName2 : 'MISSING', - ); - } - dbg('isTrio:', isTrio); - return; // Don't generate session params if we don't have all npubs - } - - // Additional validation for trio mode: ensure all 3 npubs are unique - if (isTrio && allNpubs.length === 3) { - const uniqueNpubs = new Set(allNpubs); - if (uniqueNpubs.size !== 3) { - dbg('ERROR: Duplicate npubs detected in trio mode!'); - dbg('allNpubs:', allNpubs); - return; - } - } - - // Sort alphabetically - CRITICAL: must be same order on all devices - const npubsSorted = [...allNpubs].sort().join(','); - const deviceNamesSorted = [...allDeviceNames].sort().join(','); - - // Collect all partial nonces (local + peers) - const allPartialNonces: string[] = []; - if (partialNonce) { - allPartialNonces.push(partialNonce); - } - if (peerNonce1) { - allPartialNonces.push(peerNonce1); - } - if (isTrio && peerNonce2) { - allPartialNonces.push(peerNonce2); - } - - // Sort nonces and join as CSV - const fullNonce = [...allPartialNonces].sort().join(','); - - // Log the exact inputs for session ID calculation (for debugging) - dbg('=== SESSION ID CALCULATION ==='); - dbg('Mode:', isTrio ? 'TRIO' : 'DUO'); - dbg( - 'All npubs (before sort):', - allNpubs.map(n => n.substring(0, 30) + '...'), - ); - dbg( - 'All npubs (sorted):', - npubsSorted.split(',').map(n => n.substring(0, 30) + '...'), - ); - dbg('npubsSorted (full):', npubsSorted); - dbg('All device names (before sort):', allDeviceNames); - dbg('deviceNamesSorted:', deviceNamesSorted); - dbg('All partial nonces (before sort):', allPartialNonces); - dbg('fullNonce (sorted, CSV):', fullNonce); - dbg( - 'Session ID input string:', - `${npubsSorted},${deviceNamesSorted},${fullNonce}`, - ); - - // Generate session ID - const sessionIDHash = await BBMTLibNativeModule.sha256( - `${npubsSorted},${deviceNamesSorted},${fullNonce}`, - ); - setSessionID(sessionIDHash); - - // Generate session key - const sessionKeyHash = await BBMTLibNativeModule.sha256( - `${npubsSorted},${sessionIDHash}`, - ); - setSessionKey(sessionKeyHash); - - // Generate chaincode - const chaincodeHash = await BBMTLibNativeModule.sha256( - `${sessionIDHash},${sessionKeyHash}`, - ); - setChaincode(chaincodeHash); - - dbg('Generated session params:', { - sessionID: sessionIDHash.substring(0, 16) + '...', - sessionKey: sessionKeyHash.substring(0, 16) + '...', - chaincode: chaincodeHash.substring(0, 16) + '...', - fullNonce: fullNonce, - npubsCount: allNpubs.length, - }); - dbg('=== END SESSION ID CALCULATION ==='); - } catch (error: any) { - dbg('Error generating session params:', error); - Alert.alert('Error', 'Failed to generate session parameters'); - } - }; - - const startKeygen = async () => { - if (!canStartKeygen) return; - - setIsPairing(true); - setProgress(0); - setStatus('Starting key generation...'); - - try { - // Prepare parties npubs CSV (sorted) - // IMPORTANT: Must use the same npubs and same sorting as session ID generation - const allNpubs: string[] = []; - if (localNpub && localNpub.trim()) { - allNpubs.push(localNpub.trim()); - } - if (peerNpub1 && peerNpub1.trim()) { - allNpubs.push(peerNpub1.trim()); - } - if (isTrio && peerNpub2 && peerNpub2.trim()) { - allNpubs.push(peerNpub2.trim()); - } - - // Validate we have the correct number - const expectedNpubs = isTrio ? 3 : 2; - if (allNpubs.length !== expectedNpubs) { - throw new Error( - `Expected ${expectedNpubs} npubs for ${ - isTrio ? 'trio' : 'duo' - } mode, but got ${allNpubs.length}`, - ); - } - - // Sort alphabetically (same as session ID generation) - const partiesNpubsCSV = allNpubs.sort().join(','); - - dbg('=== START KEYGEN ==='); - dbg('Mode:', isTrio ? 'TRIO' : 'DUO'); - dbg( - 'localNpub:', - localNpub ? localNpub.substring(0, 30) + '...' : 'MISSING', - ); - dbg( - 'partiesNpubsCSV (sorted, all npubs):', - partiesNpubsCSV.split(',').map(n => n.substring(0, 30) + '...'), - ); - - // Calculate expected peers (all npubs except local) - const expectedPeers = allNpubs.filter(n => { - const trimmedN = n.trim(); - const trimmedLocal = localNpub?.trim() || ''; - return trimmedN !== trimmedLocal; - }); - dbg( - 'Expected peers (excluding self):', - expectedPeers.map(n => n.substring(0, 30) + '...'), - ); - dbg( - 'Expected peer count:', - expectedPeers.length, - isTrio ? '(should be 2 for trio)' : '(should be 1 for duo)', - ); - - dbg('sessionID:', sessionID.substring(0, 16) + '...'); - dbg('sessionKey:', sessionKey.substring(0, 16) + '...'); - - if (isTrio && expectedPeers.length !== 2) { - dbg( - '⚠️ WARNING: In trio mode, expected 2 peers but got', - expectedPeers.length, - ); - dbg( - 'This device will wait for', - expectedPeers.length, - 'peers. Make sure all 3 devices have all npubs connected!', - ); - } - - // Prepare relays CSV - const relaysCSV = relays.join(','); - - // Save relays to cache - await LocalCache.setItem('nostr_relays', relaysCSV); - - // Log detailed info for debugging trio mode - dbg('Starting Nostr keygen with:', { - relays: relaysCSV, - parties: partiesNpubsCSV, - sessionID: sessionID, - ppmFile: ppmFile, - localNsec: localNsec ? localNsec.substring(0, 20) + '...' : 'MISSING', - partiesNpubsCSV: partiesNpubsCSV, - sessionKey: sessionKey.substring(0, 16) + '...', - chaincode: chaincode.substring(0, 16) + '...', - }); - - // Log which npubs will be sent to Go backend - const allPartiesList = partiesNpubsCSV.split(','); - dbg('=== GO BACKEND INPUT ==='); - dbg('partiesNpubsCSV (full):', partiesNpubsCSV); - dbg( - 'All parties count:', - allPartiesList.length, - isTrio ? '(should be 3 for trio)' : '(should be 2 for duo)', - ); - dbg( - 'All parties list:', - allPartiesList.map((n, i) => `${i + 1}. ${n.substring(0, 30)}...`), - ); - dbg( - 'localNpub (will be excluded by Go backend):', - localNpub ? localNpub.substring(0, 30) + '...' : 'MISSING', - ); - dbg( - 'Expected PeersNpub (after Go excludes localNpub):', - expectedPeers.map((n, i) => `${i + 1}. ${n.substring(0, 30)}...`), - ); - dbg( - 'Go backend will wait for', - expectedPeers.length, - 'peers to publish "ready" events', - ); - dbg('=== END GO BACKEND INPUT ==='); - - // Call native module - let keyshareJSON = await BBMTLibNativeModule.nostrMpcTssSetup( - relaysCSV, - localNsec, - partiesNpubsCSV, - sessionID, - sessionKey, - chaincode, - ppmFile, - ); - - // Validate keyshare and map keyshare positions - let keyshare: any; - try { - keyshare = JSON.parse(keyshareJSON); - if (!keyshare.pub_key) { - throw new Error('Invalid keyshare: missing pub_key'); - } - dbg('Keygen successful, party:', keyshare.local_party_key); - } catch (error) { - dbg('Error parsing keyshare:', error); - throw new Error('Invalid keyshare received'); - } - - // Map keyshare positions based on sorted npubs for UI display - const sortedNpubs = allNpubs.sort(); - const mapping: { - keyshare1?: {npub: string; deviceName: string; isLocal: boolean}; - keyshare2?: {npub: string; deviceName: string; isLocal: boolean}; - keyshare3?: {npub: string; deviceName: string; isLocal: boolean}; - } = {}; - - // Map npubs to keyshare positions using keygen_committee_keys order - // We need to match npubs to their corresponding hex keys in keygen_committee_keys - // For now, we'll use the sorted npubs order which should match the sorted keygen_committee_keys - sortedNpubs.forEach((npub, index) => { - const isLocal = npub === localNpub; - let mappedDeviceName = ''; - - if (isLocal) { - mappedDeviceName = deviceName || 'This device'; - } else if (npub === peerNpub1) { - mappedDeviceName = peerDeviceName1 || 'Peer 1'; - } else if (npub === peerNpub2) { - mappedDeviceName = peerDeviceName2 || 'Peer 2'; - } else { - mappedDeviceName = `Device ${index + 1}`; - } - - const keyshareKey = `keyshare${index + 1}` as - | 'keyshare1' - | 'keyshare2' - | 'keyshare3'; - mapping[keyshareKey] = { - npub, - deviceName: mappedDeviceName, - isLocal, - }; - }); - - setKeyshareMapping(mapping); - dbg('Keyshare mapping:', mapping); - - // Save keyshare (keyshare_position will be calculated on-the-fly when needed) - await EncryptedStorage.setItem('keyshare', keyshareJSON); - setMpcDone(true); - setStatus('Key generation complete!'); - // Don't navigate away, let the backup UI handle it - } catch (error: any) { - dbg('Keygen error:', error); - Alert.alert('Error', error?.message || 'Key generation failed'); - setStatus('Key generation failed'); - // Navigate to index 0 (reload same page) on keygen failure - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{name: 'Nostr Connect', params: route.params}], - }), - ); - } finally { - setIsPairing(false); - } - }; - - const startSendBTC = async () => { - if (!route.params) { - Alert.alert('Error', 'Missing transaction parameters'); - return; - } - - setIsPairing(true); - setProgress(0); - setStatus('Starting transaction signing...'); - - try { - // Get wallet balance in satoshis - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (!keyshareJSON) { - throw new Error('Keyshare not found'); - } - - const keyshare = JSON.parse(keyshareJSON); - - // Get nsec from keyshare (use local variable, not state) - let nsecToUse = localNsec; - if (!nsecToUse || !nsecToUse.startsWith('nsec1')) { - const nsecFromKeyshare = keyshare.nsec || ''; - if (nsecFromKeyshare) { - try { - let decodedNsec = ''; - - // Check if it's already in bech32 format - if (nsecFromKeyshare.startsWith('nsec1')) { - decodedNsec = nsecFromKeyshare; - } else { - decodedNsec = hexToString(nsecFromKeyshare); - } - - if (decodedNsec.startsWith('nsec1')) { - nsecToUse = decodedNsec; - setLocalNsec(decodedNsec); // Update state for UI - dbg( - 'Loaded nsec from keyshare in startSendBTC:', - decodedNsec.substring(0, 20) + '...', - ); - } else { - throw new Error( - `Invalid nsec format in keyshare: ${decodedNsec.substring( - 0, - 50, - )}`, - ); - } - } catch (error) { - dbg('Error loading nsec from keyshare in startSendBTC:', error); - throw new Error(`Failed to load nsec from keyshare: ${error}`); - } - } else { - throw new Error('nsec not found in keyshare'); - } - } - - // Verify nsec is valid - if (!nsecToUse || !nsecToUse.startsWith('nsec1')) { - throw new Error( - 'Invalid nsec: nsec must be in bech32 format (nsec1...)', - ); - } - - if (!activeAddress) { - throw new Error('Active address not found'); - } - - const balance = await WalletService.getInstance().getWalletBalance( - activeAddress, - 0, - 0, - false, - ); - const balanceSats = Big(balance.btc).times(1e8).toString(); - - // IMPORTANT: For session ID, we need ALL npubs from the keyshare (all participants) - // Get all hex keys from keygen_committee_keys and convert them ALL to npubs - const allNpubsFromKeyshare: string[] = []; - - // Get all keys from keygen_committee_keys and convert them ALL to npubs - const sortedKeys = [...keyshare.keygen_committee_keys].sort(); - for (const key of sortedKeys) { - try { - // Check if it's already an npub (shouldn't happen, but handle it) - if (key && typeof key === 'string' && key.startsWith('npub1')) { - allNpubsFromKeyshare.push(key); - dbg('Key already npub format:', key.substring(0, 20) + '...'); - continue; - } - - // Validate hex key format - const hexPattern = /^[0-9a-fA-F]+$/; - if (!hexPattern.test(key)) { - dbg( - 'Invalid key format (not hex, not npub), skipping:', - key.substring(0, 20) + '...', - ); - continue; - } - - // Convert hex to npub (convert ALL keys, including local) - const npub = await BBMTLibNativeModule.hexToNpub(key); - if (npub && typeof npub === 'string' && npub.startsWith('npub1')) { - allNpubsFromKeyshare.push(npub); - dbg( - 'Converted hex to npub for session ID:', - npub.substring(0, 20) + '...', - ); - } else { - dbg('Failed to convert hex to npub, result:', npub); - } - } catch (error) { - dbg('Error converting hex to npub for session ID:', error); - } - } - - // Sort all npubs - this must match on all devices - const npubsSorted = [...allNpubsFromKeyshare].sort().join(','); - - if (npubsSorted.length === 0 || allNpubsFromKeyshare.length < 2) { - throw new Error( - `Failed to get all npubs from keyshare. Got ${allNpubsFromKeyshare.length} npubs. Please ensure all devices are loaded.`, - ); - } - - dbg( - 'All npubs for session ID:', - allNpubsFromKeyshare.map(n => n.substring(0, 20) + '...'), - ); - - // Prepare parties npubs CSV for the actual signing (only participating devices) - // IMPORTANT: Use the full npubs from allNpubsFromKeyshare (already converted from hex) - // This ensures we use the same npubs that were used for session ID calculation - // Find local npub in allNpubsFromKeyshare to ensure consistency - const localNpubFromKeyshare = - allNpubsFromKeyshare.find(n => { - // Match by checking if localNpub (from state) matches or starts with this npub - return ( - n === localNpub || - (localNpub && n.startsWith(localNpub.substring(0, 20))) - ); - }) || localNpub; // Fallback to state if not found - - const allNpubs = [localNpubFromKeyshare]; - if (isTrio) { - // For trio, use selected peer - find it by matching device in sendModeDevices - if (selectedPeerNpub) { - // Find the selected device in sendModeDevices to get its keyshareLabel - const selectedDevice = sendModeDevices.find( - d => - d.npub === selectedPeerNpub || - (selectedPeerNpub.startsWith('npub1') && - d.npub && - d.npub.startsWith(selectedPeerNpub.substring(0, 20))) || - (d.npub && selectedPeerNpub.startsWith(d.npub.substring(0, 20))), - ); - - if (selectedDevice) { - // Find the corresponding hex key in keyshare by keyshareLabel - // Use the same sortedKeys from above (already sorted) - const selectedIndex = - parseInt( - selectedDevice.keyshareLabel.replace('KeyShare', ''), - 10, - ) - 1; - - if (selectedIndex >= 0 && selectedIndex < sortedKeys.length) { - const selectedHexKey = sortedKeys[selectedIndex]; - - // Find the full npub in allNpubsFromKeyshare that corresponds to this hex key - // We need to convert the hex key to npub and find it, or match by index - // Since allNpubsFromKeyshare is built from sortedKeys in the same order, we can use index - if (selectedIndex < allNpubsFromKeyshare.length) { - const fullPeerNpub = allNpubsFromKeyshare[selectedIndex]; - // Verify it's not the local device - if (fullPeerNpub !== localNpubFromKeyshare) { - allNpubs.push(fullPeerNpub); - dbg( - 'Found full peer npub for trio by index:', - fullPeerNpub.substring(0, 20) + '...', - ); - } else { - throw new Error('Selected device is the local device'); - } - } else { - // Fallback: try to convert hex key to npub - try { - const hexPattern = /^[0-9a-fA-F]+$/; - if (hexPattern.test(selectedHexKey)) { - const convertedNpub = await BBMTLibNativeModule.hexToNpub( - selectedHexKey, - ); - if ( - convertedNpub && - convertedNpub.startsWith('npub1') && - convertedNpub !== localNpubFromKeyshare - ) { - allNpubs.push(convertedNpub); - dbg( - 'Found full peer npub for trio by conversion:', - convertedNpub.substring(0, 20) + '...', - ); - } else { - throw new Error( - 'Failed to convert selected hex key to npub', - ); - } - } else { - throw new Error('Selected hex key is not valid hex'); - } - } catch (error) { - throw new Error( - `Failed to find full npub for selected peer: ${error}`, - ); - } - } - } else { - throw new Error( - `Invalid keyshare label: ${selectedDevice.keyshareLabel}`, - ); - } - } else { - // Fallback: try direct matching in allNpubsFromKeyshare - let fullPeerNpub = allNpubsFromKeyshare.find( - n => - n === selectedPeerNpub || - (selectedPeerNpub.startsWith('npub1') && - n.startsWith(selectedPeerNpub.substring(0, 20))), - ); - - if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) { - allNpubs.push(fullPeerNpub); - dbg( - 'Found full peer npub for trio by direct match:', - fullPeerNpub.substring(0, 20) + '...', - ); - } else { - throw new Error( - `Failed to find full npub for selected peer: ${selectedPeerNpub.substring( - 0, - 30, - )}. Please ensure the device is fully loaded.`, - ); - } - } - } else { - throw new Error('Please select a peer device for trio mode'); - } - } else { - // For duo, use the other device from allNpubsFromKeyshare (excluding local) - const otherNpubs = allNpubsFromKeyshare.filter( - n => n !== localNpubFromKeyshare, - ); - if (otherNpubs.length > 0) { - // In duo mode, there should be exactly one other npub - allNpubs.push(otherNpubs[0]); - dbg( - 'Using other npub for duo:', - otherNpubs[0].substring(0, 20) + '...', - ); - } else { - throw new Error('Other device npub not found in keyshare'); - } - } - const partiesNpubsCSV = allNpubs.sort().join(','); - - dbg( - 'partiesNpubsCSV for signing (full npubs, length=', - partiesNpubsCSV.length, - '):', - partiesNpubsCSV.substring(0, 100) + '...', - ); - const satoshiAmount = route.params.satoshiAmount || '0'; - const satoshiFees = route.params.satoshiFees || '0'; - - // Generate session ID (includes all transaction details that must match) - // Format: sha256(npubsSorted,balance,amount,rounded) - const rounded = Math.floor(Date.now() / 90000); - - dbg( - 'session id params:', - `${npubsSorted},${balanceSats},${satoshiAmount},${rounded}`, - ); - dbg( - 'session_params', - JSON.stringify( - {npubsSorted, balanceSats, satoshiAmount, rounded}, - null, - 4, - ), - ); - - // Prepare relays CSV - const relaysCSV = relays.join(','); - - const network = (await LocalCache.getItem('network')) || 'mainnet'; - const derivePath = getDerivePathForNetwork(network); - - // Derive the public key from the root key using the derivation path - // This is critical - we need the DERIVED public key, not the root! - const publicKey = await BBMTLibNativeModule.derivePubkey( - keyshare.pub_key, - keyshare.chain_code_hex, - derivePath, - ); - - dbg( - 'Derived public key for path:', - derivePath, - 'pubKey:', - publicKey.substring(0, 20) + '...', - ); - - // Generate BTC address using addressType (same as MobilesPairing.tsx) - const net = (await LocalCache.getItem('network')) || 'mainnet'; - const btcAddress = await BBMTLibNativeModule.btcAddress( - publicKey, - net, - addressType, - ); - - dbg('Starting Nostr send BTC with:', { - relays: relaysCSV, - parties: partiesNpubsCSV, - npubsSorted: npubsSorted.substring(0, 30) + '...', - balance: balanceSats, - amount: route.params?.satoshiAmount, - localNsec: nsecToUse ? nsecToUse.substring(0, 20) + '...' : 'MISSING', - derivePath: derivePath, - derivedPublicKey: publicKey.substring(0, 20) + '...', - btcAddress: btcAddress, - addressType: addressType, - estimatedFees: satoshiFees, - }); - - dbg( - 'Calling nostrMpcSendBTC (pre-agreement handled internally):', - nsecToUse.substring(0, 20) + '...', - ); - - // Call native module - pre-agreement is now handled internally in Go - // The function will calculate sessionFlag, do pre-agreement, update sessionID and fees - // Use btcAddress generated from addressType (same as MobilesPairing.tsx) - const txId = await BBMTLibNativeModule.nostrMpcSendBTC( - relaysCSV, - nsecToUse, - partiesNpubsCSV, - npubsSorted, - balanceSats, - keyshareJSON, - derivePath, - publicKey, - btcAddress, - route.params.toAddress || '', - route.params.satoshiAmount || '0', - route.params.satoshiFees || '0', - ); - - // Validate txId - const validTxID = /^[a-fA-F0-9]{64}$/.test(txId); - if (!validTxID) { - throw new Error(txId || 'Invalid transaction ID'); - } - - // Save pending transaction - const pendingTxs = JSON.parse( - (await LocalCache.getItem(`${activeAddress}-pendingTxs`)) || '{}', - ); - pendingTxs[txId] = { - txid: txId, - from: activeAddress, - to: route.params.toAddress, - amount: route.params.satoshiAmount, - satoshiAmount: route.params.satoshiAmount, - satoshiFees: route.params.satoshiFees, - sentAt: Date.now(), - status: { - confirmed: false, - block_height: null, - }, - }; - await LocalCache.setItem( - `${activeAddress}-pendingTxs`, - JSON.stringify(pendingTxs), - ); - - // Navigate to home (same as MobilesPairing.tsx) - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{name: 'Home', params: {txId}}], - }), - ); - - setMpcDone(true); - } catch (error: any) { - dbg('Send BTC error:', error); - Alert.alert('Error', error?.message || 'Transaction signing failed'); - setStatus('Transaction signing failed'); - } finally { - setIsPairing(false); - } - }; - - // PSBT Signing function - similar to startSendBTC but for PSBT - const startSignPSBT = async () => { - if (!route.params?.psbtBase64) { - Alert.alert('Error', 'Missing PSBT data'); - return; - } - - if (!isKeysignReady) { - Alert.alert('Not Ready', 'Please confirm that you are ready to sign the PSBT'); - return; - } - - setIsPairing(true); - setProgress(0); - setStatus('Starting PSBT signing...'); - - try { - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (!keyshareJSON) { - throw new Error('Keyshare not found'); - } - - const keyshare = JSON.parse(keyshareJSON); - - // Get nsec from keyshare - let nsecToUse = localNsec; - if (!nsecToUse || !nsecToUse.startsWith('nsec1')) { - const nsecFromKeyshare = keyshare.nsec || ''; - if (nsecFromKeyshare) { - try { - let decodedNsec = ''; - if (nsecFromKeyshare.startsWith('nsec1')) { - decodedNsec = nsecFromKeyshare; - } else { - decodedNsec = hexToString(nsecFromKeyshare); - } - - if (decodedNsec.startsWith('nsec1')) { - nsecToUse = decodedNsec; - setLocalNsec(decodedNsec); - } else { - throw new Error('Invalid nsec format in keyshare'); - } - } catch (error) { - throw new Error(`Failed to load nsec from keyshare: ${error}`); - } - } else { - throw new Error('nsec not found in keyshare'); - } - } - - if (!nsecToUse || !nsecToUse.startsWith('nsec1')) { - throw new Error('Invalid nsec: must be in bech32 format (nsec1...)'); - } - - // Get all npubs from keyshare for session ID - const allNpubsFromKeyshare: string[] = []; - const sortedKeys = [...keyshare.keygen_committee_keys].sort(); - for (const key of sortedKeys) { - try { - if (key && typeof key === 'string' && key.startsWith('npub1')) { - allNpubsFromKeyshare.push(key); - continue; - } - const hexPattern = /^[0-9a-fA-F]+$/; - if (!hexPattern.test(key)) { - continue; - } - const npub = await BBMTLibNativeModule.hexToNpub(key); - if (npub && typeof npub === 'string' && npub.startsWith('npub1')) { - allNpubsFromKeyshare.push(npub); - } - } catch (error) { - dbg('Error converting hex to npub:', error); - } - } - - const npubsSorted = [...allNpubsFromKeyshare].sort().join(','); - - if (npubsSorted.length === 0 || allNpubsFromKeyshare.length < 2) { - throw new Error('Failed to get all npubs from keyshare'); - } - - // Find local npub - const localNpubFromKeyshare = - allNpubsFromKeyshare.find(n => n === localNpub || (localNpub && n.startsWith(localNpub.substring(0, 20)))) || - localNpub; - - // Build parties CSV - const allNpubs = [localNpubFromKeyshare]; - if (isTrio) { - if (selectedPeerNpub) { - const selectedDevice = sendModeDevices.find( - d => d.npub === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && d.npub && d.npub.startsWith(selectedPeerNpub.substring(0, 20))), - ); - if (selectedDevice) { - const selectedIndex = parseInt(selectedDevice.keyshareLabel.replace('KeyShare', ''), 10) - 1; - if (selectedIndex >= 0 && selectedIndex < allNpubsFromKeyshare.length) { - const fullPeerNpub = allNpubsFromKeyshare[selectedIndex]; - if (fullPeerNpub !== localNpubFromKeyshare) { - allNpubs.push(fullPeerNpub); - } else { - throw new Error('Selected device is the local device'); - } - } - } else { - const fullPeerNpub = allNpubsFromKeyshare.find( - n => n === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && n.startsWith(selectedPeerNpub.substring(0, 20))), - ); - if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) { - allNpubs.push(fullPeerNpub); - } else { - throw new Error('Failed to find full npub for selected peer'); - } - } - } else { - throw new Error('Please select a peer device for trio mode'); - } - } else { - const otherNpubs = allNpubsFromKeyshare.filter(n => n !== localNpubFromKeyshare); - if (otherNpubs.length > 0) { - allNpubs.push(otherNpubs[0]); - } else { - throw new Error('Other device npub not found in keyshare'); - } - } - const partiesNpubsCSV = allNpubs.sort().join(','); - - const relaysCSV = relays.join(','); - const network = (await LocalCache.getItem('network')) || 'mainnet'; - const derivePath = route.params?.derivePath || getDerivePathForNetwork(network); - - dbg('Using derivation path for PSBT signing:', derivePath); - - // Derive the public key - const publicKey = await BBMTLibNativeModule.derivePubkey( - keyshare.pub_key, - keyshare.chain_code_hex, - derivePath, - ); - - dbg('Starting Nostr PSBT signing with:', { - relays: relaysCSV, - parties: partiesNpubsCSV.substring(0, 50) + '...', - npubsSorted: npubsSorted.substring(0, 30) + '...', - derivePath, - publicKey: publicKey.substring(0, 20) + '...', - psbtLength: route.params.psbtBase64?.length, - }); - - // Call native module for PSBT signing - const signedPsbt = await BBMTLibNativeModule.nostrMpcSignPSBT( - relaysCSV, - nsecToUse, - partiesNpubsCSV, - npubsSorted, - keyshareJSON, - route.params.psbtBase64, - ); - - // Validate result - if (!signedPsbt || signedPsbt.includes('error') || signedPsbt.includes('failed')) { - throw new Error(signedPsbt || 'PSBT signing failed'); - } - - dbg('PSBT signed successfully, length:', signedPsbt.length); - - // Check user's wallet mode preference before navigating - let targetRoute = 'Home'; - try { - const walletMode = - (await EncryptedStorage.getItem('wallet_mode')) || 'full'; - targetRoute = walletMode === 'psbt' ? 'PSBT' : 'Home'; - dbg( - 'PSBT signing complete: Navigating to', - targetRoute, - 'based on wallet_mode:', - walletMode, - ); - } catch (error) { - dbg('Error loading wallet_mode after PSBT signing:', error); - // Default to 'Home' if there's an error - } - - // Navigate to the appropriate screen based on user preference - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{name: targetRoute, params: {signedPsbt}}], - }), - ); - - setMpcDone(true); - } catch (error: any) { - dbg('Sign PSBT error:', error); - Alert.alert('Error', error?.message || 'PSBT signing failed'); - setStatus('PSBT signing failed'); - } finally { - setIsPairing(false); - } - }; - - // Backup functions - const allBackupChecked = isTrio - ? backupChecks.deviceOne && - backupChecks.deviceTwo && - backupChecks.deviceThree - : backupChecks.deviceOne && backupChecks.deviceTwo; - - const toggleBackedup = (key: keyof typeof backupChecks) => { - setBackupChecks(prev => ({...prev, [key]: !prev[key]})); - }; - - const formatFiat = (price?: string) => - new Intl.NumberFormat('en-US', { - style: 'decimal', - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(Number(price)); - - const sat2btcStr = (sats?: string) => - Big(sats || 0) - .div(1e8) - .toFixed(8); - - const validatePassword = (pass: string) => { - const errors: string[] = []; - const rules = { - length: pass.length >= 12, - uppercase: /[A-Z]/.test(pass), - lowercase: /[a-z]/.test(pass), - number: /\d/.test(pass), - symbol: /[!@#$%^&*(),.?":{}|<>]/.test(pass), - }; - - if (!rules.length) { - errors.push('12+ characters'); - } - if (!rules.uppercase) { - errors.push('Uppercase letter (A-Z)'); - } - if (!rules.lowercase) { - errors.push('Lowercase letter (a-z)'); - } - if (!rules.number) { - errors.push('Number (0-9)'); - } - if (!rules.symbol) { - errors.push('Special character (!@#$...)'); - } - setPasswordErrors(errors); - - // Calculate strength (0-4) - const strength = Object.values(rules).filter(Boolean).length; - setPasswordStrength(strength); - - return errors.length === 0; - }; - - const handlePasswordChange = (text: string) => { - setPassword(text); - validatePassword(text); - }; - - const getPasswordStrengthColor = () => { - if (passwordStrength <= 1) { - return theme.colors.danger; - } - if (passwordStrength <= 2) { - return '#FFA500'; - } - if (passwordStrength <= 3) { - return '#FFD700'; - } - return '#4CAF50'; - }; - - const getPasswordStrengthText = () => { - if (passwordStrength <= 1) { - return 'Very Weak'; - } - if (passwordStrength <= 2) { - return 'Weak'; - } - if (passwordStrength <= 3) { - return 'Medium'; - } - return 'Strong'; - }; - - const clearBackupModal = () => { - setPassword(''); - setConfirmPassword(''); - setPasswordVisible(false); - setConfirmPasswordVisible(false); - setPasswordStrength(0); - setPasswordErrors([]); - setIsBackupModalVisible(false); - }; - - async function backupShare() { - if (!validatePassword(password)) { - dbg('❌ [BACKUP] Password validation failed'); - const missingRequirements = passwordErrors.join('\n• '); - Alert.alert( - 'Password Requirements Not Met', - `Your password must meet all of the following requirements:\n\n• ${missingRequirements}\n\nPlease update your password and try again.`, - ); - return; - } - - if (password !== confirmPassword) { - dbg('❌ [BACKUP] Password mismatch'); - Alert.alert( - 'Passwords Do Not Match', - 'The password and confirmation password must be identical. Please check both fields and try again.', - ); - return; - } - - try { - HapticFeedback.medium(); - - const storedKeyshare = await EncryptedStorage.getItem('keyshare'); - if (storedKeyshare) { - const json = JSON.parse(storedKeyshare); - const encryptedKeyshare = await BBMTLibNativeModule.aesEncrypt( - storedKeyshare, - await BBMTLibNativeModule.sha256(password), - ); - - // Create friendly filename with date and time - const now = new Date(); - const month = now.toLocaleDateString('en-US', {month: 'short'}); - const day = now.getDate().toString().padStart(2, '0'); - const year = now.getFullYear(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key - const keyshareLabel = getKeyshareLabel(json); - const shareName = keyshareLabel || json.local_party_key || 'keyshare'; - const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`; - - const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath; - const filePath = `${tempDir}/${friendlyFilename}`; - - await RNFS.writeFile(filePath, encryptedKeyshare, 'base64'); - - await Share.open({ - title: 'Backup Your Keyshare', - message: - 'Save this encrypted file securely. It is required for wallet recovery.', - url: `file://${filePath}`, - type: 'application/octet-stream', - filename: friendlyFilename, - failOnCancel: false, - }); - - try { - await RNFS.unlink(filePath); - } catch {} - clearBackupModal(); - } else { - Alert.alert('Error', 'Invalid keyshare.'); - } - } catch (error) { - dbg('Error encrypting or sharing keyshare:', error); - Alert.alert('Error', 'Failed to encrypt or share the keyshare.'); - } - } - - const copyConnectionDetails = () => { - Clipboard.setString(connectionDetails); - HapticFeedback.medium(); - Alert.alert( - 'Copied', - '- Pairing data copied.\n- Paste them to other device(s)', - ); - }; - - const shareConnectionDetails = async () => { - HapticFeedback.medium(); - - if (!connectionDetails) { - Alert.alert('Error', 'Connection details are not ready yet'); - return; - } - - if (!connectionQrRef.current) { - Alert.alert('Error', 'QR Code is not ready yet'); - return; - } - - try { - // Generate base64 from QR component (similar to WalletHome ReceiveModal) - const base64Data: string = await new Promise((resolve, reject) => { - connectionQrRef.current.toDataURL((data: string) => { - if (data) { - resolve(data); - } else { - reject(new Error('No base64 data returned from QR code')); - } - }); - }); - - const filePath = `${RNFS.TemporaryDirectoryPath}/boldwallet-connection-details.jpg`; - const fileExists = await RNFS.exists(filePath); - if (fileExists) { - await RNFS.unlink(filePath); - } - - await RNFS.writeFile(filePath, base64Data, 'base64'); - - await Share.open({ - title: 'Bold Wallet Connection Details', - message: connectionDetails, - url: `file://${filePath}`, - subject: 'Bold Wallet Connection Details', - isNewTask: true, - failOnCancel: false, - }); - - // Best-effort cleanup - await RNFS.unlink(filePath).catch(() => {}); - - setIsQRModalVisible(false); - } catch (error: any) { - dbg('Error sharing connection details (QR + text):', error); - Alert.alert('Error', 'Failed to share connection QR code'); - } - }; - - const showQRModal = () => { - HapticFeedback.medium(); - setIsQRModalVisible(true); - }; - - const handleQRScan = (data: string, peerNum?: 1 | 2) => { - HapticFeedback.medium(); - // FOSS version: No need to close iOS scanner modal (removed) - // Use provided peerNum, or fallback to scanningForPeerRef (more reliable than state) - const targetPeer = peerNum || scanningForPeerRef.current; - dbg( - `handleQRScan: data="${data.substring( - 0, - 30, - )}...", peerNum=${targetPeer}, scanningForPeerRef=${ - scanningForPeerRef.current - }`, - ); - handlePeerConnectionInput(data, targetPeer); - }; - - const handlePaste = async (peerNum: 1 | 2) => { - try { - const text = await Clipboard.getString(); - dbg(`handlePaste: peerNum=${peerNum}, pasted text="${text}"`); - HapticFeedback.medium(); - - // Update the input field immediately so user can see what was pasted - if (peerNum === 1) { - setPeerConnectionDetails1(text); - } else { - setPeerConnectionDetails2(text); - } - - // Then validate the input - await handlePeerConnectionInput(text, peerNum); - } catch (error) { - dbg('Error pasting:', error); - Alert.alert('Error', 'Failed to paste from clipboard'); - } - }; - - const clearPeerConnection = (peerNum: 1 | 2) => { - HapticFeedback.medium(); - if (peerNum === 1) { - setPeerNpub1(''); - setPeerDeviceName1(''); - setPeerNonce1(''); - setPeerConnectionDetails1(''); - setPeerInputError1(''); - } else { - setPeerNpub2(''); - setPeerDeviceName2(''); - setPeerNonce2(''); - setPeerConnectionDetails2(''); - setPeerInputError2(''); - } - }; - - const deletePreparams = async () => { - try { - dbg(`deleting ppmFile: ${ppmFile}`); - await RNFS.unlink(ppmFile); - dbg('ppmFile deleted'); - } catch (err: any) { - dbg('error deleting ppmFile', err); - } - }; - - const prepareDevice = async () => { - setIsPreparing(true); - setIsPreParamsReady(false); - setPrepCounter(0); - - const timeoutMinutes = 20; - - if (!__DEV__) { - await deletePreparams(); - } else { - dbg('preparams dev: Not deleting ppmFile'); - } - - try { - await BBMTLibNativeModule.preparams(ppmFile, String(timeoutMinutes)); - setIsPreParamsReady(true); - HapticFeedback.medium(); - dbg('Device prepared successfully'); - } catch (error: any) { - setIsPreParamsReady(false); - dbg('Error preparing device:', error); - Alert.alert('Error', error?.toString() || 'Failed to prepare device'); - } finally { - setIsPreparing(false); - setPrepCounter(0); - } - }; - - // Increment prep counter when preparing - useEffect(() => { - if (isPreparing) { - const interval = setInterval(() => { - setPrepCounter(prevCounter => prevCounter + 1); - }, 1000); - return () => clearInterval(interval); - } - }, [isPreparing]); - - // Track elapsed time during keygen and signing - useEffect(() => { - if (isPairing) { - setPrepCounter(0); - const interval = setInterval(() => { - setPrepCounter(prevCounter => prevCounter + 1); - }, 1000); - return () => clearInterval(interval); - } - }, [isPairing]); - - // Animation for horizontal progress bar - useEffect(() => { - if (isPreparing) { - // Stop any existing animation first - if (progressAnimationLoop.current) { - progressAnimationLoop.current.stop(); - progressAnimationLoop.current = null; - } - - // Small delay to ensure modal is mounted before starting animation - const timeoutId = setTimeout(() => { - // Reset value before starting new animation (only when modal is mounted) - progressAnimation.setValue(0); - - // Start new animation loop - progressAnimationLoop.current = Animated.loop( - Animated.sequence([ - Animated.timing(progressAnimation, { - toValue: 1, - duration: 2000, - useNativeDriver: false, - }), - Animated.timing(progressAnimation, { - toValue: 0, - duration: 2000, - useNativeDriver: false, - }), - ]), - ); - progressAnimationLoop.current.start(); - }, 150); - - return () => { - clearTimeout(timeoutId); - if (progressAnimationLoop.current) { - progressAnimationLoop.current.stop(); - progressAnimationLoop.current = null; - } - // Stop any running animation without setting value - progressAnimation.stopAnimation(); - }; - } else { - // Stop animation without setting value to avoid warning - if (progressAnimationLoop.current) { - progressAnimationLoop.current.stop(); - progressAnimationLoop.current = null; - } - // Stop any running animation - progressAnimation.stopAnimation(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPreparing]); - - // Styles - const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: theme.colors.background, - }, - scrollView: { - flex: 1, - }, - content: { - padding: 20, - }, - section: { - marginBottom: 8, - }, - sectionTitle: { - fontSize: 18, - fontWeight: '700', - color: theme.colors.text, - marginBottom: 12, - }, - card: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 12, - padding: 4, - borderWidth: 1, - borderColor: theme.colors.border + '40', - }, - cardSelected: { - borderColor: theme.colors.primary, - borderWidth: 2, - backgroundColor: theme.colors.primary + '10', - }, - deviceInfoRowWithCheckbox: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 12, - paddingVertical: 8, - }, - peerCheckbox: { - width: 24, - height: 24, - borderRadius: 6, - borderWidth: 2, - borderColor: theme.colors.border, - backgroundColor: theme.colors.cardBackground, - alignItems: 'center', - justifyContent: 'center', - marginLeft: 12, - }, - peerCheckboxChecked: { - borderColor: theme.colors.primary, - backgroundColor: theme.colors.primary, - }, - peerCheckmark: { - color: theme.colors.white, - fontSize: 16, - fontWeight: '700', - }, - input: { - borderWidth: 1.5, - borderColor: theme.colors.border + '40', - borderRadius: 12, - paddingHorizontal: 16, - paddingVertical: 14, - fontSize: 16, - color: theme.colors.text, - backgroundColor: 'rgba(0,0,0,0.02)', - }, - inputFocused: { - borderColor: theme.colors.primary, - backgroundColor: 'rgba(0,0,0,0.03)', - }, - inputWithIcons: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - inputFlex: { - flex: 1, - }, - inputCentered: { - textAlignVertical: 'center', - }, - inputTextDisplay: { - paddingVertical: 14, - lineHeight: 20, - }, - iconButton: { - width: 48, - height: 48, - borderRadius: 12, - backgroundColor: theme.colors.primary + '20', - alignItems: 'center', - justifyContent: 'center', - }, - iconButtonCentered: { - alignSelf: 'center', - }, - deviceInfoRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - paddingVertical: 4, - }, - deviceInfoSingleLine: { - fontSize: 14, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - color: theme.colors.text, - flex: 1, - textAlign: 'center', - overflow: 'hidden', - }, - deviceInfoContent: { - flex: 1, - }, - hintBox: { - backgroundColor: theme.colors.primary + '10', - borderRadius: 8, - padding: 6, - borderLeftWidth: 3, - borderLeftColor: theme.colors.primary, - }, - hintText: { - fontSize: 13, - color: theme.colors.text, - lineHeight: 18, - }, - sendModeDeviceItem: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: theme.colors.cardBackground, - borderRadius: 10, - paddingVertical: 10, - paddingHorizontal: 12, - marginBottom: 8, - borderWidth: 1, - borderColor: theme.colors.border + '30', - }, - sendModeDeviceItemSelected: { - borderColor: theme.colors.primary, - borderWidth: 1.5, - backgroundColor: theme.colors.primary + '08', - }, - sendModeDeviceIcon: { - width: 20, - height: 20, - tintColor: theme.colors.primary, - marginRight: 10, - }, - sendModeDeviceContent: { - flex: 1, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - sendModeDeviceLabel: { - fontSize: 14, - fontWeight: '600', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, - sendModeDeviceNpub: { - fontSize: 12, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - marginLeft: 8, - }, - sendModeDeviceBadge: { - fontSize: 11, - color: theme.colors.primary, - marginTop: 2, - fontWeight: '500', - }, - sendModeCheckbox: { - width: 22, - height: 22, - borderRadius: 6, - borderWidth: 2, - borderColor: theme.colors.border, - backgroundColor: theme.colors.cardBackground, - alignItems: 'center', - justifyContent: 'center', - marginLeft: 8, - }, - sendModeCheckboxChecked: { - borderColor: theme.colors.primary, - backgroundColor: theme.colors.primary, - }, - sendModeCheckmark: { - color: theme.colors.background, - fontSize: 14, - fontWeight: '700', - }, - buttonHalf: { - flex: 0.48, - }, - buttonCompact: { - flex: 1, - backgroundColor: 'transparent', - borderRadius: 8, - paddingVertical: 10, - paddingHorizontal: 12, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - gap: 6, - borderWidth: 1.5, - borderColor: theme.colors.border, - }, - buttonTextCompact: { - fontSize: 14, - fontWeight: '600', - }, - iconImageCompact: { - width: 18, - height: 18, - tintColor: theme.colors.primary, - }, - iconImage: { - width: 24, - height: 24, - tintColor: theme.colors.primary, - }, - iconPrepare: { - width: 24, - height: 24, - tintColor: theme.colors.textOnPrimary, - }, - iconShare: { - width: 24, - height: 24, - tintColor: theme.colors.textOnPrimary, - }, - checkIconLeft: { - width: 20, - height: 20, - tintColor: '#4CAF50', - marginRight: 8, - }, - qrContainer: { - backgroundColor: 'white', - padding: 16, - borderRadius: 12, - alignItems: 'center', - marginBottom: 16, - }, - connectionDetailsText: { - fontSize: 12, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - color: theme.colors.textSecondary, - marginBottom: 12, - textAlign: 'center', - }, - buttonRow: { - flexDirection: 'row', - gap: 12, - marginBottom: 12, - }, - button: { - flex: 1, - backgroundColor: theme.colors.primary, - borderRadius: 12, - paddingVertical: 14, - alignItems: 'center', - justifyContent: 'center', - flexDirection: 'row', - gap: 8, - }, - buttonSecondary: { - backgroundColor: 'transparent', - borderWidth: 2, - borderColor: theme.colors.border, - }, - buttonText: { - color: theme.colors.background, - fontSize: 16, - fontWeight: '600', - }, - buttonTextSecondary: { - color: theme.colors.secondary, - }, - buttonDisabled: { - opacity: 0.5, - }, - statusIndicator: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: theme.colors.primary, - marginRight: 8, - }, - modalSubtitle: { - fontSize: 14, - color: theme.colors.textSecondary, - marginBottom: 16, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - lineHeight: 20, - }, - progressCircle: { - marginBottom: 16, - }, - progressTextWrapper: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - justifyContent: 'center', - alignItems: 'center', - }, - progressPercentage: { - fontSize: 14, - fontWeight: 'bold', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - marginBottom: 16, - }, - modalIconContainer: { - marginBottom: 10, - alignItems: 'center', - }, - modalIconBackground: { - width: 50, - height: 50, - borderRadius: 25, - backgroundColor: theme.colors.primary + '20', - alignItems: 'center', - justifyContent: 'center', - }, - finalizingModalIcon: { - width: 24, - height: 24, - tintColor: theme.colors.primary, - }, - statusContainer: { - width: '100%', - marginTop: 8, - }, - statusRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - }, - finalizingStatusText: { - fontSize: 14, - color: theme.colors.text, - fontWeight: '500', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - flex: 1, - }, - finalizingCountdownText: { - fontSize: 13, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - }, - statusCheck: { - width: 20, - height: 20, - tintColor: theme.colors.primary, - }, - statusText: { - fontSize: 14, - color: theme.colors.text, - fontWeight: '500', - }, - statusTextSecondary: { - color: theme.colors.textSecondary, - }, - progressContainer: { - marginTop: 20, - alignItems: 'center', - }, - progressText: { - fontSize: 14, - color: theme.colors.textSecondary, - marginTop: 8, - }, - scannerContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - qrFrame: { - width: 250, - height: 250, - borderWidth: 2, - borderColor: theme.colors.primary, - borderRadius: 12, - }, - closeScannerButton: { - position: 'absolute', - bottom: 40, - backgroundColor: theme.colors.primary, - paddingHorizontal: 24, - paddingVertical: 12, - borderRadius: 12, - }, - closeScannerButtonText: { - color: theme.colors.background, - fontSize: 16, - fontWeight: '600', - }, - cameraNotFound: { - color: theme.colors.text, - fontSize: 16, - }, - sessionInfo: { - marginTop: 12, - padding: 12, - backgroundColor: 'rgba(0,0,0,0.02)', - borderRadius: 8, - }, - sessionInfoText: { - fontSize: 11, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - color: theme.colors.textSecondary, - marginBottom: 4, - }, - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0,0,0,0.75)', - alignItems: 'center', - justifyContent: 'center', - }, - qrModalContent: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 16, - width: '85%', - maxWidth: 400, - shadowColor: '#000', - shadowOffset: {width: 0, height: 10}, - shadowOpacity: 0.3, - shadowRadius: 20, - elevation: 10, - overflow: 'hidden', - }, - qrModalHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: 24, - paddingTop: 24, - paddingBottom: 16, - borderBottomWidth: 1, - borderBottomColor: theme.colors.border + '40', - }, - qrModalTitle: { - fontSize: 18, - fontWeight: '700', - color: theme.colors.text, - }, - qrModalDescription: { - fontSize: 14, - color: theme.colors.textSecondary, - textAlign: 'center', - marginTop: 12, - lineHeight: 20, - }, - qrModalCloseButton: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: theme.colors.subPrimary + '10', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 1, - borderColor: theme.colors.border + '10', - }, - qrModalCloseText: { - fontSize: 18, - color: theme.colors.text, - fontWeight: '600', - }, - qrModalBody: { - padding: 24, - alignItems: 'center', - }, - headerRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - headerContent: { - flex: 1, - alignItems: 'center', - }, - sectionSubtitle: { - fontSize: 14, - color: theme.colors.textSecondary, - marginTop: 4, - }, - helpButton: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: theme.colors.primary + '20', - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - }, - helpIcon: { - width: 20, - height: 20, - tintColor: theme.colors.primary, - }, - stepIndicatorContainer: { - marginBottom: 8, - paddingVertical: 8, - }, - stepRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginBottom: 8, - }, - stepCircle: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: theme.colors.border + '40', - alignItems: 'center', - justifyContent: 'center', - borderWidth: 2, - borderColor: theme.colors.border, - }, - stepCircleCompleted: { - backgroundColor: theme.colors.primary, - borderColor: theme.colors.primary, - }, - stepNumber: { - fontSize: 14, - fontWeight: '700', - color: theme.colors.textSecondary, - }, - stepNumberCompleted: { - color: theme.colors.background, - }, - stepLine: { - flex: 1, - height: 2, - backgroundColor: theme.colors.border + '40', - marginHorizontal: 8, - }, - stepLabels: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingHorizontal: 16, - marginTop: 4, - }, - stepLabel: { - fontSize: 11, - color: theme.colors.textSecondary, - textAlign: 'center', - flex: 1, - }, - collapsibleHeader: { - paddingVertical: 12, - paddingHorizontal: 16, - backgroundColor: theme.colors.cardBackground, - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.border + '40', - }, - collapsibleHeaderText: { - fontSize: 13, - fontWeight: '600', - color: theme.colors.textSecondary, - }, - collapsibleContent: { - marginTop: 8, - padding: 16, - backgroundColor: theme.colors.cardBackground, - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.border + '40', - }, - sectionHeaderRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - }, - primaryActionButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: theme.colors.primary, - paddingHorizontal: 16, - paddingVertical: 10, - borderRadius: 8, - gap: 6, - }, - emptyStateContainer: { - alignItems: 'center', - paddingVertical: 20, - marginBottom: 16, - }, - emptyStateIcon: { - width: 48, - height: 48, - marginBottom: 12, - opacity: 0.5, - }, - emptyStateText: { - fontSize: 13, - color: theme.colors.textSecondary, - textAlign: 'center', - paddingHorizontal: 20, - lineHeight: 18, - }, - readyCard: { - backgroundColor: theme.colors.primary + '10', - borderColor: theme.colors.primary, - borderWidth: 2, - }, - helpModalBody: { - maxHeight: 400, - padding: 24, - }, - helpSection: { - marginBottom: 24, - }, - helpTitle: { - fontSize: 16, - fontWeight: '700', - color: theme.colors.text, - marginBottom: 8, - }, - helpText: { - fontSize: 14, - color: theme.colors.textSecondary, - lineHeight: 20, - }, - inputError: { - borderColor: theme.colors.danger || '#FF3B30', - backgroundColor: (theme.colors.danger || '#FF3B30') + '10', - }, - inputSuccess: { - borderColor: theme.colors.primary, - backgroundColor: theme.colors.primary + '10', - }, - inputValidating: { - borderColor: theme.colors.textSecondary, - backgroundColor: theme.colors.textSecondary + '05', - }, - errorIndicator: { - marginTop: 8, - padding: 8, - backgroundColor: (theme.colors.danger || '#FF3B30') + '10', - borderRadius: 6, - borderLeftWidth: 3, - borderLeftColor: theme.colors.danger || '#FF3B30', - }, - errorText: { - fontSize: 12, - color: theme.colors.danger || '#FF3B30', - fontWeight: '500', - }, - validatingIndicator: { - width: 48, - height: 48, - alignItems: 'center', - justifyContent: 'center', - }, - validatingText: { - fontSize: 18, - color: theme.colors.textSecondary, - fontWeight: '600', - }, - checkboxContainer: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 16, - paddingVertical: 8, - }, - checkbox: { - width: 24, - height: 24, - borderRadius: 6, - borderWidth: 2, - borderColor: theme.colors.border, - backgroundColor: 'transparent', - marginRight: 12, - alignItems: 'center', - justifyContent: 'center', - }, - checkboxChecked: { - backgroundColor: theme.colors.primary, - borderColor: theme.colors.primary, - }, - checkboxCheckmark: { - color: theme.colors.background, - fontSize: 16, - fontWeight: '700', - }, - checkboxLabel: { - fontSize: 14, - color: theme.colors.text, - flex: 1, - }, - preparingModalContent: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 16, - padding: 24, - width: '90%', - maxWidth: 400, - alignItems: 'center', - shadowColor: '#000', - shadowOffset: {width: 0, height: 8}, - shadowOpacity: 0.25, - shadowRadius: 16, - elevation: 8, - }, - preparingModalIconContainer: { - marginBottom: 16, - alignItems: 'center', - }, - preparingModalIconBackground: { - width: 64, - height: 64, - borderRadius: 32, - backgroundColor: theme.colors.primary + '20', - alignItems: 'center', - justifyContent: 'center', - }, - preparingModalIcon: { - width: 32, - height: 32, - tintColor: theme.colors.primary, - }, - preparingModalTitle: { - fontSize: 20, - fontWeight: '700', - color: theme.colors.text, - textAlign: 'center', - marginBottom: 8, - }, - preparingModalSubtitle: { - fontSize: 14, - color: theme.colors.textSecondary, - textAlign: 'center', - marginBottom: 24, - }, - preparingProgressContainer: { - width: '100%', - alignItems: 'center', - marginBottom: 16, - }, - preparingProgressTrack: { - width: 200, - height: 6, - backgroundColor: theme.colors.border + '40', - borderRadius: 3, - overflow: 'hidden', - }, - preparingProgressBar: { - height: '100%', - borderRadius: 3, - }, - preparingStatusContainer: { - width: '100%', - marginTop: 8, - }, - preparingStatusRow: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 8, - justifyContent: 'center', - }, - preparingStatusIndicator: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: theme.colors.primary, - marginRight: 8, - }, - preparingStatusText: { - fontSize: 14, - color: theme.colors.text, - fontWeight: '500', - }, - preparingCountdownText: { - fontSize: 13, - color: theme.colors.textSecondary, - textAlign: 'center', - }, - informationCard: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 16, - padding: 20, - marginBottom: 16, - shadowColor: theme.colors.shadowColor, - shadowOffset: {width: 0, height: 2}, - shadowOpacity: 0.1, - shadowRadius: 8, - elevation: 3, - borderWidth: 1, - borderColor: theme.colors.border, - }, - backupButton: { - marginTop: 12, - backgroundColor: theme.colors.subPrimary, - width: '100%', - borderRadius: 12, - paddingVertical: 14, - paddingHorizontal: 16, - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'center', - shadowColor: theme.colors.shadowColor, - shadowOffset: {width: 0, height: 4}, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 4, - }, - backupButtonText: { - color: theme.colors.background, - fontSize: 16, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - lineHeight: 22, - }, - backupConfirmationHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - }, - backupConfirmationIcon: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: theme.colors.secondary, - alignItems: 'center', - justifyContent: 'center', - marginRight: 12, - }, - backupConfirmationIconText: { - color: theme.colors.background, - fontSize: 14, - fontWeight: 'bold', - }, - backupConfirmationTitle: { - fontSize: 18, - fontWeight: '700', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - backupConfirmationDescription: { - fontSize: 14, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - lineHeight: 20, - marginBottom: 10, - }, - backupConfirmationContainer: { - marginBottom: 4, - }, - enhancedBackupCheckbox: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 8, - paddingHorizontal: 12, - marginVertical: 3, - borderRadius: 12, - backgroundColor: 'transparent', - }, - enhancedBackupCheckboxChecked: { - backgroundColor: theme.colors.secondary + '15', - }, - backupCheckboxContent: { - flex: 1, - marginLeft: 12, - }, - backupCheckboxLabel: { - fontSize: 15, - fontWeight: '600', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - marginBottom: 2, - }, - backupCheckboxHint: { - fontSize: 12, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - fontStyle: 'italic', - }, - backupCheckIcon: { - width: 20, - height: 20, - // tintColor will be set conditionally in the component - }, - enhancedCheckbox: { - width: 24, - height: 24, - borderRadius: 6, - borderWidth: 2, - borderColor: theme.colors.primary, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'transparent', - marginRight: 12, - }, - enhancedCheckboxChecked: { - backgroundColor: theme.colors.primary, - borderColor: theme.colors.primary, - }, - checkmark: { - color: theme.colors.background, - fontSize: 16, - fontWeight: 'bold', - }, - enhancedCheckboxContainer: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 8, - marginVertical: 2, - marginHorizontal: 4, - paddingVertical: 2, - borderRadius: 8, - backgroundColor: 'transparent', - }, - enhancedCheckboxContainerChecked: { - backgroundColor: theme.colors.primary + '10', - }, - checkboxTextContainer: { - flex: 1, - padding: 8, - }, - enhancedCheckboxLabel: { - fontSize: 15, - fontWeight: '500', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - warningHint: { - fontSize: 12, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - marginTop: 2, - fontStyle: 'italic', - }, - warningIcon: { - fontSize: 18, - marginLeft: 8, - }, - finalStepHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - padding: 12, - backgroundColor: theme.colors.background, - borderRadius: 12, - }, - finalStepIconContainer: { - marginRight: 12, - }, - finalStepPhoneIcon: { - width: 24, - height: 24, - tintColor: theme.colors.primary, - }, - finalStepTextContainer: { - flex: 1, - }, - finalStepTitle: { - fontSize: 18, - fontWeight: '700', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - marginBottom: 4, - }, - finalStepDescription: { - fontSize: 14, - color: theme.colors.textSecondary, - marginBottom: 12, - lineHeight: 20, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - participantsList: { - marginTop: 8, - marginBottom: 12, - paddingHorizontal: 12, - paddingVertical: 12, - backgroundColor: theme.colors.cardBackground + '80', - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.border + '30', - }, - participantsListTitle: { - fontSize: 13, - fontWeight: '600', - color: theme.colors.text, - marginBottom: 8, - }, - participantItem: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: 8, - }, - bulletPoint: { - fontSize: 16, - color: theme.colors.primary, - marginRight: 8, - marginTop: 2, - fontWeight: 'bold', - }, - participantText: { - flex: 1, - fontSize: 13, - color: theme.colors.text, - lineHeight: 18, - }, - participantLabel: { - fontWeight: '600', - color: theme.colors.text, - }, - localDeviceBadge: { - fontSize: 12, - fontWeight: '500', - color: theme.colors.primary, - fontStyle: 'italic', - }, - participantNpub: { - fontSize: 12, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - marginTop: 2, - }, - participantDevicesInfo: { - marginTop: 12, - paddingTop: 12, - borderTopWidth: 1, - borderTopColor: theme.colors.border + '40', - }, - participantDevicesInfoTitle: { - fontSize: 14, - fontWeight: '600', - color: theme.colors.text, - marginBottom: 10, - }, - participantDeviceItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 10, - paddingVertical: 6, - paddingHorizontal: 8, - backgroundColor: theme.colors.cardBackground, - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.border + '30', - }, - participantDeviceLeft: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - }, - participantDeviceIcon: { - width: 18, - height: 18, - tintColor: theme.colors.primary, - marginRight: 10, - }, - participantDeviceLabel: { - fontSize: 13, - fontWeight: '600', - color: theme.colors.text, - }, - participantDeviceNpub: { - fontSize: 12, - color: theme.colors.textSecondary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - textAlign: 'right', - }, - twoPhonesContainer: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: 8, - }, - threeDevicesContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - firstPhone: { - marginLeft: 0, - marginRight: -4, - zIndex: 2, - }, - secondPhone: { - marginLeft: 0, - opacity: 0.7, - zIndex: 1, - }, - thirdPhone: { - marginLeft: 0, - opacity: 0.5, - zIndex: 0, - }, - proceedButtonOn: { - marginTop: 16, - backgroundColor: theme.colors.primary, - borderRadius: 12, - paddingVertical: 16, - paddingHorizontal: 24, - alignItems: 'center', - justifyContent: 'center', - width: '100%', - alignSelf: 'center', - shadowColor: theme.colors.shadowColor, - shadowOffset: {width: 0, height: 4}, - shadowOpacity: 0.15, - shadowRadius: 8, - elevation: 4, - }, - proceedButtonOff: { - marginTop: 16, - backgroundColor: theme.colors.textSecondary, - borderRadius: 12, - paddingVertical: 16, - paddingHorizontal: 24, - alignItems: 'center', - justifyContent: 'center', - width: '100%', - alignSelf: 'center', - opacity: 0.6, - }, - pairButtonText: { - color: theme.colors.textOnPrimary, - fontSize: 16, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - buttonContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 8, - }, - buttonIcon: { - width: 20, - height: 20, - tintColor: theme.colors.background, - }, - modalContent: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 20, - padding: 24, - width: '90%', - maxWidth: 400, - shadowColor: '#000', - shadowOffset: {width: 0, height: 10}, - shadowOpacity: 0.3, - shadowRadius: 20, - elevation: 10, - }, - modalHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 16, - }, - modalIcon: { - width: 32, - height: 32, - marginRight: 12, - tintColor: theme.colors.primary, - }, - modalTitle: { - fontSize: 20, - fontWeight: '700', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - modalDescription: { - fontSize: 14, - color: theme.colors.textSecondary, - marginBottom: 20, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - lineHeight: 20, - }, - passwordContainer: { - marginBottom: 16, - }, - passwordLabel: { - fontSize: 14, - fontWeight: '600', - color: theme.colors.text, - marginBottom: 8, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - passwordInputContainer: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: theme.colors.background, - borderRadius: 12, - borderWidth: 1, - borderColor: theme.colors.border, - paddingHorizontal: 12, - }, - passwordInput: { - flex: 1, - paddingVertical: 12, - fontSize: 16, - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - eyeButton: { - padding: 8, - }, - eyeIcon: { - width: 20, - height: 20, - tintColor: theme.colors.textSecondary, - }, - strengthContainer: { - marginTop: 8, - }, - strengthBar: { - height: 4, - backgroundColor: theme.colors.border, - borderRadius: 2, - overflow: 'hidden', - marginBottom: 4, - }, - strengthFill: { - height: '100%', - borderRadius: 2, - }, - strengthText: { - fontSize: 12, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - }, - requirementsContainer: { - marginTop: 8, - paddingLeft: 4, - }, - requirementText: { - fontSize: 12, - color: '#FF6B35', - marginBottom: 4, - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - fontWeight: '500', - }, - errorInput: { - borderColor: theme.colors.danger || '#FF3B30', - }, - modalActions: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 8, - gap: 12, - }, - modalButton: { - flex: 1, - borderRadius: 12, - paddingVertical: 14, - alignItems: 'center', - justifyContent: 'center', - }, - cancelButton: { - backgroundColor: theme.colors.border, - }, - confirmButton: { - backgroundColor: theme.colors.primary, - }, - disabledButton: { - opacity: 0.5, - }, - cancelLinkContainer: { - marginTop: 8, - marginBottom: 4, - alignItems: 'center', - }, - cancelLinkText: { - color: theme.colors.textSecondary, - fontWeight: '600', - textDecorationLine: 'underline', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - fontSize: 14, - marginTop: 12, - }, - retryButton: { - backgroundColor: theme.colors.secondary, - borderRadius: 18, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 8, - paddingHorizontal: 12, - shadowColor: theme.colors.shadowColor, - shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.1, - shadowRadius: 2, - elevation: 2, - minHeight: 36, - }, - retryLink: { - color: theme.colors.background, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - fontSize: 14, - marginLeft: 6, - }, - buttonFlex: { - flex: 1, - marginHorizontal: 6, - }, - cancelSetupButton: { - backgroundColor: theme.colors.background, - borderColor: theme.colors.secondary, - borderWidth: 1, - borderRadius: 18, - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 8, - paddingHorizontal: 12, - minHeight: 36, - }, - cancelLink: { - color: theme.colors.secondary, - fontWeight: '600', - fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', - textAlign: 'center', - fontSize: 14, - }, - }); - - return ( - - - - {/* Hide all previous sections when mpcDone is true */} - {!mpcDone && - (() => { - // Check if Final Step should be shown - const showFinalStep = - !isSendBitcoin && !isSignPSBT && - isPreParamsReady && - localNpub && - deviceName && - ((isTrio && - peerNpub1 && - peerDeviceName1 && - peerNpub2 && - peerDeviceName2) || - (!isTrio && peerNpub1 && peerDeviceName1)); - - return ( - <> - {/* Header */} - - - {/* Help button on the left */} - { - HapticFeedback.light(); - setShowHelpModal(true); - }} - activeOpacity={0.7}> - - - - {/* Title in the center */} - - {(isSendBitcoin || isSignPSBT) ? ( - - - - {isSignPSBT ? 'PSBT Co-Signing' : 'Transaction Co-Signing'} - - - ) : ( - - Setup Wallet - - )} - - - {/* Abort Setup button on the right */} - {!mpcDone && !isPairing ? ( - { - HapticFeedback.light(); - if (isSendBitcoin || isSignPSBT) { - navigation.goBack(); - } else { - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{name: 'Welcome'}], - }), - ); - } - }} - activeOpacity={0.7}> - - {(isSendBitcoin || isSignPSBT) ? 'Cancel' : 'Abort'} - - - ) : ( - - )} - - - - {/* PSBT Info Section */} - {isSignPSBT && route.params?.psbtBase64 && ( - - - - PSBT Ready to Sign - - {psbtDetails ? ( - <> - - - Inputs: - - - {psbtDetails.inputs.length} - - - - - Outputs: - - - {psbtDetails.outputs.length} - - - - - Total Input: - - - {sat2btcStr(psbtDetails.totalInput)} BTC - - - - - Total Output: - - - {sat2btcStr(psbtDetails.totalOutput)} BTC - - - - - Fee: - - - {sat2btcStr(psbtDetails.fee)} BTC - - - {psbtDetails.derivePath && ( - - - Derivation Path: - - - {psbtDetails.derivePath} - - - )} - {psbtDetails.derivePaths && - psbtDetails.derivePaths.length > 1 && ( - - - {psbtDetails.derivePaths.length} different paths - - - )} - - ) : ( - - {route.params.psbtBase64 - ? `PSBT (${Math.round( - (route.params.psbtBase64.length || 0) / 1024, - )} KB) - Parsing...` - : 'No PSBT data'} - - )} - - - )} - - {/* Send Mode: Device Selection - Show current device and allow selecting one other */} - {(isSendBitcoin || isSignPSBT) && ( - - - This Device - - {sendModeDevices.length === 0 ? ( - - Loading... - - ) : ( - (() => { - // Separate local and other devices - const localDevice = sendModeDevices.find( - d => d.isLocal, - ); - const otherDevices = sendModeDevices - .filter(d => !d.isLocal) - .sort((a, b) => a.npub.localeCompare(b.npub)); - - return ( - <> - {/* Current Device */} - {localDevice && ( - - - - - - {localDevice.keyshareLabel} - - - This device - - - - {shortenNpub(localDevice.npub, 8, 6)} - - - - )} - - {/* Select One Other Device */} - {otherDevices.length > 0 && ( - <> - - {isTrio - ? 'Select one device to co-sign:' - : 'Co-signing device:'} - - {otherDevices.map(dev => { - // In duo mode, use View (not selectable) - // In trio mode, use TouchableOpacity (selectable) - if (!isTrio) { - return ( - - - - - {dev.keyshareLabel} - - - {shortenNpub(dev.npub, 8, 6)} - - - {selectedPeerNpub === dev.npub && ( - - - ✓ - - - )} - - ); - } - - // Trio mode: selectable - return ( - { - HapticFeedback.medium(); - // In trio, allow user to select any device - // If clicking the same device, deselect (allow empty selection) - // If clicking different device, select that one - setSelectedPeerNpub( - selectedPeerNpub === dev.npub - ? '' - : dev.npub, - ); - dbg( - 'User selected peer in trio mode:', - dev.npub === selectedPeerNpub - ? 'deselected' - : dev.npub.substring(0, 20) + - '...', - ); - }} - activeOpacity={0.7}> - - - - {dev.keyshareLabel} - - - {shortenNpub(dev.npub, 8, 6)} - - - - {selectedPeerNpub === dev.npub && ( - - ✓ - - )} - - - ); - })} - - )} - - ); - })() - )} - - )} - - {/* Relay Configuration - Collapsible - Hide when Final Step is shown */} - {!showFinalStep && ( - - { - HapticFeedback.light(); - setShowRelayConfig(!showRelayConfig); - }} - activeOpacity={0.7}> - - {showRelayConfig ? '▼' : '▶'} Advanced: Nostr Relays - Settings - - - {showRelayConfig && ( - - - Configure Nostr relays (defaults work for most - users). Enter relay URLs, one per line or - comma-separated (wss://...). - - - - )} - - )} - - {/* Step Indicator */} - {!isSendBitcoin && !isSignPSBT && ( - - - - - {localNpub ? '✓' : '1'} - - - - - - {peerNpub1 && peerDeviceName1 ? '✓' : '2'} - - - {isTrio && ( - <> - - - - {peerNpub2 && peerDeviceName2 ? '✓' : '3'} - - - - )} - - - - {isPreParamsReady ? '✓' : isTrio ? '4' : '3'} - - - - - - {canStartKeygen ? '✓' : isTrio ? '5' : '4'} - - - - - Your Device - 2nd Peer - {isTrio && ( - 3rd Peer - )} - Prepared - Ready - - - )} - - {/* Local Device Card - Hide when Final Step is shown or in send mode */} - {localNpub && - deviceName && - partialNonce && - !isSendBitcoin && !isSignPSBT && ( - - - This Device - - - - {deviceName}@{shortenNpub(localNpub, 8, 6)} - - - - - - - - - - - - )} - - {/* Peer Connection 1 - Hide when Final Step is shown or in send/sign mode */} - {!showFinalStep && !isSendBitcoin && !isSignPSBT && ( - - - {isTrio - ? 'Step 2: Second Device' - : 'Step 2: Other Device'} - - - - {peerNpub1 && ( - - )} - {peerNpub1 && peerDeviceName1 ? ( - - {formatConnectionDisplay( - peerNpub1, - peerDeviceName1, - )} - - ) : ( - { - setPeerConnectionDetails1(text); - handlePeerConnectionInput(text, 1); - }} - placeholder="Paste or scan connection details" - placeholderTextColor={ - theme.colors.textSecondary + '80' - } - autoCapitalize="none" - autoCorrect={false} - /> - )} - {peerInputValidating1 && ( - - ... - - )} - {peerNpub1 && !peerInputValidating1 && ( - clearPeerConnection(1)} - activeOpacity={0.7}> - - - )} - {!peerNpub1 && !peerInputValidating1 && ( - <> - handlePaste(1)} - activeOpacity={0.7}> - - - { - HapticFeedback.light(); - const peerNum: 1 | 2 = 1; - scanningForPeerRef.current = peerNum; // Update ref immediately - // FOSS version: Use BarcodeZxingScan for both iOS and Android - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error && data) { - handleQRScan(data, peerNum); - } - }, - ); - }} - activeOpacity={0.7}> - - - - )} - - {peerInputError1 && ( - - - ⚠ {peerInputError1} - - - )} - - - )} - - {/* Peer Connection 2 (Trio only) - Hide when Final Step is shown or in send/sign mode */} - {isTrio && !showFinalStep && !isSendBitcoin && !isSignPSBT && ( - - - Step 3: Third Device - - - - {peerNpub2 && ( - - )} - {peerNpub2 && peerDeviceName2 ? ( - - {formatConnectionDisplay( - peerNpub2, - peerDeviceName2, - )} - - ) : ( - { - setPeerConnectionDetails2(text); - handlePeerConnectionInput(text, 2); - }} - placeholder="Paste or scan connection details" - placeholderTextColor={ - theme.colors.textSecondary + '80' - } - autoCapitalize="none" - autoCorrect={false} - /> - )} - {peerInputValidating2 && ( - - ... - - )} - {peerNpub2 && !peerInputValidating2 && ( - clearPeerConnection(2)} - activeOpacity={0.7}> - - - )} - {!peerNpub2 && !peerInputValidating2 && ( - <> - handlePaste(2)} - activeOpacity={0.7}> - - - { - HapticFeedback.light(); - const peerNum: 1 | 2 = 2; - scanningForPeerRef.current = peerNum; // Update ref immediately - // FOSS version: Use BarcodeZxingScan for both iOS and Android - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error && data) { - handleQRScan(data, peerNum); - } - }, - ); - }} - activeOpacity={0.7}> - - - - )} - - {peerInputError2 && ( - - - ⚠ {peerInputError2} - - - )} - - - )} - - {/* Prepare Device Section - Hide in send/sign mode */} - {!isSendBitcoin && !isSignPSBT && - !isPreParamsReady && - localNpub && - deviceName && - ((isTrio && - peerNpub1 && - peerDeviceName1 && - peerNpub2 && - peerDeviceName2) || - (!isTrio && peerNpub1 && peerDeviceName1)) && ( - - - - - - {isPreparing ? 'Preparing...' : 'Prepare Device'} - - - { - HapticFeedback.light(); - setIsPrepared(!isPrepared); - }}> - - {isPrepared && ( - - )} - - - Keep app open during setup - - - - - )} - - {/* Preparing Modal */} - {isPreparing && ( - - - - {/* Icon Container */} - - - - - - - {/* Header Text */} - - Preparing Device - - - {/* Subtext */} - - Could take a while, given device specs. Do not leave - the app during setup. - - - {/* Loading Indicator */} - - - - - - - {/* Status and Countdown */} - - - - - Computing cryptographic params - - - - Time elapsed: {prepCounter} seconds - - - - - - )} - - {/* Help Modal */} - setShowHelpModal(false)}> - - - - How It Works - { - HapticFeedback.medium(); - setShowHelpModal(false); - }} - activeOpacity={0.7}> - - - - - - - - - Step 1: This Device - - - - This device generates a unique ID. Share this with - other devices by showing the QR code or copying - the connection details. - - - - - - - Step 2: Connect Peers - - - - On each peer device, scan your QR code or paste - your connection details. Then share their - connection details back to you. - - - - - - - Step 3: Start - - - - Once all devices are prepared, tap proceed to Key - Generation to begin the secure wallet setup - process. - - - - - - Tips - - - • QR scanning is the easiest method{'\n'}• Make - sure all devices are online{'\n'}• The process - takes 1-2 minutes - {'\n'}• Keep devices close together - - - - - - - - {/* Final Step - Check other devices are prepared */} - {!isSendBitcoin && !isSignPSBT && - isPreParamsReady && - !mpcDone && - localNpub && - deviceName && - ((isTrio && - peerNpub1 && - peerDeviceName1 && - peerNpub2 && - peerDeviceName2) || - (!isTrio && peerNpub1 && peerDeviceName1)) && ( - - - - - - - - {isTrio && ( - - )} - - - - - Final Step - - - Make sure{' '} - {isTrio ? 'all devices' : 'both devices'}{' '} - preparation step is complete. - - - - - {/* Participants Device Information */} - {Object.keys(keyshareMapping).length > 0 && ( - - - Participants: - - {keyshareMapping.keyshare1 && ( - - - - - KeyShare1 - - {keyshareMapping.keyshare1.isLocal && ( - - {' '} - (This device) - - )} - {'\n'} - - {shortenNpub( - keyshareMapping.keyshare1.npub, - 8, - 6, - )} - - - - )} - {keyshareMapping.keyshare2 && ( - - - - - KeyShare2 - - {keyshareMapping.keyshare2.isLocal && ( - - {' '} - (This device) - - )} - {'\n'} - - {shortenNpub( - keyshareMapping.keyshare2.npub, - 8, - 6, - )} - - - - )} - {keyshareMapping.keyshare3 && ( - - - - - KeyShare3 - - {keyshareMapping.keyshare3.isLocal && ( - - {' '} - (This device) - - )} - {'\n'} - - {shortenNpub( - keyshareMapping.keyshare3.npub, - 8, - 6, - )} - - - - )} - - )} - - { - HapticFeedback.medium(); - toggleKeygenReady(); - }}> - - {isKeygenReady && ( - - )} - - - - All devices are ready - - - - - {/* Participant Devices Info */} - - - Participants: - - {(() => { - // Collect all participants - const participants: Array<{ - npub: string; - deviceName: string; - }> = []; - - if (localNpub && deviceName) { - participants.push({ - npub: localNpub, - deviceName: deviceName, - }); - } - if (peerNpub1 && peerDeviceName1) { - participants.push({ - npub: peerNpub1, - deviceName: peerDeviceName1, - }); - } - if (isTrio && peerNpub2 && peerDeviceName2) { - participants.push({ - npub: peerNpub2, - deviceName: peerDeviceName2, - }); - } - - // Sort by npub - participants.sort((a, b) => - a.npub.localeCompare(b.npub), - ); - - return participants.map((participant, index) => ( - - - - - {participant.deviceName} - - - - {shortenNpub(participant.npub, 8, 6)} - - - )); - })()} - - - - )} - - {/* Transaction Summary - Show in send mode before button */} - {isSendBitcoin && !isPairing && !mpcDone && route.params && ( - - - - - To Address - - - - {route.params?.toAddress || ''} - - - - - - - Transaction Amount - - - - {sat2btcStr(route.params?.satoshiAmount)} BTC - - - {route.params?.selectedCurrency || ''}{' '} - {formatFiat(route.params?.fiatAmount)} - - - - - - - Transaction Fee - - - - {sat2btcStr(route.params?.satoshiFees)} BTC - - - {route.params?.selectedCurrency || ''}{' '} - {formatFiat(route.params?.fiatFees)} - - - - - - )} - - {/* Readiness Checkbox for PSBT Signing */} - {isSignPSBT && !isPairing && !mpcDone && ( - - - - {isKeysignReady && ( - - )} - - - Keep this app open during signing ⚠️ - - - - )} - - {/* Start Button */} - {!isPairing && !mpcDone && ( - - - - {((isSendBitcoin || isSignPSBT) || !(isSendBitcoin || isSignPSBT)) && ( - - )} - - {(isSendBitcoin || isSignPSBT) - ? (() => { - // Determine if local device is KeyShare1 - const localDevice = sendModeDevices.find( - d => d.isLocal, - ); - const isKeyShare1 = - localDevice?.keyshareLabel === 'KeyShare1'; - return isKeyShare1 - ? (isSignPSBT ? 'Start PSBT Signing' : 'Start Co-Signing') - : (isSignPSBT ? 'Join PSBT Signing' : 'Join Co-Signing'); - })() - : (() => { - // For keygen, determine if local npub is first in sorted order - const allNpubs = [localNpub]; - if (peerNpub1) allNpubs.push(peerNpub1); - if (isTrio && peerNpub2) - allNpubs.push(peerNpub2); - const sortedNpubs = allNpubs.sort(); - const isKeyShare1 = - sortedNpubs[0] === localNpub; - return isKeyShare1 - ? 'Start Key Generation' - : 'Join Key Generation'; - })()} - - - - - )} - - ); - })()} - - {/* Keygen Modal - Similar to MobilesPairing */} - {isPairing && !isSendBitcoin && !isSignPSBT && ( - - - - {/* Icon Container */} - - - - - - - {/* Header Text */} - Finalizing Your Wallet - - {/* Subtext */} - - Securing your wallet with advanced cryptography. Please stay - in the app... - - - {/* Progress Container */} - - {/* Circular Progress */} - - - {/* Progress Percentage */} - - - {Math.round(progress)}% - - - - - {/* Status and Countdown */} - - - - - {status || 'Processing cryptographic operations'} - - - - Time elapsed: {prepCounter} seconds - - - - - - )} - - {/* Co-Signing Modal - Similar to MobilesPairing send_btc and sign_psbt */} - {isPairing && (isSendBitcoin || isSignPSBT) && ( - - - - {/* Icon Container */} - - - - - - - {/* Header Text */} - - {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'} - - - {/* Subtext */} - - Securing your transaction with multi-party cryptography. - Please stay in the app... - - - {/* Progress Container */} - - {/* Circular Progress */} - - - {/* Progress Percentage */} - - - {Math.round(progress)}% - - - - - {/* Status and Countdown */} - - - - - {status || 'Processing multi-party signature'} - - - - Time elapsed: {prepCounter} seconds - - - - - - )} - - {/* Success and Backup UI - Only show for keygen, not for send BTC or sign PSBT */} - {mpcDone && !isSendBitcoin && !isSignPSBT && ( - <> - {/* Keyshare Created Success */} - - - - - - Keyshare Created! - - - - Create secure backups of your keyshares. Store each device's - backup in different locations to prevent single points of - failure. - - - { - HapticFeedback.medium(); - setIsBackupModalVisible(true); - }}> - - - - Backup{' '} - {keyshareMapping.keyshare1?.isLocal - ? 'KeyShare1' - : keyshareMapping.keyshare2?.isLocal - ? 'KeyShare2' - : keyshareMapping.keyshare3?.isLocal - ? 'KeyShare3' - : 'Keyshare'} - - - - - - - {/* Backup Confirmation */} - - - - - - - - Confirm Backups - - - - Verify that {isTrio ? 'all devices' : 'both devices'} have - successfully backed up their keyshares. - - - - {(() => { - // Build device list based on keyshare mapping (sorted order) - const devices = []; - if (keyshareMapping.keyshare1) { - devices.push({ - key: 'deviceOne', - label: `KeyShare1 (${keyshareMapping.keyshare1.deviceName}) backed up`, - device: keyshareMapping.keyshare1.deviceName, - keyshareLabel: 'KeyShare1', - }); - } - if (keyshareMapping.keyshare2) { - devices.push({ - key: 'deviceTwo', - label: `KeyShare2 (${keyshareMapping.keyshare2.deviceName}) backed up`, - device: keyshareMapping.keyshare2.deviceName, - keyshareLabel: 'KeyShare2', - }); - } - if (keyshareMapping.keyshare3) { - devices.push({ - key: 'deviceThree', - label: `KeyShare3 (${keyshareMapping.keyshare3.deviceName}) backed up`, - device: keyshareMapping.keyshare3.deviceName, - keyshareLabel: 'KeyShare3', - }); - } - return devices; - })().map(item => ( - { - HapticFeedback.medium(); - toggleBackedup(item.key as keyof typeof backupChecks); - }}> - - {backupChecks[ - item.key as keyof typeof backupChecks - ] && } - - - - {item.label} - - - {item.keyshareLabel} ({item.device}) secured - - - - - ))} - - - { - HapticFeedback.medium(); - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{name: 'Home'}], - }), - ); - }} - disabled={!allBackupChecked}> - - - Continue - - - - - - )} - - - - {/* QR Code Modal */} - setIsQRModalVisible(false)}> - - - - Connection Details - { - HapticFeedback.medium(); - setIsQRModalVisible(false); - }} - activeOpacity={0.7}> - - - - - - { - connectionQrRef.current = ref; - }} - /> - - - {shortenNpub(connectionDetails)} - - - - - Share - - - - - - - - {/* Backup Modal */} - - - { - HapticFeedback.light(); - Keyboard.dismiss(); - }}> - { - HapticFeedback.light(); - }}> - - - Backup Keyshare - - - Create an encrypted backup of your keyshare, protected by a - strong password. - - - - Set a Password - - - { - HapticFeedback.medium(); - setPasswordVisible(!passwordVisible); - }}> - - - - - {/* Password Strength Indicator */} - {password.length > 0 && ( - - - - - - {getPasswordStrengthText()} - - - )} - - {/* Password Requirements */} - {passwordErrors.length > 0 && ( - - {passwordErrors.map((error, index) => ( - - • {error} - - ))} - - )} - - - - Confirm Password - - 0 && - password !== confirmPassword && - styles.errorInput, - ]} - placeholder="Confirm your password" - secureTextEntry={!confirmPasswordVisible} - value={confirmPassword} - onChangeText={setConfirmPassword} - autoCapitalize="none" - autoCorrect={false} - /> - { - HapticFeedback.medium(); - setConfirmPasswordVisible(!confirmPasswordVisible); - }}> - - - - {confirmPassword.length > 0 && password !== confirmPassword && ( - Passwords do not match - )} - - - - { - HapticFeedback.medium(); - clearBackupModal(); - }}> - Cancel - - { - HapticFeedback.medium(); - backupShare(); - }} - disabled={ - !password || - !confirmPassword || - password !== confirmPassword || - passwordStrength < 3 - }> - - - Backup - - - - - - - - - ); -}; - -export default MobileNostrPairing; diff --git a/screens/MobileNostrPairing.tsx b/screens/MobileNostrPairing.tsx index b2865fc..aec391e 100644 --- a/screens/MobileNostrPairing.tsx +++ b/screens/MobileNostrPairing.tsx @@ -23,13 +23,20 @@ import DeviceInfo from 'react-native-device-info'; import EncryptedStorage from 'react-native-encrypted-storage'; import QRCode from 'react-native-qrcode-svg'; import Clipboard from '@react-native-clipboard/clipboard'; -import BarcodeZxingScan from 'rn-barcode-zxing-scan'; -import {Camera, useCameraDevice} from 'react-native-vision-camera'; +import QRScanner from '../components/QRScanner'; import * as Progress from 'react-native-progress'; import {CommonActions, RouteProp, useRoute} from '@react-navigation/native'; import {SafeAreaView} from 'react-native-safe-area-context'; import Big from 'big.js'; -import {dbg, HapticFeedback, getNostrRelays, getKeyshareLabel, hexToString, getDerivePathForNetwork} from '../utils'; +import { + dbg, + HapticFeedback, + getNostrRelays, + getKeyshareLabel, + hexToString, + getDerivePathForNetwork, + isLegacyWallet, +} from '../utils'; import {useTheme} from '../theme'; import {useUser} from '../context/UserContext'; import LocalCache from '../services/LocalCache'; @@ -38,92 +45,6 @@ import RNFS from 'react-native-fs'; const {BBMTLibNativeModule} = NativeModules; -// QR Scanner Component (moved outside to avoid re-render issues) -const QRScannerComponent = ({ - cameraDevice, - onScan, - onClose, - theme, -}: { - cameraDevice: any; - onScan: (data: string) => void; - onClose: () => void; - theme: any; -}) => { - const scannerStyles = StyleSheet.create({ - scannerContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - qrFrame: { - width: 250, - height: 250, - borderWidth: 2, - borderColor: theme.colors.primary, - borderRadius: 12, - }, - closeScannerButton: { - position: 'absolute', - bottom: 40, - backgroundColor: theme.colors.primary, - paddingHorizontal: 24, - paddingVertical: 12, - borderRadius: 12, - }, - closeScannerButtonText: { - color: theme.colors.background, - fontSize: 16, - fontWeight: '600', - }, - cameraNotFound: { - color: theme.colors.text, - fontSize: 16, - }, - }); - - if (!cameraDevice) { - return ( - - Camera Not Found - - Close - - - ); - } - - const codeScanner = { - codeTypes: ['qr' as const], - onCodeScanned: (codes: any[]) => { - if (codes.length > 0) { - onScan(codes[0].value); - } - }, - }; - - return ( - - - - - Close - - - ); -}; - type RouteParams = { mode?: string; // 'duo' | 'trio' | 'send_btc' | 'sign_psbt' addressType?: string; @@ -135,7 +56,6 @@ type RouteParams = { selectedCurrency?: string; spendingHash?: string; psbtBase64?: string; // For PSBT signing mode - derivePath?: string; // BIP32 derivation path for PSBT }; const MobileNostrPairing = ({navigation}: any) => { @@ -200,7 +120,6 @@ const MobileNostrPairing = ({navigation}: any) => { fee: number; totalInput: number; totalOutput: number; - derivePath?: string; derivePaths?: string[]; } | null>(null); const [isPreParamsReady, setIsPreParamsReady] = useState(false); @@ -252,13 +171,6 @@ const MobileNostrPairing = ({navigation}: any) => { const [showHelpModal, setShowHelpModal] = useState(false); const connectionQrRef = useRef(null); - // Only use camera hooks on iOS - Android uses BarcodeZxingScan - let device: any = null; - if (Platform.OS === 'ios') { - // eslint-disable-next-line react-hooks/rules-of-hooks - device = useCameraDevice('back'); - } - // Connection details for sharing (hex encoded) const connectionDetails = React.useMemo(() => { if (!localNpub || !deviceName || !partialNonce) { @@ -387,7 +299,10 @@ const MobileNostrPairing = ({navigation}: any) => { route.params.psbtBase64, ); - if (detailsJson.startsWith('error') || detailsJson.includes('failed')) { + if ( + detailsJson.startsWith('error') || + detailsJson.includes('failed') + ) { dbg('Failed to parse PSBT details:', detailsJson); setPsbtDetails(null); return; @@ -400,7 +315,6 @@ const MobileNostrPairing = ({navigation}: any) => { fee: details.fee || 0, totalInput: details.totalInput || 0, totalOutput: details.totalOutput || 0, - derivePath: details.derivePath, derivePaths: details.derivePaths || [], }); dbg('PSBT details parsed:', { @@ -1737,7 +1651,24 @@ const MobileNostrPairing = ({navigation}: any) => { const relaysCSV = relays.join(','); const network = (await LocalCache.getItem('network')) || 'mainnet'; - const derivePath = getDerivePathForNetwork(network); + // Get address type from route params or cache, default to segwit-native + const currentAddressType = + addressType || + (await LocalCache.getItem('addressType')) || + 'segwit-native'; + // Check if this is a legacy wallet (created before migration timestamp) + const useLegacyPath = isLegacyWallet(keyshare.created_at); + const derivePath = getDerivePathForNetwork( + network, + currentAddressType, + useLegacyPath, + ); + dbg('Deriving path for Nostr send:', { + network, + currentAddressType, + useLegacyPath, + derivePath, + }); // Derive the public key from the root key using the derivation path // This is critical - we need the DERIVED public key, not the root! @@ -1852,7 +1783,10 @@ const MobileNostrPairing = ({navigation}: any) => { } if (!isKeysignReady) { - Alert.alert('Not Ready', 'Please confirm that you are ready to sign the PSBT'); + Alert.alert( + 'Not Ready', + 'Please confirm that you are ready to sign the PSBT', + ); return; } @@ -1929,19 +1863,33 @@ const MobileNostrPairing = ({navigation}: any) => { // Find local npub const localNpubFromKeyshare = - allNpubsFromKeyshare.find(n => n === localNpub || (localNpub && n.startsWith(localNpub.substring(0, 20)))) || - localNpub; + allNpubsFromKeyshare.find( + n => + n === localNpub || + (localNpub && n.startsWith(localNpub.substring(0, 20))), + ) || localNpub; // Build parties CSV const allNpubs = [localNpubFromKeyshare]; if (isTrio) { if (selectedPeerNpub) { const selectedDevice = sendModeDevices.find( - d => d.npub === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && d.npub && d.npub.startsWith(selectedPeerNpub.substring(0, 20))), + d => + d.npub === selectedPeerNpub || + (selectedPeerNpub.startsWith('npub1') && + d.npub && + d.npub.startsWith(selectedPeerNpub.substring(0, 20))), ); if (selectedDevice) { - const selectedIndex = parseInt(selectedDevice.keyshareLabel.replace('KeyShare', ''), 10) - 1; - if (selectedIndex >= 0 && selectedIndex < allNpubsFromKeyshare.length) { + const selectedIndex = + parseInt( + selectedDevice.keyshareLabel.replace('KeyShare', ''), + 10, + ) - 1; + if ( + selectedIndex >= 0 && + selectedIndex < allNpubsFromKeyshare.length + ) { const fullPeerNpub = allNpubsFromKeyshare[selectedIndex]; if (fullPeerNpub !== localNpubFromKeyshare) { allNpubs.push(fullPeerNpub); @@ -1951,7 +1899,10 @@ const MobileNostrPairing = ({navigation}: any) => { } } else { const fullPeerNpub = allNpubsFromKeyshare.find( - n => n === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && n.startsWith(selectedPeerNpub.substring(0, 20))), + n => + n === selectedPeerNpub || + (selectedPeerNpub.startsWith('npub1') && + n.startsWith(selectedPeerNpub.substring(0, 20))), ); if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) { allNpubs.push(fullPeerNpub); @@ -1963,7 +1914,9 @@ const MobileNostrPairing = ({navigation}: any) => { throw new Error('Please select a peer device for trio mode'); } } else { - const otherNpubs = allNpubsFromKeyshare.filter(n => n !== localNpubFromKeyshare); + const otherNpubs = allNpubsFromKeyshare.filter( + n => n !== localNpubFromKeyshare, + ); if (otherNpubs.length > 0) { allNpubs.push(otherNpubs[0]); } else { @@ -1973,24 +1926,10 @@ const MobileNostrPairing = ({navigation}: any) => { const partiesNpubsCSV = allNpubs.sort().join(','); const relaysCSV = relays.join(','); - const network = (await LocalCache.getItem('network')) || 'mainnet'; - const derivePath = route.params?.derivePath || getDerivePathForNetwork(network); - - dbg('Using derivation path for PSBT signing:', derivePath); - - // Derive the public key - const publicKey = await BBMTLibNativeModule.derivePubkey( - keyshare.pub_key, - keyshare.chain_code_hex, - derivePath, - ); - dbg('Starting Nostr PSBT signing with:', { relays: relaysCSV, parties: partiesNpubsCSV.substring(0, 50) + '...', npubsSorted: npubsSorted.substring(0, 30) + '...', - derivePath, - publicKey: publicKey.substring(0, 20) + '...', psbtLength: route.params.psbtBase64?.length, }); @@ -2005,7 +1944,11 @@ const MobileNostrPairing = ({navigation}: any) => { ); // Validate result - if (!signedPsbt || signedPsbt.includes('error') || signedPsbt.includes('failed')) { + if ( + !signedPsbt || + signedPsbt.includes('error') || + signedPsbt.includes('failed') + ) { throw new Error(signedPsbt || 'PSBT signing failed'); } @@ -2064,7 +2007,7 @@ const MobileNostrPairing = ({navigation}: any) => { maximumFractionDigits: 2, }).format(Number(price)); - const sat2btcStr = (sats?: string) => + const sat2btcStr = (sats?: string | number) => Big(sats || 0) .div(1e8) .toFixed(8); @@ -2147,16 +2090,20 @@ const MobileNostrPairing = ({navigation}: any) => { async function backupShare() { if (!validatePassword(password)) { dbg('❌ [BACKUP] Password validation failed'); + const missingRequirements = passwordErrors.join('\n• '); Alert.alert( - 'Weak Password', - 'Please use a stronger password that meets all requirements.', + 'Password Requirements Not Met', + `Your password must meet all of the following requirements:\n\n• ${missingRequirements}\n\nPlease update your password and try again.`, ); return; } if (password !== confirmPassword) { dbg('❌ [BACKUP] Password mismatch'); - Alert.alert('Password Mismatch', 'Passwords do not match.'); + Alert.alert( + 'Passwords Do Not Match', + 'The password and confirmation password must be identical. Please check both fields and try again.', + ); return; } @@ -2171,17 +2118,34 @@ const MobileNostrPairing = ({navigation}: any) => { await BBMTLibNativeModule.sha256(password), ); - // Create friendly filename with date and time - const now = new Date(); - const month = now.toLocaleDateString('en-US', {month: 'short'}); - const day = now.getDate().toString().padStart(2, '0'); - const year = now.getFullYear(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key + // Create filename based on pub_key hash and keyshare number + if (!json.pub_key) { + Alert.alert('Error', 'Keyshare missing pub_key.'); + return; + } + + // Get SHA256 hash of pub_key and take first 4 characters + const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key); + const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase(); + + // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.) const keyshareLabel = getKeyshareLabel(json); - const shareName = keyshareLabel || json.local_party_key || 'keyshare'; - const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`; + let keyshareNumber = '1'; // default + if (keyshareLabel) { + const match = keyshareLabel.match(/KeyShare(\d+)/); + if (match) { + keyshareNumber = match[1]; + } + } else if (json.keygen_committee_keys && json.local_party_key) { + // Fallback: compute from position in sorted keygen_committee_keys + const sortedKeys = [...json.keygen_committee_keys].sort(); + const index = sortedKeys.indexOf(json.local_party_key); + if (index >= 0) { + keyshareNumber = String(index + 1); + } + } + + const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`; const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath; const filePath = `${tempDir}/${friendlyFilename}`; @@ -2190,6 +2154,7 @@ const MobileNostrPairing = ({navigation}: any) => { await Share.open({ title: 'Backup Your Keyshare', + isNewTask: true, message: 'Save this encrypted file securely. It is required for wallet recovery.', url: `file://${filePath}`, @@ -2198,9 +2163,12 @@ const MobileNostrPairing = ({navigation}: any) => { failOnCancel: false, }); + // Cleanup temp file (best-effort) try { await RNFS.unlink(filePath); - } catch {} + } catch { + // ignore cleanup errors + } clearBackupModal(); } else { Alert.alert('Error', 'Invalid keyshare.'); @@ -3792,7 +3760,8 @@ const MobileNostrPairing = ({navigation}: any) => { (() => { // Check if Final Step should be shown const showFinalStep = - !isSendBitcoin && !isSignPSBT && + !isSendBitcoin && + !isSignPSBT && isPreParamsReady && localNpub && deviceName && @@ -3825,7 +3794,7 @@ const MobileNostrPairing = ({navigation}: any) => { {/* Title in the center */} - {(isSendBitcoin || isSignPSBT) ? ( + {isSendBitcoin || isSignPSBT ? ( { fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto', }}> - {isSignPSBT ? 'PSBT Co-Signing' : 'Transaction Co-Signing'} + {isSignPSBT + ? 'PSBT Co-Signing' + : 'Transaction Co-Signing'} ) : ( @@ -3863,7 +3834,7 @@ const MobileNostrPairing = ({navigation}: any) => { {/* Abort Setup button on the right */} - {!mpcDone && !isPairing ? ( + {!mpcDone && !isPairing ? ( { @@ -3881,7 +3852,7 @@ const MobileNostrPairing = ({navigation}: any) => { }} activeOpacity={0.7}> - {(isSendBitcoin || isSignPSBT) ? 'Cancel' : 'Abort'} + {isSendBitcoin || isSignPSBT ? 'Cancel' : 'Abort'} ) : ( @@ -3998,14 +3969,15 @@ const MobileNostrPairing = ({navigation}: any) => { color: theme.colors.text, fontWeight: '600', }}> - {sat2btcStr(String(psbtDetails.totalOutput))} BTC + {sat2btcStr(String(psbtDetails.totalOutput))}{' '} + BTC { {sat2btcStr(String(psbtDetails.fee))} BTC - {psbtDetails.derivePath && ( - - - Derivation Path: - - - {psbtDetails.derivePath} - - - )} {psbtDetails.derivePaths && psbtDetails.derivePaths.length > 1 && ( { fontSize: 10, color: theme.colors.textSecondary, }}> - {psbtDetails.derivePaths.length} different paths + {psbtDetails.derivePaths.length} different + paths )} @@ -4442,7 +4388,8 @@ const MobileNostrPairing = ({navigation}: any) => { {localNpub && deviceName && partialNonce && - !isSendBitcoin && !isSignPSBT && ( + !isSendBitcoin && + !isSignPSBT && ( { const peerNum: 1 | 2 = 1; setScanningForPeer(peerNum); scanningForPeerRef.current = peerNum; // Update ref immediately - if (Platform.OS === 'android') { - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error && data) { - handleQRScan(data, peerNum); - } - }, - ); - } else { - setIsQRScannerVisible(true); - } + setIsQRScannerVisible(true); }} activeOpacity={0.7}> { )} {/* Peer Connection 2 (Trio only) - Hide when Final Step is shown or in send/sign mode */} - {isTrio && !showFinalStep && !isSendBitcoin && !isSignPSBT && ( - - - Step 3: Third Device - - - - {peerNpub2 && ( - - )} - {peerNpub2 && peerDeviceName2 ? ( - - {formatConnectionDisplay( - peerNpub2, - peerDeviceName2, - )} - - ) : ( - { - setPeerConnectionDetails2(text); - handlePeerConnectionInput(text, 2); - }} - placeholder="Paste or scan connection details" - placeholderTextColor={ - theme.colors.textSecondary + '80' - } - autoCapitalize="none" - autoCorrect={false} - /> - )} - {peerInputValidating2 && ( - - ... - - )} - {peerNpub2 && !peerInputValidating2 && ( - clearPeerConnection(2)} - activeOpacity={0.7}> + {isTrio && + !showFinalStep && + !isSendBitcoin && + !isSignPSBT && ( + + + Step 3: Third Device + + + + {peerNpub2 && ( - - )} - {!peerNpub2 && !peerInputValidating2 && ( - <> - handlePaste(2)} - activeOpacity={0.7}> - - + numberOfLines={1} + ellipsizeMode="middle" + adjustsFontSizeToFit={true} + minimumFontScale={0.7}> + {formatConnectionDisplay( + peerNpub2, + peerDeviceName2, + )} + + ) : ( + { + setPeerConnectionDetails2(text); + handlePeerConnectionInput(text, 2); + }} + placeholder="Paste or scan connection details" + placeholderTextColor={ + theme.colors.textSecondary + '80' + } + autoCapitalize="none" + autoCorrect={false} + /> + )} + {peerInputValidating2 && ( + + ... + + )} + {peerNpub2 && !peerInputValidating2 && ( { - HapticFeedback.light(); - const peerNum: 1 | 2 = 2; - setScanningForPeer(peerNum); - scanningForPeerRef.current = peerNum; // Update ref immediately - if (Platform.OS === 'android') { - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error && data) { - handleQRScan(data, peerNum); - } - }, - ); - } else { - setIsQRScannerVisible(true); - } - }} + onPress={() => clearPeerConnection(2)} activeOpacity={0.7}> - + )} + {!peerNpub2 && !peerInputValidating2 && ( + <> + handlePaste(2)} + activeOpacity={0.7}> + + + { + HapticFeedback.light(); + const peerNum: 1 | 2 = 2; + setScanningForPeer(peerNum); + scanningForPeerRef.current = peerNum; // Update ref immediately + setIsQRScannerVisible(true); + }} + activeOpacity={0.7}> + + + + )} + + {peerInputError2 && ( + + + ⚠ {peerInputError2} + + )} - {peerInputError2 && ( - - - ⚠ {peerInputError2} - - - )} - - )} + )} {/* Prepare Device Section - Hide in send/sign mode */} - {!isSendBitcoin && !isSignPSBT && + {!isSendBitcoin && + !isSignPSBT && !isPreParamsReady && localNpub && deviceName && @@ -5048,7 +4980,8 @@ const MobileNostrPairing = ({navigation}: any) => { {/* Final Step - Check other devices are prepared */} - {!isSendBitcoin && !isSignPSBT && + {!isSendBitcoin && + !isSignPSBT && isPreParamsReady && !mpcDone && localNpub && @@ -5481,16 +5414,22 @@ const MobileNostrPairing = ({navigation}: any) => { { } activeOpacity={0.8}> - {((isSendBitcoin || isSignPSBT) || !(isSendBitcoin || isSignPSBT)) && ( + {(isSendBitcoin || + isSignPSBT || + !(isSendBitcoin || isSignPSBT)) && ( { /> )} - {(isSendBitcoin || isSignPSBT) + {isSendBitcoin || isSignPSBT ? (() => { // Determine if local device is KeyShare1 const localDevice = sendModeDevices.find( @@ -5520,8 +5461,12 @@ const MobileNostrPairing = ({navigation}: any) => { const isKeyShare1 = localDevice?.keyshareLabel === 'KeyShare1'; return isKeyShare1 - ? (isSignPSBT ? 'Start PSBT Signing' : 'Start Co-Signing') - : (isSignPSBT ? 'Join PSBT Signing' : 'Join Co-Signing'); + ? isSignPSBT + ? 'Start PSBT Signing' + : 'Start Co-Signing' + : isSignPSBT + ? 'Join PSBT Signing' + : 'Join Co-Signing'; })() : (() => { // For keygen, determine if local npub is first in sorted order @@ -5626,7 +5571,9 @@ const MobileNostrPairing = ({navigation}: any) => { {/* Header Text */} - {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'} + {isSignPSBT + ? 'PSBT Co-Signing' + : 'Co-Signing Your Transaction'} {/* Subtext */} @@ -5875,20 +5822,14 @@ const MobileNostrPairing = ({navigation}: any) => { {/* QR Scanner Modal */} - {Platform.OS === 'ios' && isQRScannerVisible && ( - setIsQRScannerVisible(false)}> - handleQRScan(data, scanningForPeer)} - onClose={() => setIsQRScannerVisible(false)} - theme={theme} - /> - - )} + setIsQRScannerVisible(false)} + onScan={(data: string) => handleQRScan(data, scanningForPeer)} + mode="single" + title="Scan Connection QR" + subtitle="Point camera at the connection QR from the other device" + /> {/* QR Code Modal */} { const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false); const [passwordStrength, setPasswordStrength] = useState(0); const [passwordErrors, setPasswordErrors] = useState([]); - + // VPN detection state const [isVPNConnected, setIsVPNConnected] = useState(false); const [psbtDetails, setPsbtDetails] = useState<{ @@ -98,7 +106,6 @@ const MobilesPairing = ({navigation}: any) => { fee: number; totalInput: number; totalOutput: number; - derivePath?: string; derivePaths?: string[]; } | null>(null); @@ -118,7 +125,6 @@ const MobilesPairing = ({navigation}: any) => { selectedCurrency?: string; spendingHash?: string; psbtBase64?: string; // For PSBT signing mode - derivePath?: string; // BIP32 derivation path for PSBT }; const route = useRoute>(); @@ -297,7 +303,10 @@ const MobilesPairing = ({navigation}: any) => { route.params.psbtBase64, ); - if (detailsJson.startsWith('error') || detailsJson.includes('failed')) { + if ( + detailsJson.startsWith('error') || + detailsJson.includes('failed') + ) { dbg('Failed to parse PSBT details:', detailsJson); setPsbtDetails(null); return; @@ -310,7 +319,6 @@ const MobilesPairing = ({navigation}: any) => { fee: details.fee || 0, totalInput: details.totalInput || 0, totalOutput: details.totalOutput || 0, - derivePath: details.derivePath, derivePaths: details.derivePaths || [], }); dbg('PSBT details parsed:', { @@ -719,8 +727,25 @@ const MobilesPairing = ({navigation}: any) => { const satoshiFees = `${decoded[2]}`; const peerShare = `${decoded[3]}`; - // Derive public key and address for regular BTC sending (not needed for PSBT) - const path = route.params?.derivePath || getDerivePathForNetwork(net); + // Get address type from route params or cache, default to segwit-native + const currentAddressType = + addressType || + (await LocalCache.getItem('addressType')) || + 'segwit-native'; + // Check if this is a legacy wallet (created before migration timestamp) + const useLegacyPath = isLegacyWallet(ks.created_at); + const path = getDerivePathForNetwork( + net, + currentAddressType, + useLegacyPath, + ); + dbg('Deriving path for send:', { + net, + currentAddressType, + useLegacyPath, + path, + }); + const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -731,6 +756,35 @@ const MobilesPairing = ({navigation}: any) => { net, addressType, ); + dbg('Derived address for send:', { + path, + btcAddress, + addressType, + network: net, + publicKey: btcPub.substring(0, 20) + '...', + }); + + // Log warning if address doesn't match expected format for address type + if ( + addressType === 'segwit-native' && + !btcAddress.startsWith('tb1q') && + !btcAddress.startsWith('bc1q') + ) { + dbg( + 'WARNING: Address type is segwit-native but address does not start with tb1q/bc1q:', + btcAddress, + ); + } else if ( + addressType === 'legacy' && + !btcAddress.startsWith('1') && + !btcAddress.startsWith('m') && + !btcAddress.startsWith('n') + ) { + dbg( + 'WARNING: Address type is legacy but address format does not match:', + btcAddress, + ); + } dbg('starting...', { peerShare, @@ -895,7 +949,7 @@ const MobilesPairing = ({navigation}: any) => { } try { - HapticFeedback.light(); + HapticFeedback.medium(); const storedKeyshare = await EncryptedStorage.getItem('keyshare'); if (storedKeyshare) { @@ -905,15 +959,34 @@ const MobilesPairing = ({navigation}: any) => { await BBMTLibNativeModule.sha256(password), ); - // Create friendly filename with date and time (match WalletSettings) - const now = new Date(); - const month = now.toLocaleDateString('en-US', {month: 'short'}); - const day = now.getDate().toString().padStart(2, '0'); - const year = now.getFullYear(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - const share = json.local_party_key; - const friendlyFilename = `${share}.${month}${day}.${year}.${hours}${minutes}.share`; + // Create filename based on pub_key hash and keyshare number + if (!json.pub_key) { + Alert.alert('Error', 'Keyshare missing pub_key.'); + return; + } + + // Get SHA256 hash of pub_key and take first 4 characters + const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key); + const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase(); + + // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.) + const keyshareLabel = getKeyshareLabel(json); + let keyshareNumber = '1'; // default + if (keyshareLabel) { + const match = keyshareLabel.match(/KeyShare(\d+)/); + if (match) { + keyshareNumber = match[1]; + } + } else if (json.keygen_committee_keys && json.local_party_key) { + // Fallback: compute from position in sorted keygen_committee_keys + const sortedKeys = [...json.keygen_committee_keys].sort(); + const index = sortedKeys.indexOf(json.local_party_key); + if (index >= 0) { + keyshareNumber = String(index + 1); + } + } + + const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`; const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath; const filePath = `${tempDir}/${friendlyFilename}`; @@ -931,9 +1004,12 @@ const MobilesPairing = ({navigation}: any) => { failOnCancel: false, }); + // Cleanup temp file (best-effort) try { await RNFS.unlink(filePath); - } catch {} + } catch { + // ignore cleanup errors + } clearBackupModal(); } else { Alert.alert('Error', 'Invalid keyshare.'); @@ -1470,21 +1546,29 @@ const MobilesPairing = ({navigation}: any) => { const netInfo = await NetInfo.fetch(); // Check for VPN on both platforms let isVPN = false; - + if (netInfo.type === 'vpn') { isVPN = true; } else if (Platform.OS === 'android' && netInfo.details) { // Android: Check details.isVPN if available const details = netInfo.details as any; isVPN = details.isVPN === true || false; - } else if (Platform.OS === 'ios' && netInfo.type === 'other' && netInfo.details) { + } else if ( + Platform.OS === 'ios' && + netInfo.type === 'other' && + netInfo.details + ) { // iOS: Check details.isVPN if available const details = netInfo.details as any; isVPN = details.isVPN === true || false; } - + setIsVPNConnected(isVPN); - dbg('VPN Status:', {isVPN, type: netInfo.type, details: netInfo.details}); + dbg('VPN Status:', { + isVPN, + type: netInfo.type, + details: netInfo.details, + }); } catch (error) { dbg('Error checking VPN status:', error); setIsVPNConnected(false); @@ -1497,19 +1581,27 @@ const MobilesPairing = ({navigation}: any) => { // Subscribe to network state changes const unsubscribe = NetInfo.addEventListener(state => { let isVPN = false; - + if (state.type === 'vpn') { isVPN = true; } else if (Platform.OS === 'android' && state.details) { const details = state.details as any; isVPN = details.isVPN === true || false; - } else if (Platform.OS === 'ios' && state.type === 'other' && state.details) { + } else if ( + Platform.OS === 'ios' && + state.type === 'other' && + state.details + ) { const details = state.details as any; isVPN = details.isVPN === true || false; } - + setIsVPNConnected(isVPN); - dbg('VPN Status Changed:', {isVPN, type: state.type, details: state.details}); + dbg('VPN Status Changed:', { + isVPN, + type: state.type, + details: state.details, + }); }); return () => { @@ -1523,17 +1615,21 @@ const MobilesPairing = ({navigation}: any) => { // Re-check VPN status when screen is focused NetInfo.fetch().then(state => { let isVPN = false; - + if (state.type === 'vpn') { isVPN = true; } else if (Platform.OS === 'android' && state.details) { const details = state.details as any; isVPN = details.isVPN === true || false; - } else if (Platform.OS === 'ios' && state.type === 'other' && state.details) { + } else if ( + Platform.OS === 'ios' && + state.type === 'other' && + state.details + ) { const details = state.details as any; isVPN = details.isVPN === true || false; } - + setIsVPNConnected(isVPN); }); return () => { @@ -2770,11 +2866,10 @@ const MobilesPairing = ({navigation}: any) => { resizeMode="contain" /> - - VPN Detected - + VPN Detected - Please turn off your VPN to ensure a secure local network connection for device pairing. + Please turn off your VPN to ensure a secure local network + connection for device pairing. @@ -3898,7 +3993,11 @@ const MobilesPairing = ({navigation}: any) => { )} {isSignPSBT && ( - + PSBT Ready to Sign {psbtDetails ? ( @@ -3906,75 +4005,148 @@ const MobilesPairing = ({navigation}: any) => { - + Inputs: - + {psbtDetails.inputs.length} - + Outputs: - + {psbtDetails.outputs.length} - - + + Total Input: - + {sat2btcStr(psbtDetails.totalInput)} BTC - - + + Total Output: - + {sat2btcStr(psbtDetails.totalOutput)} BTC - - + + Fee: - + {sat2btcStr(psbtDetails.fee)} BTC - {psbtDetails.derivePath && ( - - - Derivation Path: - - 1 && ( + - {psbtDetails.derivePath} - - - )} - {psbtDetails.derivePaths && - psbtDetails.derivePaths.length > 1 && ( - - - {psbtDetails.derivePaths.length} different paths + + {psbtDetails.derivePaths.length} different + paths )} @@ -4030,7 +4202,9 @@ const MobilesPairing = ({navigation}: any) => { {/* Header Text */} - {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'} + {isSignPSBT + ? 'PSBT Co-Signing' + : 'Co-Signing Your Transaction'} {/* Subtext */} diff --git a/screens/PSBTModal.foss.tsx b/screens/PSBTModal.foss.tsx deleted file mode 100644 index e7fe17c..0000000 --- a/screens/PSBTModal.foss.tsx +++ /dev/null @@ -1,1661 +0,0 @@ -import React, {useState, useCallback, useRef, useEffect} from 'react'; -import { - View, - Text, - Modal, - TouchableOpacity, - StyleSheet, - Image, - ScrollView, - Platform, - NativeModules, - DeviceEventEmitter, - EmitterSubscription, -} from 'react-native'; -import DocumentPicker from 'react-native-document-picker'; -import * as RNFS from 'react-native-fs'; -import BarcodeZxingScan from 'rn-barcode-zxing-scan'; -// @ts-ignore - bc-ur types (Buffer polyfill is in polyfills.js) -import {URDecoder} from '@ngraveio/bc-ur'; -import {dbg, HapticFeedback} from '../utils'; -import {useTheme} from '../theme'; - -const {BBMTLibNativeModule} = NativeModules; - -// PSBT details structure (will be populated when parsing is implemented) -interface PSBTDetails { - inputs: Array<{ - txid: string; - vout: number; - amount: number; // in satoshis - }>; - outputs: Array<{ - address: string; - amount: number; // in satoshis - }>; - fee: number; // in satoshis - totalInput: number; - totalOutput: number; - derivePaths: string[]; // Derivation path for each input (indexed array) -} - -// UR (Uniform Resource) animated QR support for large PSBTs - -export interface PSBTLoaderProps { - btcRate?: number; // BTC to fiat rate - currencySymbol?: string; // e.g., "$", "€" - network?: 'mainnet' | 'testnet' | string; - /** - * When true, PSBTLoader renders with a full-screen overlay (modal style). - * When false, it renders as an embedded card without the dimmed backdrop. - */ - useOverlay?: boolean; - /** - * When true, the Cancel button is disabled while no PSBT is loaded. - * This is used on the dedicated PSBT screen so Cancel only resets state - * after something has been imported. - */ - disableCancelWhenEmpty?: boolean; - /** - * Optional middle floating button (used on PSBT screen for the lock button). - */ - middleButton?: React.ReactNode; - onClose: () => void; - onSign: (psbtBase64: string) => void; -} - -export interface PSBTModalProps extends PSBTLoaderProps { - visible: boolean; -} - -export const PSBTLoader: React.FC = ({ - btcRate = 0, - currencySymbol = '$', - network, - useOverlay = true, - disableCancelWhenEmpty = false, - middleButton, - onClose, - onSign, -}) => { - const {theme} = useTheme(); - const [psbtBase64, setPsbtBase64] = useState(null); - const [psbtDetails, setPsbtDetails] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - const [isAndroidScanning, setIsAndroidScanning] = useState(false); // For Android continuous scanning modal - - // UR (Uniform Resource) animated QR state - using bc-ur library - const urDecoderRef = useRef(null); - const isScanningRef = useRef(false); // Track if we're actively scanning (Android) - const scanSessionIdRef = useRef(0); // Track scan sessions to ignore stale events - const continuousScanSubscriptionRef = useRef( - null, - ); - const [urProgress, setUrProgress] = useState<{ - total: number; - received: number; - percentage: number; - } | null>(null); - - // Debug: Log isScanningRef periodically when scanning - useEffect(() => { - if (Platform.OS === 'android' && isAndroidScanning) { - const interval = setInterval(() => { - dbg( - 'Android: Periodic check - isScanningRef.current =', - isScanningRef.current, - 'isAndroidScanning =', - isAndroidScanning, - ); - }, 3000); // Log every 3 seconds when scanning - - return () => clearInterval(interval); - } - }, [isAndroidScanning]); - - // Update progress text overlay in Android zxing activity when urProgress changes - // This updates the native scanner's progress overlay (not a modal) - useEffect(() => { - if (Platform.OS === 'android' && isAndroidScanning && urProgress) { - const progressPercent = Math.min(100, Math.round(urProgress.received || 0)); - const progressText = `PSBT scanning progress... ${progressPercent}%`; - BarcodeZxingScan.updateProgressText(progressText); - } else if (Platform.OS === 'android' && isAndroidScanning && !urProgress) { - // Show initial message when scanning starts but no progress yet - BarcodeZxingScan.updateProgressText('Scanning PSBT QR Code...'); - } else if (Platform.OS === 'android' && !isAndroidScanning) { - // Clear progress text when scanning stops - BarcodeZxingScan.updateProgressText(''); - } - }, [urProgress, isAndroidScanning]); - - // Reset state when modal closes - const handleClose = useCallback(() => { - setPsbtBase64(null); - setPsbtDetails(null); - setError(null); - setIsLoading(false); - setIsAndroidScanning(false); - isScanningRef.current = false; - // Invalidate session to ignore any pending events - scanSessionIdRef.current = 0; - // Stop continuous scan on Android - if (Platform.OS === 'android') { - BarcodeZxingScan.stopContinuousScan(); - BarcodeZxingScan.updateProgressText(''); - // DON'T remove event listener here - let useEffect cleanup handle it - // The listener should persist across scans - } - urDecoderRef.current = null; - setUrProgress(null); - onClose(); - }, [onClose]); - - // Parse PSBT and extract details using native module - const parsePSBT = useCallback(async (base64Data: string) => { - setIsLoading(true); - setError(null); - - try { - dbg('PSBT Base64 length:', base64Data.length); - - // Use native module to parse PSBT details - const detailsJson = await BBMTLibNativeModule.parsePSBTDetails( - base64Data, - ); - dbg('Native PSBT parse result:', detailsJson.substring(0, 200)); - - // Check for error response - if (detailsJson.startsWith('error') || detailsJson.includes('failed')) { - throw new Error(detailsJson); - } - - // Parse the JSON response - const parsed = JSON.parse(detailsJson); - - const details: PSBTDetails = { - inputs: parsed.inputs || [], - outputs: parsed.outputs || [], - fee: parsed.fee || 0, - totalInput: parsed.totalInput || 0, - totalOutput: parsed.totalOutput || 0, - derivePaths: parsed.derivePaths || parsed.derivePathPerInput || [], // Per-input derivation paths - }; - - dbg('PSBT details:', { - inputCount: details.inputs.length, - outputCount: details.outputs.length, - totalInput: details.totalInput, - totalOutput: details.totalOutput, - fee: details.fee, - derivePaths: details.derivePaths, - }); - - setPsbtBase64(base64Data); - setPsbtDetails(details); - // Use most common derivation path for initial display/navigation - // The signing functions will extract the correct path per input from the PSBT - dbg('PSBT loaded successfully.'); - if (details.derivePaths && details.derivePaths.length > 0) { - dbg('Derivation paths per input:', details.derivePaths); - } - } catch (e: any) { - dbg('Error parsing PSBT:', e.message || e); - setError(e.message || 'Failed to parse PSBT'); - setPsbtBase64(null); - setPsbtDetails(null); - } finally { - setIsLoading(false); - } - }, []); - - // Handle file upload - const handleUploadFile = useCallback(async () => { - HapticFeedback.light(); - try { - const result = await DocumentPicker.pick({ - type: [DocumentPicker.types.allFiles], - copyTo: 'cachesDirectory', - }); - - const file = result[0]; - dbg('Selected file:', file.name, file.type); - - if (file.fileCopyUri) { - const fileContent = await RNFS.readFile( - file.fileCopyUri.replace('file://', ''), - 'base64', - ); - await parsePSBT(fileContent); - } else if (file.uri) { - // Try reading from original URI - const uri = - Platform.OS === 'ios' ? file.uri : file.uri.replace('file://', ''); - const fileContent = await RNFS.readFile(uri, 'base64'); - await parsePSBT(fileContent); - } - } catch (e: any) { - if (!DocumentPicker.isCancel(e)) { - dbg('Error picking file:', e); - setError('Failed to load PSBT file'); - } - } - }, [parsePSBT]); - - // Process UR QR code using bc-ur library (supports fountain codes) - const processURCode = useCallback( - (data: string): {isUR: boolean; complete: boolean; psbtBase64?: string} => { - const lowerData = data.toLowerCase(); - - // Check if it's a UR code - if (!lowerData.startsWith('ur:')) { - return {isUR: false, complete: false}; - } - - // Check if it's a PSBT type - const urType = lowerData.split('/')[0].substring(3); - if (urType !== 'psbt' && urType !== 'crypto-psbt') { - dbg('Not a PSBT UR, type:', urType); - return {isUR: true, complete: false}; - } - - try { - // Initialize decoder if needed - if (!urDecoderRef.current) { - urDecoderRef.current = new URDecoder(); - dbg('Created new URDecoder'); - } - - // Feed the QR data to the decoder - // UR decoder uses fountain codes - it can reconstruct from any subset of frames - // Duplicate frames are automatically handled by the decoder - urDecoderRef.current.receivePart(data.toLowerCase()); - - // Get progress - const progress = urDecoderRef.current.getProgress(); - const estimatedPercentComplete = Math.round( - urDecoderRef.current.estimatedPercentComplete() * 100, - ); - - dbg(`UR progress: ${estimatedPercentComplete}%, received: ${progress}`); - - // Update progress - use percentage directly for more accurate display - // On iOS, ensure we always update even if the value seems the same - // to trigger re-renders for progress bar - setUrProgress({ - total: 100, - received: estimatedPercentComplete, - percentage: estimatedPercentComplete, - }); - - HapticFeedback.light(); - - // Check if complete - if (urDecoderRef.current.isComplete()) { - dbg('UR decoder reports complete'); - if (urDecoderRef.current.isSuccess()) { - const ur = urDecoderRef.current.resultUR(); - dbg('UR complete! Type:', ur.type); - - // Get the CBOR payload - const cborPayload = ur.decodeCBOR(); - dbg( - 'CBOR payload type:', - typeof cborPayload, - 'length:', - cborPayload?.length, - ); - - // The CBOR payload should be the raw PSBT bytes - let psbtBytes: Uint8Array; - if (cborPayload instanceof Uint8Array) { - psbtBytes = cborPayload; - } else if (Buffer.isBuffer(cborPayload)) { - psbtBytes = new Uint8Array(cborPayload); - } else if (typeof cborPayload === 'object' && cborPayload.data) { - // Sometimes it comes wrapped - psbtBytes = new Uint8Array(cborPayload.data); - } else { - dbg('Unknown CBOR payload format:', cborPayload); - return {isUR: true, complete: false}; - } - - dbg('PSBT bytes length:', psbtBytes.length); - - // Verify PSBT magic bytes (psbt = 0x70736274) - if ( - psbtBytes.length > 4 && - psbtBytes[0] === 0x70 && - psbtBytes[1] === 0x73 && - psbtBytes[2] === 0x62 && - psbtBytes[3] === 0x74 - ) { - // Convert to base64 - const base64 = btoa( - String.fromCharCode.apply(null, Array.from(psbtBytes)), - ); - dbg('PSBT base64 length:', base64.length); - - // Reset decoder - urDecoderRef.current = null; - setUrProgress(null); - - return {isUR: true, complete: true, psbtBase64: base64}; - } else { - dbg( - 'Invalid PSBT magic bytes:', - psbtBytes.slice(0, 4).toString(), - ); - } - } else { - dbg('UR decoding failed:', urDecoderRef.current.resultError()); - } - - // Reset on failure - urDecoderRef.current = null; - setUrProgress(null); - } - - return {isUR: true, complete: false}; - } catch (e: any) { - dbg('UR processing error:', e.message || e); - // Reset decoder on error - urDecoderRef.current = null; - setUrProgress(null); - return {isUR: true, complete: false}; - } - }, - [], - ); - - // Process scanned QR data (handles both plain base64 and UR format) - const processScannedData = useCallback( - async (data: string, shouldContinueScanning?: () => void) => { - dbg( - 'Processing scanned data:', - data.substring(0, 50) + '...', - 'isScanningRef:', - isScanningRef.current, - ); - - // Check if it's UR format - const urResult = processURCode(data); - - if (urResult.isUR) { - if (urResult.complete && urResult.psbtBase64) { - // Complete! Stop scanning and parse - dbg('Android: UR complete, stopping scan'); - setIsAndroidScanning(false); - isScanningRef.current = false; - // Reset session ID to invalidate any pending events - scanSessionIdRef.current = 0; - // Stop continuous scan on Android - if (Platform.OS === 'android') { - BarcodeZxingScan.stopContinuousScan(); - // Clear progress text - BarcodeZxingScan.updateProgressText(''); - } - // Decoder is already reset in processURCode when complete - await parsePSBT(urResult.psbtBase64); - } else { - // Not complete yet - scanner stays open for next frame (Android continuous mode) - dbg( - 'Android: UR incomplete, continuing scan. isScanningRef:', - isScanningRef.current, - ); - // Ensure isScanningRef stays true for continuous scanning - if (!isScanningRef.current) { - dbg( - 'Android: WARNING - isScanningRef was false during UR processing, resetting to true', - ); - isScanningRef.current = true; - } - // On iOS, shouldContinueScanning callback is used - if (shouldContinueScanning && Platform.OS === 'ios') { - shouldContinueScanning(); - } - // On Android, scanner is already open and will emit more events - } - return; - } - - // Not UR format - could be: - // 1. Plain base64 PSBT (single scan) - stop and parse - // 2. Random QR code (not a PSBT) - ignore and keep scanning in continuous mode - // Check if it looks like base64 PSBT (starts with cHNidP8BA) - if ( - data.startsWith('cHNidP8BA') || - (data.length > 100 && !data.startsWith('UR:')) - ) { - // Looks like a PSBT - stop scanning and parse - dbg('Android: Found plain PSBT, stopping scan'); - setIsAndroidScanning(false); - isScanningRef.current = false; - // Reset session ID to invalidate any pending events - scanSessionIdRef.current = 0; - // Stop continuous scan on Android - if (Platform.OS === 'android') { - BarcodeZxingScan.stopContinuousScan(); - // Clear progress text - BarcodeZxingScan.updateProgressText(''); - } - // Reset decoder for next scan - urDecoderRef.current = null; - setUrProgress(null); - await parsePSBT(data); - } else { - // Doesn't look like a PSBT - ignore and keep scanning - dbg( - 'Android: Ignoring non-PSBT QR code, continuing scan. isScanningRef:', - isScanningRef.current, - ); - // Ensure isScanningRef stays true for continuous scanning - if (!isScanningRef.current) { - dbg( - 'Android: WARNING - isScanningRef was false when ignoring non-PSBT, resetting to true', - ); - isScanningRef.current = true; - } - } - }, - [processURCode, parsePSBT], - ); - - // Helper function to set up the event listener for continuous scanning - // Called every time we start a scan to ensure listener is fresh and active - const setupEventListener = useCallback(() => { - if (Platform.OS !== 'android') { - return; - } - - // Remove existing listener if any - if (continuousScanSubscriptionRef.current) { - dbg('Android: Removing existing event listener before setting up new one'); - continuousScanSubscriptionRef.current.remove(); - continuousScanSubscriptionRef.current = null; - } - - dbg('Android: Setting up EventEmitter listener for continuous scanning'); - - const subscription = DeviceEventEmitter.addListener( - 'BarcodeZxingScanContinuous', - (event: {data?: string; error?: string}) => { - // Always check current ref value - refs don't have closure issues - const currentIsScanning = isScanningRef.current; - const currentSessionId = scanSessionIdRef.current; - dbg( - 'Android: Received continuous scan event. isScanningRef:', - currentIsScanning, - 'sessionId:', - currentSessionId, - 'hasData:', - !!event.data, - 'hasError:', - !!event.error, - 'eventKeys:', - Object.keys(event), - ); - - if (event.error) { - // Error or cancellation - dbg('Android scan error:', event.error); - setIsAndroidScanning(false); - isScanningRef.current = false; - urDecoderRef.current = null; - setUrProgress(null); - BarcodeZxingScan.stopContinuousScan(); - BarcodeZxingScan.updateProgressText(''); - return; - } - - if (event.data) { - dbg('Android: Processing event with data:', event.data.substring(0, 50) + '...'); - - // CRITICAL: Simplified logic - if we receive an event, process it UNLESS - // we explicitly know the scan was completed and stopped - // The session ID helps, but if scanner is open and we get events, process them - - // Only reject if BOTH conditions are true: not scanning AND session ID is 0 - // This means the scan was explicitly stopped and we're not in a new session - if (!currentIsScanning && currentSessionId === 0) { - // Both false - scan was stopped, ignore this stale event - dbg( - 'Android: Received event but scan was stopped (sessionId=0, isScanningRef=false) - ignoring stale event', - ); - // Don't stop scanner here - it might already be stopped - return; - } - - // If we get here, we should process the event - // Re-enable scanning state if needed (handles race conditions) - if (!currentIsScanning) { - dbg( - 'Android: Received event but isScanningRef is false. Enabling scanning (sessionId=' + currentSessionId + ')', - ); - isScanningRef.current = true; - setIsAndroidScanning(true); - } - - // CRITICAL: Ensure UR decoder is initialized for this scan session - // Initialize decoder when we receive the first UR frame - if (!urDecoderRef.current && event.data.toLowerCase().startsWith('ur:')) { - dbg('Android: Initializing new UR decoder for scan session'); - urDecoderRef.current = new URDecoder(); - } - - // If decoder exists but we're getting non-UR data, reset decoder for new scan - if (urDecoderRef.current && !event.data.toLowerCase().startsWith('ur:')) { - dbg('Android: Received non-UR data with existing decoder, resetting decoder for new scan'); - urDecoderRef.current = null; - setUrProgress(null); - } - - dbg( - 'Android scanned QR frame:', - event.data.substring(0, 50) + '...', - ); - // Process the scanned data - the scanner stays open for next frame - // Use the latest processScannedData via closure, but also ensure refs are current - processScannedData(event.data, () => { - // This callback is called if UR is incomplete - scanner stays open - dbg( - 'Android: UR incomplete, scanner stays open for next frame...', - ); - // Ensure isScanningRef is still true (only if we're still scanning) - if (isScanningRef.current) { - // Only reset if we're still supposed to be scanning - setIsAndroidScanning(true); - } - }); - } - }, - ); - - continuousScanSubscriptionRef.current = subscription; - dbg('Android: EventEmitter listener set up successfully'); - }, [processScannedData]); - - // Cleanup listener on component unmount - useEffect(() => { - return () => { - if (Platform.OS === 'android' && continuousScanSubscriptionRef.current) { - dbg('Android: Component unmounting, removing EventEmitter listener'); - continuousScanSubscriptionRef.current.remove(); - continuousScanSubscriptionRef.current = null; - } - }; - }, []); - - // Android continuous scanning function - uses native continuous scanning - // The scanner stays open and sends results via EventEmitter until UR is complete - const startAndroidContinuousScan = useCallback(() => { - if (isScanningRef.current) { - dbg('Android: Already scanning, ignoring duplicate call'); - return; // Already scanning - } - - // CRITICAL: Set up event listener every time we start a scan - // This ensures the listener is fresh and active for each scan session - dbg('Android: Setting up event listener for new scan'); - setupEventListener(); - - // CRITICAL: Reset decoder and progress FIRST - // This ensures clean state for subsequent scans - urDecoderRef.current = null; - setUrProgress(null); - // Clear any progress text from previous scan - BarcodeZxingScan.updateProgressText(''); - - // CRITICAL: Increment session ID BEFORE setting scanning flags - // This ensures events are associated with the correct session - scanSessionIdRef.current = (scanSessionIdRef.current || 0) + 1; - const currentSessionId = scanSessionIdRef.current; - dbg('Android: Starting new scan session, sessionId:', currentSessionId); - - // CRITICAL: Set scanning flags BEFORE starting scanner - // This ensures event listener processes events correctly - dbg('Android: Setting isScanningRef to true BEFORE starting scanner'); - isScanningRef.current = true; - setIsAndroidScanning(true); - - dbg( - 'Android: Starting continuous scan for animated QR, sessionId:', - currentSessionId, - 'isScanningRef:', - isScanningRef.current, - 'isAndroidScanning:', - true, - 'listenerActive:', - !!continuousScanSubscriptionRef.current, - ); - - // Use the new continuous scanning API that keeps the activity open - // The callback is only invoked once to acknowledge scanner start - // Subsequent results come via EventEmitter - BarcodeZxingScan.showQrReaderContinuous((scanError: any, data: any) => { - dbg( - 'Android: Scanner start callback. Error:', - scanError, - 'Data:', - data, - 'isScanningRef:', - isScanningRef.current, - ); - - if (scanError) { - // Error starting scanner - dbg('Android: Failed to start scanner:', scanError); - setIsAndroidScanning(false); - isScanningRef.current = false; - BarcodeZxingScan.updateProgressText(''); - // Reset session ID on error - if (scanSessionIdRef.current === currentSessionId) { - scanSessionIdRef.current = 0; - } - return; - } - - // Scanner started successfully - results will come via EventEmitter - if (data === 'SCANNER_STARTED') { - // Verify this is still the current session (not a stale callback) - if (scanSessionIdRef.current !== currentSessionId) { - dbg('Android: Scanner start callback for stale session, ignoring'); - return; - } - dbg( - 'Android: Scanner started, waiting for QR frames via EventEmitter. sessionId:', - currentSessionId, - 'isScanningRef:', - isScanningRef.current, - ); - // Double-check that isScanningRef is still true - if (!isScanningRef.current) { - dbg( - 'Android: WARNING - isScanningRef was set to false, resetting to true', - ); - isScanningRef.current = true; - setIsAndroidScanning(true); - } - // Show progress text now that scanner activity is created - // Do this after a small delay to ensure activity is ready - setTimeout(() => { - BarcodeZxingScan.updateProgressText('Scanning PSBT QR Code...'); - }, 100); - } - }); - }, [setupEventListener]); // Include setupEventListener to ensure we have the latest version - - // iOS single scan function - uses BarcodeZxingScan (FOSS version) - const startIOSScan = useCallback(() => { - // Reset decoder and progress - urDecoderRef.current = null; - setUrProgress(null); - - // FOSS version: Use BarcodeZxingScan for iOS - BarcodeZxingScan.showQrReader((scanError: any, data: any) => { - if (scanError) { - dbg('iOS scan error:', scanError); - return; - } - - if (data) { - dbg('iOS scanned QR:', data.substring(0, 50) + '...'); - - // Initialize UR decoder if needed - if (!urDecoderRef.current && data.toLowerCase().startsWith('ur:')) { - dbg('iOS: Initializing new UR decoder for scan session'); - urDecoderRef.current = new URDecoder(); - } - - // Process the scanned data - processScannedData(data, () => { - // If UR is incomplete, prompt user to scan again - if (urDecoderRef.current && !urDecoderRef.current.isComplete()) { - dbg('iOS: UR incomplete, user needs to scan next frame'); - // Recursively call startIOSScan to scan next frame - // This allows user to scan multiple frames for animated QR - setTimeout(() => { - startIOSScan(); - }, 500); // Small delay before next scan - } - }); - } - }); - }, [processScannedData]); - - // Handle QR scan button press - const handleScanQR = useCallback(() => { - HapticFeedback.light(); - - // Reset all state when starting a new scan - setError(null); // Clear any previous errors - setIsLoading(false); // Clear loading state - setPsbtBase64(null); // Clear any previous PSBT data - setPsbtDetails(null); // Clear PSBT details - - if (Platform.OS === 'android') { - // Ensure any previous scan is fully stopped before starting a new one - // Always stop any existing scan first to ensure clean state - if (isScanningRef.current || isAndroidScanning) { - dbg('handleScanQR: Stopping previous scan before starting new one'); - // Stop the scanner first - BarcodeZxingScan.stopContinuousScan(); - setIsAndroidScanning(false); - isScanningRef.current = false; - // Reset decoder and progress when stopping previous scan - urDecoderRef.current = null; - setUrProgress(null); - BarcodeZxingScan.updateProgressText(''); - - // Add a small delay to ensure native scanner is fully stopped - // before starting a new scan (fixes Android subsequent scan issue) - // NOTE: Don't reset sessionId to 0 here - let the new scan set it - setTimeout(() => { - dbg('handleScanQR: Starting new Android scan after cleanup delay'); - startAndroidContinuousScan(); - }, 250); // 250ms delay to ensure cleanup completes - } else { - // No previous scan - reset state and start immediately - urDecoderRef.current = null; - setUrProgress(null); - // Don't reset sessionId here - let startAndroidContinuousScan handle it - dbg('handleScanQR: Starting new Android scan immediately'); - startAndroidContinuousScan(); - } - } else { - // iOS: Use BarcodeZxingScan (FOSS version) - dbg('handleScanQR: Starting iOS scan'); - startIOSScan(); - } - }, [startAndroidContinuousScan, startIOSScan, isAndroidScanning]); - - // Handle sign button - const handleSign = useCallback(() => { - HapticFeedback.medium(); - if (psbtBase64) { - // Pass both PSBT and derivation path to the signing handler - // The handler will need to accept an object with both values - onSign(psbtBase64); - } - }, [psbtBase64, onSign]); - - // Format satoshis to BTC string (compact) - const formatBTC = (sats: number): string => { - const btc = sats / 100000000; - // Show fewer decimals for readability, but keep precision for small amounts - if (btc >= 0.01) { - return btc.toFixed(6) + ' BTC'; - } - return btc.toFixed(8) + ' BTC'; - }; - - // Format satoshis to fiat string - const formatFiat = (sats: number): string => { - if (!btcRate || btcRate === 0) { - return ''; - } - const btc = sats / 100000000; - const fiat = btc * btcRate; - return `${currencySymbol}${fiat.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })}`; - }; - - const styles = createStyles(theme); - - const isCancelDisabled = disableCancelWhenEmpty && !psbtBase64; - - return ( - - - {/* Header */} - - - Sign • PSBT - - - {network === 'mainnet' ? 'MAINNET' : 'TESTNET'} - - - - - {/* Description */} - - Import a Partially Signed Bitcoin Transaction (PSBT) from Sparrow or - another wallet to sign with your keyshare on. - - - {/* Import buttons */} - {!psbtBase64 && ( - - - - Load PSBT File - - - - - Scan PSBT QR - - - )} - - {/* Loading indicator */} - {isLoading && ( - - Parsing PSBT... - - )} - - {/* Error message */} - {error && ( - - {error} - { - setError(null); - setPsbtBase64(null); - setPsbtDetails(null); - }}> - Try Again - - - )} - - {/* PSBT Details - Sparrow-style Visual Flow */} - {psbtDetails && psbtBase64 && !error && ( - - {/* Transaction Flow Diagram - Vertical Mobile-Friendly Layout */} - - {/* Inputs Section */} - - Inputs - {psbtDetails.inputs.map((input, index) => { - const derivePath = psbtDetails.derivePaths[index] || 'N/A'; - return ( - - - - - - - {input.txid.slice(0, 8)}... - {input.txid.slice(-6)}:{input.vout} - - - {derivePath} - - - - - - {formatBTC(input.amount)} - - {btcRate > 0 && ( - - {formatFiat(input.amount)} - - )} - - - {/* Flow line connector */} - {index < psbtDetails.inputs.length - 1 && ( - - )} - - ); - })} - - - {/* Transaction Hub (Center Arrow) */} - - - - - - Transaction - - - - {/* Outputs Section */} - - Outputs - {psbtDetails.outputs.map((output, index) => { - // Determine output type: change (likely if small amount), recipient, or fee - const isLikelyChange = - output.amount < psbtDetails.totalInput * 0.1; // Heuristic: small outputs are often change - - let outputIcon = require('../assets/bitcoin-icon.png'); - let outputType = 'recipient'; - - if (isLikelyChange) { - outputIcon = require('../assets/consolidate-icon.png'); - outputType = 'change'; - } - - return ( - - - - - - - {output.address.slice(0, 8) + - '...' + - output.address.slice(-6)} - - {outputType === 'change' && ( - Change - )} - - - - - {formatBTC(output.amount)} - - {btcRate > 0 && ( - - {formatFiat(output.amount)} - - )} - - - {/* Flow line connector */} - {index < psbtDetails.outputs.length - 1 && ( - - )} - - ); - })} - - {/* Fee as separate item */} - {psbtDetails.fee > 0 && ( - - - - - - Fee - - - - - {formatBTC(psbtDetails.fee)} - - {btcRate > 0 && ( - - {formatFiat(psbtDetails.fee)} - - )} - - - - )} - - - - {/* Summary Bar */} - - - - {psbtDetails.inputs.length} input - {psbtDetails.inputs.length !== 1 ? 's' : ''} →{' '} - {psbtDetails.outputs.length} output - {psbtDetails.outputs.length !== 1 ? 's' : ''} •{' '} - {formatBTC(psbtDetails.totalOutput + psbtDetails.fee)} total - - {psbtDetails.derivePaths.length > 0 && ( - - Path: {psbtDetails.derivePaths.join(', ')} - - )} - - - - {Math.round(psbtBase64.length / 1024)} KB - - - - - )} - - {/* Action buttons */} - - - - Cancel - - - - {middleButton && ( - - {middleButton} - - )} - - - - - Co-Sign - - - - - - ); -}; - -const PSBTModal: React.FC = ({ - visible, - btcRate = 0, - currencySymbol = '$', - network, - onClose, - onSign, -}) => { - if (!visible) { - return null; - } - - return ( - {}}> - - - ); -}; - -const createStyles = (theme: any) => - StyleSheet.create({ - modalOverlay: { - flex: 1, - backgroundColor: 'rgba(0, 0, 0, 0.85)', - justifyContent: 'center', - alignItems: 'center', - padding: 20, - }, - modalContent: { - backgroundColor: theme.colors.background, - borderRadius: 16, - padding: 20, - width: '100%', - maxWidth: 400, - maxHeight: '85%', - // Add shadow for better visibility - shadowColor: '#000', - shadowOffset: {width: 0, height: 4}, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 8, - }, - // Embedded version used on the dedicated PSBT screen (no overlay) - // No border/shadow here since it's inside a collapsible section that already has borders - embeddedContent: { - backgroundColor: theme.colors.cardBackground, - borderRadius: 8, - padding: 16, - width: '100%', - overflow: 'hidden', - }, - headerRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - marginBottom: 12, - }, - headerIcon: { - width: 24, - height: 24, - marginRight: 10, - tintColor: theme.colors.primary, - }, - headerTitle: { - fontSize: 20, - fontWeight: '700', - color: theme.colors.text, - flex: 1, - }, - description: { - fontSize: 14, - color: theme.colors.textSecondary, - lineHeight: 20, - marginBottom: 16, - }, - networkBadge: { - backgroundColor: theme.colors.background, - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.border, - }, - networkBadgeText: { - fontSize: 10, - fontWeight: '700', - color: theme.colors.text, - letterSpacing: 0.5, - }, - importButtonsContainer: { - marginBottom: 20, - }, - importButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: theme.colors.background, - borderRadius: 12, - padding: 16, - borderWidth: 1, - borderColor: theme.colors.border, - borderStyle: 'dashed', - marginBottom: 12, - }, - importButtonIcon: { - width: 24, - height: 24, - marginRight: 12, - tintColor: theme.colors.primary, - }, - importButtonText: { - fontSize: 16, - fontWeight: '600', - color: theme.colors.text, - }, - loadingContainer: { - alignItems: 'center', - padding: 20, - }, - loadingText: { - fontSize: 14, - color: theme.colors.textSecondary, - }, - errorContainer: { - backgroundColor: 'rgba(255, 59, 48, 0.1)', - borderRadius: 12, - padding: 16, - marginBottom: 20, - alignItems: 'center', - }, - errorText: { - fontSize: 14, - color: '#FF3B30', - textAlign: 'center', - marginBottom: 12, - }, - retryButton: { - backgroundColor: theme.colors.primary, - paddingHorizontal: 20, - paddingVertical: 8, - borderRadius: 8, - }, - retryButtonText: { - fontSize: 14, - fontWeight: '600', - color: '#FFFFFF', - }, - detailsContainer: { - maxHeight: 400, - marginBottom: 16, - }, - summaryHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 12, - }, - summaryLabel: { - fontSize: 13, - color: theme.colors.textSecondary, - }, - psbtSizeBadge: { - backgroundColor: 'rgba(52, 199, 89, 0.15)', - paddingHorizontal: 8, - paddingVertical: 3, - borderRadius: 6, - }, - psbtSizeText: { - fontSize: 11, - fontWeight: '600', - color: '#34C759', - }, - detailsSection: { - marginBottom: 12, - }, - detailsSectionTitle: { - fontSize: 10, - fontWeight: '700', - color: theme.colors.textSecondary, - marginBottom: 6, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - inputRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - backgroundColor: theme.colors.card || theme.colors.background, - borderRadius: 8, - paddingVertical: 10, - paddingHorizontal: 10, - marginBottom: 6, - }, - inputInfo: { - flex: 1, - marginRight: 8, - }, - inputTxidText: { - fontSize: 11, - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - marginBottom: 4, - }, - derivePathContainer: { - flexDirection: 'row', - alignItems: 'center', - marginTop: 4, - }, - derivePathLabel: { - fontSize: 9, - color: theme.colors.textSecondary, - marginRight: 6, - fontWeight: '600', - }, - derivePathText: { - fontSize: 10, - color: theme.colors.primary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - flex: 1, - }, - outputRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - backgroundColor: theme.colors.card || theme.colors.background, - borderRadius: 8, - paddingVertical: 8, - paddingHorizontal: 10, - marginBottom: 6, - }, - addressText: { - fontSize: 11, - color: theme.colors.text, - flex: 1, - marginRight: 8, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, - derivePathSummary: { - backgroundColor: theme.colors.card || theme.colors.background, - borderRadius: 8, - padding: 12, - marginTop: 8, - borderWidth: 1, - borderColor: theme.colors.border, - }, - derivePathSummaryLabel: { - fontSize: 10, - fontWeight: '700', - color: theme.colors.textSecondary, - marginBottom: 4, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - derivePathSummaryText: { - fontSize: 12, - color: theme.colors.primary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - fontWeight: '600', - }, - derivePathNote: { - fontSize: 9, - color: theme.colors.textSecondary, - marginTop: 4, - fontStyle: 'italic', - }, - // Sparrow-style Transaction Flow (Vertical for Mobile) - transactionFlow: { - paddingVertical: 12, - paddingHorizontal: 4, - }, - flowSection: { - width: '100%', - }, - flowSectionTitle: { - fontSize: 10, - fontWeight: '700', - color: theme.colors.textSecondary, - marginBottom: 12, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - flowItem: { - marginBottom: 8, - }, - flowItemContent: { - backgroundColor: theme.colors.card || theme.colors.background, - borderRadius: 8, - padding: 10, - borderWidth: 1, - borderColor: theme.colors.border, - }, - flowItemHeader: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 6, - }, - flowIcon: { - width: 20, - height: 20, - marginRight: 8, - tintColor: theme.colors.primary, - }, - flowItemInfo: { - flex: 1, - }, - flowItemLabel: { - fontSize: 11, - fontWeight: '600', - color: theme.colors.text, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - marginBottom: 2, - }, - flowItemPath: { - fontSize: 9, - color: theme.colors.primary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, - flowItemType: { - fontSize: 9, - color: theme.colors.textSecondary, - fontStyle: 'italic', - marginTop: 2, - }, - flowAmount: { - alignItems: 'flex-end', - }, - flowAmountBTC: { - fontSize: 12, - fontWeight: '700', - color: theme.colors.primary, - }, - flowAmountFiat: { - fontSize: 9, - color: theme.colors.textSecondary, - marginTop: 2, - }, - flowConnectorVertical: { - width: 1, - height: 8, - backgroundColor: theme.colors.border, - marginLeft: 14, - marginVertical: 4, - }, - transactionHubVertical: { - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 12, - paddingHorizontal: 8, - }, - hubArrow: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: theme.colors.primary + '20', - alignItems: 'center', - justifyContent: 'center', - marginBottom: 4, - }, - hubArrowText: { - fontSize: 20, - color: theme.colors.primary, - fontWeight: '700', - }, - hubLabel: { - marginTop: 4, - }, - hubLabelText: { - fontSize: 11, - fontWeight: '600', - color: theme.colors.textSecondary, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - summaryBar: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - backgroundColor: theme.colors.card || theme.colors.background, - borderRadius: 12, - padding: 12, - marginTop: 16, - borderWidth: 1, - borderColor: theme.colors.border, - }, - summaryBarContent: { - flex: 1, - marginRight: 12, - }, - summaryBarText: { - fontSize: 12, - fontWeight: '600', - color: theme.colors.text, - marginBottom: 4, - }, - summaryBarPath: { - fontSize: 10, - color: theme.colors.primary, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, - summaryBarBadge: { - backgroundColor: theme.colors.primary + '20', - borderRadius: 8, - paddingHorizontal: 10, - paddingVertical: 6, - }, - summaryBarBadgeText: { - fontSize: 10, - fontWeight: '700', - color: theme.colors.primary, - }, - amountContainer: { - alignItems: 'flex-end', - }, - amountText: { - fontSize: 12, - fontWeight: '600', - color: theme.colors.primary, - }, - fiatText: { - fontSize: 10, - color: theme.colors.textSecondary, - marginTop: 1, - }, - feeTotalContainer: { - borderTopWidth: 1, - borderTopColor: theme.colors.border, - paddingTop: 10, - }, - feeRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 6, - }, - feeLabel: { - fontSize: 12, - color: theme.colors.textSecondary, - }, - feeValueContainer: { - alignItems: 'flex-end', - }, - feeValue: { - fontSize: 12, - color: theme.colors.text, - }, - feeFiatValue: { - fontSize: 10, - color: theme.colors.textSecondary, - }, - totalRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingTop: 8, - borderTopWidth: 1, - borderTopColor: theme.colors.border, - }, - totalLabel: { - fontSize: 14, - fontWeight: '700', - color: theme.colors.text, - }, - totalValueContainer: { - alignItems: 'flex-end', - }, - totalValue: { - fontSize: 14, - fontWeight: '700', - color: theme.colors.primary, - }, - totalFiatValue: { - fontSize: 11, - color: theme.colors.textSecondary, - marginTop: 1, - }, - middleButtonContainer: { - marginHorizontal: 6, - justifyContent: 'center', - alignItems: 'center', - }, - actionButtonsContainer: { - flexDirection: 'row', - marginTop: 20, - }, - cancelButton: { - flex: 1, - backgroundColor: theme.colors.background, - borderRadius: 12, - paddingVertical: 14, - alignItems: 'center', - borderWidth: 1, - borderColor: theme.colors.border, - marginRight: 6, - }, - cancelButtonDisabled: { - opacity: 0.5, - }, - cancelButtonText: { - fontSize: 16, - fontWeight: '600', - color: theme.colors.text, - }, - cancelButtonTextDisabled: { - color: theme.colors.textSecondary, - }, - signButton: { - flex: 1, - flexDirection: 'row', - backgroundColor: theme.colors.primary, - borderRadius: 12, - paddingVertical: 14, - alignItems: 'center', - justifyContent: 'center', - marginLeft: 6, - }, - signButtonDisabled: { - backgroundColor: theme.colors.border, - }, - signButtonIcon: { - width: 20, - height: 20, - marginRight: 8, - tintColor: '#FFFFFF', - }, - signButtonIconDisabled: { - tintColor: theme.colors.textSecondary, - }, - signButtonText: { - fontSize: 16, - fontWeight: '600', - color: '#FFFFFF', - }, - signButtonTextDisabled: { - color: theme.colors.textSecondary, - }, - }); - -export default PSBTModal; diff --git a/screens/PSBTModal.tsx b/screens/PSBTModal.tsx index 66958fc..b39c5c9 100644 --- a/screens/PSBTModal.tsx +++ b/screens/PSBTModal.tsx @@ -14,11 +14,7 @@ import { } from 'react-native'; import DocumentPicker from 'react-native-document-picker'; import * as RNFS from 'react-native-fs'; -import { - Camera, - useCameraDevice, - useCodeScanner, -} from 'react-native-vision-camera'; +import QRScanner from '../components/QRScanner'; import BarcodeZxingScan from 'rn-barcode-zxing-scan'; // @ts-ignore - bc-ur types (Buffer polyfill is in polyfills.js) import {URDecoder} from '@ngraveio/bc-ur'; @@ -68,76 +64,6 @@ export interface PSBTModalProps extends PSBTLoaderProps { visible: boolean; } -// QR Scanner component with UR progress display -const QRScanner = ({styles, device, codeScanner, onClose, urProgress}: any) => { - if (!device) { - return ( - - - Camera Not Available - - Please check camera permissions in Settings - - - - Close - - - ); - } - - const isAnimatedQR = urProgress && urProgress.total > 1; - // Use percentage directly for more accurate progress display (fixes iOS progress issue) - const progressPercent = isAnimatedQR - ? Math.min( - 100, - urProgress.percentage || - Math.round((urProgress.received / urProgress.total) * 100), - ) - : 0; - const isComplete = isAnimatedQR && urProgress.received >= urProgress.total; - - return ( - - - - - - {isAnimatedQR ? 'Scanning Animated QR...' : 'Scan PSBT QR Code'} - - - {isAnimatedQR - ? isComplete - ? 'Processing PSBT...' - : `Keep scanning animated QR: ${progressPercent}%` - : 'Position the QR code within the frame'} - - {isAnimatedQR && ( - - - - )} - - - Cancel - - - ); -}; export const PSBTLoader: React.FC = ({ btcRate = 0, @@ -873,36 +799,23 @@ export const PSBTLoader: React.FC = ({ const styles = createStyles(theme); - // Only use camera hooks on iOS - Android uses BarcodeZxingScan with continuous scanning - let device; - let codeScanner; - - if (Platform.OS === 'ios') { - // eslint-disable-next-line react-hooks/rules-of-hooks - device = useCameraDevice('back'); - // eslint-disable-next-line react-hooks/rules-of-hooks - codeScanner = useCodeScanner({ - codeTypes: ['qr'], - onCodeScanned: codes => { - if (codes.length > 0 && codes[0].value) { - dbg('Scanned QR (iOS):', codes[0].value.substring(0, 50) + '...'); - - // CRITICAL: Ensure UR decoder is initialized for this scan session - // This ensures progress tracking works correctly on iOS - if ( - !urDecoderRef.current && - codes[0].value.toLowerCase().startsWith('ur:') - ) { - dbg('iOS: Initializing new UR decoder for scan session'); - urDecoderRef.current = new URDecoder(); - } + // Handler for QR scan results (wraps processScannedData for new QRScanner component) + const handleQRScan = useCallback((data: string) => { + dbg('QR Scanner: Scanned data:', data.substring(0, 50) + '...'); + + // CRITICAL: Ensure UR decoder is initialized for this scan session + // This ensures progress tracking works correctly + if ( + !urDecoderRef.current && + data.toLowerCase().startsWith('ur:') + ) { + dbg('QR Scanner: Initializing new UR decoder for scan session'); + urDecoderRef.current = new URDecoder(); + } - // Use processScannedData to handle both plain PSBT and UR format - processScannedData(codes[0].value); - } - }, - }); - } + // Use processScannedData to handle both plain PSBT and UR format + processScannedData(data); + }, [processScannedData]); return ( @@ -1204,22 +1117,28 @@ export const PSBTLoader: React.FC = ({ - {/* QR Scanner Modal (iOS only) */} - - - - - {/* Android scanner opens directly via native activity - no modal needed */} + onClose={handleScannerClose} + onScan={handleQRScan} + mode="continuous" + title="Scan PSBT QR Code" + subtitle={ + urProgress && urProgress.total > 1 + ? urProgress.received >= urProgress.total + ? 'Processing PSBT...' + : `Keep scanning animated QR: ${Math.min( + 100, + urProgress.percentage || + Math.round((urProgress.received / urProgress.total) * 100), + )}%` + : 'Point camera at the PSBT QR code to scan' + } + showProgress={!!urProgress && urProgress.total > 1} + progress={urProgress || undefined} + closeButtonText="Cancel" + /> ); diff --git a/screens/PSBTScreen.tsx b/screens/PSBTScreen.tsx index 59c313e..fc25c22 100644 --- a/screens/PSBTScreen.tsx +++ b/screens/PSBTScreen.tsx @@ -16,8 +16,8 @@ import {NativeModules} from 'react-native'; import {useTheme} from '../theme'; import {useUser} from '../context/UserContext'; import {HeaderRightButton, HeaderTitle} from '../components/Header'; -import {PSBTLoader} from './PSBTModal.foss'; -import {dbg, HapticFeedback, getDerivePathForNetwork, isLegacyWallet, generateAllOutputDescriptors} from '../utils'; +import {PSBTLoader} from './PSBTModal'; +import {dbg, HapticFeedback, generateAllOutputDescriptors} from '../utils'; import {CommonActions, useRoute, RouteProp} from '@react-navigation/native'; import TransportModeSelector from '../components/TransportModeSelector'; import Clipboard from '@react-native-clipboard/clipboard'; @@ -63,7 +63,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { useState(false); const [pendingPSBTParams, setPendingPSBTParams] = useState<{ psbtBase64: string; - derivePath: string; } | null>(null); const [signedPsbt, setSignedPsbt] = useState(null); const [isSignedPSBTModalVisible, setIsSignedPSBTModalVisible] = @@ -304,20 +303,11 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { ); // Handle PSBT signing - same logic as WalletHome + // Note: The actual signing functions extract derivation paths from PSBT's Bip32Derivation internally const handlePSBTSign = useCallback( - async (psbtBase64: string, derivePath?: string) => { - // Use provided derivation path or default to current address type - if (!derivePath) { - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (keyshareJSON) { - const keyshare = JSON.parse(keyshareJSON); - const currentAddressType = addressType || 'legacy'; - // Check if this is a legacy wallet (created before migration timestamp) - const useLegacyPath = isLegacyWallet(keyshare.created_at); - derivePath = getDerivePathForNetwork(network, currentAddressType, useLegacyPath); - } - } - const psbtDerivePath = derivePath || getDerivePathForNetwork(network, 'legacy', true); + async (psbtBase64: string, _derivePath?: string) => { + // The actual PSBT signing will extract paths from PSBT's Bip32Derivation field + // derivePath parameter is kept for API compatibility but not used // Check if keyshare supports Nostr (has nostr_npub) try { @@ -336,7 +326,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { mode: 'sign_psbt', addressType, psbtBase64, - derivePath: psbtDerivePath, }, }), ); @@ -349,19 +338,19 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { } // Store params and show transport selector - setPendingPSBTParams({psbtBase64, derivePath: psbtDerivePath}); + setPendingPSBTParams({psbtBase64}); setTimeout(() => { setIsPSBTTransportModalVisible(true); }, 300); }, - [network, addressType, navigation], + [addressType, navigation], ); const navigateToPSBTSigning = useCallback( (transport: 'local' | 'nostr') => { if (!pendingPSBTParams) return; - const {psbtBase64, derivePath} = pendingPSBTParams; + const {psbtBase64} = pendingPSBTParams; const routeName = transport === 'local' ? 'Devices Pairing' : 'Nostr Connect'; @@ -372,7 +361,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { mode: 'sign_psbt', addressType, psbtBase64, - derivePath, }, }), ); diff --git a/screens/SendBitcoinModal.foss.tsx b/screens/SendBitcoinModal.foss.tsx deleted file mode 100644 index 002b6ec..0000000 --- a/screens/SendBitcoinModal.foss.tsx +++ /dev/null @@ -1,782 +0,0 @@ -import React, {useState, useCallback, useEffect, useMemo} from 'react'; -import { - View, - Text, - TextInput, - TouchableOpacity, - StyleSheet, - Modal, - Alert, - Keyboard, - TouchableWithoutFeedback, - KeyboardAvoidingView, - Platform, - Image, - ActivityIndicator, - NativeModules, - ScrollView, - Linking, -} from 'react-native'; - -import BarcodeZxingScan from 'rn-barcode-zxing-scan'; -import Clipboard from '@react-native-clipboard/clipboard'; -import debounce from 'lodash/debounce'; -import Big from 'big.js'; -import {dbg, HapticFeedback} from '../utils'; -import {useTheme} from '../theme'; -import LocalCache from '../services/LocalCache'; -import {SafeAreaView} from 'react-native-safe-area-context'; -import {validate as validateBitcoinAddress} from 'bitcoin-address-validation'; - -const {BBMTLibNativeModule} = NativeModules; - -interface SendBitcoinModalProps { - visible: boolean; - onClose: () => void; - onSend: (address: string, amount: Big, estimatedFee: Big, spendingHash: string) => void; - btcToFiatRate: Big; - walletBalance: Big; - walletAddress: string; - selectedCurrency: string; -} - -const E8 = Big(10).pow(8); - -const SendBitcoinModal: React.FC = ({ - visible, - onClose, - onSend, - btcToFiatRate, - walletBalance, - walletAddress, - selectedCurrency, -}) => { - const [address, setAddress] = useState(''); - const [btcAmount, setBtcAmount] = useState(Big(0)); - const [inBtcAmount, setInBtcAmount] = useState(''); - const [inUsdAmount, setInUsdAmount] = useState(''); - const [estimatedFee, setEstimatedFee] = useState(null); - const [isCalculatingFee, setIsCalculatingFee] = useState(false); - const [spendingHash, setSpendingHash] = useState(''); - const [_activeInput, setActiveInput] = useState<'btc' | 'usd' | null>(null); - const [feeStrategy, setFeeStrategy] = useState('eco'); - - const {theme} = useTheme(); - - const styles = StyleSheet.create({ - feeStrategyContainer: { - marginBottom: 10, - }, - feeStrategyButton: { - backgroundColor: '#e9ecef', - paddingVertical: 8, - paddingHorizontal: 12, - borderRadius: 16, - marginRight: 8, - borderWidth: 1, - borderColor: theme.colors.border, - }, - feeStrategyButtonSelected: { - backgroundColor: theme.colors.primary, - borderColor: theme.colors.primary, - }, - feeStrategyText: { - fontSize: 14, - color: '#495057', - fontWeight: '600', - }, - feeStrategyTextSelected: { - color: '#fff', - }, - label: { - fontSize: 14, - fontWeight: '600', - marginBottom: 8, - color: '#7f8c8d', - }, - modalBackdrop: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.8)', - }, - modalContainer: { - width: '90%', - backgroundColor: theme.colors.background, - borderRadius: 10, - padding: 20, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - width: '100%', - marginBottom: 20, - paddingHorizontal: 4, - }, - titleContainer: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - flex: 1, - }, - bitcoinLogo: { - width: 24, - height: 24, - resizeMode: 'contain', - }, - title: { - fontSize: 20, - fontWeight: 'bold', - color: theme.colors.text, - flex: 1, - }, - closeButton: { - width: 30, - height: 30, - }, - closeButtonText: { - fontSize: 16, - color: theme.colors.text, - fontWeight: '600', - textAlign: 'center', - verticalAlign: 'middle', - lineHeight: 30, - }, - input: { - borderWidth: 1, - borderColor: theme.colors.secondary, - borderRadius: 8, - padding: 12, - fontSize: 16, - maxHeight: 50, - backgroundColor: '#FFF', - marginBottom: 10, - }, - inputWithIcons: { - position: 'relative', - marginBottom: 20, - marginTop: 20, - }, - inputAddressWithIcons: { - borderWidth: 1, - borderColor: theme.colors.secondary, - borderRadius: 8, - padding: 12, - paddingRight: 80, - minHeight: 48, - maxHeight: 120, - fontSize: 14, - backgroundColor: '#FFF', - textAlignVertical: 'top', - fontFamily: Platform.select({ios: 'Menlo', android: 'monospace'}) as any, - }, - iconImage: { - width: 24, - height: 24, - }, - pasteIconContainer: { - position: 'absolute', - top: 12, - right: 40, - }, - qrIconContainer: { - position: 'absolute', - top: 12, - right: 10, - }, - labelContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 5, - }, - maxText: { - color: theme.colors.accent, - fontSize: 14, - fontWeight: 'bold', - marginBottom: 10, - textDecorationLine: 'underline', - }, - inputContainer: { - marginBottom: 0, - }, - inputLabel: { - fontSize: 14, - fontWeight: '600', - marginBottom: 8, - color: '#7f8c8d', - }, - inputError: { - borderColor: theme.colors.danger || '#DC3545', - }, - errorText: { - fontSize: 12, - color: theme.colors.danger || '#DC3545', - marginTop: -8, - marginBottom: 8, - marginLeft: 4, - }, - feeContainer: { - marginTop: 15, - padding: 10, - backgroundColor: '#f8f9fa', - borderRadius: 8, - borderWidth: 1, - borderColor: theme.colors.secondary, - }, - feeLabel: { - fontSize: 14, - fontWeight: '600', - color: '#7f8c8d', - }, - feeInfoContainer: { - marginTop: 5, - }, - feeAmount: { - fontSize: 16, - fontWeight: 'bold', - color: theme.colors.text, - }, - feeCalculating: { - marginLeft: 10, - color: '#7f8c8d', - fontSize: 14, - }, - feeAmountContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - feeLoadingContainer: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginTop: 5, - }, - feeAmountUsd: { - fontSize: 14, - color: '#7f8c8d', - }, - sendCancelButtons: { - flexDirection: 'row', - justifyContent: 'space-between', - marginTop: 20, - }, - sendButton: { - flex: 1, - backgroundColor: theme.colors.primary, - padding: 15, - borderRadius: 8, - alignItems: 'center', - marginRight: 10, - }, - cancelButton: { - flex: 1, - backgroundColor: theme.colors.secondary, - padding: 15, - borderRadius: 8, - alignItems: 'center', - marginLeft: 10, - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, - disabledButton: { - opacity: 0.5, - }, - scannerContainer: { - flex: 1, - backgroundColor: 'black', - }, - qrFrame: { - position: 'absolute', - borderWidth: 2, - borderColor: 'white', - width: 250, - height: 250, - alignSelf: 'center', - top: '25%', - }, - closeScannerButton: { - position: 'absolute', - top: 50, - right: 20, - backgroundColor: theme.colors.accent, - padding: 10, - borderRadius: 50, - }, - closeScannerButtonText: { - color: '#fff', - fontWeight: 'bold', - }, - cameraNotFound: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, - // Setup Guide Hint Styles - setupGuideHint: { - marginTop: 12, - alignItems: 'center', - }, - setupGuideHintTouchable: { - paddingVertical: 8, - paddingHorizontal: 12, - borderRadius: 8, - }, - setupGuideHintRow: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - }, - setupGuideHintIcon: { - width: 16, - height: 16, - tintColor: theme.colors.primary, - }, - setupGuideHintText: { - fontSize: 13, - color: theme.colors.primary, - fontWeight: '500', - textDecorationLine: 'underline', - textDecorationColor: theme.colors.primary + '80', - }, - }); - - const feeStrategies = [ - {label: 'Economy', value: 'eco'}, - {label: 'Top Priority', value: 'top'}, - {label: '30 Min', value: '30m'}, - {label: '1 Hour', value: '1hr'}, - ]; - - const formatUSD = (price: number) => - new Intl.NumberFormat('en-US', { - style: 'decimal', - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(price); - - const getFee = useCallback( - async (addr: string, amt: string) => { - if (!addr || !amt || btcAmount.eq(0)) { - setEstimatedFee(null); - return; - } - - const amount = Big(amt); - - if (amount.gt(walletBalance) || !walletBalance) { - setEstimatedFee(null); - return; - } - - setIsCalculatingFee(true); - const satoshiAmount = amount.times(1e8).toFixed(0); - BBMTLibNativeModule.spendingHash( - walletAddress, - addr, - satoshiAmount, - ) - .then((hash: string) => { - setSpendingHash(hash); - dbg('got spending hash:', hash); - BBMTLibNativeModule.estimateFees( - walletAddress, - addr, - satoshiAmount, - ) - .then((fee: string) => { - if (fee && typeof fee === 'string') { - // Check if the response contains an error message - if ( - fee.includes('failed') || - fee.includes('error') || - fee.includes('[') - ) { - dbg('Fee estimation API returned error:', fee); - setEstimatedFee(null); - return; - } - - // Try to parse the fee as a valid number - try { - const feeNumber = parseFloat(fee); - if (isNaN(feeNumber) || feeNumber <= 0) { - dbg('Invalid fee amount received:', fee); - setEstimatedFee(null); - return; - } - - dbg('got fees:', fee); - const feeAmt = Big(feeNumber.toString()); - setEstimatedFee(feeAmt); - if (Big(inBtcAmount).eq(walletBalance)) { - setInBtcAmount( - walletBalance.minus(feeAmt.div(1e8)).toString(), - ); - } - } catch (parseError) { - dbg('Failed to parse fee amount:', fee, parseError); - setEstimatedFee(null); - } - } else { - dbg('No fee data received from API'); - setEstimatedFee(null); - } - }) - .catch((e: any) => { - dbg('Fee estimation failed:', e); - setEstimatedFee(null); - // Only show alert for network/API errors, not parsing errors - if (e.message && !e.message.includes('Invalid number')) { - Alert.alert( - 'Fee Estimation Error', - 'Unable to estimate transaction fee. Please try again later.', - ); - } - }) - .finally(() => { - setIsCalculatingFee(false); - }); - }) - .catch((e: any) => { - dbg('Spending hash failed:', e); - setIsCalculatingFee(false); - setEstimatedFee(null); - }); - }, - [btcAmount, walletBalance, walletAddress, inBtcAmount], - ); - - const debouncedGetFee = useMemo(() => debounce(getFee, 1000), [getFee]); - - useEffect(() => { - const initFee = async () => { - const feeOption = await LocalCache.getItem('feeStrategy'); - const defaultFee = feeOption && feeOption !== 'min' ? feeOption : 'eco'; - setFeeStrategy(defaultFee); - BBMTLibNativeModule.setFeePolicy(defaultFee); - dbg('using fee strategy', defaultFee); - }; - initFee(); - }, []); - - useEffect(() => { - // Only trigger fee estimation if we have a valid address and non-zero amount - if (address && btcAmount && btcAmount.gt(0) && validateBitcoinAddress(address)) { - debouncedGetFee(address, btcAmount.toString()); - } else { - // Clear fee if conditions aren't met - setEstimatedFee(null); - } - }, [address, btcAmount, debouncedGetFee, feeStrategy]); - - const pasteAddress = useCallback(async () => { - HapticFeedback.light(); - const text = await Clipboard.getString(); - setAddress(text); - }, []); - - const handleBtcChange = (text: string) => { - setActiveInput('btc'); - setInBtcAmount(text); - try { - const btc = Big(text || 0); - setBtcAmount(btc); - // Always update USD amount when BTC changes (no need to check activeInput) - setInUsdAmount(btc.mul(btcToFiatRate).toFixed(2)); - } catch { - dbg('Invalid BTC input:', text); - } - }; - - const handleUsdChange = (text: string) => { - setActiveInput('usd'); - setInUsdAmount(text); - try { - const usd = Big(text || 0); - const calculatedBtc = usd.div(btcToFiatRate); - // Always update BTC amount when USD changes (no need to check activeInput) - setBtcAmount(calculatedBtc); - setInBtcAmount(calculatedBtc.toFixed(8)); - } catch { - dbg('Invalid USD input:', text); - } - }; - - const handleMaxClick = () => { - HapticFeedback.medium(); - setBtcAmount(walletBalance); - setInBtcAmount(walletBalance.toFixed(8)); - setInUsdAmount(walletBalance.times(btcToFiatRate).toFixed(2)); - }; - - const handleFeeStrategyChange = (value: string) => { - HapticFeedback.selection(); - setFeeStrategy(value); - dbg('setting fee strategy to', value); - BBMTLibNativeModule.setFeePolicy(value); - LocalCache.setItem('feeStrategy', value); - }; - - const handleSendClick = () => { - // Client-side Bitcoin address validation - if (!address || !validateBitcoinAddress(address)) { - Alert.alert('Error', 'Please enter a valid Bitcoin address'); - return; - } - - if (!estimatedFee) { - Alert.alert('Error', 'Please wait for fee estimation'); - return; - } - const feeBTC = estimatedFee.div(1e8); - const totalAmount = Big(inBtcAmount).add(feeBTC); - if (totalAmount.gt(walletBalance)) { - Alert.alert('Error', 'Total amount including fee exceeds wallet balance'); - return; - } - HapticFeedback.heavy(); - onSend(address, Big(inBtcAmount).times(1e8), estimatedFee, spendingHash); - }; - - // Check if amount exceeds balance - const amountExceedsBalance = btcAmount.gt(0) && btcAmount.gt(walletBalance); - - const renderFeeSection = () => { - if (!address || !btcAmount) { - return null; - } - return ( - - {isCalculatingFee ? ( - - - Calculating... - - ) : estimatedFee ? ( - - - Network Fee: - - {feeStrategies.map(strategy => ( - handleFeeStrategyChange(strategy.value)}> - - {strategy.label} - - - ))} - - - - - {estimatedFee.div(E8).toFixed(8)} BTC - - - ({selectedCurrency}{' '} - {formatUSD( - estimatedFee.div(E8).times(btcToFiatRate).toNumber(), - )} - ) - - - - ) : null} - - ); - }; - - return ( - - - - - - - - - - Send Bitcoin - - - ✖️ - - - - - - - - { - HapticFeedback.light(); - if (Platform.OS === 'android') { - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error) { - setAddress(data); - } - }, - ); - } - }} - style={styles.qrIconContainer}> - - - - - - - Amount in BTC (₿) - - Max - - - setActiveInput('btc')} - keyboardType="decimal-pad" - /> - {amountExceedsBalance && ( - - Amount exceeds wallet balance ({walletBalance.toFixed(8)} BTC) - - )} - - - - - Amount in {selectedCurrency} ($) - - setActiveInput('usd')} - onChangeText={handleUsdChange} - keyboardType="decimal-pad" - /> - - - {renderFeeSection()} - - {/* Setup Guide Hint */} - - { - HapticFeedback.medium(); - const url = - 'https://x.com/boldbtcwallet/status/1988332367489237160'; - Linking.openURL(url).catch(err => { - Alert.alert('Error', 'Unable to open the video link'); - dbg('Error opening URL:', err); - }); - }} - activeOpacity={0.7}> - - - - 🎥 Watch Send Bitcoin video guide → - - - - - - - - Send - - { - HapticFeedback.light(); - onClose(); - }} - activeOpacity={0.7}> - Cancel - - - - - - - - - ); -}; - -export default SendBitcoinModal; diff --git a/screens/SendBitcoinModal.tsx b/screens/SendBitcoinModal.tsx index aed9e2e..550df83 100644 --- a/screens/SendBitcoinModal.tsx +++ b/screens/SendBitcoinModal.tsx @@ -17,16 +17,11 @@ import { ScrollView, Linking, } from 'react-native'; -import { - Camera, - useCameraDevice, - useCodeScanner, -} from 'react-native-vision-camera'; -import BarcodeZxingScan from 'rn-barcode-zxing-scan'; +import QRScanner from '../components/QRScanner'; import Clipboard from '@react-native-clipboard/clipboard'; import debounce from 'lodash/debounce'; import Big from 'big.js'; -import {dbg, HapticFeedback} from '../utils'; +import {dbg, HapticFeedback, decodeSendBitcoinQR} from '../utils'; import {useTheme} from '../theme'; import LocalCache from '../services/LocalCache'; import {SafeAreaView} from 'react-native-safe-area-context'; @@ -51,30 +46,6 @@ interface SendBitcoinModalProps { const E8 = Big(10).pow(8); -const QRScanner = ({styles, device, codeScanner, onClose}: any) => { - if (!device) { - return Camera Not Found; - } - return ( - - - - - Close - - - ); -}; - const SendBitcoinModal: React.FC = ({ visible, onClose, @@ -322,36 +293,6 @@ const SendBitcoinModal: React.FC = ({ disabledButton: { opacity: 0.5, }, - scannerContainer: { - flex: 1, - backgroundColor: 'black', - }, - qrFrame: { - position: 'absolute', - borderWidth: 2, - borderColor: 'white', - width: 250, - height: 250, - alignSelf: 'center', - top: '25%', - }, - closeScannerButton: { - position: 'absolute', - top: 50, - right: 20, - backgroundColor: theme.colors.accent, - padding: 10, - borderRadius: 50, - }, - closeScannerButtonText: { - color: '#fff', - fontWeight: 'bold', - }, - cameraNotFound: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, // Setup Guide Hint Styles setupGuideHint: { marginTop: 12, @@ -381,24 +322,6 @@ const SendBitcoinModal: React.FC = ({ }, }); - // Only use camera hooks on iOS - Android uses BarcodeZxingScan - let device; - let codeScanner; - - if (Platform.OS === 'ios') { - // eslint-disable-next-line react-hooks/rules-of-hooks - device = useCameraDevice('back'); - // eslint-disable-next-line react-hooks/rules-of-hooks - codeScanner = useCodeScanner({ - codeTypes: ['qr'], - onCodeScanned: codes => { - if (codes.length > 0) { - setAddress(codes[0].value!!); - setIsScannerVisible(false); - } - }, - }); - } const feeStrategies = [ {label: 'Economy', value: 'eco'}, @@ -469,9 +392,11 @@ const SendBitcoinModal: React.FC = ({ const feeAmt = Big(feeNumber.toString()); setEstimatedFee(feeAmt); if (Big(inBtcAmount).eq(walletBalance)) { - setInBtcAmount( - walletBalance.minus(feeAmt.div(1e8)).toString(), - ); + // When MAX is clicked, adjust amount to account for fee + const adjustedAmount = walletBalance.minus(feeAmt.div(1e8)); + setInBtcAmount(adjustedAmount.toFixed(8)); + setBtcAmount(adjustedAmount); + setInUsdAmount(adjustedAmount.times(btcToFiatRate).toFixed(2)); } } catch (parseError) { dbg('Failed to parse fee amount:', fee, parseError); @@ -503,7 +428,7 @@ const SendBitcoinModal: React.FC = ({ setEstimatedFee(null); }); }, - [btcAmount, walletBalance, walletAddress, inBtcAmount], + [btcAmount, walletBalance, walletAddress, inBtcAmount, btcToFiatRate], ); const debouncedGetFee = useMemo(() => debounce(getFee, 1000), [getFee]); @@ -594,6 +519,60 @@ const SendBitcoinModal: React.FC = ({ setInUsdAmount(walletBalance.times(btcToFiatRate).toFixed(2)); }; + // Handle QR scan - supports both regular addresses and send bitcoin QR format + const handleQRScan = useCallback((qrData: string) => { + if (!qrData || !qrData.trim()) { + return; + } + + // Check if it's a send bitcoin QR format (address|amount|fee|hash) + const decoded = decodeSendBitcoinQR(qrData) as { + toAddress: string; + amountSats: string; + feeSats: string; + spendingHash?: string; + } | null; + if (decoded && decoded.toAddress && decoded.amountSats && decoded.feeSats) { + // It's a send bitcoin QR format - populate all fields + if (!validateBitcoinAddress(decoded.toAddress)) { + Alert.alert('Invalid Address', 'The scanned QR code contains an invalid Bitcoin address.'); + return; + } + + const amountSats = Big(decoded.amountSats); + const feeSats = Big(decoded.feeSats); + const amountBTC = amountSats.div(1e8); + + if (amountSats.lte(0) || feeSats.lte(0)) { + Alert.alert('Invalid Amount', 'The scanned QR code contains invalid amount or fee values.'); + return; + } + + // Populate form fields + setAddress(decoded.toAddress); + setBtcAmount(amountBTC); + setInBtcAmount(amountBTC.toFixed(8)); + setInUsdAmount(amountBTC.times(btcToFiatRate).toFixed(2)); + setSpendingHash(decoded.spendingHash || ''); + + // Set the fee (will be validated when fee estimation runs) + // Note: The fee from QR might not match current network conditions, + // but we'll let the fee estimation handle that + + Alert.alert( + 'Transaction Details Loaded', + `Address and amount have been filled from the QR code.\n\nAddress: ${decoded.toAddress.substring(0, 10)}...\nAmount: ${amountBTC.toFixed(8)} BTC\n\nPlease review and confirm.`, + ); + } else { + // It's a regular Bitcoin address - just set the address + if (validateBitcoinAddress(qrData.trim())) { + setAddress(qrData.trim()); + } else { + Alert.alert('Invalid QR Code', 'The scanned QR code is not a valid Bitcoin address or send bitcoin data.'); + } + } + }, [btcToFiatRate]); + const handleFeeStrategyChange = (value: string) => { HapticFeedback.selection(); setFeeStrategy(value); @@ -734,17 +713,7 @@ const SendBitcoinModal: React.FC = ({ { HapticFeedback.light(); - if (Platform.OS === 'android') { - BarcodeZxingScan.showQrReader( - (error: any, data: any) => { - if (!error) { - setAddress(data); - } - }, - ); - } else { - setIsScannerVisible(true); - } + setIsScannerVisible(true); }} style={styles.qrIconContainer}> = ({ - setIsScannerVisible(false)}> - setIsScannerVisible(false)} - /> - + onClose={() => setIsScannerVisible(false)} + onScan={handleQRScan} + mode="single" + title="Scan QR Code" + subtitle="Point camera at the QR data" + /> diff --git a/screens/ShowcaseScreen.tsx b/screens/ShowcaseScreen.tsx index ca3783f..d122882 100644 --- a/screens/ShowcaseScreen.tsx +++ b/screens/ShowcaseScreen.tsx @@ -1083,7 +1083,7 @@ const ShowcaseScreen = ({navigation}: any) => { styles.modeOptionTitle, {color: theme.colors.primary}, ]}> - Basic + Duo @@ -1168,7 +1168,7 @@ const ShowcaseScreen = ({navigation}: any) => { styles.modeOptionTitle, {color: theme.colors.primary}, ]}> - Flexi + Trio @@ -1185,7 +1185,7 @@ const ShowcaseScreen = ({navigation}: any) => { {selectedMode === 'duo' ? ( - Basic (2/2) + Duo (2/2) : two devices needed for wallet setup. both of them must approve transactions when spending funds. @@ -1193,7 +1193,7 @@ const ShowcaseScreen = ({navigation}: any) => { ) : ( - Flexi (2/3) + Trio (2/3) : three devices needed for wallet setup. any 2 of them must approve transactions when spending funds. diff --git a/screens/WalletHome.tsx b/screens/WalletHome.tsx index e010c1c..d18d165 100644 --- a/screens/WalletHome.tsx +++ b/screens/WalletHome.tsx @@ -12,6 +12,7 @@ import { DeviceEventEmitter, Linking, } from 'react-native'; +import QRScanner from '../components/QRScanner'; import { useFocusEffect, useNavigation, @@ -28,7 +29,7 @@ import {CommonActions} from '@react-navigation/native'; import Big from 'big.js'; import ReceiveModal from './ReceiveModal'; import SignedPSBTModal from './SignedPSBTModal'; -import PSBTModal from './PSBTModal.foss'; +import PSBTModal from './PSBTModal'; import KeyshareModal from '../components/KeyshareModal'; import QRCodeModal from '../components/QRCodeModal'; import { @@ -42,7 +43,9 @@ import { getDerivePathForNetwork, isLegacyWallet, generateAllOutputDescriptors, + decodeSendBitcoinQR, } from '../utils'; +import {validate as validateBitcoinAddress} from 'bitcoin-address-validation'; import {useTheme} from '../theme'; import {WalletService} from '../services/WalletService'; import WalletSkeleton from '../components/WalletSkeleton'; @@ -85,14 +88,13 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { useState(false); const [pendingPSBTParams, setPendingPSBTParams] = useState<{ psbtBase64: string; - derivePath: string; } | null>(null); const [btcPrice, setBtcPrice] = useState(''); const [btcRate, setBtcRate] = useState(0); const [balanceBTC, setBalanceBTC] = useState('0.00000000'); const [balanceFiat, setBalanceFiat] = useState('0'); const [party, setParty] = useState(''); - const [isBlurred, setIsBlurred] = useState(true); + const [isBlurred, setIsBlurred] = useState(false); const [isReceiveModalVisible, setIsReceiveModalVisible] = useState(false); const [isSignedPSBTModalVisible, setIsSignedPSBTModalVisible] = useState(false); @@ -117,6 +119,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const [isCurrencySelectorVisible, setIsCurrencySelectorVisible] = useState(false); const [selectedCurrency, setSelectedCurrency] = useState(''); + const [isQRScannerVisible, setIsQRScannerVisible] = useState(false); + const [scannedFromQR, setScannedFromQR] = useState(false); // Track if data came from QR scan const [priceData, setPriceData] = useState<{[key: string]: number}>({}); const [segwitCompatibleAddress, setSegwitCompatibleAddress] = React.useState(''); @@ -393,10 +397,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { if (jks) { const ks = JSON.parse(jks); // Get current address type for derivation path - const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy'; + const currentAddressType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath); + const path = getDerivePathForNetwork( + network, + currentAddressType, + useLegacyPath, + ); btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -451,7 +460,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Get the current address type from cache or state const currentAddressType = - addressType || (await LocalCache.getItem('addressType')) || 'P2WPKH'; + addressType || + (await LocalCache.getItem('addressType')) || + 'segwit-native'; dbg( 'Using address type:', currentAddressType, @@ -471,17 +482,17 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { return; } - const ks = JSON.parse(jks); - // Get current address type for derivation path - const deriveAddressType = - (await LocalCache.getItem('addressType')) || 'legacy'; - // Check if this is a legacy wallet (created before migration timestamp) - const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork( - newNetwork, - deriveAddressType, - useLegacyPath, - ); + const ks = JSON.parse(jks); + // Get current address type for derivation path + const deriveAddressType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; + // Check if this is a legacy wallet (created before migration timestamp) + const useLegacyPath = isLegacyWallet(ks.created_at); + const path = getDerivePathForNetwork( + newNetwork, + deriveAddressType, + useLegacyPath, + ); btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -619,10 +630,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const ks = JSON.parse(jks); // Get current address type for derivation path - const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy'; + const currentAddressType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath); + const path = getDerivePathForNetwork( + network, + currentAddressType, + useLegacyPath, + ); // Always derive btcPub fresh to ensure it's current const btcPub = await BBMTLibNativeModule.derivePubkey( @@ -641,7 +657,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { dbg('Re-initializing for network:', net); // Get current address type - const addrType = (await LocalCache.getItem('addressType')) || 'legacy'; + const addrType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; setAddressType(addrType); // Set up network parameters @@ -813,7 +830,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { label: string; supportsLocal: boolean; supportsNostr: boolean; - type: 'basic' | 'flexi'; + type: 'duo' | 'trio'; pubKey: string; chainCode: string; xpub: string; @@ -847,9 +864,50 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } }; - const {theme} = useTheme(); - const styles = createStyles(theme); + const styles = { + ...createStyles(theme), + // Lock FAB + lockFAB: { + position: 'absolute' as const, + bottom: 24, + right: 20, + width: 56, + height: 56, + borderRadius: 28, + backgroundColor: + Platform.OS === 'android' + ? 'rgba(40, 40, 50, 0.92)' // More visible dark blue-gray background on Android + : 'rgba(0, 0, 0, 0.6)', + justifyContent: 'center' as const, + alignItems: 'center' as const, + elevation: 12, + shadowColor: '#000', + shadowOffset: {width: 0, height: 4}, + shadowOpacity: 0.5, + shadowRadius: 10, + borderWidth: Platform.OS === 'android' ? 2 : 1, + borderColor: + Platform.OS === 'android' + ? 'rgba(255, 255, 255, 0.35)' // More visible border on Android + : 'rgba(255, 255, 255, 0.2)', + overflow: 'hidden' as const, + } as const, + lockFABIcon: { + width: 28, + height: 28, + tintColor: theme.colors.background, + } as const, + lockFABOverlay: { + position: 'absolute' as const, + width: '100%', + height: '100%', + borderRadius: 28, + backgroundColor: 'rgba(255, 255, 255, 0.08)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.15)', + } as const, + }; const headerRight = React.useCallback( () => , @@ -909,11 +967,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { useEffect(() => { LocalCache.getItem('addressType').then(addrType => { - setAddressType(addrType || 'legacy'); + setAddressType(addrType || 'segwit-native'); }); LocalCache.getItem('currency').then(currency => { setSelectedCurrency(currency || 'USD'); }); + // Load balance visibility preference + LocalCache.getItem('balanceHidden').then(hidden => { + setIsBlurred(hidden === 'true'); + }); }); // Simplified focus effect - just refresh data when screen comes into focus @@ -1063,10 +1125,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } // Get current address type for derivation path - const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy'; + const currentAddressType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath); + const path = getDerivePathForNetwork( + network, + currentAddressType, + useLegacyPath, + ); const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -1088,9 +1155,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Set default address type if not set let addrType = await LocalCache.getItem('addressType'); if (!addrType) { - addrType = 'legacy'; + addrType = 'segwit-native'; await LocalCache.setItem('addressType', addrType); - dbg('WalletHome: Setting default address type to legacy'); + dbg('WalletHome: Setting default address type to segwit-native'); } // Set default currency if not set let currency = (await LocalCache.getItem('currency')) || 'USD'; @@ -1243,7 +1310,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const handleBlurred = () => { const blurr = !isBlurred; setIsBlurred(blurr); - LocalCache.setItem('mode', blurr ? 'private' : ''); + LocalCache.setItem('balanceHidden', blurr ? 'true' : 'false'); }; const loadKeyshareInfo = useCallback(async () => { @@ -1261,9 +1328,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const supportsNostr = !!(nostrNpub && nostrNpub.trim() !== ''); const supportsLocal = true; // Always supported - // Determine type: basic (2 devices) or flexi (3 devices) + // Determine type: duo (2 devices) or trio (3 devices) const committeeKeys = keyshare.keygen_committee_keys || []; - const type = committeeKeys.length === 3 ? 'flexi' : 'basic'; + const type = committeeKeys.length === 3 ? 'trio' : 'duo'; // Determine label: if Nostr, use sorted order; otherwise use generic let label = 'KeyShare1'; @@ -1292,7 +1359,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { chainCode, network, keyshare.created_at, - addressType || 'legacy', + addressType || 'segwit-native', ); const outputDescriptors = { @@ -1415,8 +1482,74 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { }), ); setPendingSendParams(null); + setScannedFromQR(false); // Reset flag }; + // Process scanned QR data + const processScannedQRData = useCallback((qrData: string) => { + dbg('Scanned QR data:', qrData.substring(0, 100)); + + const decoded = decodeSendBitcoinQR(qrData) as { + toAddress: string; + amountSats: string; + feeSats: string; + spendingHash?: string; + } | null; + if ( + !decoded || + !decoded.toAddress || + !decoded.amountSats || + !decoded.feeSats + ) { + Alert.alert( + 'Invalid QR Code', + 'The scanned QR code does not contain valid send bitcoin data. Please scan the QR code from the device that initiated the transaction.', + ); + return; + } + + // Validate Bitcoin address + if (!validateBitcoinAddress(decoded.toAddress)) { + Alert.alert( + 'Invalid Address', + 'The scanned QR code contains an invalid Bitcoin address.', + ); + return; + } + + // Convert to Big for consistency + const amountSats = Big(decoded.amountSats); + const feeSats = Big(decoded.feeSats); + + if (amountSats.lte(0) || feeSats.lte(0)) { + Alert.alert( + 'Invalid Amount', + 'The scanned QR code contains invalid amount or fee values.', + ); + return; + } + + // Store params and mark as scanned from QR + setPendingSendParams({ + to: decoded.toAddress, + amountSats, + feeSats, + spendingHash: decoded.spendingHash || '', + }); + setScannedFromQR(true); + + // Show transport selector immediately (no QR code shown since data came from scan) + setTimeout(() => { + setIsTransportModalVisible(true); + }, 300); + }, []); + + // Handle QR scan for send bitcoin data + const handleScanQRForSend = useCallback(() => { + HapticFeedback.medium(); + setIsQRScannerVisible(true); + }, []); + // Handle PSBT signing - similar to handleSend (kept for compatibility with PSBT flows) const handlePSBTSign = async (psbtBase64: string, derivePath?: string) => { setIsPSBTModalVisible(false); @@ -1425,13 +1558,19 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const jks = await EncryptedStorage.getItem('keyshare'); if (jks) { const ks = JSON.parse(jks); - const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy'; + const currentAddressType = + (await LocalCache.getItem('addressType')) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - derivePath = getDerivePathForNetwork(network, currentAddressType, useLegacyPath); + derivePath = getDerivePathForNetwork( + network, + currentAddressType, + useLegacyPath, + ); } } - const psbtDerivePath = derivePath || getDerivePathForNetwork(network, 'legacy', true); + const psbtDerivePath = + derivePath || getDerivePathForNetwork(network, 'segwit-native', true); // Check if keyshare supports Nostr (has nostr_npub) try { @@ -1463,7 +1602,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } // Store params and show transport selector - setPendingPSBTParams({psbtBase64, derivePath: psbtDerivePath}); + setPendingPSBTParams({psbtBase64}); setTimeout(() => { setIsPSBTTransportModalVisible(true); }, 300); @@ -1472,7 +1611,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const navigateToPSBTSigning = (transport: 'local' | 'nostr') => { if (!pendingPSBTParams) return; - const {psbtBase64, derivePath} = pendingPSBTParams; + const {psbtBase64} = pendingPSBTParams; const routeName = transport === 'local' ? 'Devices Pairing' : 'Nostr Connect'; @@ -1483,7 +1622,6 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { mode: 'sign_psbt', addressType, psbtBase64, - derivePath, }, }), ); @@ -1508,7 +1646,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } return ( - + @@ -1687,16 +1825,13 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { /> Send - {/* Lock icon button replaces address type change button */} + {/* Scan QR button replaces lock button in action row */} { - HapticFeedback.light(); - // Emit a reload event to App.tsx to trigger authentication lock - DeviceEventEmitter.emit('app:reload'); - }}> + onPress={handleScanQRForSend} + activeOpacity={0.8}> @@ -1774,6 +1909,37 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { isBlurred={isBlurred} /> + {/* Lock FAB Button - Bottom Right */} + {isInitialized && address && ( + { + HapticFeedback.light(); + // Emit a reload event to App.tsx to trigger authentication lock + DeviceEventEmitter.emit('app:reload'); + }} + activeOpacity={0.7}> + {Platform.OS === 'android' && } + + + )} + {/* Scan QR Button - Hidden, accessible via SendBitcoinModal or other means */} + {/* QR Scanner Modal */} + setIsQRScannerVisible(false)} + onScan={(data: string) => { + setIsQRScannerVisible(false); + processScannedQRData(data); + }} + mode="single" + title="Scan Send Bitcoin QR" + subtitle="Point camera at the QR data on the sending device" + /> = ({navigation}) => { HapticFeedback.medium(); setIsTransportModalVisible(false); setPendingSendParams(null); + setScannedFromQR(false); }} onSelect={(transport: 'local' | 'nostr') => { navigateToPairing(transport); setIsTransportModalVisible(false); }} title="Select Signing Method" - description="Choose how to sign your transaction" + description="" + sendBitcoinData={ + pendingSendParams + ? { + toAddress: pendingSendParams.to, + amountSats: pendingSendParams.amountSats + .toString() + .split('.')[0], + feeSats: pendingSendParams.feeSats.toString().split('.')[0], + spendingHash: pendingSendParams.spendingHash, + } + : null + } + showQRCode={!scannedFromQR} // Don't show QR if data came from scan /> {isReceiveModalVisible && ( @@ -2005,7 +2185,6 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { nonDismissible={true} /> - { diff --git a/screens/WalletSettings.tsx b/screens/WalletSettings.tsx index 7bc6e42..5efc259 100644 --- a/screens/WalletSettings.tsx +++ b/screens/WalletSettings.tsx @@ -1168,28 +1168,44 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { } try { - // Haptic feedback - HapticFeedback.light(); + HapticFeedback.medium(); - const keyshare = await EncryptedStorage.getItem('keyshare'); - if (keyshare) { - const json = JSON.parse(keyshare); + const storedKeyshare = await EncryptedStorage.getItem('keyshare'); + if (storedKeyshare) { + const json = JSON.parse(storedKeyshare); const encryptedKeyshare = await BBMTLibNativeModule.aesEncrypt( - keyshare, + storedKeyshare, await BBMTLibNativeModule.sha256(password), ); - // Create friendly filename with date and time - const now = new Date(); - const month = now.toLocaleDateString('en-US', {month: 'short'}); - const day = now.getDate().toString().padStart(2, '0'); - const year = now.getFullYear(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key + // Create filename based on pub_key hash and keyshare number + if (!json.pub_key) { + Alert.alert('Error', 'Keyshare missing pub_key.'); + return; + } + + // Get SHA256 hash of pub_key and take first 4 characters + const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key); + const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase(); + + // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.) const keyshareLabel = getKeyshareLabel(json); - const shareName = keyshareLabel || json.local_party_key || 'keyshare'; - const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`; + let keyshareNumber = '1'; // default + if (keyshareLabel) { + const match = keyshareLabel.match(/KeyShare(\d+)/); + if (match) { + keyshareNumber = match[1]; + } + } else if (json.keygen_committee_keys && json.local_party_key) { + // Fallback: compute from position in sorted keygen_committee_keys + const sortedKeys = [...json.keygen_committee_keys].sort(); + const index = sortedKeys.indexOf(json.local_party_key); + if (index >= 0) { + keyshareNumber = String(index + 1); + } + } + + const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`; const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath; const filePath = `${tempDir}/${friendlyFilename}`; @@ -1213,11 +1229,12 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { } catch { // ignore cleanup errors } + clearBackupModal(); } else { Alert.alert('Error', 'Invalid keyshare.'); } } catch (error) { - dbg('backup error', error); + dbg('Error encrypting or sharing keyshare:', error); Alert.alert('Error', 'Failed to encrypt or share the keyshare.'); } }; diff --git a/utils.js b/utils.js index c4f2a0b..7e7944a 100644 --- a/utils.js +++ b/utils.js @@ -539,3 +539,56 @@ export const getKeyshareLabel = keyshare => { // Fallback: return empty string return ''; }; + +/** + * Encode send bitcoin data into QR code format + * Format: ||| + * @param {string} toAddress - Bitcoin address to send to + * @param {string|number} amountSats - Amount in satoshis + * @param {string|number} feeSats - Fee in satoshis + * @param {string} spendingHash - Spending hash (can be empty) + * @returns {string} - Encoded QR data string + */ +export const encodeSendBitcoinQR = (toAddress, amountSats, feeSats, spendingHash = '') => { + const amount = typeof amountSats === 'string' ? amountSats : amountSats.toString(); + const fee = typeof feeSats === 'string' ? feeSats : feeSats.toString(); + return `${toAddress}|${amount}|${fee}|${spendingHash || ''}`; +}; + +/** + * Decode send bitcoin data from QR code format + * Format: ||| + * @param {string} qrData - QR code data string + * @returns {Object|null} - Decoded data object or null if invalid + */ +export const decodeSendBitcoinQR = (qrData) => { + if (!qrData || typeof qrData !== 'string') { + return null; + } + + const parts = qrData.split('|'); + if (parts.length < 3 || parts.length > 4) { + return null; + } + + const [toAddress, amountSats, feeSats, spendingHash = ''] = parts; + + // Validate address is not empty + if (!toAddress || toAddress.trim() === '') { + return null; + } + + // Validate amounts are valid numbers + const amount = parseInt(amountSats, 10); + const fee = parseInt(feeSats, 10); + if (isNaN(amount) || isNaN(fee) || amount < 0 || fee < 0) { + return null; + } + + return { + toAddress: toAddress.trim(), + amountSats: amount.toString(), + feeSats: fee.toString(), + spendingHash: spendingHash || '', + }; +};