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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,6 @@ vendor/bundle/
# PR documentation (local only, not for git)
PR_SUMMARY.md
PR_README.md

# third_party
third_party/
122 changes: 70 additions & 52 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
NativeEventEmitter,
Platform,
DeviceEventEmitter,
View,
StyleSheet,
} from 'react-native';
import WalletSettings from './screens/WalletSettings';
import {NativeModules} from 'react-native';
Expand Down Expand Up @@ -313,58 +315,67 @@ const App = () => {
<ThemeProvider>
<UserProvider>
<WalletProvider>
<NavigationContainer>
<Stack.Navigator
initialRouteName={initialRoute}
screenOptions={{
headerShown: false,
}}>
<Stack.Screen
name="PSBT"
component={PSBTScreen}
options={{
headerShown: true,
headerLeft: () => null,
}}
/>
<Stack.Screen
name="Home"
component={WalletHome}
options={{
headerShown: true,
headerLeft: () => null,
}}
/>
<Stack.Screen
name="Welcome"
component={ShowcaseScreen}
options={{
headerShown: true,
}}
/>
<Stack.Screen
name="Settings"
component={WalletSettings}
options={{
headerShown: true,
}}
/>
<Stack.Screen
name="Devices Pairing"
component={MobilesPairing}
options={{
headerShown: true,
}}
/>
<Stack.Screen
name="Nostr Connect"
component={MobileNostrPairing}
options={{
headerShown: true,
}}
/>
</Stack.Navigator>
</NavigationContainer>
<View style={styles.navigationContainer}>
<NavigationContainer>
<Stack.Navigator
initialRouteName={initialRoute}
screenOptions={{
headerShown: false,
contentStyle: {backgroundColor: '#ffffff'},
}}>
<Stack.Screen
name="PSBT"
component={PSBTScreen}
options={{
headerShown: true,
headerLeft: () => null,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
<Stack.Screen
name="Home"
component={WalletHome}
options={{
headerShown: true,
headerLeft: () => null,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
<Stack.Screen
name="Welcome"
component={ShowcaseScreen}
options={{
headerShown: true,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
<Stack.Screen
name="Settings"
component={WalletSettings}
options={{
headerShown: true,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
<Stack.Screen
name="Devices Pairing"
component={MobilesPairing}
options={{
headerShown: true,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
<Stack.Screen
name="Nostr Connect"
component={MobileNostrPairing}
options={{
headerShown: true,
contentStyle: {backgroundColor: '#ffffff'},
}}
/>
</Stack.Navigator>
</NavigationContainer>
</View>
</WalletProvider>
</UserProvider>
</ThemeProvider>
Expand All @@ -373,4 +384,11 @@ const App = () => {
);
};

const styles = StyleSheet.create({
navigationContainer: {
flex: 1,
backgroundColor: '#ffffff',
},
});

export default App;
File renamed without changes.
129 changes: 122 additions & 7 deletions BBMTLib/tss/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
14 changes: 7 additions & 7 deletions BBMTLib/tss/mpc_nostr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading