diff --git a/.github/workflows/bbmtlib-test.yml b/.github/workflows/bbmtlib-test.yml index e66707ed..5c84e9ed 100644 --- a/.github/workflows/bbmtlib-test.yml +++ b/.github/workflows/bbmtlib-test.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.24.2' + go-version: '1.25' cache-dependency-path: BBMTLib/go.sum - name: Verify Go version diff --git a/.gitignore b/.gitignore index 6a1d11d9..267585f7 100644 --- a/.gitignore +++ b/.gitignore @@ -143,6 +143,10 @@ build.log # Local gradle properties with secrets (never commit) android/gradle.properties.local +# IDE - Cursor / project-local +.cursor/ +mcps/ + # IDE - VSCode .vscode/ *.code-workspace diff --git a/App.tsx b/App.tsx index de8a91ea..e0dc4b4f 100644 --- a/App.tsx +++ b/App.tsx @@ -24,6 +24,8 @@ import { useSafeAreaInsets, } from 'react-native-safe-area-context'; import {initializeHaptics} from './utils'; +import database from './services/Database'; +import {runMigrationIfNeeded} from './services/LocalCacheMigration'; import ErrorBoundary from './components/ErrorBoundary'; import { Alert, @@ -517,6 +519,15 @@ const App = () => { useEffect(() => { initializeHaptics(); const checkWallet = async () => { + // Open SQLite database and run one-time LocalCache → SQLite migration. + // Wrapped in try/catch so a DB failure never blocks app startup. + try { + await database.open(); + dbg('App: SQLite database ready'); + await runMigrationIfNeeded(); + } catch (dbErr) { + dbg('App: Database init error (non-fatal):', dbErr); + } try { const keyshare = await EncryptedStorage.getItem('keyshare'); dbg('initializeApp keyshare found', !!keyshare); diff --git a/BBMTLib/Dockerfile.fips b/BBMTLib/Dockerfile.fips index b582e984..d1e18519 100644 --- a/BBMTLib/Dockerfile.fips +++ b/BBMTLib/Dockerfile.fips @@ -65,4 +65,4 @@ RUN go install golang.org/x/mobile/cmd/gomobile@v0.0.0-20250408133729-978277e7ea RUN gomobile init WORKDIR /workspace -# This stage has no CMD – we use it only for docker cp / export \ No newline at end of file +# This stage has no CMD – we use it only for docker cp / export diff --git a/BBMTLib/build.sh b/BBMTLib/build.sh index 2787acee..45e2a22d 100755 --- a/BBMTLib/build.sh +++ b/BBMTLib/build.sh @@ -12,9 +12,12 @@ YELLOW="\033[33m" info() { echo -e "${BOLD}${GREEN}==>${RESET} ${BOLD}$1${RESET}"; } warn() { echo -e "${BOLD}${YELLOW}Warning:${RESET} $1"; } -info "Starting BoldWallet TSS gomobile build (FIPS-aware, Go 1.25+)" +info "Starting BoldWallet TSS gomobile build (FIPS-aware, Go 1.24+)" # --- 1. Environment Checks --- +# Note: Go 1.25's tagged-pointer runtime can trigger "fatal error: taggedPointerPack" +# in some container/virtualized environments. For FIPS Android build, use +# fips-android.sh (Docker) which uses Go 1.24.x, or install Go 1.24.x on the host. echo "Go environment:" go version @@ -107,10 +110,13 @@ export GOFLAGS="-mod=mod" # Run Bind # Note: -androidapi 21 is the standard min version -gomobile bind -v -target=android -androidapi 21 -o tss.aar github.com/BoldBitcoinWallet/BBMTLib/tss +# Android 15 requires 16 KB page size support. Go 1.23+ supports it, but we explicitly +# set the max-page-size for the linker to ensure libgojni.so is compliant. +gomobile bind -v -target=android -androidapi 21 -ldflags="-extldflags=-Wl,-z,max-page-size=16384" -o tss.aar github.com/BoldBitcoinWallet/BBMTLib/tss # Copy Artifacts if [[ -d "../android/app/libs" ]]; then + # Run go mod tidy again at the end to ensure go.mod/go.sum are clean info "Copying Android artifacts..." cp -v tss.aar ../android/app/libs/tss.aar || warn "Copy tss.aar failed" echo "✓ tss.aar copied to ../android/app/libs/tss.aar" @@ -148,10 +154,7 @@ else info "Not running on macOS → Skipping iOS/macOS targets" fi -# --- 5. Cleanup --- - -info "Finalizing..." -# Run go mod tidy again at the end to ensure go.mod/go.sum are clean -go mod tidy - -info "Build complete!" \ No newline at end of file +info "Tidying dependencies..." +go mod tidy || warn "go mod tidy failed" + +info "Build complete!" diff --git a/BBMTLib/fips-android.sh b/BBMTLib/fips-android.sh index 394cc3d4..b93d24d6 100755 --- a/BBMTLib/fips-android.sh +++ b/BBMTLib/fips-android.sh @@ -1,5 +1,11 @@ +#!/bin/bash +set -euo pipefail + +# Always run from the BBMTLib directory regardless of where the script is called from. +cd "$(dirname "$0")" + # Dockerfile.fips -docker buildx build --platform linux/amd64 -f Dockerfile.fips -t boldwallet-builder:fips . +docker buildx build --load --platform linux/amd64 -f Dockerfile.fips -t boldwallet-builder:fips . # Generate lib docker run --rm \ diff --git a/BBMTLib/tss/btc.go b/BBMTLib/tss/btc.go index bd4d04b2..dfdc69d5 100644 --- a/BBMTLib/tss/btc.go +++ b/BBMTLib/tss/btc.go @@ -36,6 +36,15 @@ type UTXO struct { } `json:"status,omitempty"` // Status is optional, includes both confirmed and unconfirmed UTXOs } +// UTXOWithPath extends UTXO with derivation path and scriptpubkey for HD wallets (per-input signing). +// Scriptpubkey (hex) is optional: when present, FetchUTXODetails is skipped during signing, +// removing the last network call from the MPC signing loop. +type UTXOWithPath struct { + UTXO + DerivationPath string `json:"derivation_path,omitempty"` + Scriptpubkey string `json:"scriptpubkey,omitempty"` +} + var _btc_net = "testnet3" // default to testnet var _api_url = "https://mempool.space/testnet/api" var _api_urls = []string{"https://mempool.space/api", "https://benpool.space/api"} @@ -275,6 +284,17 @@ func RecommendedFees(feeType string) (int, error) { return 0, errors.New("failed to get fees") } +// ComputeTxId returns the txid (reversed double-SHA256 of serialized tx) for a raw tx hex. +// Used by the app to name the shared file before broadcasting. +func ComputeTxId(rawTxHex string) (string, error) { + rawTx, err := hex.DecodeString(rawTxHex) + if err != nil { + return "", fmt.Errorf("invalid raw tx hex: %w", err) + } + hash := chainhash.DoubleHashH(rawTx) + return hash.String(), nil +} + func PostTx(rawTxHex string) (string, error) { const maxRetries = 4 var lastErr error @@ -376,6 +396,95 @@ func SelectUTXOs(utxos []UTXO, totalAmount int64, strategy string) (result []UTX return selected, totalSelected, nil } +// utxoWithPathJSON is used for JSON unmarshaling from RN (supports both derivation_path and derivationPath). +type utxoWithPathJSON struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Value int64 `json:"value"` + Path string `json:"derivation_path"` + PathAlt string `json:"derivationPath"` + Address string `json:"address"` // optional, for fee estimation fallback + Scriptpubkey string `json:"scriptpubkey"` // hex locking script; when set avoids FetchUTXODetails during signing +} + +func (u *utxoWithPathJSON) toUTXOWithPath() UTXOWithPath { + path := u.Path + if path == "" { + path = u.PathAlt + } + return UTXOWithPath{ + UTXO: UTXO{TxID: u.TxID, Vout: u.Vout, Value: u.Value}, + DerivationPath: path, + Scriptpubkey: u.Scriptpubkey, + } +} + +// SelectUTXOsWithPaths selects UTXOs from a pool with per-UTXO derivation paths. +func SelectUTXOsWithPaths(utxos []UTXOWithPath, totalAmount int64, strategy string) (result []UTXOWithPath, totalSelectedResult int64, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in SelectUTXOsWithPaths: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic) selecting UTXOs: %v", r) + result = nil + totalSelectedResult = 0 + } + }() + + // Sort by (TxID, Vout) first for determinism, then by strategy + sort.Slice(utxos, func(i, j int) bool { + if utxos[i].TxID != utxos[j].TxID { + return utxos[i].TxID < utxos[j].TxID + } + if utxos[i].Vout != utxos[j].Vout { + return utxos[i].Vout < utxos[j].Vout + } + return false + }) + switch strategy { + case "smallest": + sort.Slice(utxos, func(i, j int) bool { return utxos[i].Value < utxos[j].Value }) + case "largest": + sort.Slice(utxos, func(i, j int) bool { return utxos[i].Value > utxos[j].Value }) + default: + sort.Slice(utxos, func(i, j int) bool { return utxos[i].Value > utxos[j].Value }) + } + + var selected []UTXOWithPath + var totalSelected int64 + for _, utxo := range utxos { + Logf("Selecting UTXO: %s vout=%d value=%d path=%s", utxo.TxID, utxo.Vout, utxo.Value, utxo.DerivationPath) + selected = append(selected, utxo) + totalSelected += utxo.Value + if totalSelected >= totalAmount { + break + } + } + + if totalSelected < totalAmount { + return nil, 0, fmt.Errorf("insufficient funds: needed %d, got %d", totalAmount, totalSelected) + } + Logf("SelectUTXOsWithPaths: selected %d UTXOs, total %d", len(selected), totalSelected) + return selected, totalSelected, nil +} + +// parseUTXOsWithPathsJSON parses JSON array of UTXOs with paths from RN. +func parseUTXOsWithPathsJSON(jsonStr string) ([]UTXOWithPath, error) { + if jsonStr == "" { + return nil, fmt.Errorf("empty utxos JSON") + } + var raw []utxoWithPathJSON + if err := json.Unmarshal([]byte(jsonStr), &raw); err != nil { + return nil, fmt.Errorf("failed to parse utxos JSON: %w", err) + } + out := make([]UTXOWithPath, 0, len(raw)) + for _, u := range raw { + out = append(out, u.toUTXOWithPath()) + } + return out, nil +} + func wifECDSASign(senderWIF string, data []byte) []byte { wifKey, err := btcutil.DecodeWIF(senderWIF) if err != nil { @@ -418,16 +527,26 @@ func SpendingHash(senderAddress, receiverAddress string, amountSatoshi int64) (r Logln("BBMTLog", "invoking SpendingHash...") - // Fetch UTXOs (same as EstimateFees) + // Fetch UTXOs (same as EstimateFees), but be conservative: + // if there are no UTXOs or selection fails, return an empty hash instead + // of treating it as a hard error. The caller already validates wallet + // balance (potentially using HD/multi-path), so single-address insufficiency + // here should not block UX. utxos, err := FetchUTXOs(senderAddress) if err != nil { - return "", fmt.Errorf("failed to fetch UTXOs: %w", err) + Logf("SpendingHash: failed to fetch UTXOs for %s: %v", senderAddress, err) + return "", nil + } + if len(utxos) == 0 { + Logf("SpendingHash: no UTXOs for %s, returning empty hash", senderAddress) + return "", nil } // Select UTXOs using the same strategy as EstimateFees selectedUTXOs, _, err := SelectUTXOs(utxos, amountSatoshi, "smallest") if err != nil { - return "", err + Logf("SpendingHash: SelectUTXOs error for %s amount=%d: %v", senderAddress, amountSatoshi, err) + return "", nil } // Sort selected UTXOs deterministically by TxID, then Vout @@ -457,6 +576,68 @@ func SpendingHash(senderAddress, receiverAddress string, amountSatoshi int64) (r return hashHex, nil } +// SpendingHashWithUTXOs is the multi-path counterpart of SpendingHash. +// Instead of fetching UTXOs from a single address, it accepts a pre-fetched +// pool (JSON-encoded []utxoWithPathJSON) that covers all HD addresses. +// It selects UTXOs using the same "smallest-first" strategy and returns a +// deterministic SHA-256 hex over "txid:vout" pairs - identical across +// co-signing devices as long as they supply the same UTXO set. +func SpendingHashWithUTXOs(utxosWithPathsJSON, receiverAddress, amountSatoshiStr string) (result string, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in SpendingHashWithUTXOs: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic): %v", r) + result = "" + } + }() + + Logln("BBMTLog", "invoking SpendingHashWithUTXOs...") + + amountSatoshi, parseErr := strconv.ParseInt(amountSatoshiStr, 10, 64) + if parseErr != nil { + Logf("SpendingHashWithUTXOs: invalid amount %q: %v", amountSatoshiStr, parseErr) + return "", fmt.Errorf("invalid amount: %w", parseErr) + } + + utxos, err := parseUTXOsWithPathsJSON(utxosWithPathsJSON) + if err != nil { + Logf("SpendingHashWithUTXOs: failed to parse utxosWithPathsJSON: %v", err) + return "", nil + } + if len(utxos) == 0 { + Logf("SpendingHashWithUTXOs: no UTXOs provided, returning empty hash") + return "", nil + } + + selected, _, err := SelectUTXOsWithPaths(utxos, amountSatoshi, "smallest") + if err != nil { + Logf("SpendingHashWithUTXOs: SelectUTXOsWithPaths failed amount=%d: %v", amountSatoshi, err) + return "", nil + } + + // Sort selected UTXOs deterministically by TxID, then Vout + sort.Slice(selected, func(i, j int) bool { + if selected[i].TxID != selected[j].TxID { + return selected[i].TxID < selected[j].TxID + } + return selected[i].Vout < selected[j].Vout + }) + + var utxoStrings []string + for _, u := range selected { + utxoStrings = append(utxoStrings, fmt.Sprintf("%s:%d", u.TxID, u.Vout)) + } + utxoData := strings.Join(utxoStrings, ",") + + hash := sha256.Sum256([]byte(utxoData)) + hashHex := hex.EncodeToString(hash[:]) + + Logf("SpendingHashWithUTXOs: selected %d UTXOs, hash: %s", len(selected), hashHex) + return hashHex, nil +} + func EstimateFees(senderAddress, receiverAddress string, amountSatoshi int64) (result string, err error) { defer func() { if r := recover(); r != nil { @@ -520,6 +701,94 @@ func EstimateFees(senderAddress, receiverAddress string, amountSatoshi int64) (r return strconv.FormatInt(_fee, 10), nil } +// EstimateFeeWithUTXOs estimates fees using a pre-fetched UTXO pool with paths (multi-path send). +// utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +// changeAddress: used for change output size estimation (e.g. next HD change address) +func EstimateFeeWithUTXOs(utxosWithPathsJSON, receiverAddress, amountSatoshiStr, changeAddress string) (result string, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in EstimateFeeWithUTXOs: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic): %v", r) + result = "" + } + }() + + Logln("BBMTLog", "invoking EstimateFeeWithUTXOs...") + Logf("GoLog: EstimateFeeWithUTXOs input receiverAddress=%s amountSatoshi=%s changeAddress=%s", receiverAddress, amountSatoshiStr, changeAddress) + Logf("GoLog: Current network: %s, API: %s", _btc_net, _api_url) + + amountSatoshi, parseErr := strconv.ParseInt(amountSatoshiStr, 10, 64) + if parseErr != nil { + Logf("GoLog: EstimateFeeWithUTXOs - invalid amount %q: %v", amountSatoshiStr, parseErr) + return "", fmt.Errorf("invalid amount: %w", parseErr) + } + + utxos, err := parseUTXOsWithPathsJSON(utxosWithPathsJSON) + if err != nil { + Logf("GoLog: EstimateFeeWithUTXOs - failed to parse utxosWithPathsJSON: %v", err) + return "", err + } + if len(utxos) == 0 { + Logf("GoLog: EstimateFeeWithUTXOs - utxosWithPathsJSON parsed but no UTXOs available") + return "", fmt.Errorf("no UTXOs available. Please ensure you have confirmed transactions before sending") + } + + // Use changeAddress for fee estimation (change output type) + addrForFee := changeAddress + if addrForFee == "" && len(utxos) > 0 { + // Fallback: re-parse to get address from first item (parseUTXOsWithPathsJSON doesn't store it) + var raw []utxoWithPathJSON + if json.Unmarshal([]byte(utxosWithPathsJSON), &raw) == nil && len(raw) > 0 && raw[0].Address != "" { + addrForFee = raw[0].Address + } + } + if addrForFee == "" { + Logf("GoLog: EstimateFeeWithUTXOs - missing changeAddress and unable to infer from utxosWithPathsJSON") + return "", fmt.Errorf("changeAddress required for multi-path fee estimation") + } + + Logf("GoLog: EstimateFeeWithUTXOs - using addrForFee=%s and %d candidate UTXOs", addrForFee, len(utxos)) + + // First iteration: select for amount only + selected, _, err := SelectUTXOsWithPaths(utxos, amountSatoshi, "smallest") + if err != nil { + Logf("GoLog: EstimateFeeWithUTXOs - failed SelectUTXOsWithPaths for amount=%d: %v", amountSatoshi, err) + return "", err + } + selectedUTXOs := make([]UTXO, len(selected)) + for i := range selected { + selectedUTXOs[i] = selected[i].UTXO + } + + _fee, _err := calculateFees(addrForFee, selectedUTXOs, amountSatoshi, receiverAddress) + if _err != nil { + Logf("GoLog: EstimateFeeWithUTXOs - calculateFees (first pass) error: %v", _err) + return "", _err + } + Logf("GoLog: EstimateFeeWithUTXOs - first pass fee=%d (amount=%d, selectedUTXOs=%d)", _fee, amountSatoshi, len(selectedUTXOs)) + + // Second iteration: re-select for amount+fee + selected, _, err = SelectUTXOsWithPaths(utxos, amountSatoshi+_fee, "smallest") + if err != nil { + Logf("GoLog: Could not select UTXOs for amount+fee=%d, using original estimate: %v", amountSatoshi+_fee, err) + return strconv.FormatInt(_fee, 10), nil + } + selectedUTXOs = make([]UTXO, len(selected)) + for i := range selected { + selectedUTXOs[i] = selected[i].UTXO + } + + _fee, _err = calculateFees(addrForFee, selectedUTXOs, amountSatoshi, receiverAddress) + if _err != nil { + Logf("GoLog: EstimateFeeWithUTXOs - calculateFees (second pass) error: %v", _err) + return "", _err + } + Logf("GoLog: EstimateFeeWithUTXOs: final fee %d (amount=%d, selectedUTXOs=%d)", _fee, amountSatoshi, len(selectedUTXOs)) + return strconv.FormatInt(_fee, 10), nil +} + func SendBitcoin(wifKey, publicKey, senderAddress, receiverAddress string, preview, amountSatoshi int64) (result string, err error) { defer func() { if r := recover(); r != nil { @@ -1214,16 +1483,230 @@ func MpcSendBTC( } rawTx := hex.EncodeToString(signedTx.Bytes()) - Logln("Raw Transaction:", rawTx) + Logln("Raw Transaction (signed, not broadcast)") + mpcHook("signed", session, utxoSession, utxoIndex, utxoCount, true) + return rawTx, nil +} - txid, err := PostTx(rawTx) +// MpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +// utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +// changeAddress: HD change address for change output (required) +func MpcSendBTCWithUTXOs( + server, key, partiesCSV, session, sessionKey, encKey, decKey, keyshare string, + publicKey, receiverAddress, amountSatoshiStr, estimatedFeeStr, utxosWithPathsJSON, changeAddress string, +) (result string, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in MpcSendBTCWithUTXOs: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic): %v", r) + result = "" + } + }() + + amountSatoshi, parseErr := strconv.ParseInt(amountSatoshiStr, 10, 64) + if parseErr != nil { + return "", fmt.Errorf("invalid amountSatoshi %q: %w", amountSatoshiStr, parseErr) + } + estimatedFee, parseErr := strconv.ParseInt(estimatedFeeStr, 10, 64) + if parseErr != nil { + return "", fmt.Errorf("invalid estimatedFee %q: %w", estimatedFeeStr, parseErr) + } + + utxos, err := parseUTXOsWithPathsJSON(utxosWithPathsJSON) if err != nil { - Logf("Error broadcasting transaction: %v", err) - return "", fmt.Errorf("failed to broadcast transaction: %w", err) + return "", err } - mpcHook("txid:"+txid, session, utxoSession, utxoIndex, utxoCount, true) - Logf("Transaction broadcasted successfully, txid: %s", txid) - return txid, nil + if len(utxos) == 0 { + return "", fmt.Errorf("no UTXOs available. Please ensure you have confirmed transactions before sending") + } + + var ks struct { + PubKey string `json:"pub_key"` + ChainCodeHex string `json:"chain_code_hex"` + } + if err := json.Unmarshal([]byte(keyshare), &ks); err != nil || ks.PubKey == "" || ks.ChainCodeHex == "" { + return "", fmt.Errorf("invalid keyshare: need pub_key and chain_code_hex") + } + + selectedUTXOs, totalAmount, err := SelectUTXOsWithPaths(utxos, amountSatoshi+estimatedFee, "smallest") + if err != nil { + return "", err + } + + params := &chaincfg.TestNet3Params + if _btc_net == "mainnet" { + params = &chaincfg.MainNetParams + } + toAddr, err := btcutil.DecodeAddress(receiverAddress, params) + if err != nil { + return "", fmt.Errorf("failed to decode receiver address: %w", err) + } + changeAddr, err := btcutil.DecodeAddress(changeAddress, params) + if err != nil { + return "", fmt.Errorf("failed to decode change address: %w", err) + } + + tx := wire.NewMsgTx(wire.TxVersion) + for _, utxo := range selectedUTXOs { + hash, err := chainhash.NewHashFromStr(utxo.TxID) + if err != nil { + return "", fmt.Errorf("invalid UTXO TxID %s: %w", utxo.TxID, err) + } + txIn := wire.NewTxIn(wire.NewOutPoint(hash, utxo.Vout), nil, nil) + txIn.Sequence = 0xfffffffd + tx.AddTxIn(txIn) + } + + if totalAmount < amountSatoshi+estimatedFee { + return "", fmt.Errorf("insufficient funds: available %d, needed %d", totalAmount, amountSatoshi+estimatedFee) + } + + pkScript, _ := txscript.PayToAddrScript(toAddr) + tx.AddTxOut(wire.NewTxOut(amountSatoshi, pkScript)) + + changeAmount := totalAmount - amountSatoshi - estimatedFee + if changeAmount > 546 { + changePkScript, _ := txscript.PayToAddrScript(changeAddr) + tx.AddTxOut(wire.NewTxOut(changeAmount, changePkScript)) + } + + // Build prevOuts map from inline scriptpubkey (no network call). + // Falls back to FetchUTXODetails only when scriptpubkey was not supplied by the caller. + prevOuts := make(map[wire.OutPoint]*wire.TxOut) + for _, utxo := range selectedUTXOs { + var txOut *wire.TxOut + if utxo.Scriptpubkey != "" { + sb, spkErr := hex.DecodeString(utxo.Scriptpubkey) + if spkErr != nil || len(sb) == 0 { + return "", fmt.Errorf("invalid scriptpubkey for %s:%d", utxo.TxID, utxo.Vout) + } + txOut = &wire.TxOut{PkScript: sb, Value: utxo.Value} + } else { + var fetchErr error + txOut, _, fetchErr = FetchUTXODetails(utxo.TxID, utxo.Vout) + if fetchErr != nil { + return "", fmt.Errorf("failed to fetch UTXO details for %s:%d: %w", utxo.TxID, utxo.Vout, fetchErr) + } + } + hash, _ := chainhash.NewHashFromStr(utxo.TxID) + prevOuts[wire.OutPoint{Hash: *hash, Index: utxo.Vout}] = txOut + } + prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts) + + utxoCount := len(selectedUTXOs) + for i, utxo := range selectedUTXOs { + derivePath := utxo.DerivationPath + if derivePath == "" { + return "", fmt.Errorf("UTXO %d missing derivation path", i) + } + derivedPubHex, err := GetDerivedPubKey(ks.PubKey, ks.ChainCodeHex, derivePath, false) + if err != nil { + return "", fmt.Errorf("failed to derive pubkey for input %d: %w", i, err) + } + pubKeyBytes, err := hex.DecodeString(derivedPubHex) + if err != nil { + return "", fmt.Errorf("invalid derived pubkey for input %d: %w", i, err) + } + + utxoSession := fmt.Sprintf("%s%d", session, i) + // Re-use the already-resolved prevout (no second network call per input). + outpointHash, _ := chainhash.NewHashFromStr(utxo.TxID) + txOut := prevOuts[wire.OutPoint{Hash: *outpointHash, Index: utxo.Vout}] + isWitness := txscript.IsWitnessProgram(txOut.PkScript) + hashCache := txscript.NewTxSigHashes(tx, prevOutFetcher) + + var sigHash []byte + var isP2SHP2WPKH bool + if isWitness { + if txscript.IsPayToWitnessPubKeyHash(txOut.PkScript) { + sigHash, err = txscript.CalcWitnessSigHash(txOut.PkScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } else if txscript.IsPayToTaproot(txOut.PkScript) { + return "", fmt.Errorf("taproot (P2TR) inputs are not supported") + } else { + sigHash, err = txscript.CalcWitnessSigHash(txOut.PkScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } + } else { + if txscript.IsPayToPubKeyHash(txOut.PkScript) { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } else if txscript.IsPayToScriptHash(txOut.PkScript) { + pubKeyHash := btcutil.Hash160(pubKeyBytes) + redeemScript := make([]byte, 22) + redeemScript[0], redeemScript[1] = 0x00, 0x14 + copy(redeemScript[2:], pubKeyHash) + scriptHash := btcutil.Hash160(redeemScript) + expectedP2SH := make([]byte, 23) + expectedP2SH[0], expectedP2SH[1], expectedP2SH[22] = 0xa9, 0x14, 0x87 + copy(expectedP2SH[2:22], scriptHash) + if bytes.Equal(txOut.PkScript, expectedP2SH) { + isP2SHP2WPKH = true + sigHash, err = txscript.CalcWitnessSigHash(redeemScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } else { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } + } else { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } + } + if err != nil { + return "", fmt.Errorf("failed to calc sighash for input %d: %w", i, err) + } + + sighashBase64 := base64.StdEncoding.EncodeToString(sigHash) + mpcHook("joining keysign", session, utxoSession, i+1, utxoCount, false) + sigJSON, err := JoinKeysign(server, key, partiesCSV, utxoSession, sessionKey, encKey, decKey, keyshare, derivePath, sighashBase64) + if err != nil { + return "", fmt.Errorf("failed to sign input %d: %w", i, err) + } + var sig KeysignResponse + if err := json.Unmarshal([]byte(sigJSON), &sig); err != nil { + return "", fmt.Errorf("failed to parse signature for input %d: %w", i, err) + } + signature, err := hex.DecodeString(sig.DerSignature) + if err != nil { + return "", fmt.Errorf("failed to decode signature for input %d: %w", i, err) + } + sigWithHashType := append(signature, byte(txscript.SigHashAll)) + + if isWitness { + tx.TxIn[i].Witness = wire.TxWitness{sigWithHashType, pubKeyBytes} + tx.TxIn[i].SignatureScript = nil + } else if isP2SHP2WPKH { + redeemScript := make([]byte, 22) + redeemScript[0], redeemScript[1] = 0x00, 0x14 + copy(redeemScript[2:], btcutil.Hash160(pubKeyBytes)) + builder := txscript.NewScriptBuilder() + builder.AddData(redeemScript) + canonical, _ := builder.Script() + tx.TxIn[i].SignatureScript = canonical + tx.TxIn[i].Witness = wire.TxWitness{sigWithHashType, pubKeyBytes} + } else { + builder := txscript.NewScriptBuilder() + builder.AddData(sigWithHashType) + builder.AddData(pubKeyBytes) + scriptSig, _ := builder.Script() + tx.TxIn[i].SignatureScript = scriptSig + tx.TxIn[i].Witness = nil + } + + vm, err := txscript.NewEngine(txOut.PkScript, tx, i, txscript.StandardVerifyFlags, nil, hashCache, txOut.Value, prevOutFetcher) + if err != nil { + return "", fmt.Errorf("script engine for input %d: %w", i, err) + } + if err := vm.Execute(); err != nil { + return "", fmt.Errorf("script validation failed for input %d: %w", i, err) + } + } + + var signedTx bytes.Buffer + if err := tx.Serialize(&signedTx); err != nil { + return "", fmt.Errorf("failed to serialize transaction: %w", err) + } + rawTx := hex.EncodeToString(signedTx.Bytes()) + Logln("Raw Transaction (signed, not broadcast)") + mpcHook("signed", session, "", utxoCount, utxoCount, true) + return rawTx, nil } func DecodeAddress(address string) (result string, err error) { @@ -1847,17 +2330,13 @@ func ReplaceTransaction( } } - // Serialize and broadcast + // Serialize and return raw tx (app broadcasts via PostTx when user taps Broadcast) var signedTx bytes.Buffer if err := tx.Serialize(&signedTx); err != nil { return "", fmt.Errorf("failed to serialize transaction: %w", err) } rawTx := hex.EncodeToString(signedTx.Bytes()) - txid, err := PostTx(rawTx) - if err != nil { - return "", fmt.Errorf("failed to broadcast transaction: %w", err) - } - - return txid, nil + Logln("Raw Transaction (signed, not broadcast)") + return rawTx, nil } diff --git a/BBMTLib/tss/cancel.go b/BBMTLib/tss/cancel.go new file mode 100644 index 00000000..659dedca --- /dev/null +++ b/BBMTLib/tss/cancel.go @@ -0,0 +1,138 @@ +package tss + +import ( + "context" + "fmt" + "strings" + "sync" + "time" +) + +// --------------------------------------------------------------------------- +// MPC session cancellation (mobile-triggered) +// --------------------------------------------------------------------------- + +// We key cancellation by "sessionID prefix" because multi-input signing uses +// derived session IDs like: sessionID + strconv(i). Mobile only knows the base. + +var cancelMu sync.Mutex + +// Per-session cancel channel. Closed means "cancel requested". +var cancelChBySession = map[string]chan struct{}{} + +// Optional context cancel funcs (used by nostrtransport / other ctx-based loops). +var ctxCancelBySession = map[string]context.CancelFunc{} + +// Prefixes that have been cancelled (so future derived session IDs start cancelled). +var cancelledPrefixes = map[string]time.Time{} + +const cancelPrefixTTL = 15 * time.Minute + +func pruneCancelledPrefixesLocked(now time.Time) { + for p, t := range cancelledPrefixes { + if now.Sub(t) > cancelPrefixTTL { + delete(cancelledPrefixes, p) + } + } +} + +func isPrefixCancelledLocked(sessionID string) bool { + for p := range cancelledPrefixes { + if strings.HasPrefix(sessionID, p) { + return true + } + } + return false +} + +func getOrCreateCancelCh(sessionID string) chan struct{} { + cancelMu.Lock() + defer cancelMu.Unlock() + + now := time.Now() + pruneCancelledPrefixesLocked(now) + + if ch, ok := cancelChBySession[sessionID]; ok { + return ch + } + + ch := make(chan struct{}) + cancelChBySession[sessionID] = ch + + // If a prefix cancellation already happened, start cancelled immediately. + if isPrefixCancelledLocked(sessionID) { + close(ch) + } + + return ch +} + +func sessionIsCancelled(sessionID string) bool { + ch := getOrCreateCancelCh(sessionID) + select { + case <-ch: + return true + default: + return false + } +} + +func registerCtxCancel(sessionID string, cancel context.CancelFunc) { + if cancel == nil { + return + } + cancelMu.Lock() + defer cancelMu.Unlock() + ctxCancelBySession[sessionID] = cancel +} + +func unregisterCtxCancel(sessionID string) { + cancelMu.Lock() + defer cancelMu.Unlock() + delete(ctxCancelBySession, sessionID) +} + +func cleanupCancelState(sessionID string) { + cancelMu.Lock() + defer cancelMu.Unlock() + delete(cancelChBySession, sessionID) + delete(ctxCancelBySession, sessionID) +} + +// CancelMpcSession requests cancellation for a given base session ID. +// It cancels any currently-running derived sessions (prefix match) and ensures +// any future derived sessions start cancelled. +// +// Exposed to mobile via gomobile bind. +func CancelMpcSession(sessionID string) (string, error) { + if sessionID == "" { + return "", fmt.Errorf("sessionID is empty") + } + + cancelMu.Lock() + now := time.Now() + pruneCancelledPrefixesLocked(now) + cancelledPrefixes[sessionID] = now + + // Cancel all known sessions with this prefix. + for sid, ch := range cancelChBySession { + if strings.HasPrefix(sid, sessionID) { + select { + case <-ch: + // already closed + default: + close(ch) + } + } + } + for sid, cancel := range ctxCancelBySession { + if strings.HasPrefix(sid, sessionID) { + // Best-effort; cancel should be idempotent. + cancel() + // Keep entry; callers may still unregister on exit. + } + } + cancelMu.Unlock() + + return "ok", nil +} diff --git a/BBMTLib/tss/interfaces.go b/BBMTLib/tss/interfaces.go index e770153b..f67377da 100644 --- a/BBMTLib/tss/interfaces.go +++ b/BBMTLib/tss/interfaces.go @@ -27,6 +27,9 @@ type ServiceImpl struct { messenger Messenger stateAccessor LocalStateAccessor inboundMessageCh chan string + // cancelCh is closed when the mobile app requests cancellation for the + // active MPC session. nil means "not cancellable". + cancelCh <-chan struct{} } type MessageFromTss struct { diff --git a/BBMTLib/tss/mpc.go b/BBMTLib/tss/mpc.go index 74f5c3d9..3ecd64d6 100644 --- a/BBMTLib/tss/mpc.go +++ b/BBMTLib/tss/mpc.go @@ -2,6 +2,7 @@ package tss import ( "bytes" + "context" "crypto/aes" "crypto/cipher" "crypto/md5" @@ -314,6 +315,10 @@ func JoinKeysign(server, key, partiesCSV, session, sessionKey, encKey, decKey, k }() parties := strings.Split(partiesCSV, ",") + // Ensure the session has a cancel channel (prefix-cancellable) and clean it up at end. + cancelCh := getOrCreateCancelCh(session) + defer cleanupCancelState(session) + if len(sessionKey) > 0 && (len(encKey) > 0 || len(decKey) > 0) { return "", fmt.Errorf("either a session key, either enc/dec keys") } @@ -336,6 +341,10 @@ func JoinKeysign(server, key, partiesCSV, session, sessionKey, encKey, decKey, k status.Info = "start joinSession" setStatus(session, status) + if sessionIsCancelled(session) { + return "", context.Canceled + } + if err := joinSession(server, session, key); err != nil { return "", fmt.Errorf("fail to register session: %w", err) } @@ -345,6 +354,10 @@ func JoinKeysign(server, key, partiesCSV, session, sessionKey, encKey, decKey, k status.Info = "waiting parties" setStatus(session, status) + if sessionIsCancelled(session) { + return "", context.Canceled + } + if err := awaitJoiners(parties, server, session); err != nil { Logln("BBMTLog", "fail to wait all parties", "error", err) return "", fmt.Errorf("fail to wait all parties: %w", err) @@ -374,12 +387,19 @@ func JoinKeysign(server, key, partiesCSV, session, sessionKey, encKey, decKey, k if err != nil { return "", fmt.Errorf("fail to create tss server: %w", err) } + // Wire cancellation signal into the signing loop. + tssServerImp.cancelCh = cancelCh endCh := make(chan struct{}) wg := &sync.WaitGroup{} wg.Add(1) Logln("BBMTLog", "downloadMessage active...") go downloadMessage(server, session, sessionKey, key, *tssServerImp, endCh, wg) Logln("BBMTLog", "start ECDSA keysign...") + if sessionIsCancelled(session) { + close(endCh) + wg.Wait() + return "", context.Canceled + } resp, err := tssServerImp.KeysignECDSA(&KeysignRequest{ PubKey: keyshare, MessageToSign: message, diff --git a/BBMTLib/tss/mpc_nostr.go b/BBMTLib/tss/mpc_nostr.go index 5427462f..57e7286e 100644 --- a/BBMTLib/tss/mpc_nostr.go +++ b/BBMTLib/tss/mpc_nostr.go @@ -468,7 +468,7 @@ func runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionF // Context for the pre-agreement phase // 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) + ctx, cancel := context.WithTimeout(getActiveNostrCtx(), 16*time.Second) defer cancel() // Channel to receive peer's message @@ -606,7 +606,10 @@ func NostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionFlag // - npubsSorted: Comma-separated sorted list of all party npubs (for sessionFlag calculation) // - balanceSats: Balance in satoshis (for sessionFlag calculation) // - amountSatoshi: Transaction amount in satoshis (for sessionFlag calculation) -func NostrMpcSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress string, amountSatoshi, estimatedFee int64) (result string, err error) { +// +// NostrMpcSendBTC performs a Nostr-based MPC Bitcoin transaction. +// changeAddress: when non-empty, change output is sent here (HD internal chain); otherwise to senderAddress. +func NostrMpcSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress string, amountSatoshi, estimatedFee int64, changeAddress string) (result string, err error) { defer func() { if r := recover(); r != nil { errMsg := fmt.Sprintf("PANIC in NostrMpcSendBTC: %v", r) @@ -616,14 +619,38 @@ func NostrMpcSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balance result = "" } }() + return runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress, amountSatoshi, estimatedFee, changeAddress) +} + +// NostrMpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +// utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +// changeAddress: HD change address for change output (required) +func NostrMpcSendBTCWithUTXOs(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, receiverAddress, amountSatoshiStr, estimatedFeeStr, utxosWithPathsJSON, changeAddress string) (result string, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in NostrMpcSendBTCWithUTXOs: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic): %v", r) + result = "" + } + }() - return runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress, amountSatoshi, estimatedFee) + amountSatoshi, parseErr := strconv.ParseInt(amountSatoshiStr, 10, 64) + if parseErr != nil { + return "", fmt.Errorf("invalid amountSatoshi %q: %w", amountSatoshiStr, parseErr) + } + estimatedFee, parseErr := strconv.ParseInt(estimatedFeeStr, 10, 64) + if parseErr != nil { + return "", fmt.Errorf("invalid estimatedFee %q: %w", estimatedFeeStr, parseErr) + } + return runNostrMpcSendBTCInternalWithUTXOs(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, receiverAddress, amountSatoshi, estimatedFee, utxosWithPathsJSON, changeAddress) } // runNostrMpcSendBTCInternal implements the Nostr-based MPC Bitcoin transaction. // This is analogous to MpcSendBTC but uses NostrJoinKeysign instead of JoinKeysign. // It performs pre-agreement internally to establish sessionID and unified fees. -func runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress string, amountSatoshi, estimatedFee int64) (result string, err error) { +func runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress string, amountSatoshi, estimatedFee int64, changeAddress string) (result string, err error) { defer func() { if r := recover(); r != nil { errMsg := fmt.Sprintf("PANIC in runNostrMpcSendBTCInternal: %v", r) @@ -633,6 +660,9 @@ func runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSort result = "" } }() + if changeAddress == "" { + changeAddress = senderAddress + } Logln("BBMTLog", "invoking NostrMpcSendBTC...") @@ -762,18 +792,28 @@ func runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSort tx.AddTxOut(wire.NewTxOut(amountSatoshi, pkScript)) Logf("Added recipient output: %d satoshis to %s", amountSatoshi, receiverAddress) - // Add change output if necessary + // Add change output if necessary (use changeAddress for HD internal chain when set) changeAmount := totalAmount - amountSatoshi - agreedFee mpcHook("calculating change amount", sessionID, utxoSession, utxoIndex, utxoCount, false) if changeAmount > 546 { - changePkScript, err := txscript.PayToAddrScript(fromAddr) + changeAddr := fromAddr + if changeAddress != "" && changeAddress != senderAddress { + decoded, errDecode := btcutil.DecodeAddress(changeAddress, params) + if errDecode != nil { + Logf("Error decoding change address %s: %v", changeAddress, errDecode) + return "", fmt.Errorf("failed to decode change address: %w", errDecode) + } + changeAddr = decoded + Logf("Using HD change address: %s", changeAddress) + } + changePkScript, err := txscript.PayToAddrScript(changeAddr) if err != nil { Logf("Error creating change script: %v", err) return "", fmt.Errorf("failed to create change script: %w", err) } tx.AddTxOut(wire.NewTxOut(changeAmount, changePkScript)) - Logf("Added change output: %d satoshis to %s", changeAmount, senderAddress) + Logf("Added change output: %d satoshis to %s", changeAmount, changeAddress) } // Create prevOutFetcher for all inputs (needed for SegWit) @@ -1091,16 +1131,235 @@ func runNostrMpcSendBTCInternal(relaysCSV, partyNsec, partiesNpubsCSV, npubsSort } rawTx := hex.EncodeToString(signedTx.Bytes()) - Logln("Raw Transaction:", rawTx) + Logln("Raw Transaction (signed, not broadcast)") + mpcHook("signed", sessionID, utxoSession, utxoIndex, utxoCount, true) + return rawTx, nil +} - txid, err := PostTx(rawTx) +// runNostrMpcSendBTCInternalWithUTXOs implements multi-path Nostr MPC send using pre-fetched UTXOs. +func runNostrMpcSendBTCInternalWithUTXOs(relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, receiverAddress string, amountSatoshi, estimatedFee int64, utxosWithPathsJSON, changeAddress string) (result string, err error) { + defer func() { + if r := recover(); r != nil { + errMsg := fmt.Sprintf("PANIC in runNostrMpcSendBTCInternalWithUTXOs: %v", r) + Logf("BBMTLog: %s", errMsg) + Logf("BBMTLog: Stack trace: %s", string(debug.Stack())) + err = fmt.Errorf("internal error (panic): %v", r) + result = "" + } + }() + + sessionFlag, err := Sha256(fmt.Sprintf("%s,%s,%d", npubsSorted, balanceSats, amountSatoshi)) + if err != nil { + return "", fmt.Errorf("failed to calculate sessionFlag: %w", err) + } + mpcHook("pre-agreement phase", sessionFlag, "", 0, 0, false) + preAgreement, err := runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionFlag, estimatedFee) + if err != nil { + return "", fmt.Errorf("pre-agreement failed: %w", err) + } + sessionID, err := Sha256(fmt.Sprintf("%s,%s,%d,%s", npubsSorted, balanceSats, amountSatoshi, preAgreement.fullNonce)) if err != nil { - Logf("Error broadcasting transaction: %v", err) - return "", fmt.Errorf("failed to broadcast transaction: %w", err) + return "", err + } + sessionKey, err := Sha256(fmt.Sprintf("%s,%s", npubsSorted, sessionID)) + if err != nil { + return "", err } - mpcHook("txid:"+txid, sessionID, utxoSession, utxoIndex, utxoCount, true) - Logf("Transaction broadcasted successfully, txid: %s", txid) - return txid, nil + agreedFee := preAgreement.averageFees + + utxos, err := parseUTXOsWithPathsJSON(utxosWithPathsJSON) + if err != nil { + return "", err + } + if len(utxos) == 0 { + return "", fmt.Errorf("no UTXOs available") + } + + var ks struct { + PubKey string `json:"pub_key"` + ChainCodeHex string `json:"chain_code_hex"` + } + if err := json.Unmarshal([]byte(keyshareJSON), &ks); err != nil || ks.PubKey == "" || ks.ChainCodeHex == "" { + return "", fmt.Errorf("invalid keyshare") + } + + selectedUTXOs, totalAmount, err := SelectUTXOsWithPaths(utxos, amountSatoshi+agreedFee, "smallest") + if err != nil { + return "", err + } + + params := &chaincfg.TestNet3Params + if _btc_net == "mainnet" { + params = &chaincfg.MainNetParams + } + toAddr, err := btcutil.DecodeAddress(receiverAddress, params) + if err != nil { + return "", fmt.Errorf("failed to decode receiver address: %w", err) + } + changeAddr, err := btcutil.DecodeAddress(changeAddress, params) + if err != nil { + return "", fmt.Errorf("failed to decode change address: %w", err) + } + + tx := wire.NewMsgTx(wire.TxVersion) + for _, utxo := range selectedUTXOs { + hash, err := chainhash.NewHashFromStr(utxo.TxID) + if err != nil { + return "", fmt.Errorf("invalid UTXO TxID: %w", err) + } + txIn := wire.NewTxIn(wire.NewOutPoint(hash, utxo.Vout), nil, nil) + txIn.Sequence = 0xfffffffd + tx.AddTxIn(txIn) + } + + if totalAmount < amountSatoshi+agreedFee { + return "", fmt.Errorf("insufficient funds") + } + + pkScript, _ := txscript.PayToAddrScript(toAddr) + tx.AddTxOut(wire.NewTxOut(amountSatoshi, pkScript)) + + changeAmount := totalAmount - amountSatoshi - agreedFee + if changeAmount > 546 { + changePkScript, _ := txscript.PayToAddrScript(changeAddr) + tx.AddTxOut(wire.NewTxOut(changeAmount, changePkScript)) + } + + // Build prevOuts map from inline scriptpubkey (no network call). + // Falls back to FetchUTXODetails only when scriptpubkey was not supplied by the caller. + prevOuts := make(map[wire.OutPoint]*wire.TxOut) + for _, utxo := range selectedUTXOs { + var txOut *wire.TxOut + if utxo.Scriptpubkey != "" { + sb, spkErr := hex.DecodeString(utxo.Scriptpubkey) + if spkErr != nil || len(sb) == 0 { + return "", fmt.Errorf("invalid scriptpubkey for %s:%d", utxo.TxID, utxo.Vout) + } + txOut = &wire.TxOut{PkScript: sb, Value: utxo.Value} + } else { + var fetchErr error + txOut, _, fetchErr = FetchUTXODetails(utxo.TxID, utxo.Vout) + if fetchErr != nil { + return "", fmt.Errorf("failed to fetch UTXO details for %s:%d: %w", utxo.TxID, utxo.Vout, fetchErr) + } + } + hash, _ := chainhash.NewHashFromStr(utxo.TxID) + prevOuts[wire.OutPoint{Hash: *hash, Index: utxo.Vout}] = txOut + } + prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts) + + utxoCount := len(selectedUTXOs) + for i, utxo := range selectedUTXOs { + derivePath := utxo.DerivationPath + if derivePath == "" { + return "", fmt.Errorf("UTXO %d missing derivation path", i) + } + derivedPubHex, err := GetDerivedPubKey(ks.PubKey, ks.ChainCodeHex, derivePath, false) + if err != nil { + return "", fmt.Errorf("failed to derive pubkey for input %d: %w", i, err) + } + pubKeyBytes, err := hex.DecodeString(derivedPubHex) + if err != nil { + return "", fmt.Errorf("invalid derived pubkey for input %d: %w", i, err) + } + + utxoSession := fmt.Sprintf("%s%d", sessionID, i) + // Re-use the already-resolved prevout (no second network call per input). + outpointHash, _ := chainhash.NewHashFromStr(utxo.TxID) + txOut := prevOuts[wire.OutPoint{Hash: *outpointHash, Index: utxo.Vout}] + isWitness := txscript.IsWitnessProgram(txOut.PkScript) + hashCache := txscript.NewTxSigHashes(tx, prevOutFetcher) + + var sigHash []byte + var isP2SHP2WPKH bool + if isWitness { + if txscript.IsPayToWitnessPubKeyHash(txOut.PkScript) { + sigHash, err = txscript.CalcWitnessSigHash(txOut.PkScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } else if txscript.IsPayToTaproot(txOut.PkScript) { + return "", fmt.Errorf("taproot (P2TR) inputs are not supported") + } else { + sigHash, err = txscript.CalcWitnessSigHash(txOut.PkScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } + } else { + if txscript.IsPayToPubKeyHash(txOut.PkScript) { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } else if txscript.IsPayToScriptHash(txOut.PkScript) { + pubKeyHash := btcutil.Hash160(pubKeyBytes) + redeemScript := make([]byte, 22) + redeemScript[0], redeemScript[1] = 0x00, 0x14 + copy(redeemScript[2:], pubKeyHash) + scriptHash := btcutil.Hash160(redeemScript) + expectedP2SH := make([]byte, 23) + expectedP2SH[0], expectedP2SH[1], expectedP2SH[22] = 0xa9, 0x14, 0x87 + copy(expectedP2SH[2:22], scriptHash) + if bytes.Equal(txOut.PkScript, expectedP2SH) { + isP2SHP2WPKH = true + sigHash, err = txscript.CalcWitnessSigHash(redeemScript, hashCache, txscript.SigHashAll, tx, i, txOut.Value) + } else { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } + } else { + sigHash, err = txscript.CalcSignatureHash(txOut.PkScript, txscript.SigHashAll, tx, i) + } + } + if err != nil { + return "", fmt.Errorf("failed to calc sighash for input %d: %w", i, err) + } + + sighashBase64 := base64.StdEncoding.EncodeToString(sigHash) + mpcHook("joining keysign", sessionID, utxoSession, i+1, utxoCount, false) + sigJSON, err := NostrJoinKeysignWithSighash(relaysCSV, partyNsec, partiesNpubsCSV, utxoSession, sessionKey, keyshareJSON, derivePath, sighashBase64) + if err != nil { + return "", fmt.Errorf("failed to sign input %d: %w", i, err) + } + var sig KeysignResponse + if err := json.Unmarshal([]byte(sigJSON), &sig); err != nil { + return "", fmt.Errorf("failed to parse signature for input %d: %w", i, err) + } + signature, err := hex.DecodeString(sig.DerSignature) + if err != nil { + return "", fmt.Errorf("failed to decode signature for input %d: %w", i, err) + } + sigWithHashType := append(signature, byte(txscript.SigHashAll)) + + if isWitness { + tx.TxIn[i].Witness = wire.TxWitness{sigWithHashType, pubKeyBytes} + tx.TxIn[i].SignatureScript = nil + } else if isP2SHP2WPKH { + redeemScript := make([]byte, 22) + redeemScript[0], redeemScript[1] = 0x00, 0x14 + copy(redeemScript[2:], btcutil.Hash160(pubKeyBytes)) + builder := txscript.NewScriptBuilder() + builder.AddData(redeemScript) + canonical, _ := builder.Script() + tx.TxIn[i].SignatureScript = canonical + tx.TxIn[i].Witness = wire.TxWitness{sigWithHashType, pubKeyBytes} + } else { + builder := txscript.NewScriptBuilder() + builder.AddData(sigWithHashType) + builder.AddData(pubKeyBytes) + scriptSig, _ := builder.Script() + tx.TxIn[i].SignatureScript = scriptSig + tx.TxIn[i].Witness = nil + } + + vm, err := txscript.NewEngine(txOut.PkScript, tx, i, txscript.StandardVerifyFlags, nil, hashCache, txOut.Value, prevOutFetcher) + if err != nil { + return "", fmt.Errorf("script engine for input %d: %w", i, err) + } + if err := vm.Execute(); err != nil { + return "", fmt.Errorf("script validation failed for input %d: %w", i, err) + } + } + + var signedTx bytes.Buffer + if err := tx.Serialize(&signedTx); err != nil { + return "", fmt.Errorf("failed to serialize transaction: %w", err) + } + rawTx := hex.EncodeToString(signedTx.Bytes()) + Logln("Raw Transaction (signed, not broadcast)") + mpcHook("signed", sessionID, "", utxoCount, utxoCount, true) + return rawTx, nil } // runNostrKeygenInternal is the internal implementation of Nostr keygen. @@ -1114,8 +1373,17 @@ func runNostrKeygenInternal(cfg nostrtransport.Config, chaincode, ppmPath, local result = "" } }() - ctx, cancel := context.WithTimeout(context.Background(), cfg.MaxTimeout) + rootCtx, rootCancel := context.WithCancel(context.Background()) + setActiveNostrCtx(rootCtx, rootCancel) + defer func() { + clearActiveNostrCtx() + rootCancel() + }() + + ctx, cancel := context.WithTimeout(rootCtx, cfg.MaxTimeout) defer cancel() + registerCtxCancel(sessionID, cancel) + defer unregisterCtxCancel(sessionID) // Get current status and increment step status := getStatus(sessionID) @@ -1190,6 +1458,8 @@ func runNostrKeygenInternal(cfg nostrtransport.Config, chaincode, ppmPath, local if err != nil { return "", fmt.Errorf("create TSS service: %w", err) } + // Allow mobile to abort the active Nostr MPC operation. + tssService.cancelCh = ctx.Done() Logln("BBMTLog", "starting message pump...") // Create message pump @@ -1335,8 +1605,17 @@ func runNostrKeysignInternal(cfg nostrtransport.Config, keyshare *LocalStateNost status := Status{Step: 0, SeqNo: 0, Index: 0, Info: "initializing...", Type: "keysign", Done: false, Time: 0} setStatus(sessionID, status) - ctx, cancel := context.WithTimeout(context.Background(), cfg.MaxTimeout) + rootCtx, rootCancel := context.WithCancel(context.Background()) + setActiveNostrCtx(rootCtx, rootCancel) + defer func() { + clearActiveNostrCtx() + rootCancel() + }() + + ctx, cancel := context.WithTimeout(rootCtx, cfg.MaxTimeout) defer cancel() + registerCtxCancel(sessionID, cancel) + defer unregisterCtxCancel(sessionID) // Create Nostr client status.Step++ @@ -1404,6 +1683,8 @@ func runNostrKeysignInternal(cfg nostrtransport.Config, keyshare *LocalStateNost if err != nil { return "", fmt.Errorf("create TSS service: %w", err) } + // Allow mobile to abort the active Nostr MPC operation. + tssService.cancelCh = ctx.Done() // Create message pump pump := nostrtransport.NewMessagePump(cfg, client) @@ -1589,8 +1870,17 @@ func runNostrKeysignInternalWithSighash(cfg nostrtransport.Config, keyshare *Loc status := Status{Step: 0, SeqNo: 0, Index: 0, Info: "initializing...", Type: "keysign", Done: false, Time: 0} setStatus(sessionID, status) - ctx, cancel := context.WithTimeout(context.Background(), cfg.MaxTimeout) + rootCtx, rootCancel := context.WithCancel(context.Background()) + setActiveNostrCtx(rootCtx, rootCancel) + defer func() { + clearActiveNostrCtx() + rootCancel() + }() + + ctx, cancel := context.WithTimeout(rootCtx, cfg.MaxTimeout) defer cancel() + registerCtxCancel(sessionID, cancel) + defer unregisterCtxCancel(sessionID) // Create Nostr client status.Step++ @@ -1661,6 +1951,8 @@ func runNostrKeysignInternalWithSighash(cfg nostrtransport.Config, keyshare *Loc if err != nil { return "", fmt.Errorf("create TSS service: %w", err) } + // Allow mobile to abort the active Nostr MPC operation. + tssService.cancelCh = ctx.Done() // Create message pump pump := nostrtransport.NewMessagePump(cfg, client) diff --git a/BBMTLib/tss/nostr_cancel.go b/BBMTLib/tss/nostr_cancel.go new file mode 100644 index 00000000..8bd67e5d --- /dev/null +++ b/BBMTLib/tss/nostr_cancel.go @@ -0,0 +1,51 @@ +package tss + +import ( + "context" + "fmt" + "sync" +) + +// Global cancellation for the currently-running Nostr MPC operation. +// Mobile does not provide an explicit sessionID to Nostr MPC entrypoints today, +// so we keep a single active cancel handle (UI guarantees only one operation). + +var nostrCancelMu sync.Mutex +var nostrActiveCtx context.Context +var nostrActiveCancel context.CancelFunc + +func setActiveNostrCtx(ctx context.Context, cancel context.CancelFunc) { + nostrCancelMu.Lock() + defer nostrCancelMu.Unlock() + nostrActiveCtx = ctx + nostrActiveCancel = cancel +} + +func clearActiveNostrCtx() { + nostrCancelMu.Lock() + defer nostrCancelMu.Unlock() + nostrActiveCtx = nil + nostrActiveCancel = nil +} + +func getActiveNostrCtx() context.Context { + nostrCancelMu.Lock() + defer nostrCancelMu.Unlock() + if nostrActiveCtx != nil { + return nostrActiveCtx + } + return context.Background() +} + +// CancelNostrMpc cancels the currently running Nostr MPC operation (best-effort). +// Exposed to mobile via gomobile bind. +func CancelNostrMpc() (string, error) { + nostrCancelMu.Lock() + cancel := nostrActiveCancel + nostrCancelMu.Unlock() + if cancel == nil { + return "", fmt.Errorf("no active nostr mpc operation") + } + cancel() + return "ok", nil +} diff --git a/BBMTLib/tss/nostrtransport/config.go b/BBMTLib/tss/nostrtransport/config.go index ae59da8c..d714bc4a 100644 --- a/BBMTLib/tss/nostrtransport/config.go +++ b/BBMTLib/tss/nostrtransport/config.go @@ -27,7 +27,7 @@ func (c *Config) ApplyDefaults() { c.MaxTimeout = 90 * time.Second } if c.ConnectTimeout == 0 { - c.ConnectTimeout = 20 * time.Second + c.ConnectTimeout = 45 * time.Second } } diff --git a/BBMTLib/tss/nostrtransport/pump.go b/BBMTLib/tss/nostrtransport/pump.go index 8b3094c3..1124ad9e 100644 --- a/BBMTLib/tss/nostrtransport/pump.go +++ b/BBMTLib/tss/nostrtransport/pump.go @@ -103,8 +103,9 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err cleanupTicker := time.NewTicker(30 * time.Second) defer cleanupTicker.Stop() - retryTicker := time.NewTicker(1 * time.Second) - defer retryTicker.Stop() + // retryDelay implements exponential backoff for subscription failures: + // first retry is immediate (0s), then 500 ms, then capped at 1 s. + retryDelay := time.Duration(0) // Helper function to process an event (unwrap, verify, and call handler) processEvent := func(event *nostr.Event) (err error) { @@ -225,16 +226,10 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err } 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 + // Add chunk to assembler. The assembler is mutex-protected and + // idempotent for duplicate chunk indices, so concurrent goroutines + // (e.g. the parallel initial-query goroutines) can safely call Add + // simultaneously for the same message. 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 { @@ -250,16 +245,22 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err 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 + // Atomically claim this message. When the same event arrives from + // multiple relays simultaneously (parallel initial-query goroutines), + // both goroutines complete assembly independently. The lock+check + // here ensures the TSS handler is called exactly once per message. p.processedMu.Lock() + if p.processed[meta.Hash] { + fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump message %s already dispatched by concurrent goroutine, skipping\n", meta.Hash) + p.processedMu.Unlock() + return nil + } p.processed[meta.Hash] = true p.processedMu.Unlock() - // Call handler with plaintext payload + // Exactly one goroutine per message hash reaches this point. 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) @@ -333,9 +334,10 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump initial query timeout, proceeding with subscription\n") } - // Retry loop: resubscribe when channel closes (e.g., network disconnection) + // Retry loop: resubscribe when channel closes (e.g., network disconnection). + // Uses exponential backoff: first retry is immediate, then 500 ms, capped at 1 s. for { - // Check if context is cancelled before attempting subscription + // Check if context is cancelled before attempting subscription. select { case <-ctx.Done(): return ctx.Err() @@ -345,18 +347,24 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump subscribing to session %s, local npub %s (hex: %s), expecting authors (hex): %v\n", p.cfg.SessionID, p.cfg.LocalNpub, localNpubHex, authorsHex) events, err := p.client.Subscribe(ctx, filter) if err != nil { - fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to subscribe: %v, retrying in 1 second...\n", err) - // Wait for retry ticker or context cancellation + fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to subscribe: %v, retrying in %v...\n", err, retryDelay) select { case <-ctx.Done(): return ctx.Err() - case <-retryTicker.C: - continue // Retry subscription + case <-time.After(retryDelay): + } + if retryDelay == 0 { + retryDelay = 500 * time.Millisecond + } else if retryDelay < time.Second { + retryDelay = time.Second } + continue } + // Successful subscription — reset backoff. + retryDelay = 0 fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump subscription active\n") - // Process events from this subscription until channel closes + // Process events from this subscription until channel closes. subscriptionActive := true for subscriptionActive { select { @@ -366,32 +374,32 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) (err p.assembler.Cleanup() case event, ok := <-events: if !ok { - // Channel closed (e.g., network disconnection) - retry subscription - fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump event channel closed (network may have disconnected), retrying subscription in 1 second...\n") + // Channel closed — relay disconnected; resubscribe with backoff. + fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump event channel closed (network may have disconnected), retrying in %v...\n", retryDelay) subscriptionActive = false - // Wait before retrying select { case <-ctx.Done(): return ctx.Err() - case <-retryTicker.C: - // Continue to outer loop to resubscribe + case <-time.After(retryDelay): + } + if retryDelay == 0 { + retryDelay = 500 * time.Millisecond + } else if retryDelay < time.Second { + retryDelay = time.Second } break } - // 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 } - // Process event using the helper function if err := processEvent(event); err != nil { - // Handler error - return to stop processing return err } } } - // If we break out of the inner loop, we'll retry subscribing in the outer loop + // Inner loop exited — outer loop will resubscribe. } } diff --git a/BBMTLib/tss/nostrtransport/session.go b/BBMTLib/tss/nostrtransport/session.go index 72fc002a..15aa5ca8 100644 --- a/BBMTLib/tss/nostrtransport/session.go +++ b/BBMTLib/tss/nostrtransport/session.go @@ -165,26 +165,38 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { } queryDone <- true }() + // Query all relays in parallel so a slow/offline relay does not block + // the others from being scanned within the timeout budget. + var wg sync.WaitGroup 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) - continue - } - existingEvents, err := relay.QuerySync(queryCtx, filter) - if err == nil { - fmt.Fprintf(os.Stderr, "BBMTLog: Query on relay %s returned %d wrap events for session %s\n", url, len(existingEvents), s.cfg.SessionID) + wg.Add(1) + go func(relayURL string) { + defer wg.Done() + defer func() { + if r := recover(); r != nil { + fmt.Fprintf(os.Stderr, "BBMTLog: PANIC in AwaitPeers relay query goroutine (%s): %v\n", relayURL, r) + } + }() + relay, err := s.client.GetPool().EnsureRelay(relayURL) + if err != nil { + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to ensure relay %s: %v\n", relayURL, err) + return + } + existingEvents, err := relay.QuerySync(queryCtx, filter) + if err != nil { + fmt.Fprintf(os.Stderr, "BBMTLog: Query on relay %s failed (non-fatal): %v\n", relayURL, err) + return + } + fmt.Fprintf(os.Stderr, "BBMTLog: Query on relay %s returned %d wrap events for session %s\n", relayURL, len(existingEvents), s.cfg.SessionID) for _, wrapEvent := range existingEvents { if wrapEvent == nil || wrapEvent.Kind != 1059 { continue } - // Unwrap and unseal to get sender seal, err := unwrapGift(wrapEvent, s.cfg.LocalNsec) if err != nil { fmt.Fprintf(os.Stderr, "BBMTLog: Failed to unwrap gift from query: %v\n", err) continue } - // Verify seal is from an expected peer sealSenderHex := seal.PubKey sealSenderNpub := "" for hex, npub := range expectedHex { @@ -197,7 +209,6 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { fmt.Fprintf(os.Stderr, "BBMTLog: Seal from unexpected sender (hex: %s)\n", sealSenderHex) continue } - // Unseal to verify it's a ready message sealSenderNpubBech32 := sealSenderNpub for _, npub := range s.cfg.PeersNpub { npubHex, err := npubToHex(npub) @@ -211,7 +222,6 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { fmt.Fprintf(os.Stderr, "BBMTLog: Failed to unseal from query: %v\n", err) continue } - // Parse rumor content to verify it's a ready message var readyMsg map[string]interface{} if err := json.Unmarshal([]byte(rumor.Content), &readyMsg); err != nil { continue @@ -221,10 +231,9 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { seen.Store(sealSenderNpub, true) } } - } else { - fmt.Fprintf(os.Stderr, "BBMTLog: Query on relay %s failed (non-fatal): %v\n", url, err) - } + }(url) } + wg.Wait() }() // Wait for initial query to complete (with timeout) before starting subscription @@ -236,37 +245,46 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { fmt.Fprintf(os.Stderr, "BBMTLog: Initial query timeout, proceeding with subscription (found %d peers so far)\n", s.countSeen(&seen)) } - // Now start subscription to catch new events - // Retry subscription if it fails or channel closes (resilient to relay failures) - retryTicker := time.NewTicker(1 * time.Second) - defer retryTicker.Stop() + // Now start subscription to catch new events. + // Uses exponential backoff on failure: first retry immediate, then 500 ms, capped at 1 s. + subRetryDelay := time.Duration(0) - var eventsCh <-chan *Event - subscriptionActive := false + subscribe := func() (<-chan *Event, error) { + fmt.Fprintf(os.Stderr, "BBMTLog: Starting subscription for ready wraps for session %s\n", s.cfg.SessionID) + ch, err := s.client.Subscribe(ctx, filter) + if err != nil { + return nil, err + } + fmt.Fprintf(os.Stderr, "BBMTLog: Subscription active for session %s\n", s.cfg.SessionID) + return ch, nil + } - // Retry loop for subscription - for !subscriptionActive { + // Establish first subscription. + var eventsCh <-chan *Event + for { select { case <-ctx.Done(): - fmt.Fprintf(os.Stderr, "BBMTLog: AwaitPeers timed out during subscription (seen: %d/%d)\n", s.countSeen(&seen), len(expected)) + fmt.Fprintf(os.Stderr, "BBMTLog: AwaitPeers timed out during subscription setup (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 - } + eventsCh, err = subscribe() + if err == nil { + subRetryDelay = 0 + break + } + fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe failed: %v, retrying in %v...\n", err, subRetryDelay) + select { + case <-ctx.Done(): + return fmt.Errorf("waiting for peers timed out: %w", ctx.Err()) + case <-time.After(subRetryDelay): + } + if subRetryDelay == 0 { + subRetryDelay = 500 * time.Millisecond + } else if subRetryDelay < time.Second { + subRetryDelay = time.Second } - subscriptionActive = true - fmt.Fprintf(os.Stderr, "BBMTLog: Subscription active for session %s\n", s.cfg.SessionID) } ticker := time.NewTicker(5 * time.Second) @@ -280,37 +298,43 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) (err error) { return fmt.Errorf("waiting for peers timed out: %w", ctx.Err()) case evt, ok := <-eventsCh: if !ok { - // 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 + // Channel closed — relay disconnected; resubscribe with backoff. + fmt.Fprintf(os.Stderr, "BBMTLog: Subscription channel closed, retrying in %v...\n", subRetryDelay) 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) + case <-time.After(subRetryDelay): + } + if subRetryDelay == 0 { + subRetryDelay = 500 * time.Millisecond + } else if subRetryDelay < time.Second { + subRetryDelay = time.Second + } + // Re-establish subscription. + for { + select { + case <-ctx.Done(): + return fmt.Errorf("waiting for peers timed out: %w", ctx.Err()) + default: + } + var err error + eventsCh, err = subscribe() + if err == nil { + subRetryDelay = 0 + break + } + fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe retry failed: %v, retrying in %v...\n", err, subRetryDelay) + select { + case <-ctx.Done(): + return fmt.Errorf("waiting for peers timed out: %w", ctx.Err()) + case <-time.After(subRetryDelay): + } + if subRetryDelay < time.Second { + subRetryDelay = time.Second } - continue // Continue processing events } + fmt.Fprintf(os.Stderr, "BBMTLog: Subscription re-established for session %s\n", s.cfg.SessionID) + continue } if evt == nil || evt.Kind != 1059 { continue @@ -419,32 +443,43 @@ func (s *SessionCoordinator) PublishReady(ctx context.Context) (err error) { fmt.Fprintf(os.Stderr, "BBMTLog: Publishing ready event for session %s, npub %s, expecting peers: %v\n", s.cfg.SessionID, s.cfg.LocalNpub, s.cfg.PeersNpub) - // Publish encrypted wrap to each peer using rumor/wrap/seal pattern + // Publish to every peer; continue even if one peer's publish fails so that + // the remaining peers still receive the ready signal. Report the first + // error after all peers have been attempted. + var firstReadyErr error for _, peerNpub := range s.cfg.PeersNpub { - // Step 1: Create rumor (kind:14) - unsigned event rumor := createRumor(string(readyJSON), senderNpubHex) - // Step 2: Create seal (kind:13) - encrypt rumor with NIP-44 seal, err := createSeal(rumor, s.cfg.LocalNsec, peerNpub) if err != nil { - return fmt.Errorf("create seal for peer %s: %w", peerNpub, err) + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to create seal for peer %s: %v (continuing)\n", peerNpub, err) + if firstReadyErr == nil { + firstReadyErr = fmt.Errorf("create seal for peer %s: %w", peerNpub, err) + } + continue } - // Step 3: Create wrap (kind:1059) - wrap seal in gift wrap - // Include session tag for filtering (must be added before signing) wrap, err := createWrap(seal, peerNpub, s.cfg.SessionID, "") if err != nil { - return fmt.Errorf("create wrap for peer %s: %w", peerNpub, err) + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to create wrap for peer %s: %v (continuing)\n", peerNpub, err) + if firstReadyErr == nil { + firstReadyErr = fmt.Errorf("create wrap for peer %s: %w", peerNpub, err) + } + continue } fmt.Fprintf(os.Stderr, "BBMTLog: Publishing ready wrap to peer %s\n", peerNpub) - // Publish the wrap (kind:1059) - err = s.client.PublishWrap(ctx, wrap) - if err != nil { - return fmt.Errorf("publish ready wrap to peer %s: %w", peerNpub, err) + if err := s.client.PublishWrap(ctx, wrap); err != nil { + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to publish ready wrap to peer %s: %v (continuing)\n", peerNpub, err) + if firstReadyErr == nil { + firstReadyErr = fmt.Errorf("publish ready wrap to peer %s: %w", peerNpub, err) + } } } + if firstReadyErr != nil { + return firstReadyErr + } fmt.Fprintf(os.Stderr, "BBMTLog: Ready event published successfully to all peers with tag t=%s\n", s.cfg.SessionID) @@ -483,32 +518,41 @@ func (s *SessionCoordinator) PublishComplete(ctx context.Context, phase string) fmt.Fprintf(os.Stderr, "BBMTLog: Publishing complete event for session %s, phase %s, npub %s, expecting peers: %v\n", s.cfg.SessionID, phase, s.cfg.LocalNpub, s.cfg.PeersNpub) - // Publish encrypted wrap to each peer using rumor/wrap/seal pattern + // Publish to every peer; continue past individual failures (same policy as PublishReady). + var firstCompleteErr error for _, peerNpub := range s.cfg.PeersNpub { - // Step 1: Create rumor (kind:14) - unsigned event rumor := createRumor(string(completeJSON), senderNpubHex) - // Step 2: Create seal (kind:13) - encrypt rumor with NIP-44 seal, err := createSeal(rumor, s.cfg.LocalNsec, peerNpub) if err != nil { - return fmt.Errorf("create complete seal for peer %s: %w", peerNpub, err) + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to create complete seal for peer %s: %v (continuing)\n", peerNpub, err) + if firstCompleteErr == nil { + firstCompleteErr = fmt.Errorf("create complete seal for peer %s: %w", peerNpub, err) + } + continue } - // Step 3: Create wrap (kind:1059) - wrap seal in gift wrap - // Include session tag for filtering (must be added before signing) wrap, err := createWrap(seal, peerNpub, s.cfg.SessionID, "") if err != nil { - return fmt.Errorf("create complete wrap for peer %s: %w", peerNpub, err) + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to create complete wrap for peer %s: %v (continuing)\n", peerNpub, err) + if firstCompleteErr == nil { + firstCompleteErr = fmt.Errorf("create complete wrap for peer %s: %w", peerNpub, err) + } + continue } fmt.Fprintf(os.Stderr, "BBMTLog: Publishing complete wrap (phase=%s) to peer %s\n", phase, peerNpub) - // Publish the wrap (kind:1059) - err = s.client.PublishWrap(ctx, wrap) - if err != nil { - return fmt.Errorf("publish complete wrap to peer %s: %w", peerNpub, err) + if err := s.client.PublishWrap(ctx, wrap); err != nil { + fmt.Fprintf(os.Stderr, "BBMTLog: Failed to publish complete wrap to peer %s: %v (continuing)\n", peerNpub, err) + if firstCompleteErr == nil { + firstCompleteErr = fmt.Errorf("publish complete wrap to peer %s: %w", peerNpub, err) + } } } + if firstCompleteErr != nil { + return firstCompleteErr + } fmt.Fprintf(os.Stderr, "BBMTLog: Complete event (phase=%s) published successfully to all peers with tag t=%s\n", phase, s.cfg.SessionID) diff --git a/BBMTLib/tss/tss.go b/BBMTLib/tss/tss.go index 859b995b..432321ee 100644 --- a/BBMTLib/tss/tss.go +++ b/BBMTLib/tss/tss.go @@ -1,6 +1,7 @@ package tss import ( + "context" "crypto/ecdsa" "encoding/base64" "encoding/hex" @@ -501,6 +502,12 @@ func (s *ServiceImpl) processKeySign(localParty tss.Party, for { select { + case <-s.cancelCh: + // Best-effort stop. Not all Party implementations expose Stop(). + if stopper, ok := localParty.(interface{ Stop() }); ok { + stopper.Stop() + } + return nil, context.Canceled case <-errCh: return nil, errors.New("failed to start keysign process") case msg := <-outCh: diff --git a/CHANGELOG.md b/CHANGELOG.md index fc16d51c..80b086b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,63 @@ # Changelog +## [3.0.0] - 2026-03-04 + +### Added +- **HTTP caching layer** (`MempoolClient`): In-memory cache wrapping all `fetch` calls to mempool.space; per-endpoint TTL (30 s default, 5 min for immutable tx details, 60 s for fee rates and BTC price); in-flight request deduplication prevents duplicate concurrent fetches; only HTTP-200 responses are cached; `invalidate(prefix)` and `invalidateAll()` for manual eviction. +- **Universal 10-second API timeout**: Every outbound `fetch` through `MempoolClient` is capped at 10 s via `AbortController`; caller signals are combined using a React Native–compatible `combineSignals` helper. +- **Balance via address-stats endpoint**: `getWalletBalanceAggregate` now uses `GET /api/address/:address` (`chain_stats` + `mempool_stats`) instead of fetching full UTXO arrays — responses are ~50× smaller and are fully cached through `MempoolClient`. Formula: `(chain_stats.funded − chain_stats.spent) + (mempool_stats.funded − mempool_stats.spent)` per address. +- **Confirmed / unconfirmed balance breakdown in `WalletBalance`**: `pendingSats` field exposes the net mempool delta (positive = incoming unconfirmed, negative = outgoing unconfirmed) alongside the total balance. +- **WalletHome pending chip**: When `pendingSats ≠ 0`, a small amber pill is shown below the fiat balance (`⏳ +X.XXXXXXXX BTC incoming` / `⏳ X.XXXXXXXX BTC outgoing`); hidden while loading or in privacy-blur mode. +- **UtxosScreen balance summary card**: Static card pinned above the scrollable UTXO list showing Total, ✓ Confirmed, and ⏳ Pending balances with BTC, fiat, and UTXO counts; computed locally from the already-loaded UTXO set (zero extra API calls). +- **Multi-path HD wallet (no-address-reuse)**: Full support for per-address indexing and spending across receive and change paths. + - **WalletService**: Aggregate balance and UTXO fetch across all HD addresses; `getHdAddressesWithPaths` with in-memory cache; `fetchUtxosWithPaths` with sequential API calls and empty-address skip cache; `fetchMoreTransactionsForAddresses` for cursor-based multi-address transaction pagination. + - **HdIndexService**: Centralized HD index management; discovery uses `GAP_LIMIT` and `MIN_SCAN_INDEX`; runtime address range based on `max(externalIndex, maxUsedExternal)` and `changeIndex`. + - **Receive**: Restoring-indexes flow; receive index advances only when current address is used; receive button shows busy state while computing next address. +- **Multi-path UTXO spend and fee estimation**: + - **BBMTLib (Go)**: `SpendingHashWithUTXOs` for deterministic spending hash from a pre-fetched UTXO set; `estimateFeeWithUTXOs`; sequential API behavior and improved logging in fee/UTXO paths. + - **Native bridges**: iOS/Android expose `spendingHashWithUTXOs`; Send flow uses multi-path UTXOs for fee and spending hash. +- **Transaction list and details**: + - **TransactionList**: Multi-address transaction list with per-address cursor pagination; consolidation (1 internal output) vs rebalancing (2+ internal outputs) labels; animations for Sending/Receiving/Consolidating/Rebalancing; derivation path in details only (no Ix row in list). + - **TransactionDetailsModal**: Inputs/outputs flow diagram (PSBT-style); internal (ours) outputs highlighted with theme accent; summary bar with fee; address path map for paths. +- **Pairing screens**: Inputs/outputs flow in MobilesPairing and MobileNostrPairing; “Signing Path” instead of “From Address”; network badge and flow diagram aligned. +- **RestoringIndexesModal**: Dedicated modal for index discovery progress. +- **Deterministic co-signing via QR (changeAddress field)**: The send-bitcoin QR now encodes a 9th field (`changeAddress`); both the sender and the scanning device use the identical pre-computed change output in UI preview and in the native MPC call. `decodeSendBitcoinQR` / `encodeSendBitcoinQR` are fully backward-compatible with v1–v4 QRs (3–8 fields). +- **Change address derivation path in outputs preview**: If a transaction output is the wallet's change address, its HD derivation path (e.g., `m/84'/0'/0'/1/3`) is displayed below the address in both MobilesPairing and MobileNostrPairing; `WalletService.getNextChangeAddressWithPath` returns `{address, path}` as a single atomic call. +- **Address shortening in transaction previews**: All Bitcoin addresses in the inputs/outputs flow on MobilesPairing and MobileNostrPairing are displayed in a compact “first 4…last 4” format (e.g., `bc1q…xyz1`) via a new `shortenAddress` utility in `utils.js`. +- **Abort functionality in MobileNostrPairing**: Full abort support added to the Nostr co-signing modal — abort button in the in-progress modal, `nostrAbortRef` reset at the start of each signing session, mid-flow abort checks after raw TX is produced, and suppressed error alerts when the user aborts intentionally; behaviour is now consistent with MobilesPairing. + +### Changed +- **`TransactionList`**: Migrated from `axios` to `MempoolClient`; manual `timeoutPromise` race removed (timeout now enforced inside `MempoolClient`). +- **WalletHome balance rendering**: `ActivityIndicator` spinners replaced with a `react-native-reanimated` shimmer animation while balances load; fiat balance derived via `useMemo` from `balanceBTC × btcRate` to prevent the 0-BTC / non-zero-fiat display mismatch. +- **Mempool API**: All WalletService calls to mempool.space (UTXOs, transactions) are sequential/synchronous to avoid rate limits and non-determinism. +- **SendBitcoinModal**: Passes `activeNetwork` (e.g. testnet3) to WalletService; fee estimation uses multi-path UTXOs and native `estimateFeeWithUTXOs` with fallback. +- **UtxosScreen / WalletHome**: Use aggregate balance and multi-address–aware flows. +- **BBMTLib build**: `build.sh` banner and note for Go 1.25 taggedPointerPack; FIPS check and toolchain steps unchanged. +- **FIPS Android (Dockerfile.fips and fips-android.sh)**: + - `WORKDIR /workspace` so `docker run -v $(pwd):/workspace ./build.sh` runs against mounted source; `-w /workspace` in script. + - BuildKit cache mounts: dnf, Go tarball, Android SDK download and per-platform install cache, Go modules/gomobile. + - Platform default: `linux/arm64` on Apple Silicon (avoids Go 1.25 taggedPointerPack under QEMU), `linux/amd64` elsewhere; optional `FIPS_ANDROID_PLATFORM` override. + - Run-from-BBMTLib check so `build.sh` exists in mounted dir. + +### Fixed +- **Receive flow address mismatch**: `getCurrentReceivePathInfo` now atomically derives and returns `{path, index, address}` in one call; `WalletHome` passes the address from this combined result to `ReceiveModal`, eliminating QR flicker and stale-address display when advancing to a new receive index. +- **Receive index**: No longer advances on network errors; discovery records partial/failed state without bumping indexes; `bumpExternalIndexIfCurrentUsed` only when address is actually used. +- **Fee estimation**: Multi-path UTXO set used for native fee estimation; “insufficient funds” handling and logging aligned with multi-path. +- **Transaction list**: Removed legacy single-address consolidation detection and Ix row; correct merge/sort for multi-address pagination. +- **Scanning device UTXO mismatch / session stall**: The scanning device now populates both the UI preview and the native MPC signing call directly from the UTXOs embedded in the QR code (enriched with `scriptpubkey` via a targeted API call); previously the scanner re-fetched UTXOs independently, causing mismatches that stalled co-signing sessions. +- **Transaction inputs not shown on scanning device**: The `txPreview` effect now uses `route.params.utxosJson` when available, bypassing a cold-cache `fetchUtxosWithPaths` call that returned empty results on the scanner. +- **Inconsistent change address between devices**: The sender’s pre-computed change address is now threaded end-to-end — from `SendBitcoinModal` → `WalletHome` → QR field 9 → `processScannedQRData` → `navigationParams` → pairing screens → native MPC call — guaranteeing both devices sign with the same change output. + +### Technical Details +- **Version**: `package.json` 3.0.0. +- **New file**: `services/MempoolClient.ts` — `MempoolClient` class; `MempoolResponse` interface; `buildKey`, `ttlForUrl`, `combineSignals` helpers; singleton `mempoolClient` export. +- **`WalletBalance` interface**: Added optional `pendingSats?: number` (backward-compatible with cached entries). +- **BBMTLib**: `go.mod`/`go.sum`; `tss/btc.go` (SpendingHashWithUTXOs, fee/UTXO); `tss/mpc_nostr.go`; iOS/Android native module updates; `Dockerfile.fips` (Go 1.25.x, TARGETARCH, cache mounts, WORKDIR /workspace). +- **Context**: UserContext/WalletContext and utils.js updates for tabs and routing where applicable. +- **QR format v5**: `encodeSendBitcoinQR` / `decodeSendBitcoinQR` in `utils.js` extended to 9 fields; `TransportModeSelector` passes `changeAddress`; `WalletHome.processScannedQRData` extracts and stores it in `pendingSendParams`. +- **`shortenAddress` utility** (`utils.js`): Returns first 4…last 4 characters for addresses longer than 8 chars; used across pairing screen previews. +- **`WalletService.getNextChangeAddressWithPath`**: New method returning `{address, path}`; `getNextChangeAddress` delegates to it for backward compatibility. + ## [2.2.0] - 2026-02-15 ### Added diff --git a/Dockerfile b/Dockerfile index fc3f0e3b..f8ab80a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -144,7 +144,8 @@ RUN --mount=type=cache,target=/root/go/pkg/mod,id=go-modules-cache,sharing=share # gomobile is already installed and initialized in base stage, skip redundant steps \ export GOFLAGS="-mod=mod" && \ # Build Android AAR (iOS build not needed for Android APK) \ - /root/go/bin/gomobile bind -v -target=android -androidapi 21 github.com/BoldBitcoinWallet/BBMTLib/tss && \ + # Android 15 requires 16 KB page size support. \ + /root/go/bin/gomobile bind -v -target=android -androidapi 21 -ldflags="-extldflags=-Wl,-z,max-page-size=16384" github.com/BoldBitcoinWallet/BBMTLib/tss && \ # Copy Android artifacts to android/app/libs \ cp tss.aar ../android/app/libs/tss.aar && \ cp tss-sources.jar ../android/app/libs/tss-sources.jar diff --git a/android/app/build.gradle b/android/app/build.gradle index 6b69be0d..c52167d4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,8 +27,8 @@ android { applicationId "com.boldwallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 49 - versionName "2.2.0" + versionCode 50 + versionName "3.0.0" 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 7d1ec397..b15bd410 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 b84f9ee4..60171429 100644 Binary files a/android/app/libs/tss.aar and b/android/app/libs/tss.aar differ diff --git a/android/app/src/main/java/com/boldwallet/BBMTLibNativeModule.kt b/android/app/src/main/java/com/boldwallet/BBMTLibNativeModule.kt index de77f47f..599119fc 100644 --- a/android/app/src/main/java/com/boldwallet/BBMTLibNativeModule.kt +++ b/android/app/src/main/java/com/boldwallet/BBMTLibNativeModule.kt @@ -163,6 +163,20 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : }.start() } + @ReactMethod + fun spendingHashWithUTXOs(utxosWithPathsJSON: String, receiverAddress: String, amountSatoshi: String, promise: Promise) { + Thread { + try { + val result = Tss.spendingHashWithUTXOs(utxosWithPathsJSON, receiverAddress, amountSatoshi) + ld("spendingHashWithUTXOs", result) + promise.resolve(result) + } catch (e: Exception) { + ld("spendingHashWithUTXOs", "error: ${e.stackTraceToString()}") + promise.reject(e) + } + }.start() + } + @ReactMethod fun estimateFees(senderAddress: String, receiverAddress: String, amountSatoshi: String, promise: Promise) { Thread { @@ -179,6 +193,31 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : }.start() } + @ReactMethod + fun estimateFeeWithUTXOs( + utxosWithPathsJSON: String, + receiverAddress: String, + amountSatoshi: String, + changeAddress: String, + promise: Promise + ) { + Thread { + try { + val result = Tss.estimateFeeWithUTXOs( + utxosWithPathsJSON, + receiverAddress, + amountSatoshi, + changeAddress + ) + ld("estimateFeeWithUTXOs", result) + promise.resolve(result) + } catch (e: Throwable) { + ld("estimateFeeWithUTXOs", "error: ${e.stackTraceToString()}") + promise.reject(e) + } + }.start() + } + @ReactMethod fun mpcSendBTC( // tss @@ -222,6 +261,52 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : } }.start() } + + @ReactMethod + fun mpcSendBTCWithUTXOs( + server: String, + partyID: String, + partiesCSV: String, + sessionID: String, + sessionKey: String, + encKey: String, + decKey: String, + keyshare: String, + publicKey: String, + receiverAddress: String, + amountSatoshi: String, + feeSatoshi: String, + utxosWithPathsJSON: String, + changeAddress: String, + promise: Promise + ) { + Thread { + try { + val result = Tss.mpcSendBTCWithUTXOs( + server, + partyID, + partiesCSV, + sessionID, + sessionKey, + encKey, + decKey, + keyshare, + publicKey, + receiverAddress, + amountSatoshi, + feeSatoshi, + utxosWithPathsJSON, + changeAddress + ) + ld("mpcSendBTCWithUTXOs", result) + promise.resolve(result) + } catch (e: Throwable) { + ld("mpcSendBTCWithUTXOs", "error: ${e.stackTraceToString()}") + promise.reject("MPC_SEND_BTC_ERROR", "Failed to send BTC: ${e.message}", e) + } + }.start() + } + @ReactMethod fun nostrMpcSendBTC( relaysCSV: String, @@ -236,6 +321,7 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : receiverAddress: String, amountSatoshi: String, estimatedFee: String, + changeAddress: String, promise: Promise ) { Thread { @@ -252,7 +338,8 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : senderAddress, receiverAddress, amountSatoshi.toLong(), - estimatedFee.toLong() + estimatedFee.toLong(), + changeAddress ?: "" ) ld("nostrMpcSendBTC", result) promise.resolve(result) @@ -263,6 +350,89 @@ class BBMTLibNativeModule(reactContext: ReactApplicationContext) : }.start() } + @ReactMethod + fun nostrMpcSendBTCWithUTXOs( + relaysCSV: String, + partyNsec: String, + partiesNpubsCSV: String, + npubsSorted: String, + balanceSats: String, + keyshareJSON: String, + receiverAddress: String, + amountSatoshi: String, + estimatedFee: String, + utxosWithPathsJSON: String, + changeAddress: String, + promise: Promise + ) { + Thread { + try { + val result = Tss.nostrMpcSendBTCWithUTXOs( + relaysCSV, + partyNsec, + partiesNpubsCSV, + npubsSorted, + balanceSats, + keyshareJSON, + receiverAddress, + amountSatoshi, + estimatedFee, + utxosWithPathsJSON, + changeAddress ?: "" + ) + ld("nostrMpcSendBTCWithUTXOs", result) + promise.resolve(result) + } catch (e: Throwable) { + ld("nostrMpcSendBTCWithUTXOs", "error: ${e.stackTraceToString()}") + promise.reject("NOSTR_MPC_SEND_BTC_ERROR", "Failed to send BTC via Nostr: ${e.message}", e) + } + }.start() + } + + @ReactMethod + fun postTx(rawTxHex: String, promise: Promise) { + Thread { + try { + val txid = Tss.postTx(rawTxHex) + ld("postTx", txid) + promise.resolve(txid) + } catch (e: Throwable) { + ld("postTx", "error: ${e.stackTraceToString()}") + promise.reject("POST_TX_ERROR", "Failed to broadcast: ${e.message}", e) + } + }.start() + } + + @ReactMethod + fun computeTxId(rawTxHex: String, promise: Promise) { + try { + val txid = Tss.computeTxId(rawTxHex) + promise.resolve(txid) + } catch (e: Throwable) { + promise.reject("COMPUTE_TXID_ERROR", "Failed to compute txid: ${e.message}", e) + } + } + + @ReactMethod + fun cancelMpcSession(sessionID: String, promise: Promise) { + try { + val out = Tss.cancelMpcSession(sessionID) + promise.resolve(out) + } catch (e: Throwable) { + promise.reject("CANCEL_MPC_ERROR", "Failed to cancel MPC session: ${e.message}", e) + } + } + + @ReactMethod + fun cancelNostrMpc(promise: Promise) { + try { + val out = Tss.cancelNostrMpc() + promise.resolve(out) + } catch (e: Throwable) { + promise.reject("CANCEL_NOSTR_MPC_ERROR", "Failed to cancel Nostr MPC: ${e.message}", e) + } + } + @ReactMethod fun runRelay(port: String, promise: Promise) { try { diff --git a/android/gradle.properties b/android/gradle.properties index d76e551e..20f6bd77 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,26 +1,33 @@ -# Optimized for Docker on macOS host (QEMU emulation) -# Conservative settings to avoid QEMU linker crashes and memory issues -# Allocate 4GB to Gradle (leaves 6GB for QEMU emulation, OS, and overhead) -org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -XX:MaxGCPauseMillis=200 +# Optimized for local builds (native Linux/macOS, no QEMU emulation) +# Use more aggressive settings since you have full system resources +# Allocate more memory and use more parallel workers for faster builds + +# Memory allocation - adjust based on your system RAM +# Recommended: 50-70% of available RAM (e.g., 8GB for 16GB system) +org.gradle.jvmargs=-Xmx8192m -XX:MaxMetaspaceSize=2048m -XX:+HeapDumpOnOutOfMemoryError -XX:+UseG1GC -XX:MaxGCPauseMillis=200 + org.gradle.parallel=true org.gradle.caching=true -# Use 4 workers to reduce memory pressure during C++ linking (QEMU emulation needs more headroom) -# C++ linking is memory-intensive and can crash with too many parallel workers under QEMU -org.gradle.workers.max=4 -# Kotlin daemon with reasonable memory allocation -kotlin.daemon.jvmargs=-Xmx1024m -XX:MaxMetaspaceSize=512m + +# Use more workers for local builds (adjust based on CPU cores) +# Recommended: Number of CPU cores - 2 (e.g., 6 workers for 8-core CPU) +org.gradle.workers.max=6 + +# Kotlin daemon with more memory for local builds +kotlin.daemon.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=1024m kotlin.incremental=true + # Disable configuration cache - React Native doesn't fully support it yet -# Enabling it causes build failures with React Native tasks org.gradle.configuration-cache=false + # Enable build cache for faster subsequent builds org.gradle.build-cache=true android.useAndroidX=true -# Build only ARM architectures (most Android devices are ARM) -# x86/x86_64 cause QEMU linker crashes on macOS, and are rarely needed -# ARM-only builds are faster and avoid QEMU emulation issues -reactNativeArchitectures=armeabi-v7a,arm64-v8a + +# Build all architectures for local builds (no QEMU issues) +# You can reduce this if you only need ARM: reactNativeArchitectures=armeabi-v7a,arm64-v8a +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 newArchEnabled=true hermesEnabled=true @@ -29,3 +36,4 @@ VisionCamera_enableCodeScanner=false # 16KB page size support android.enableJetifier=true + diff --git a/android/mapping.txt b/android/mapping.txt index 371519d7..a1f87373 100644 --- a/android/mapping.txt +++ b/android/mapping.txt @@ -3,8 +3,8 @@ # min_api: 24 # common_typos_disable # {"id":"com.android.tools.r8.mapping","version":"2.2"} -# pg_map_id: 13e5f3622a4d770a45ec7a90dd7cfbda90bd21b0ab0e21922a7154045e4cbbe5 -# pg_map_hash: SHA-256 13e5f3622a4d770a45ec7a90dd7cfbda90bd21b0ab0e21922a7154045e4cbbe5 +# pg_map_id: dc6b07e432130166a8d44c5e2c3d7af548793a59f181597c8f739087c72f6bcf +# pg_map_hash: SHA-256 dc6b07e432130166a8d44c5e2c3d7af548793a59f181597c8f739087c72f6bcf android.app.AppComponentFactory -> android.app.AppComponentFactory: # {"id":"com.android.tools.r8.synthesized"} void () -> @@ -72210,326 +72210,386 @@ com.boldwallet.BBMTLibNativeModule -> com.boldwallet.BBMTLibNativeModule: 6:9:void (com.facebook.react.bridge.ReactApplicationContext):20:20 -> 10:11:void (com.facebook.react.bridge.ReactApplicationContext):23:23 -> 12:16:void (com.facebook.react.bridge.ReactApplicationContext):26:26 -> - 1:1:void $r8$lambda$3Be428r53dqOweObk9dakKD10qQ(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> a + 1:1:void $r8$lambda$8PmGrvu9xwUvl9tupsrAyXI9hZI(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> a # {"id":"com.android.tools.r8.synthesized"} 6:8:void addListener(java.lang.String):42:42 -> addListener 9:12:void addListener(java.lang.String):43:43 -> addListener - 18:21:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):677:677 -> aesDecrypt - 22:27:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):678:678 -> aesDecrypt - 28:32:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):679:679 -> aesDecrypt - 33:56:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):681:681 -> aesDecrypt - 57:64:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):682:682 -> aesDecrypt - 18:21:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):665:665 -> aesEncrypt - 22:27:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):666:666 -> aesEncrypt - 28:32:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):667:667 -> aesEncrypt - 33:56:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):669:669 -> aesEncrypt - 57:64:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):670:670 -> aesEncrypt - 1:1:void $r8$lambda$B4KrfEOJArcU85FebqaFlyvqdSY(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> b - # {"id":"com.android.tools.r8.synthesized"} - 24:40:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):626:626 -> btcAddress - 41:47:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):630:630 -> btcAddress - 48:56:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):626:626 -> btcAddress - 57:61:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):628:628 -> btcAddress - 62:70:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):626:626 -> btcAddress - 71:75:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):629:629 -> btcAddress - 76:83:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):626:626 -> btcAddress - 84:88:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):632:632 -> btcAddress - 89:91:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):633:633 -> btcAddress - 92:95:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):627:627 -> btcAddress - 96:101:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):636:636 -> btcAddress - 102:107:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):639:639 -> btcAddress - 108:133:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):642:642 -> btcAddress - 134:160:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):645:645 -> btcAddress - 1:1:void $r8$lambda$BEp_4dmJsXsyKoUoWLgHDzsURMg(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> c - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void $r8$lambda$C0Mo44XyK0xhAOnslWJlEiNST4s(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> d - # {"id":"com.android.tools.r8.synthesized"} - 24:27:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):589:589 -> derivePubkey - 28:33:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):590:590 -> derivePubkey - 34:38:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):591:591 -> derivePubkey - 39:62:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):593:593 -> derivePubkey - 63:89:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):594:594 -> derivePubkey + 18:21:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):847:847 -> aesDecrypt + 22:27:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):848:848 -> aesDecrypt + 28:32:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):849:849 -> aesDecrypt + 33:56:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):851:851 -> aesDecrypt + 57:64:void aesDecrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):852:852 -> aesDecrypt + 18:21:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):835:835 -> aesEncrypt + 22:27:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):836:836 -> aesEncrypt + 28:32:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):837:837 -> aesEncrypt + 33:56:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):839:839 -> aesEncrypt + 57:64:void aesEncrypt(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):840:840 -> aesEncrypt + 1:1:void $r8$lambda$9HwaGS7WMYk8QFnKHOhXQ6FJ6A4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> b + # {"id":"com.android.tools.r8.synthesized"} + 24:40:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):796:796 -> btcAddress + 41:47:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):800:800 -> btcAddress + 48:56:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):796:796 -> btcAddress + 57:61:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):798:798 -> btcAddress + 62:70:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):796:796 -> btcAddress + 71:75:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):799:799 -> btcAddress + 76:83:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):796:796 -> btcAddress + 84:88:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):802:802 -> btcAddress + 89:91:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):803:803 -> btcAddress + 92:95:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):797:797 -> btcAddress + 96:101:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):806:806 -> btcAddress + 102:107:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):809:809 -> btcAddress + 108:133:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):812:812 -> btcAddress + 134:160:void btcAddress(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):815:815 -> btcAddress + 1:1:void $r8$lambda$Gx6yPS5fI-L6PowRwR2UmFfB4WY(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> c + # {"id":"com.android.tools.r8.synthesized"} + 11:14:void cancelMpcSession(java.lang.String,com.facebook.react.bridge.Promise):419:419 -> cancelMpcSession + 15:19:void cancelMpcSession(java.lang.String,com.facebook.react.bridge.Promise):420:420 -> cancelMpcSession + 20:46:void cancelMpcSession(java.lang.String,com.facebook.react.bridge.Promise):422:422 -> cancelMpcSession + 6:9:void cancelNostrMpc(com.facebook.react.bridge.Promise):429:429 -> cancelNostrMpc + 10:14:void cancelNostrMpc(com.facebook.react.bridge.Promise):430:430 -> cancelNostrMpc + 15:41:void cancelNostrMpc(com.facebook.react.bridge.Promise):432:432 -> cancelNostrMpc + 11:14:void computeTxId(java.lang.String,com.facebook.react.bridge.Promise):409:409 -> computeTxId + 15:19:void computeTxId(java.lang.String,com.facebook.react.bridge.Promise):410:410 -> computeTxId + 20:46:void computeTxId(java.lang.String,com.facebook.react.bridge.Promise):412:412 -> computeTxId + 1:1:void $r8$lambda$H9LP91VnYDcTFfOzktwcvIZte_c(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> d + # {"id":"com.android.tools.r8.synthesized"} + 24:27:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):759:759 -> derivePubkey + 28:33:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):760:760 -> derivePubkey + 34:38:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):761:761 -> derivePubkey + 39:62:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):763:763 -> derivePubkey + 63:89:void derivePubkey(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):764:764 -> derivePubkey 12:13:void disableLogging(java.lang.String,com.facebook.react.bridge.Promise):83:83 -> disableLogging 14:16:void disableLogging(java.lang.String,com.facebook.react.bridge.Promise):84:84 -> disableLogging 17:20:void disableLogging(java.lang.String,com.facebook.react.bridge.Promise):85:85 -> disableLogging - 49:50:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):335:335 -> discoverPeers - 51:60:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):344:344 -> discoverPeers - 51:60:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):344 -> discoverPeers - 61:63:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):335:335 -> discoverPeers - 64:67:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):344:344 -> discoverPeers - 64:67:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):344 -> discoverPeers - 1:2:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):336:336 -> discoverPeers$lambda$9 - 3:6:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):337:337 -> discoverPeers$lambda$9 - 7:12:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):338:338 -> discoverPeers$lambda$9 - 13:18:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):339:339 -> discoverPeers$lambda$9 - 19:42:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):341:341 -> discoverPeers$lambda$9 - 43:48:void discoverPeers$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):342:342 -> discoverPeers$lambda$9 - 1:1:void $r8$lambda$Dh1lQGH-zcMLOTVQpmwXQ95OlnU(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> e - # {"id":"com.android.tools.r8.synthesized"} - 8:11:void eciesKeypair(com.facebook.react.bridge.Promise):653:653 -> eciesKeypair - 12:17:void eciesKeypair(com.facebook.react.bridge.Promise):654:654 -> eciesKeypair - 18:22:void eciesKeypair(com.facebook.react.bridge.Promise):655:655 -> eciesKeypair - 23:46:void eciesKeypair(com.facebook.react.bridge.Promise):657:657 -> eciesKeypair - 47:54:void eciesKeypair(com.facebook.react.bridge.Promise):658:658 -> eciesKeypair - 23:26:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):601:601 -> encodeXpub - 27:32:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):602:602 -> encodeXpub - 33:37:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):603:603 -> encodeXpub - 38:61:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):605:605 -> encodeXpub - 62:88:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):606:606 -> encodeXpub - 21:22:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):168:168 -> estimateFees - 23:32:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):179:179 -> estimateFees - 23:32:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):179 -> estimateFees - 33:35:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):168:168 -> estimateFees - 36:39:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):179:179 -> estimateFees - 36:39:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):179 -> estimateFees - 1:2:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):169:169 -> estimateFees$lambda$3 - 3:6:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):170:170 -> estimateFees$lambda$3 - 7:10:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):172:172 -> estimateFees$lambda$3 - 11:16:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):173:173 -> estimateFees$lambda$3 - 17:21:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):174:174 -> estimateFees$lambda$3 - 22:45:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):176:176 -> estimateFees$lambda$3 - 46:49:void estimateFees$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):177:177 -> estimateFees$lambda$3 - 1:1:void $r8$lambda$EvT2vxOMsTcdMY93XkuPxkbGty0(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> f - # {"id":"com.android.tools.r8.synthesized"} - 21:22:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):307:307 -> fetchData - 23:32:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):316:316 -> fetchData - 23:32:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):316 -> fetchData - 33:35:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):307:307 -> fetchData - 36:39:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):316:316 -> fetchData - 36:39:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):316 -> fetchData - 1:2:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):308:308 -> fetchData$lambda$7 - 3:6:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):309:309 -> fetchData$lambda$7 - 7:12:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):310:310 -> fetchData$lambda$7 - 13:17:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):311:311 -> fetchData$lambda$7 - 18:41:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):313:313 -> fetchData$lambda$7 - 42:47:void fetchData$lambda$7(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):314:314 -> fetchData$lambda$7 - 1:1:void $r8$lambda$HE7fQdZaofy1w6-0bN6Zm2duy4c(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> g + 49:50:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):505:505 -> discoverPeers + 51:60:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):514:514 -> discoverPeers + 51:60:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):514 -> discoverPeers + 61:63:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):505:505 -> discoverPeers + 64:67:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):514:514 -> discoverPeers + 64:67:void discoverPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):514 -> discoverPeers + 1:2:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):506:506 -> discoverPeers$lambda$14 + 3:6:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):507:507 -> discoverPeers$lambda$14 + 7:12:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):508:508 -> discoverPeers$lambda$14 + 13:18:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):509:509 -> discoverPeers$lambda$14 + 19:42:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):511:511 -> discoverPeers$lambda$14 + 43:48:void discoverPeers$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):512:512 -> discoverPeers$lambda$14 + 1:1:void $r8$lambda$MQa_hMCLJQF7UmAI2nG1QrPVs8Y(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> e + # {"id":"com.android.tools.r8.synthesized"} + 8:11:void eciesKeypair(com.facebook.react.bridge.Promise):823:823 -> eciesKeypair + 12:17:void eciesKeypair(com.facebook.react.bridge.Promise):824:824 -> eciesKeypair + 18:22:void eciesKeypair(com.facebook.react.bridge.Promise):825:825 -> eciesKeypair + 23:46:void eciesKeypair(com.facebook.react.bridge.Promise):827:827 -> eciesKeypair + 47:54:void eciesKeypair(com.facebook.react.bridge.Promise):828:828 -> eciesKeypair + 23:26:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):771:771 -> encodeXpub + 27:32:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):772:772 -> encodeXpub + 33:37:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):773:773 -> encodeXpub + 38:61:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):775:775 -> encodeXpub + 62:88:void encodeXpub(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):776:776 -> encodeXpub + 26:27:void estimateFeeWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):204:204 -> estimateFeeWithUTXOs + 28:38:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):218:218 -> estimateFeeWithUTXOs + 28:38:void estimateFeeWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):218 -> estimateFeeWithUTXOs + 39:41:void estimateFeeWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):204:204 -> estimateFeeWithUTXOs + 42:45:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):218:218 -> estimateFeeWithUTXOs + 42:45:void estimateFeeWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):218 -> estimateFeeWithUTXOs + 1:2:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):205:205 -> estimateFeeWithUTXOs$lambda$5 + 3:6:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):206:206 -> estimateFeeWithUTXOs$lambda$5 + 7:12:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):212:212 -> estimateFeeWithUTXOs$lambda$5 + 13:17:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):213:213 -> estimateFeeWithUTXOs$lambda$5 + 18:41:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):215:215 -> estimateFeeWithUTXOs$lambda$5 + 42:45:void estimateFeeWithUTXOs$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):216:216 -> estimateFeeWithUTXOs$lambda$5 + 21:22:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):182:182 -> estimateFees + 23:32:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):193:193 -> estimateFees + 23:32:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):193 -> estimateFees + 33:35:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):182:182 -> estimateFees + 36:39:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):193:193 -> estimateFees + 36:39:void estimateFees(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):193 -> estimateFees + 1:2:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):183:183 -> estimateFees$lambda$4 + 3:6:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):184:184 -> estimateFees$lambda$4 + 7:10:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):186:186 -> estimateFees$lambda$4 + 11:16:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):187:187 -> estimateFees$lambda$4 + 17:21:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):188:188 -> estimateFees$lambda$4 + 22:45:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):190:190 -> estimateFees$lambda$4 + 46:49:void estimateFees$lambda$4(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):191:191 -> estimateFees$lambda$4 + 1:1:void $r8$lambda$OmYaHwyzJtKsPj-QpElJnHCRFZI(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> f + # {"id":"com.android.tools.r8.synthesized"} + 21:22:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):477:477 -> fetchData + 23:32:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):486:486 -> fetchData + 23:32:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):486 -> fetchData + 33:35:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):477:477 -> fetchData + 36:39:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):486:486 -> fetchData + 36:39:void fetchData(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):486 -> fetchData + 1:2:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):478:478 -> fetchData$lambda$12 + 3:6:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):479:479 -> fetchData$lambda$12 + 7:12:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):480:480 -> fetchData$lambda$12 + 13:17:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):481:481 -> fetchData$lambda$12 + 18:41:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):483:483 -> fetchData$lambda$12 + 42:47:void fetchData$lambda$12(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):484:484 -> fetchData$lambda$12 + 1:1:void $r8$lambda$P_ox8NfA03LlcrLL4G6LGMT-AhE(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> g # {"id":"com.android.tools.r8.synthesized"} 1:12:java.util.Map getConstants():77:77 -> getConstants 13:17:java.util.Map getConstants():76:76 -> getConstants - 23:30:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):351:351 -> getLanIp - 31:56:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):358:358 -> getLanIp - 57:80:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):360:360 -> getLanIp - 81:84:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):361:361 -> getLanIp - 85:109:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):362:362 -> getLanIp - 110:119:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):363:363 -> getLanIp - 120:129:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):364:364 -> getLanIp - 130:137:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):367:367 -> getLanIp - 138:145:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):371:371 -> getLanIp - 146:201:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):374:374 -> getLanIp - 202:209:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):391:391 -> getLanIp - 210:217:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):392:392 -> getLanIp - 218:225:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):393:393 -> getLanIp - 226:231:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):394:394 -> getLanIp - 232:234:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):395:395 -> getLanIp - 235:240:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):401:401 -> getLanIp - 241:243:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):404:404 -> getLanIp - 244:269:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):405:405 -> getLanIp - 270:273:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):409:409 -> getLanIp + 23:30:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):521:521 -> getLanIp + 31:56:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):528:528 -> getLanIp + 57:80:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):530:530 -> getLanIp + 81:84:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):531:531 -> getLanIp + 85:109:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):532:532 -> getLanIp + 110:119:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):533:533 -> getLanIp + 120:129:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):534:534 -> getLanIp + 130:137:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):537:537 -> getLanIp + 138:145:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):541:541 -> getLanIp + 146:201:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):544:544 -> getLanIp + 202:209:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):561:561 -> getLanIp + 210:217:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):562:562 -> getLanIp + 218:225:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):563:563 -> getLanIp + 226:231:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):564:564 -> getLanIp + 232:234:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):565:565 -> getLanIp + 235:240:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):571:571 -> getLanIp + 241:243:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):574:574 -> getLanIp + 244:269:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):575:575 -> getLanIp + 270:273:void getLanIp(java.lang.String,com.facebook.react.bridge.Promise):579:579 -> getLanIp 1:3:java.lang.String getName():65:65 -> getName - 28:31:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):613:613 -> getOutputDescriptor - 32:37:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):614:614 -> getOutputDescriptor - 38:42:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):615:615 -> getOutputDescriptor - 43:66:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):617:617 -> getOutputDescriptor - 67:93:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):618:618 -> getOutputDescriptor - 1:1:void $r8$lambda$JQIInN-0ChZIJpzWcqe73PLPPBo(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> h - # {"id":"com.android.tools.r8.synthesized"} - 11:12:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):449:449 -> hexToNpub - 13:17:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):458:458 -> hexToNpub - 13:17:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):458 -> hexToNpub - 18:20:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):449:449 -> hexToNpub - 21:24:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):458:458 -> hexToNpub - 21:24:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):458 -> hexToNpub - 1:2:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):450:450 -> hexToNpub$lambda$11 - 3:6:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):451:451 -> hexToNpub$lambda$11 - 7:12:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):452:452 -> hexToNpub$lambda$11 - 13:17:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):453:453 -> hexToNpub$lambda$11 - 18:41:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):455:455 -> hexToNpub$lambda$11 - 42:49:void hexToNpub$lambda$11(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):456:456 -> hexToNpub$lambda$11 - 1:1:void $r8$lambda$S_Jzk3NYxk8T-Tt1AU-5q8mvH5A(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> i - # {"id":"com.android.tools.r8.synthesized"} - 1:15:boolean isClassC(java.lang.String):431:431 -> isClassC + 28:31:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):783:783 -> getOutputDescriptor + 32:37:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):784:784 -> getOutputDescriptor + 38:42:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):785:785 -> getOutputDescriptor + 43:66:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):787:787 -> getOutputDescriptor + 67:93:void getOutputDescriptor(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):788:788 -> getOutputDescriptor + 1:1:void $r8$lambda$QweISMD0qITobKRW4fvIv9VgjE0(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> h + # {"id":"com.android.tools.r8.synthesized"} + 11:12:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):619:619 -> hexToNpub + 13:17:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):628:628 -> hexToNpub + 13:17:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):628 -> hexToNpub + 18:20:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):619:619 -> hexToNpub + 21:24:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):628:628 -> hexToNpub + 21:24:void hexToNpub(java.lang.String,com.facebook.react.bridge.Promise):628 -> hexToNpub + 1:2:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):620:620 -> hexToNpub$lambda$16 + 3:6:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):621:621 -> hexToNpub$lambda$16 + 7:12:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):622:622 -> hexToNpub$lambda$16 + 13:17:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):623:623 -> hexToNpub$lambda$16 + 18:41:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):625:625 -> hexToNpub$lambda$16 + 42:49:void hexToNpub$lambda$16(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):626:626 -> hexToNpub$lambda$16 + 1:1:void $r8$lambda$S-ev3EsYCGwV8XtG6KFJr84whBU(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> i + # {"id":"com.android.tools.r8.synthesized"} + 1:15:boolean isClassC(java.lang.String):601:601 -> isClassC 16:20:java.util.List kotlin.collections.CollectionsKt___CollectionsKt.mapNotNull(java.lang.Iterable,kotlin.jvm.functions.Function1):1617:1617 -> isClassC - 16:20:boolean isClassC(java.lang.String):431 -> isClassC + 16:20:boolean isClassC(java.lang.String):601 -> isClassC 21:34:void kotlin.collections.CollectionsKt___CollectionsKt.forEach(java.lang.Iterable,kotlin.jvm.functions.Function1):1869:1869 -> isClassC - 21:34:boolean isClassC(java.lang.String):431 -> isClassC + 21:34:boolean isClassC(java.lang.String):601 -> isClassC 35:36:java.util.Collection kotlin.collections.CollectionsKt___CollectionsKt.mapNotNullTo(java.lang.Iterable,java.util.Collection,kotlin.jvm.functions.Function1):1625:1625 -> isClassC - 35:36:boolean isClassC(java.lang.String):431 -> isClassC - 37:42:boolean isClassC(java.lang.String):431:431 -> isClassC + 35:36:boolean isClassC(java.lang.String):601 -> isClassC + 37:42:boolean isClassC(java.lang.String):601:601 -> isClassC 43:46:java.util.Collection kotlin.collections.CollectionsKt___CollectionsKt.mapNotNullTo(java.lang.Iterable,java.util.Collection,kotlin.jvm.functions.Function1):1625:1625 -> isClassC - 43:46:boolean isClassC(java.lang.String):431 -> isClassC - 47:75:boolean isClassC(java.lang.String):432:432 -> isClassC - 1:3:boolean isSameSubnet(java.lang.String,java.lang.String):416:416 -> isSameSubnet - 4:16:boolean isSameSubnet(java.lang.String,java.lang.String):417:417 -> isSameSubnet - 17:29:boolean isSameSubnet(java.lang.String,java.lang.String):418:418 -> isSameSubnet - 30:44:boolean isSameSubnet(java.lang.String,java.lang.String):422:422 -> isSameSubnet - 45:59:boolean isSameSubnet(java.lang.String,java.lang.String):423:423 -> isSameSubnet - 60:75:boolean isSameSubnet(java.lang.String,java.lang.String):424:424 -> isSameSubnet + 43:46:boolean isClassC(java.lang.String):601 -> isClassC + 47:75:boolean isClassC(java.lang.String):602:602 -> isClassC + 1:3:boolean isSameSubnet(java.lang.String,java.lang.String):586:586 -> isSameSubnet + 4:16:boolean isSameSubnet(java.lang.String,java.lang.String):587:587 -> isSameSubnet + 17:29:boolean isSameSubnet(java.lang.String,java.lang.String):588:588 -> isSameSubnet + 30:44:boolean isSameSubnet(java.lang.String,java.lang.String):592:592 -> isSameSubnet + 45:59:boolean isSameSubnet(java.lang.String,java.lang.String):593:593 -> isSameSubnet + 60:75:boolean isSameSubnet(java.lang.String,java.lang.String):594:594 -> isSameSubnet 1:1:void $r8$lambda$SubPChAgUX_Pgzrn3hHEZuaQz-s(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> j # {"id":"com.android.tools.r8.synthesized"} - 1:1:void $r8$lambda$ikDevTFBKm6qpBcjUfZOoO-PAGk(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> k + 1:1:void $r8$lambda$Veyq-fwVSz9xazu7Xc_otw_wRk8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> k # {"id":"com.android.tools.r8.synthesized"} - 1:1:void $r8$lambda$kLMNKHmbsxJxre_IoWqCWiLxSRg(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> l + 1:1:void $r8$lambda$WveLL6qnv5rP_9U4melZQCIUYIA(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> l # {"id":"com.android.tools.r8.synthesized"} 1:4:void ld(java.lang.String,java.lang.String):69:69 -> ld 5:7:void ld(java.lang.String,java.lang.String):70:70 -> ld 8:11:void ld(java.lang.String,java.lang.String):71:71 -> ld - 31:32:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):321:321 -> listenForPeers - 33:44:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):330:330 -> listenForPeers - 33:44:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):330 -> listenForPeers - 45:47:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):321:321 -> listenForPeers - 48:51:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):330:330 -> listenForPeers - 48:51:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):330 -> listenForPeers - 1:2:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):322:322 -> listenForPeers$lambda$8 - 3:6:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):323:323 -> listenForPeers$lambda$8 - 7:12:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):324:324 -> listenForPeers$lambda$8 - 13:17:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):325:325 -> listenForPeers$lambda$8 - 18:41:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):327:327 -> listenForPeers$lambda$8 - 42:47:void listenForPeers$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):328:328 -> listenForPeers$lambda$8 - 1:1:void $r8$lambda$pQmcPuc0cFOxMaBdzJ9_FSAgMCM(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> m - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):201:201 -> mpcSendBTC - 2:2:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):223:223 -> mpcSendBTC - 2:2:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):223 -> mpcSendBTC - 3:3:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):201:201 -> mpcSendBTC - 4:4:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):223:223 -> mpcSendBTC - 4:4:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):223 -> mpcSendBTC - 1:1:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):202:202 -> mpcSendBTC$lambda$4 - 2:3:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):215:216 -> mpcSendBTC$lambda$4 - 4:4:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):203:203 -> mpcSendBTC$lambda$4 - 5:6:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):217:218 -> mpcSendBTC$lambda$4 - 7:8:void mpcSendBTC$lambda$4(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):220:221 -> mpcSendBTC$lambda$4 - 67:68:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):713:713 -> mpcSignPSBT - 69:76:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):732:732 -> mpcSignPSBT - 69:76:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):732 -> mpcSignPSBT - 77:79:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):713:713 -> mpcSignPSBT - 80:83:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):732:732 -> mpcSignPSBT - 80:83:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):732 -> mpcSignPSBT - 1:4:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):715:715 -> mpcSignPSBT$lambda$16 - 5:12:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):726:726 -> mpcSignPSBT$lambda$16 - 13:18:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):727:727 -> mpcSignPSBT$lambda$16 - 19:44:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):729:729 -> mpcSignPSBT$lambda$16 - 45:71:void mpcSignPSBT$lambda$16(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):730:730 -> mpcSignPSBT$lambda$16 - 67:68:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):538:538 -> mpcTssSetup - 69:76:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):557:557 -> mpcTssSetup - 69:76:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):557 -> mpcTssSetup - 77:79:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):538:538 -> mpcTssSetup - 80:83:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):557:557 -> mpcTssSetup - 80:83:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):557 -> mpcTssSetup - 1:2:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):539:539 -> mpcTssSetup$lambda$14 - 3:6:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):540:540 -> mpcTssSetup$lambda$14 - 7:13:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):551:551 -> mpcTssSetup$lambda$14 - 14:19:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):552:552 -> mpcTssSetup$lambda$14 - 20:43:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):554:554 -> mpcTssSetup$lambda$14 - 44:70:void mpcTssSetup$lambda$14(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):555:555 -> mpcTssSetup$lambda$14 - 1:1:void $r8$lambda$q4_xxhMrkic6TR8wAb_pGSlik8c(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> n - # {"id":"com.android.tools.r8.synthesized"} - 58:59:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):504:504 -> nostrJoinKeysign - 60:68:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):522:522 -> nostrJoinKeysign - 60:68:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):522 -> nostrJoinKeysign - 69:71:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):504:504 -> nostrJoinKeysign - 72:75:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):522:522 -> nostrJoinKeysign - 72:75:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):522 -> nostrJoinKeysign - 1:2:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):505:505 -> nostrJoinKeysign$lambda$13 - 3:6:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):506:506 -> nostrJoinKeysign$lambda$13 - 7:12:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):516:516 -> nostrJoinKeysign$lambda$13 - 13:18:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):517:517 -> nostrJoinKeysign$lambda$13 - 19:42:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):519:519 -> nostrJoinKeysign$lambda$13 - 43:69:void nostrJoinKeysign$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):520:520 -> nostrJoinKeysign$lambda$13 - 8:11:void nostrKeypair(com.facebook.react.bridge.Promise):438:438 -> nostrKeypair - 12:17:void nostrKeypair(com.facebook.react.bridge.Promise):439:439 -> nostrKeypair - 18:22:void nostrKeypair(com.facebook.react.bridge.Promise):440:440 -> nostrKeypair - 23:46:void nostrKeypair(com.facebook.react.bridge.Promise):442:442 -> nostrKeypair - 47:54:void nostrKeypair(com.facebook.react.bridge.Promise):443:443 -> nostrKeypair - 92:93:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):241:241 -> nostrMpcSendBTC - 94:100:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):263:263 -> nostrMpcSendBTC - 94:100:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):263 -> nostrMpcSendBTC - 101:103:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):241:241 -> nostrMpcSendBTC - 104:107:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):263:263 -> nostrMpcSendBTC - 104:107:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):263 -> nostrMpcSendBTC - 1:1:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):242:242 -> nostrMpcSendBTC$lambda$5 - 2:3:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):254:255 -> nostrMpcSendBTC$lambda$5 - 4:4:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):243:243 -> nostrMpcSendBTC$lambda$5 - 5:6:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):257:258 -> nostrMpcSendBTC$lambda$5 - 7:8:void nostrMpcSendBTC$lambda$5(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):260:261 -> nostrMpcSendBTC$lambda$5 - 40:41:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):745:745 -> nostrMpcSignPSBT - 42:52:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):761:761 -> nostrMpcSignPSBT - 42:52:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):761 -> nostrMpcSignPSBT - 53:55:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):745:745 -> nostrMpcSignPSBT - 56:59:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):761:761 -> nostrMpcSignPSBT - 56:59:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):761 -> nostrMpcSignPSBT - 1:2:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):746:746 -> nostrMpcSignPSBT$lambda$17 - 3:6:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):747:747 -> nostrMpcSignPSBT$lambda$17 - 7:12:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):755:755 -> nostrMpcSignPSBT$lambda$17 - 13:18:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):756:756 -> nostrMpcSignPSBT$lambda$17 - 19:42:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):758:758 -> nostrMpcSignPSBT$lambda$17 - 43:69:void nostrMpcSignPSBT$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):759:759 -> nostrMpcSignPSBT$lambda$17 - 49:50:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):472:472 -> nostrMpcTssSetup - 51:60:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):489:489 -> nostrMpcTssSetup - 51:60:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):489 -> nostrMpcTssSetup - 61:63:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):472:472 -> nostrMpcTssSetup - 64:67:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):489:489 -> nostrMpcTssSetup - 64:67:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):489 -> nostrMpcTssSetup - 1:2:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):473:473 -> nostrMpcTssSetup$lambda$12 - 3:6:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):474:474 -> nostrMpcTssSetup$lambda$12 - 7:12:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):483:483 -> nostrMpcTssSetup$lambda$12 - 13:18:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):484:484 -> nostrMpcTssSetup$lambda$12 - 19:42:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):486:486 -> nostrMpcTssSetup$lambda$12 - 43:69:void nostrMpcTssSetup$lambda$12(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):487:487 -> nostrMpcTssSetup$lambda$12 - 1:1:void $r8$lambda$rPHy5RSVi0FN6XQ4UPN0upW8AtM(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> o + 31:32:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):491:491 -> listenForPeers + 33:44:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):500:500 -> listenForPeers + 33:44:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):500 -> listenForPeers + 45:47:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):491:491 -> listenForPeers + 48:51:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):500:500 -> listenForPeers + 48:51:void listenForPeers(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):500 -> listenForPeers + 1:2:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):492:492 -> listenForPeers$lambda$13 + 3:6:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):493:493 -> listenForPeers$lambda$13 + 7:12:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):494:494 -> listenForPeers$lambda$13 + 13:17:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):495:495 -> listenForPeers$lambda$13 + 18:41:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):497:497 -> listenForPeers$lambda$13 + 42:47:void listenForPeers$lambda$13(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):498:498 -> listenForPeers$lambda$13 + 1:1:void $r8$lambda$XzUDB8YXzTvZalAUBgX5-C2Zsis(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> m + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):240:240 -> mpcSendBTC + 2:2:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):262:262 -> mpcSendBTC + 2:2:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):262 -> mpcSendBTC + 3:3:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):240:240 -> mpcSendBTC + 4:4:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):262:262 -> mpcSendBTC + 4:4:void mpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):262 -> mpcSendBTC + 1:1:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):241:241 -> mpcSendBTC$lambda$6 + 2:3:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):254:255 -> mpcSendBTC$lambda$6 + 4:4:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):242:242 -> mpcSendBTC$lambda$6 + 5:6:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):256:257 -> mpcSendBTC$lambda$6 + 7:8:void mpcSendBTC$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):259:260 -> mpcSendBTC$lambda$6 + 1:1:void mpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):283:283 -> mpcSendBTCWithUTXOs + 2:2:void mpcSendBTCWithUTXOs$lambda$7(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):307:307 -> mpcSendBTCWithUTXOs + 2:2:void mpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):307 -> mpcSendBTCWithUTXOs + 3:3:void mpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):283:283 -> mpcSendBTCWithUTXOs + 4:4:void mpcSendBTCWithUTXOs$lambda$7(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):307:307 -> mpcSendBTCWithUTXOs + 4:4:void mpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):307 -> mpcSendBTCWithUTXOs + 1:2:void mpcSendBTCWithUTXOs$lambda$7(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):284:285 -> mpcSendBTCWithUTXOs$lambda$7 + 3:4:void mpcSendBTCWithUTXOs$lambda$7(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):301:302 -> mpcSendBTCWithUTXOs$lambda$7 + 5:6:void mpcSendBTCWithUTXOs$lambda$7(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):304:305 -> mpcSendBTCWithUTXOs$lambda$7 + 67:68:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):883:883 -> mpcSignPSBT + 69:76:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):902:902 -> mpcSignPSBT + 69:76:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):902 -> mpcSignPSBT + 77:79:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):883:883 -> mpcSignPSBT + 80:83:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):902:902 -> mpcSignPSBT + 80:83:void mpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):902 -> mpcSignPSBT + 1:4:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):885:885 -> mpcSignPSBT$lambda$21 + 5:12:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):896:896 -> mpcSignPSBT$lambda$21 + 13:18:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):897:897 -> mpcSignPSBT$lambda$21 + 19:44:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):899:899 -> mpcSignPSBT$lambda$21 + 45:71:void mpcSignPSBT$lambda$21(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):900:900 -> mpcSignPSBT$lambda$21 + 67:68:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):708:708 -> mpcTssSetup + 69:76:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):727:727 -> mpcTssSetup + 69:76:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):727 -> mpcTssSetup + 77:79:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):708:708 -> mpcTssSetup + 80:83:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):727:727 -> mpcTssSetup + 80:83:void mpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):727 -> mpcTssSetup + 1:2:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):709:709 -> mpcTssSetup$lambda$19 + 3:6:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):710:710 -> mpcTssSetup$lambda$19 + 7:13:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):721:721 -> mpcTssSetup$lambda$19 + 14:19:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):722:722 -> mpcTssSetup$lambda$19 + 20:43:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):724:724 -> mpcTssSetup$lambda$19 + 44:70:void mpcTssSetup$lambda$19(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):725:725 -> mpcTssSetup$lambda$19 + 1:1:void $r8$lambda$YJrwbQyh_Zve7xQgmvfhUpmvMFM(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> n + # {"id":"com.android.tools.r8.synthesized"} + 58:59:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):674:674 -> nostrJoinKeysign + 60:68:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):692:692 -> nostrJoinKeysign + 60:68:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):692 -> nostrJoinKeysign + 69:71:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):674:674 -> nostrJoinKeysign + 72:75:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):692:692 -> nostrJoinKeysign + 72:75:void nostrJoinKeysign(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):692 -> nostrJoinKeysign + 1:2:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):675:675 -> nostrJoinKeysign$lambda$18 + 3:6:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):676:676 -> nostrJoinKeysign$lambda$18 + 7:12:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):686:686 -> nostrJoinKeysign$lambda$18 + 13:18:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):687:687 -> nostrJoinKeysign$lambda$18 + 19:42:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):689:689 -> nostrJoinKeysign$lambda$18 + 43:69:void nostrJoinKeysign$lambda$18(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):690:690 -> nostrJoinKeysign$lambda$18 + 8:11:void nostrKeypair(com.facebook.react.bridge.Promise):608:608 -> nostrKeypair + 12:17:void nostrKeypair(com.facebook.react.bridge.Promise):609:609 -> nostrKeypair + 18:22:void nostrKeypair(com.facebook.react.bridge.Promise):610:610 -> nostrKeypair + 23:46:void nostrKeypair(com.facebook.react.bridge.Promise):612:612 -> nostrKeypair + 47:54:void nostrKeypair(com.facebook.react.bridge.Promise):613:613 -> nostrKeypair + 1:1:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):327:327 -> nostrMpcSendBTC + 2:2:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):350:350 -> nostrMpcSendBTC + 2:2:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):350 -> nostrMpcSendBTC + 3:3:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):327:327 -> nostrMpcSendBTC + 4:4:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):350:350 -> nostrMpcSendBTC + 4:4:void nostrMpcSendBTC(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):350 -> nostrMpcSendBTC + 1:1:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):328:328 -> nostrMpcSendBTC$lambda$8 + 2:4:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):340:342 -> nostrMpcSendBTC$lambda$8 + 5:5:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):329:329 -> nostrMpcSendBTC$lambda$8 + 6:7:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):344:345 -> nostrMpcSendBTC$lambda$8 + 8:9:void nostrMpcSendBTC$lambda$8(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):347:348 -> nostrMpcSendBTC$lambda$8 + 85:86:void nostrMpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):368:368 -> nostrMpcSendBTCWithUTXOs + 87:92:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):389:389 -> nostrMpcSendBTCWithUTXOs + 87:92:void nostrMpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):389 -> nostrMpcSendBTCWithUTXOs + 93:95:void nostrMpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):368:368 -> nostrMpcSendBTCWithUTXOs + 96:99:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):389:389 -> nostrMpcSendBTCWithUTXOs + 96:99:void nostrMpcSendBTCWithUTXOs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):389 -> nostrMpcSendBTCWithUTXOs + 5:37:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):381:381 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 38:41:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):370:370 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 42:47:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):383:383 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 48:51:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):384:384 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 52:75:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):386:386 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 76:102:void nostrMpcSendBTCWithUTXOs$lambda$9(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):387:387 -> nostrMpcSendBTCWithUTXOs$lambda$9 + 40:41:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):915:915 -> nostrMpcSignPSBT + 42:52:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):931:931 -> nostrMpcSignPSBT + 42:52:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):931 -> nostrMpcSignPSBT + 53:55:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):915:915 -> nostrMpcSignPSBT + 56:59:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):931:931 -> nostrMpcSignPSBT + 56:59:void nostrMpcSignPSBT(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):931 -> nostrMpcSignPSBT + 1:2:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):916:916 -> nostrMpcSignPSBT$lambda$22 + 3:6:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):917:917 -> nostrMpcSignPSBT$lambda$22 + 7:12:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):925:925 -> nostrMpcSignPSBT$lambda$22 + 13:18:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):926:926 -> nostrMpcSignPSBT$lambda$22 + 19:42:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):928:928 -> nostrMpcSignPSBT$lambda$22 + 43:69:void nostrMpcSignPSBT$lambda$22(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):929:929 -> nostrMpcSignPSBT$lambda$22 + 49:50:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):642:642 -> nostrMpcTssSetup + 51:60:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):659:659 -> nostrMpcTssSetup + 51:60:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):659 -> nostrMpcTssSetup + 61:63:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):642:642 -> nostrMpcTssSetup + 64:67:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):659:659 -> nostrMpcTssSetup + 64:67:void nostrMpcTssSetup(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):659 -> nostrMpcTssSetup + 1:2:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):643:643 -> nostrMpcTssSetup$lambda$17 + 3:6:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):644:644 -> nostrMpcTssSetup$lambda$17 + 7:12:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):653:653 -> nostrMpcTssSetup$lambda$17 + 13:18:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):654:654 -> nostrMpcTssSetup$lambda$17 + 19:42:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):656:656 -> nostrMpcTssSetup$lambda$17 + 43:69:void nostrMpcTssSetup$lambda$17(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):657:657 -> nostrMpcTssSetup$lambda$17 + 1:1:void $r8$lambda$YN0a_Kn31BykV2b58eRsCMeXZ_U(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> o # {"id":"com.android.tools.r8.synthesized"} 3:8:void onGoLog(java.lang.String):37:37 -> onGoLog 3:7:void onMessage(java.lang.String):31:31 -> onMessage 8:11:void onMessage(java.lang.String):33:33 -> onMessage - 1:1:void $r8$lambda$yYD5Psl2DdKqb_Biv8BsNqnAECo(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> p - # {"id":"com.android.tools.r8.synthesized"} - 11:12:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):766:766 -> parsePSBTDetails - 13:17:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):775:775 -> parsePSBTDetails - 13:17:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):775 -> parsePSBTDetails - 18:20:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):766:766 -> parsePSBTDetails - 21:24:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):775:775 -> parsePSBTDetails - 21:24:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):775 -> parsePSBTDetails - 1:2:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):767:767 -> parsePSBTDetails$lambda$18 - 3:6:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):768:768 -> parsePSBTDetails$lambda$18 - 7:12:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):769:769 -> parsePSBTDetails$lambda$18 - 13:17:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):770:770 -> parsePSBTDetails$lambda$18 - 18:41:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):772:772 -> parsePSBTDetails$lambda$18 - 42:68:void parsePSBTDetails$lambda$18(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):773:773 -> parsePSBTDetails$lambda$18 - 16:17:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):562:562 -> preparams - 18:22:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):571:571 -> preparams - 18:22:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):571 -> preparams - 23:25:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):562:562 -> preparams - 26:29:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):571:571 -> preparams - 26:29:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):571 -> preparams - 1:2:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):563:563 -> preparams$lambda$15 - 3:10:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):564:564 -> preparams$lambda$15 - 11:17:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):565:565 -> preparams$lambda$15 - 18:26:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):566:566 -> preparams$lambda$15 - 27:50:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):568:568 -> preparams$lambda$15 - 51:77:void preparams$lambda$15(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):569:569 -> preparams$lambda$15 - 31:32:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):293:293 -> publishData - 33:44:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):302:302 -> publishData - 33:44:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):302 -> publishData - 45:47:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):293:293 -> publishData - 48:51:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):302:302 -> publishData - 48:51:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):302 -> publishData - 1:2:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):294:294 -> publishData$lambda$6 - 3:6:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):295:295 -> publishData$lambda$6 - 7:12:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):296:296 -> publishData$lambda$6 - 13:17:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):297:297 -> publishData$lambda$6 - 18:41:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):299:299 -> publishData$lambda$6 - 42:47:void publishData$lambda$6(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):300:300 -> publishData$lambda$6 - 28:31:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):577:577 -> recoverPubkey - 32:37:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):578:578 -> recoverPubkey - 38:42:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):579:579 -> recoverPubkey - 43:66:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):581:581 -> recoverPubkey - 67:93:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):582:582 -> recoverPubkey + 1:1:void $r8$lambda$b_pbpXQBNkaFG7zl1OlWI7knxVU(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> p + # {"id":"com.android.tools.r8.synthesized"} + 11:12:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):936:936 -> parsePSBTDetails + 13:17:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):945:945 -> parsePSBTDetails + 13:17:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):945 -> parsePSBTDetails + 18:20:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):936:936 -> parsePSBTDetails + 21:24:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):945:945 -> parsePSBTDetails + 21:24:void parsePSBTDetails(java.lang.String,com.facebook.react.bridge.Promise):945 -> parsePSBTDetails + 1:2:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):937:937 -> parsePSBTDetails$lambda$23 + 3:6:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):938:938 -> parsePSBTDetails$lambda$23 + 7:12:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):939:939 -> parsePSBTDetails$lambda$23 + 13:17:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):940:940 -> parsePSBTDetails$lambda$23 + 18:41:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):942:942 -> parsePSBTDetails$lambda$23 + 42:68:void parsePSBTDetails$lambda$23(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):943:943 -> parsePSBTDetails$lambda$23 + 11:12:void postTx(java.lang.String,com.facebook.react.bridge.Promise):394:394 -> postTx + 13:17:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):403:403 -> postTx + 13:17:void postTx(java.lang.String,com.facebook.react.bridge.Promise):403 -> postTx + 18:20:void postTx(java.lang.String,com.facebook.react.bridge.Promise):394:394 -> postTx + 21:24:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):403:403 -> postTx + 21:24:void postTx(java.lang.String,com.facebook.react.bridge.Promise):403 -> postTx + 1:2:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):395:395 -> postTx$lambda$10 + 3:6:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):396:396 -> postTx$lambda$10 + 7:12:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):397:397 -> postTx$lambda$10 + 13:17:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):398:398 -> postTx$lambda$10 + 18:41:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):400:400 -> postTx$lambda$10 + 42:68:void postTx$lambda$10(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):401:401 -> postTx$lambda$10 + 16:17:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):732:732 -> preparams + 18:22:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):741:741 -> preparams + 18:22:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):741 -> preparams + 23:25:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):732:732 -> preparams + 26:29:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):741:741 -> preparams + 26:29:void preparams(java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):741 -> preparams + 1:2:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):733:733 -> preparams$lambda$20 + 3:10:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):734:734 -> preparams$lambda$20 + 11:17:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):735:735 -> preparams$lambda$20 + 18:26:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):736:736 -> preparams$lambda$20 + 27:50:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):738:738 -> preparams$lambda$20 + 51:77:void preparams$lambda$20(java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):739:739 -> preparams$lambda$20 + 31:32:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):463:463 -> publishData + 33:44:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):472:472 -> publishData + 33:44:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):472 -> publishData + 45:47:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):463:463 -> publishData + 48:51:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):472:472 -> publishData + 48:51:void publishData(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):472 -> publishData + 1:2:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):464:464 -> publishData$lambda$11 + 3:6:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):465:465 -> publishData$lambda$11 + 7:12:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):466:466 -> publishData$lambda$11 + 13:17:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):467:467 -> publishData$lambda$11 + 18:41:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):469:469 -> publishData$lambda$11 + 42:47:void publishData$lambda$11(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):470:470 -> publishData$lambda$11 + 1:1:void $r8$lambda$nlnmf2XV_JudacQ98578T5aJVLw(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> q + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void $r8$lambda$uXgLVjJtHXK_nlQ1a2oapEibDao(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> r + # {"id":"com.android.tools.r8.synthesized"} + 28:31:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):747:747 -> recoverPubkey + 32:37:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):748:748 -> recoverPubkey + 38:42:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):749:749 -> recoverPubkey + 43:66:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):751:751 -> recoverPubkey + 67:93:void recoverPubkey(java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):752:752 -> recoverPubkey 2:5:void removeListeners(int):48:48 -> removeListeners - 13:16:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):269:269 -> runRelay - 17:22:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):270:270 -> runRelay - 23:27:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):271:271 -> runRelay - 28:51:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):273:273 -> runRelay - 52:55:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):274:274 -> runRelay + 13:16:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):439:439 -> runRelay + 17:22:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):440:440 -> runRelay + 23:27:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):441:441 -> runRelay + 28:51:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):443:443 -> runRelay + 52:55:void runRelay(java.lang.String,com.facebook.react.bridge.Promise):444:444 -> runRelay + 1:1:void $r8$lambda$uooOAXrab9CoIOkRvYjCHatsE-E(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> s + # {"id":"com.android.tools.r8.synthesized"} 1:4:void sendLogEvent(java.lang.String,java.lang.String):53:53 -> sendLogEvent 5:9:void sendLogEvent(java.lang.String,java.lang.String):54:54 -> sendLogEvent 10:14:void sendLogEvent(java.lang.String,java.lang.String):55:55 -> sendLogEvent @@ -72557,11 +72617,11 @@ com.boldwallet.BBMTLibNativeModule -> com.boldwallet.BBMTLibNativeModule: 23:27:void setFeePolicy(java.lang.String,com.facebook.react.bridge.Promise):106:106 -> setFeePolicy 28:51:void setFeePolicy(java.lang.String,com.facebook.react.bridge.Promise):108:108 -> setFeePolicy 52:55:void setFeePolicy(java.lang.String,com.facebook.react.bridge.Promise):109:109 -> setFeePolicy - 13:16:void sha256(java.lang.String,com.facebook.react.bridge.Promise):689:689 -> sha256 - 17:22:void sha256(java.lang.String,com.facebook.react.bridge.Promise):690:690 -> sha256 - 23:27:void sha256(java.lang.String,com.facebook.react.bridge.Promise):691:691 -> sha256 - 28:51:void sha256(java.lang.String,com.facebook.react.bridge.Promise):693:693 -> sha256 - 52:78:void sha256(java.lang.String,com.facebook.react.bridge.Promise):694:694 -> sha256 + 13:16:void sha256(java.lang.String,com.facebook.react.bridge.Promise):859:859 -> sha256 + 17:22:void sha256(java.lang.String,com.facebook.react.bridge.Promise):860:860 -> sha256 + 23:27:void sha256(java.lang.String,com.facebook.react.bridge.Promise):861:861 -> sha256 + 28:51:void sha256(java.lang.String,com.facebook.react.bridge.Promise):863:863 -> sha256 + 52:78:void sha256(java.lang.String,com.facebook.react.bridge.Promise):864:864 -> sha256 21:22:void spendingHash(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):152:152 -> spendingHash 23:32:void spendingHash$lambda$2(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):163:163 -> spendingHash 23:32:void spendingHash(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):163 -> spendingHash @@ -72575,52 +72635,72 @@ com.boldwallet.BBMTLibNativeModule -> com.boldwallet.BBMTLibNativeModule: 17:21:void spendingHash$lambda$2(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):158:158 -> spendingHash$lambda$2 22:45:void spendingHash$lambda$2(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):160:160 -> spendingHash$lambda$2 46:49:void spendingHash$lambda$2(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):161:161 -> spendingHash$lambda$2 - 13:16:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):281:281 -> stopRelay - 17:39:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):282:282 -> stopRelay - 40:44:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):283:283 -> stopRelay - 45:68:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):285:285 -> stopRelay - 69:72:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):286:286 -> stopRelay + 21:22:void spendingHashWithUTXOs(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):168:168 -> spendingHashWithUTXOs + 23:32:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):177:177 -> spendingHashWithUTXOs + 23:32:void spendingHashWithUTXOs(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):177 -> spendingHashWithUTXOs + 33:35:void spendingHashWithUTXOs(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):168:168 -> spendingHashWithUTXOs + 36:39:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):177:177 -> spendingHashWithUTXOs + 36:39:void spendingHashWithUTXOs(java.lang.String,java.lang.String,java.lang.String,com.facebook.react.bridge.Promise):177 -> spendingHashWithUTXOs + 1:2:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):169:169 -> spendingHashWithUTXOs$lambda$3 + 3:6:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):170:170 -> spendingHashWithUTXOs$lambda$3 + 7:12:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):171:171 -> spendingHashWithUTXOs$lambda$3 + 13:17:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):172:172 -> spendingHashWithUTXOs$lambda$3 + 18:41:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):174:174 -> spendingHashWithUTXOs$lambda$3 + 42:45:void spendingHashWithUTXOs$lambda$3(java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):175:175 -> spendingHashWithUTXOs$lambda$3 + 13:16:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):451:451 -> stopRelay + 17:39:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):452:452 -> stopRelay + 40:44:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):453:453 -> stopRelay + 45:68:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):455:455 -> stopRelay + 69:72:void stopRelay(java.lang.String,com.facebook.react.bridge.Promise):456:456 -> stopRelay + 1:1:void $r8$lambda$v8RyqVPXSPRS7r38urcbmX0RJXs(java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> t + # {"id":"com.android.tools.r8.synthesized"} 13:16:void totalUTXO(java.lang.String,com.facebook.react.bridge.Promise):116:116 -> totalUTXO 17:22:void totalUTXO(java.lang.String,com.facebook.react.bridge.Promise):117:117 -> totalUTXO 23:27:void totalUTXO(java.lang.String,com.facebook.react.bridge.Promise):118:118 -> totalUTXO 28:51:void totalUTXO(java.lang.String,com.facebook.react.bridge.Promise):120:120 -> totalUTXO 52:55:void totalUTXO(java.lang.String,com.facebook.react.bridge.Promise):121:121 -> totalUTXO + 1:1:void $r8$lambda$y_oXvc5y9cfdul0_BQMNGwJIP2k(java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> u + # {"id":"com.android.tools.r8.synthesized"} com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda0 -> com.boldwallet.a: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$0 -> d + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$0 -> d # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$1 -> e + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$1 -> e # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$2 -> f + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$2 -> f # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$3 -> g + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$3 -> g # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$4 -> h + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$4 -> h # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$5 -> i + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$5 -> i # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$6 -> j + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$6 -> j # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$7 -> k + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$7 -> k # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$8 -> l + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$8 -> l # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$9 -> m + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$9 -> m # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$10 -> n + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$10 -> n # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$11 -> o + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$11 -> o # {"id":"com.android.tools.r8.synthesized"} - com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$12 -> p + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$12 -> p # {"id":"com.android.tools.r8.synthesized"} - com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$173c383d6ec67b01fdc787671261b4a3a96b70798abcee0fc7e389b26573eeb5$0.f$13 -> q + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$13 -> q # {"id":"com.android.tools.r8.synthesized"} - 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$14 -> r + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$158d439bf3753733c25261090101002fda0cdffc824c19523f04364aa6880147$0.f$15 -> s + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda1 -> com.boldwallet.h: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda1 -> com.boldwallet.l: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$1c38f99b7c1576fcd2601cd140e06fe1a04f3f9ad8de96f552cfffe00a27a15e$0.f$0 -> d @@ -72643,6 +72723,92 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda1 -> com.boldwallet.h # {"id":"com.android.tools.r8.synthesized"} com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda10 -> com.boldwallet.b: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8753535261c2f0395361c92d57737080da0371b0a9298a5b3e4a399735b78e62$0.f$5 -> i + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda11 -> com.boldwallet.c: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$5 -> i + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$6 -> j + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda12 -> com.boldwallet.d: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$5 -> i + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$6 -> j + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$7 -> k + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$8 -> l + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$9 -> m + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$10 -> n + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda13 -> com.boldwallet.e: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$b269b8fa7ab26f710a91cacfb27ba62a4c35255d31846591c1aa539b99d4e2ff$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$b269b8fa7ab26f710a91cacfb27ba62a4c35255d31846591c1aa539b99d4e2ff$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$b269b8fa7ab26f710a91cacfb27ba62a4c35255d31846591c1aa539b99d4e2ff$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$b269b8fa7ab26f710a91cacfb27ba62a4c35255d31846591c1aa539b99d4e2ff$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$b269b8fa7ab26f710a91cacfb27ba62a4c35255d31846591c1aa539b99d4e2ff$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda14 -> com.boldwallet.f: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$ba1fb41ed97a276e0660780e342e5362c171b0b922927d4826be589d88a5ce21$0.f$0 -> d # {"id":"com.android.tools.r8.synthesized"} @@ -72666,7 +72832,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda10 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda11 -> com.boldwallet.c: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda15 -> com.boldwallet.g: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$caa077ddadbee96f00da4f38f7553a74c7e724e247baebacc521cfb060a583ed$0.f$0 -> d @@ -72689,7 +72855,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda11 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda12 -> com.boldwallet.d: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda16 -> com.boldwallet.h: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$cb3e0800a117222ed73dd6a58df9f09f6c3c6c1dca81cff33e0e464bf7c7afc2$0.f$0 -> d @@ -72706,7 +72872,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda12 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda13 -> com.boldwallet.e: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda17 -> com.boldwallet.i: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$cf95996836445f24056e7f445e312e8af45aa2622af278dd6c011317bd64c43e$0.f$0 -> d @@ -72731,7 +72897,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda13 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda14 -> com.boldwallet.f: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda18 -> com.boldwallet.j: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$ecd3dd064cb5b9d5e00793e666a12e7ece84329f979112741e24eb6b2e0ab64a$0.f$0 -> d @@ -72746,7 +72912,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda14 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda15 -> com.boldwallet.g: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda19 -> com.boldwallet.k: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$eeec1b7083799dc7fc36153a6f99942e8a2e9f1d5c4f55b57736642b73e50d86$0.f$0 -> d @@ -72763,7 +72929,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda15 -> com.boldwallet. # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda2 -> com.boldwallet.i: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda2 -> com.boldwallet.n: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$274b62fafc34a84aa3b2b4352258b3b889bdd0ff3c91d6abe60fd42c79251e32$0.f$0 -> d @@ -72776,7 +72942,57 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda2 -> com.boldwallet.i # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda3 -> com.boldwallet.j: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda20 -> com.boldwallet.m: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$f7c2fbbabcc7abec31e98f1faa2a4000af8a17f5f5cf75500ff3524916c96576$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$f7c2fbbabcc7abec31e98f1faa2a4000af8a17f5f5cf75500ff3524916c96576$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$f7c2fbbabcc7abec31e98f1faa2a4000af8a17f5f5cf75500ff3524916c96576$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda3 -> com.boldwallet.o: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$5 -> i + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$6 -> j + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$7 -> k + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$8 -> l + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$9 -> m + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$10 -> n + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$11 -> o + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$12 -> p + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$13 -> q + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$4204f4fb0a00f31ad06dff900c75bd10a8baa18fae7b5925c9b4eb3601c5ccdf$0.f$14 -> r + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda4 -> com.boldwallet.p: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$468f8fc0141e15c891ae1b7290faf0fc5f150619576cdfbbd25b16673fda6939$0.f$0 -> d @@ -72789,7 +73005,40 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda3 -> com.boldwallet.j # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda4 -> com.boldwallet.k: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda5 -> com.boldwallet.q: +# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} +# {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$0 -> d + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$1 -> e + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$2 -> f + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$3 -> g + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$4 -> h + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$5 -> i + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$6 -> j + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$7 -> k + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$8 -> l + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$9 -> m + # {"id":"com.android.tools.r8.synthesized"} + java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$10 -> n + # {"id":"com.android.tools.r8.synthesized"} + com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$11 -> o + # {"id":"com.android.tools.r8.synthesized"} + com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$546c374b82bea37566d68638a69fbce1948911ce0db3cf677796e137660a1f80$0.f$12 -> p + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> + # {"id":"com.android.tools.r8.synthesized"} + 1:1:void run():0:0 -> run + # {"id":"com.android.tools.r8.synthesized"} +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda6 -> com.boldwallet.r: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$6ecf0fe02e06b6f28d3eab08dc839f4b10adbd10d7a7887d1ae2037beb65b8a4$0.f$0 -> d @@ -72806,7 +73055,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda4 -> com.boldwallet.k # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda5 -> com.boldwallet.l: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda7 -> com.boldwallet.s: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$7010fa270982d74d6460fae9793350b2caea8046c53f3feaadd801c1fe1e310e$0.f$0 -> d @@ -72845,7 +73094,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda5 -> com.boldwallet.l # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda6 -> com.boldwallet.m: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda8 -> com.boldwallet.t: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$7725d450759678c6d9be24f76e642d4eb475989368a915c7305834df1ce77ef2$0.f$0 -> d @@ -72874,7 +73123,7 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda6 -> com.boldwallet.m # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda7 -> com.boldwallet.n: +com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda9 -> com.boldwallet.u: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$86dfb80bb0ba8bdd0c415c8ac0f435e6ada4b978296afabd555ac03039be9e98$0.f$0 -> d @@ -72901,56 +73150,6 @@ com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda7 -> com.boldwallet.n # {"id":"com.android.tools.r8.synthesized"} 1:1:void run():0:0 -> run # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda8 -> com.boldwallet.o: -# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} -# {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$0 -> d - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$1 -> e - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$2 -> f - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$3 -> g - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$4 -> h - # {"id":"com.android.tools.r8.synthesized"} - com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$5 -> i - # {"id":"com.android.tools.r8.synthesized"} - com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$8e61454d3debe4783c1d7d69c5e1f507663b95088b314401afe0a3db0e213aab$0.f$6 -> j - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void run():0:0 -> run - # {"id":"com.android.tools.r8.synthesized"} -com.boldwallet.BBMTLibNativeModule$$ExternalSyntheticLambda9 -> com.boldwallet.p: -# {"id":"sourceFile","fileName":"R8$$SyntheticClass"} -# {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$0 -> d - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$1 -> e - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$2 -> f - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$3 -> g - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$4 -> h - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$5 -> i - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$6 -> j - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$7 -> k - # {"id":"com.android.tools.r8.synthesized"} - java.lang.String com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$8 -> l - # {"id":"com.android.tools.r8.synthesized"} - com.boldwallet.BBMTLibNativeModule com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$9 -> m - # {"id":"com.android.tools.r8.synthesized"} - com.facebook.react.bridge.Promise com.boldwallet.BBMTLibNativeModule$$InternalSyntheticLambda$1$a1e33d3b43876bed48993a44c9d69885b319c47e2dd981e8c082a02cd5267fdb$0.f$10 -> n - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void (java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String,com.boldwallet.BBMTLibNativeModule,com.facebook.react.bridge.Promise):0:0 -> - # {"id":"com.android.tools.r8.synthesized"} - 1:1:void run():0:0 -> run - # {"id":"com.android.tools.r8.synthesized"} com.boldwallet.BBMTLibNativePackage -> com.boldwallet.BBMTLibNativePackage: # {"id":"sourceFile","fileName":"BBMTLibNativePackage.kt"} 1:4:void ():8:8 -> @@ -73067,7 +73266,7 @@ com.boldwallet.MainApplication -> com.boldwallet.MainApplication: 75:82:com.facebook.react.ReactHost reactHost_delegate$lambda$1(com.boldwallet.MainApplication):29:29 -> reactHost_delegate$lambda$1 83:98:com.facebook.react.ReactHost reactHost_delegate$lambda$1(com.boldwallet.MainApplication):21:21 -> reactHost_delegate$lambda$1 99:103:com.facebook.react.ReactHost reactHost_delegate$lambda$1(com.boldwallet.MainApplication):19:19 -> reactHost_delegate$lambda$1 -com.boldwallet.MainApplication$$ExternalSyntheticLambda0 -> com.boldwallet.q: +com.boldwallet.MainApplication$$ExternalSyntheticLambda0 -> com.boldwallet.v: # {"id":"sourceFile","fileName":"R8$$SyntheticClass"} # {"id":"com.android.tools.r8.synthesized"} com.boldwallet.MainApplication com.boldwallet.MainApplication$$InternalSyntheticLambda$1$c34d62ab7f87fbba2ec12a60f8979a51f9b7d50ed1c4299660909eff58f1cb90$0.f$0 -> d @@ -90332,32 +90531,32 @@ com.facebook.imagepipeline.producers.DecodeProducer -> com.facebook.imagepipelin # {"id":"com.android.tools.r8.residualsignature","signature":"LB0/n;"} com.facebook.imagepipeline.producers.DecodeProducer$Companion Companion -> m # {"id":"com.android.tools.r8.residualsignature","signature":"Lcom/facebook/imagepipeline/producers/p$a;"} - 1:1:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):49:49 -> + 41:43:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):49:49 -> # {"id":"com.android.tools.r8.residualsignature","signature":"(LE0/a;Ljava/util/concurrent/Executor;Ll1/c;Ll1/e;Li1/n;ZZLcom/facebook/imagepipeline/producers/d0;ILi1/a;Ljava/lang/Runnable;LB0/n;)V"} - 2:2:com.facebook.common.memory.ByteArrayPool getByteArrayPool():50:50 -> - 2:2:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):50 -> - 3:3:java.util.concurrent.Executor getExecutor():51:51 -> - 3:3:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):51 -> - 4:4:com.facebook.imagepipeline.decoder.ImageDecoder getImageDecoder():52:52 -> - 4:4:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):52 -> - 5:5:com.facebook.imagepipeline.decoder.ProgressiveJpegConfig getProgressiveJpegConfig():53:53 -> - 5:5:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):53 -> - 6:6:com.facebook.imagepipeline.core.DownsampleMode getDownsampleMode():54:54 -> - 6:6:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):54 -> - 7:7:boolean getDownsampleEnabledForNetwork():55:55 -> - 7:7:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):55 -> - 8:8:boolean getDecodeCancellationEnabled():56:56 -> - 8:8:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):56 -> - 9:9:com.facebook.imagepipeline.producers.Producer getInputProducer():57:57 -> - 9:9:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):57 -> - 10:10:int getMaxBitmapDimension():58:58 -> - 10:10:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):58 -> - 11:11:com.facebook.imagepipeline.core.CloseableReferenceFactory getCloseableReferenceFactory():59:59 -> - 11:11:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):59 -> - 12:12:java.lang.Runnable getReclaimMemoryRunnable():60:60 -> - 12:12:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):60 -> - 13:13:com.facebook.common.internal.Supplier getRecoverFromDecoderOOM():61:61 -> - 13:13:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):61 -> + 44:45:com.facebook.common.memory.ByteArrayPool getByteArrayPool():50:50 -> + 44:45:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):50 -> + 46:47:java.util.concurrent.Executor getExecutor():51:51 -> + 46:47:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):51 -> + 48:49:com.facebook.imagepipeline.decoder.ImageDecoder getImageDecoder():52:52 -> + 48:49:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):52 -> + 50:51:com.facebook.imagepipeline.decoder.ProgressiveJpegConfig getProgressiveJpegConfig():53:53 -> + 50:51:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):53 -> + 52:53:com.facebook.imagepipeline.core.DownsampleMode getDownsampleMode():54:54 -> + 52:53:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):54 -> + 54:55:boolean getDownsampleEnabledForNetwork():55:55 -> + 54:55:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):55 -> + 56:57:boolean getDecodeCancellationEnabled():56:56 -> + 56:57:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):56 -> + 58:59:com.facebook.imagepipeline.producers.Producer getInputProducer():57:57 -> + 58:59:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):57 -> + 60:61:int getMaxBitmapDimension():58:58 -> + 60:61:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):58 -> + 62:63:com.facebook.imagepipeline.core.CloseableReferenceFactory getCloseableReferenceFactory():59:59 -> + 62:63:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):59 -> + 64:65:java.lang.Runnable getReclaimMemoryRunnable():60:60 -> + 64:65:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):60 -> + 66:68:com.facebook.common.internal.Supplier getRecoverFromDecoderOOM():61:61 -> + 66:68:void (com.facebook.common.memory.ByteArrayPool,java.util.concurrent.Executor,com.facebook.imagepipeline.decoder.ImageDecoder,com.facebook.imagepipeline.decoder.ProgressiveJpegConfig,com.facebook.imagepipeline.core.DownsampleMode,boolean,boolean,com.facebook.imagepipeline.producers.Producer,int,com.facebook.imagepipeline.core.CloseableReferenceFactory,java.lang.Runnable,com.facebook.common.internal.Supplier):61 -> 11:16:java.lang.Object com.facebook.imagepipeline.systrace.FrescoSystrace.traceSection(java.lang.String,kotlin.jvm.functions.Function0):40:40 -> b 11:16:void produceResults(com.facebook.imagepipeline.producers.Consumer,com.facebook.imagepipeline.producers.ProducerContext):68 -> b # {"id":"com.android.tools.r8.residualsignature","signature":"(Lcom/facebook/imagepipeline/producers/n;Lcom/facebook/imagepipeline/producers/e0;)V"} @@ -203129,11 +203328,15 @@ okhttp3.Address -> N3.a: # {"id":"com.android.tools.r8.residualsignature","signature":"LN3/b;"} java.net.Proxy proxy -> j java.net.ProxySelector proxySelector -> k - 1:1:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):34:34 -> + 36:54:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):34:34 -> # {"id":"com.android.tools.r8.residualsignature","signature":"(Ljava/lang/String;ILN3/q;Ljavax/net/SocketFactory;Ljavax/net/ssl/SSLSocketFactory;Ljavax/net/ssl/HostnameVerifier;LN3/g;LN3/b;Ljava/net/Proxy;Ljava/util/List;Ljava/util/List;Ljava/net/ProxySelector;)V"} - 2:6:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):74:78 -> - 7:7:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):84:84 -> - 8:8:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):87:87 -> + 55:61:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):74:74 -> + 62:70:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):75:75 -> + 71:74:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):76:76 -> + 75:78:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):77:77 -> + 79:84:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):78:78 -> + 85:90:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):84:84 -> + 91:97:void (java.lang.String,int,okhttp3.Dns,javax.net.SocketFactory,javax.net.ssl.SSLSocketFactory,javax.net.ssl.HostnameVerifier,okhttp3.CertificatePinner,okhttp3.Authenticator,java.net.Proxy,java.util.List,java.util.List,java.net.ProxySelector):87:87 -> 1:3:okhttp3.CertificatePinner certificatePinner():50:50 -> a # {"id":"com.android.tools.r8.residualsignature","signature":"()LN3/g;"} 1:3:java.util.List connectionSpecs():86:86 -> b @@ -205263,9 +205466,9 @@ okhttp3.HttpUrl$Companion -> N3.u$b: 91:93:java.lang.String canonicalize$okhttp(java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset):1794:1794 -> a 94:98:java.lang.String canonicalize$okhttp(java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset):1805:1805 -> a 99:108:java.lang.String canonicalize$okhttp(java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset):1811:1811 -> a - 1:1:java.lang.String canonicalize$okhttp$default(okhttp3.HttpUrl$Companion,java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset,int,java.lang.Object):1772:1772 -> b + 11:39:java.lang.String canonicalize$okhttp$default(okhttp3.HttpUrl$Companion,java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset,int,java.lang.Object):1772:1772 -> b # {"id":"com.android.tools.r8.residualsignature","signature":"(LN3/u$b;Ljava/lang/String;IILjava/lang/String;ZZZZLjava/nio/charset/Charset;ILjava/lang/Object;)Ljava/lang/String;"} - 2:2:java.lang.String canonicalize$okhttp$default(okhttp3.HttpUrl$Companion,java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset,int,java.lang.Object):1778:1778 -> b + 40:44:java.lang.String canonicalize$okhttp$default(okhttp3.HttpUrl$Companion,java.lang.String,int,int,java.lang.String,boolean,boolean,boolean,boolean,java.nio.charset.Charset,int,java.lang.Object):1778:1778 -> b 6:20:int defaultPort(java.lang.String):1573:1573 -> c 21:31:int defaultPort(java.lang.String):1575:1575 -> c 32:44:int defaultPort(java.lang.String):1574:1574 -> c @@ -216574,3 +216777,24 @@ tss.UTXO -> tss.UTXO: 16:32:java.lang.String toString():72:72 -> toString 33:47:java.lang.String toString():73:73 -> toString 48:57:java.lang.String toString():74:74 -> toString +tss.UTXOWithPath -> tss.UTXOWithPath: +# {"id":"sourceFile","fileName":"UTXOWithPath.java"} + 1:4:void ():16:16 -> + 1:1:void (int):25:25 -> + 2:2:void ():27:27 -> + 4:8:boolean equals(java.lang.Object):40:40 -> equals + 9:10:boolean equals(java.lang.Object):43:43 -> equals + 11:14:boolean equals(java.lang.Object):46:46 -> equals + 15:23:boolean equals(java.lang.Object):47:47 -> equals + 24:30:boolean equals(java.lang.Object):52:52 -> equals + 31:34:boolean equals(java.lang.Object):55:55 -> equals + 35:43:boolean equals(java.lang.Object):56:56 -> equals + 44:53:boolean equals(java.lang.Object):61:61 -> equals + 1:17:int hashCode():68:68 -> hashCode + 1:5:int incRefnum():21:21 -> incRefnum + 6:8:int incRefnum():22:22 -> incRefnum + 1:5:java.lang.String toString():72:72 -> toString + 6:15:java.lang.String toString():73:73 -> toString + 16:32:java.lang.String toString():74:74 -> toString + 33:47:java.lang.String toString():75:75 -> toString + 48:57:java.lang.String toString():76:76 -> toString diff --git a/components/Header.tsx b/components/Header.tsx index 8862406b..5f43f955 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,10 +1,5 @@ import React from 'react'; -import { - Text, - View as RNView, - StyleSheet, - Platform, -} from 'react-native'; +import {Text, View as RNView, StyleSheet, Platform} from 'react-native'; import AppPressable from './AppPressable'; import {Image} from 'react-native'; import {View} from 'react-native'; @@ -39,7 +34,11 @@ export const HeaderNetworkProvider: React.FC = ({ const {theme} = useTheme(); const styles = createStyles(theme); const cleanProviderUrl = apiBase - ? apiBase.replace('https://', '').replace('/api', '').replace(/\/+$/, '') + ? apiBase + .replace('https://', '') + .replace('http://', '') + .replace('/api', '') + .replace(/\/+$/, '') : 'Loading...'; const providerHost = cleanProviderUrl.includes('/') ? cleanProviderUrl.split('/')[0] @@ -267,7 +266,11 @@ interface HeaderProviderProps { export const HeaderProvider: React.FC = ({apiBase}) => { const {theme} = useTheme(); const cleanProviderUrl = apiBase - ? apiBase.replace('https://', '').replace('/api', '').replace(/\/+$/, '') + ? apiBase + .replace('https://', '') + .replace('http://', '') + .replace('/api', '') + .replace(/\/+$/, '') : ''; const providerHost = cleanProviderUrl.includes('/') ? cleanProviderUrl.split('/')[0] diff --git a/components/LegacyWalletModal.tsx b/components/LegacyWalletModal.tsx index 095e4f5c..8f928e00 100644 --- a/components/LegacyWalletModal.tsx +++ b/components/LegacyWalletModal.tsx @@ -3,7 +3,7 @@ import {Modal, View, Text, Image, Pressable} from 'react-native'; import AppPressable from './AppPressable'; import {useTheme} from '../theme'; import {createStyles} from './Styles'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; interface LegacyWalletModalProps { visible: boolean; onCancel: () => void; @@ -101,18 +101,11 @@ const LegacyWalletModal: React.FC = ({ }; const handleCancel = async () => { // Save checkbox state: "yes" = do not remind, "no" = show again - await LocalCache.setItem( - 'legacyWalletModalDoNotRemind', - doNotRemind ? 'yes' : 'no', - ); + appConfigRepository.set(CONFIG_KEYS.LEGACY_WALLET_DO_NOT_REMIND, doNotRemind ? 'yes' : 'no'); onCancel(); }; const handleUnderstand = async () => { - // Save checkbox state: "yes" = do not remind, "no" = show again - await LocalCache.setItem( - 'legacyWalletModalDoNotRemind', - doNotRemind ? 'yes' : 'no', - ); + appConfigRepository.set(CONFIG_KEYS.LEGACY_WALLET_DO_NOT_REMIND, doNotRemind ? 'yes' : 'no'); onUnderstand(); }; const handleCheckboxToggle = () => { diff --git a/components/RestoringIndexesModal.tsx b/components/RestoringIndexesModal.tsx new file mode 100644 index 00000000..0d863f05 --- /dev/null +++ b/components/RestoringIndexesModal.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import {Modal, View, StyleSheet, ActivityIndicator} from 'react-native'; +import AppText from './AppText'; +import {useTheme} from '../theme'; +import {GAP_LIMIT} from '../utils'; + +/** + * Non-dismissible modal shown during restore discovery / chain indexing. + * Used when clearing storage or importing keyshare. + * + * phase — optional free-text label displayed instead of the chain scan + * progress when a post-discovery sync step is running + * (e.g. "Syncing balances…", "Syncing transactions…"). + */ +const RestoringIndexesModal: React.FC<{ + visible: boolean; + chain?: 'external' | 'internal'; + index?: number; + gapIndex?: number; + phase?: string; +}> = ({visible, chain, index = 0, gapIndex = 0, phase}) => { + const {theme} = useTheme(); + const chainLabel = chain === 'external' ? 'Receive' : chain === 'internal' ? 'Change' : null; + const styles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.modalBackdrop, + }, + content: { + backgroundColor: theme.colors.cardBackground, + padding: 24, + borderRadius: 12, + alignItems: 'center', + minWidth: 240, + borderWidth: 1, + borderColor: + theme.colors.background === '#ffffff' + ? theme.colors.blackOverlay10 + : theme.colors.whiteOverlay20, + }, + spinner: { + marginBottom: 16, + }, + title: { + fontSize: theme.fontSizes?.lg || 16, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + textAlign: 'center', + marginBottom: 4, + }, + subtitle: { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + }); + + return ( + { + // Non-dismissible: ignore back button + }} + statusBarTranslucent> + + + + + {phase ? 'Syncing wallet' : 'Restoring indexes'} + + + {phase + ? phase + : chainLabel + ? `Scanning ${chainLabel} chain… index ${index}, gap ${gapIndex}/${GAP_LIMIT}` + : 'Scanning chain for addresses…'} + + + + + ); +}; + +export default RestoringIndexesModal; diff --git a/components/SignedTxBroadcastModal.tsx b/components/SignedTxBroadcastModal.tsx new file mode 100644 index 00000000..b9b69d47 --- /dev/null +++ b/components/SignedTxBroadcastModal.tsx @@ -0,0 +1,298 @@ +import React, {useState} from 'react'; +import { + Modal, + View, + Text, + Image, + ActivityIndicator, + Alert, + StyleSheet, +} from 'react-native'; +import Share from 'react-native-share'; +import RNFS from 'react-native-fs'; +import Clipboard from '@react-native-clipboard/clipboard'; +import AppPressable from './AppPressable'; +import {BBMTLibNativeModule} from '../native_modules'; +import {useTheme} from '../theme'; +import {dbg} from '../utils'; + +interface SignedTxBroadcastModalProps { + visible: boolean; + rawTxHex: string; + onBroadcastSuccess: (txId: string) => void; + onClose: () => void; +} + +const SignedTxBroadcastModal: React.FC = ({ + visible, + rawTxHex, + onBroadcastSuccess, + onClose, +}) => { + const {theme} = useTheme(); + const [broadcasting, setBroadcasting] = useState(false); + + const handleCopy = () => { + Clipboard.setString(rawTxHex); + Alert.alert('Copied', 'Raw transaction copied to clipboard'); + }; + + const handleShare = async () => { + try { + const txid = await BBMTLibNativeModule.computeTxId(rawTxHex); + const filename = `${txid}.txt`; + const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath; + const filePath = `${tempDir}/${filename}`; + await RNFS.writeFile(filePath, rawTxHex, 'utf8'); + await Share.open({ + title: 'Signed transaction', + message: 'Raw signed transaction (broadcast when ready)', + url: `file://${filePath}`, + type: 'text/plain', + filename, + failOnCancel: false, + }); + await RNFS.unlink(filePath).catch(() => {}); + } catch (e: any) { + dbg('SignedTxBroadcastModal share error', e); + Alert.alert('Error', e?.message || 'Failed to share transaction'); + } + }; + + const handleBroadcast = async () => { + if (broadcasting || !rawTxHex) return; + setBroadcasting(true); + try { + const txId = await BBMTLibNativeModule.postTx(rawTxHex); + if (txId && /^[a-fA-F0-9]{64}$/.test(txId)) { + onBroadcastSuccess(txId); + } else { + throw new Error(txId || 'Invalid txid from broadcast'); + } + } catch (e: any) { + dbg('SignedTxBroadcastModal broadcast error', e); + Alert.alert( + 'Broadcast failed', + e?.message || 'Failed to broadcast transaction', + ); + } finally { + setBroadcasting(false); + } + }; + + if (!visible) return null; + + return ( + + + + + + Signed transaction + + + + × + + + + + + Your transaction is signed but not broadcast yet. You can copy/share + the raw serialized transaction, or broadcast it. + + + + + + + Copy + + + + + + + Share + + + + + {broadcasting ? ( + + ) : ( + <> + + + Broadcast + + + )} + + + + + + ); +}; + +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: 20, + }, + card: { + borderRadius: 16, + padding: 18, + width: '100%', + maxWidth: 360, + borderWidth: 1, + shadowOffset: {width: 0, height: 6}, + shadowOpacity: 0.18, + shadowRadius: 18, + elevation: 6, + }, + title: { + flex: 1, + fontSize: 18, + fontWeight: '700', + textAlign: 'left', + }, + headerRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 10, + }, + closeButton: { + width: 36, + height: 36, + borderRadius: 18, + alignItems: 'center', + justifyContent: 'center', + }, + closeButtonText: { + fontSize: 26, + lineHeight: 28, + fontWeight: '400', + opacity: 0.8, + marginTop: -2, + }, + hint: { + fontSize: 13, + marginBottom: 16, + textAlign: 'left', + lineHeight: 18, + }, + actionsRow: { + flexDirection: 'row', + alignItems: 'stretch', + justifyContent: 'space-between', + gap: 10, + }, + actionButton: { + flex: 1, + minHeight: 72, + borderRadius: 12, + paddingVertical: 12, + paddingHorizontal: 10, + borderWidth: 1, + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + broadcastButton: { + borderWidth: 0, + }, + actionIcon: { + width: 22, + height: 22, + opacity: 0.95, + }, + actionText: { + fontSize: 12, + fontWeight: '700', + }, +}); + +export default SignedTxBroadcastModal; diff --git a/components/TransactionDetailsModal.tsx b/components/TransactionDetailsModal.tsx index ddc05b7a..f749195f 100644 --- a/components/TransactionDetailsModal.tsx +++ b/components/TransactionDetailsModal.tsx @@ -3,6 +3,7 @@ import { Modal, View, Text, + Image, StyleSheet, ScrollView, Linking, @@ -18,9 +19,14 @@ interface TransactionDetailsModalProps { transaction: any; baseApi: string; selectedCurrency: string; - btcRate: number; + /** Historical BTC rate at tx time; fiat shown only when set. */ + historicalRate: number | null; getCurrencySymbol: (currency: string) => string; - address: string; + /** HD address → path map, used to annotate our inputs/outputs with derivation path. */ + addressPathMap?: Record< + string, + {derivationPath: string; chain: 'receive' | 'change'; index: number} + > | null; status: { confirmed: boolean; text: string; @@ -38,9 +44,9 @@ const TransactionDetailsModal: React.FC = ({ transaction, baseApi, selectedCurrency, - btcRate, + historicalRate, getCurrencySymbol, - address, + addressPathMap, status, amounts, isBlurred = false, @@ -89,13 +95,12 @@ const TransactionDetailsModal: React.FC = ({ if (!transaction || !status || !amounts) { return null; } - const getFiatAmount = (btcAmount: number) => { - if (!btcRate || btcRate <= 0) { - return '0.00'; - } - const amount = btcAmount * btcRate; + const getFiatAmount = (btcAmount: number): string | null => { + if (historicalRate == null || historicalRate <= 0) return null; + const amount = btcAmount * historicalRate; return amount.toFixed(2); }; + const hasFiat = historicalRate != null && historicalRate > 0; const isSent = status.text.includes('Sen') || transaction.sentAt; const amount = isSent ? amounts.sent : amounts.received; const hasValidAmount = typeof amount === 'number' && Number.isFinite(amount); @@ -103,70 +108,6 @@ const TransactionDetailsModal: React.FC = ({ typeof amounts.sent === 'number' && Number.isFinite(amounts.sent); const hasValidReceived = typeof amounts.received === 'number' && Number.isFinite(amounts.received); - // Get the relevant address(es) with amounts based on transaction type - // For sent: show ALL recipient addresses with their amounts (all outputs that aren't the sender's address) - // For received: show ALL input addresses (excluding the receiver's own address if it appears) - interface AddressWithAmount { - address: string; - amount: number; // in BTC - } - let relevantAddresses: AddressWithAmount[] = []; - let addressLabel = ''; - if (isSent) { - // Sent transaction: show ALL recipient addresses with their amounts - const recipientOutputs = - transaction.vout?.filter((output: any) => { - // Exclude outputs that match the sender's address (change outputs) - return ( - output.scriptpubkey_address && output.scriptpubkey_address !== address - ); - }) || []; - // Group by address and sum amounts (in case same address appears multiple times) - const addressAmountMap = new Map(); - recipientOutputs.forEach((output: any) => { - const addr = output.scriptpubkey_address; - const amountSats = output.value || 0; - const currentAmount = addressAmountMap.get(addr) || 0; - addressAmountMap.set(addr, currentAmount + amountSats); - }); - // Convert to array with amounts in BTC - relevantAddresses = Array.from(addressAmountMap.entries()).map( - ([addr, amountSats]) => ({ - address: addr, - amount: amountSats / 1e8, // Convert satoshis to BTC - }), - ); - addressLabel = relevantAddresses.length > 1 ? 'To Addresses' : 'To Address'; - } else { - // Received transaction: collect ALL unique input addresses (these are the senders) - // Exclude the user's own address (change) from the list since it's not a "from" address - // For received transactions, show the output amount that went to user's address, not input amounts - const inputAddresses: string[] = (transaction.vin - ?.map((input: any) => input.prevout?.scriptpubkey_address) - .filter( - (addr: any): addr is string => - typeof addr === 'string' && addr !== address, - ) || []) as string[]; // Exclude user's own address (change) - // Remove duplicates - const uniqueAddresses: string[] = [...new Set(inputAddresses)]; - // Calculate total received amount from outputs to user's address - const totalReceivedSats = - transaction.vout - ?.filter((output: any) => output.scriptpubkey_address === address) - .reduce( - (total: number, output: any) => total + (output.value || 0), - 0, - ) || 0; - const totalReceivedBTC = totalReceivedSats / 1e8; - // Show all sender addresses with the total received amount - // (We can't attribute portions to individual senders since Bitcoin doesn't work that way) - relevantAddresses = uniqueAddresses.map((addr: string) => ({ - address: addr, - amount: totalReceivedBTC, // Show the received output amount, not input amounts - })); - addressLabel = - relevantAddresses.length > 1 ? 'From Addresses' : 'From Address'; - } const renderDetailRow = (label: string, value: string | React.ReactNode) => ( {label} @@ -258,17 +199,175 @@ const TransactionDetailsModal: React.FC = ({ color: theme.colors.text, flexShrink: 1, }, - addressItem: { - flexDirection: 'row', - alignItems: 'flex-start', - marginBottom: 12, + transactionFlow: { + paddingVertical: 4, }, - addressIndex: { - fontSize: theme.fontSizes?.base || 14, + flowSection: { + width: '100%', + }, + flowSectionTitle: { + fontSize: theme.fontSizes?.xs || 10, fontFamily: theme.fontFamilies?.bold, - color: theme.colors.textSecondary, // Use textSecondary for better readability + color: theme.colors.textSecondary, + marginBottom: 10, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + flowItem: { + marginBottom: 6, + }, + flowItemContent: { + backgroundColor: theme.colors.cardBackground || theme.colors.background, + borderRadius: 10, + padding: 10, + borderWidth: 1, + borderColor: theme.colors.border, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + overflow: 'hidden', // clips the accent bar to rounded corners + }, + flowItemContentOurs: { + backgroundColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + '10' // light: very subtle primary wash + : theme.colors.bitcoinOrange + '1A', // dark: warm amber tint + borderColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + borderWidth: 2, + paddingLeft: 10, // make room for the left accent bar + }, + flowItemAccentBar: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 3, + backgroundColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + }, + flowItemHeader: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, marginRight: 8, - minWidth: 20, + }, + flowIcon: { + width: 18, + height: 18, + marginRight: 8, + tintColor: theme.colors.textSecondary, + }, + flowIconOurs: { + tintColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + }, + flowItemInfo: { + flex: 1, + }, + flowItemLabel: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + marginBottom: 2, + }, + flowItemLabelOurs: { + color: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + }, + flowItemPath: { + fontSize: theme.fontSizes?.xs || 9, + fontFamily: theme.fontFamilies?.monospaceMedium || theme.fontFamilies?.monospace, + color: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + }, + flowItemType: { + fontSize: theme.fontSizes?.xs || 9, + color: theme.colors.textSecondary, + fontStyle: 'italic', + marginTop: 1, + }, + flowAmount: { + alignItems: 'flex-end', + }, + flowAmountBTC: { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + }, + flowAmountBTCOurs: { + color: + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange, + }, + flowAmountFiat: { + fontSize: theme.fontSizes?.xs || 9, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + marginTop: 1, + }, + flowConnectorVertical: { + width: 1, + height: 6, + backgroundColor: theme.colors.border, + marginLeft: 13, + marginVertical: 2, + }, + transactionHubVertical: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 10, + }, + hubArrow: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: theme.colors.primary + '20', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 4, + }, + hubArrowText: { + fontSize: theme.fontSizes?.xl || 18, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.primary, + }, + hubLabel: { + marginTop: 2, + }, + hubLabelText: { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.textSecondary, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + summaryBar: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.cardBackground || theme.colors.background, + borderRadius: 10, + padding: 10, + marginTop: 10, + borderWidth: 1, + borderColor: theme.colors.border, + }, + summaryBarText: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + flex: 1, }, txIdContainer: { backgroundColor: @@ -285,22 +384,6 @@ const TransactionDetailsModal: React.FC = ({ : theme.colors.blackOverlay06, // Light mode border marginRight: 12, }, - addressAmountContainer: { - alignItems: 'flex-end', - justifyContent: 'center', - minWidth: 100, - }, - addressAmount: { - fontSize: theme.fontSizes?.base || 14, - fontFamily: theme.fontFamilies?.monospaceBold, - color: theme.colors.text, - marginBottom: 2, - }, - addressAmountFiat: { - fontSize: theme.fontSizes?.sm || 12, - fontFamily: theme.fontFamilies?.monospace, - color: theme.colors.textSecondary, // Use textSecondary for better readability - }, txId: { fontSize: theme.fontSizes?.base || 14, fontFamily: theme.fontFamilies?.monospace, @@ -425,54 +508,264 @@ const TransactionDetailsModal: React.FC = ({ 'Value', isBlurred ? '***' - : `${getCurrencySymbol(selectedCurrency)}${getFiatAmount( - amount, - )}`, + : hasFiat && getFiatAmount(amount) != null + ? `${getCurrencySymbol(selectedCurrency)}${getFiatAmount(amount)}` + : '—', )} - {relevantAddresses.length > 0 && ( + {/* Transaction Flow Diagram */} + {(transaction.vin?.length > 0 || transaction.vout?.length > 0) && ( - {addressLabel} - {relevantAddresses.map((addrWithAmount, index) => { - const addressExplorerLink = `${baseUrl}/address/${addrWithAmount.address}`; - const showAmount = addrWithAmount.amount > 0; - return ( - - {relevantAddresses.length > 1 && ( - {index + 1}. - )} - - { - Linking.openURL(addressExplorerLink); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - {addrWithAmount.address} - - - - {showAmount && ( - - - {isBlurred - ? '***' - : formatBitcoinDisplay(addrWithAmount.amount, { - inSats: showSats, - formatted: balanceFormattingEnabled, - })} - - {!isBlurred && btcRate > 0 && ( - - {getCurrencySymbol(selectedCurrency)} - {getFiatAmount(addrWithAmount.amount)} - - )} - - )} + + {/* Inputs */} + {transaction.vin?.length > 0 && ( + + Inputs + {transaction.vin.map((input: any, idx: number) => { + const addr: string = input.prevout?.scriptpubkey_address || ''; + const sats: number = input.prevout?.value || 0; + const pathInfo = addr ? addressPathMap?.[addr] : undefined; + const short = addr + ? `${addr.slice(0, 9)}…${addr.slice(-6)}` + : 'coinbase'; + const addrLink = addr ? `${baseUrl}/address/${addr}` : null; + return ( + + + {pathInfo && } + + + + {addrLink ? ( + Linking.openURL(addrLink)} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + {short} + + + ) : ( + + {short} + + )} + {pathInfo ? ( + + {pathInfo.derivationPath} · {pathInfo.chain} #{pathInfo.index} + + ) : ( + external + )} + + + + + {isBlurred + ? '***' + : formatBitcoinDisplay(sats / 1e8, { + inSats: showSats, + formatted: balanceFormattingEnabled, + })} + + {!isBlurred && hasFiat && ( + + {getCurrencySymbol(selectedCurrency)} + {getFiatAmount(sats / 1e8) ?? '—'} + + )} + + + {idx < transaction.vin.length - 1 && ( + + )} + + ); + })} + + )} + {/* Hub Arrow */} + + + + + + Transaction - ); - })} + + {/* Outputs */} + {transaction.vout?.length > 0 && ( + + Outputs + {transaction.vout.map((output: any, idx: number) => { + const addr: string = output.scriptpubkey_address || ''; + const sats: number = output.value || 0; + const pathInfo = addr ? addressPathMap?.[addr] : undefined; + const isChange = pathInfo?.chain === 'change'; + const short = addr + ? `${addr.slice(0, 9)}…${addr.slice(-6)}` + : 'OP_RETURN'; + const addrLink = addr ? `${baseUrl}/address/${addr}` : null; + const outputIcon = pathInfo + ? isChange + ? require('../assets/consolidate-icon.png') + : require('../assets/in-icon.png') + : require('../assets/bitcoin-icon.png'); + return ( + + + {pathInfo && } + + + + {addrLink ? ( + Linking.openURL(addrLink)} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + {short} + + + ) : ( + + {short} + + )} + {pathInfo ? ( + + {pathInfo.derivationPath} · {pathInfo.chain} #{pathInfo.index} + + ) : ( + external + )} + + + + + {isBlurred + ? '***' + : formatBitcoinDisplay(sats / 1e8, { + inSats: showSats, + formatted: balanceFormattingEnabled, + })} + + {!isBlurred && hasFiat && ( + + {getCurrencySymbol(selectedCurrency)} + {getFiatAmount(sats / 1e8) ?? '—'} + + )} + + + {idx < transaction.vout.length - 1 && ( + + )} + + ); + })} + {/* Fee as final item */} + {typeof transaction.fee === 'number' && + Number.isFinite(transaction.fee) && + transaction.fee > 0 && ( + + + + + + + Fee + + + + + {isBlurred + ? '***' + : formatBitcoinDisplay(transaction.fee / 1e8, { + inSats: showSats, + formatted: balanceFormattingEnabled, + })} + + {!isBlurred && hasFiat && ( + + {getCurrencySymbol(selectedCurrency)} + {getFiatAmount(transaction.fee / 1e8) ?? '—'} + + )} + + + + )} + + )} + + {/* Summary bar */} + + + {`${transaction.vin?.length || 0} input${transaction.vin?.length !== 1 ? 's' : ''} → ${transaction.vout?.length || 0} output${transaction.vout?.length !== 1 ? 's' : ''}`} + {typeof transaction.fee === 'number' && + Number.isFinite(transaction.fee) && + transaction.fee > 0 && + !isBlurred && + ` · fee ${formatBitcoinDisplay(transaction.fee / 1e8, {inSats: showSats, formatted: balanceFormattingEnabled})}`} + + )} @@ -504,9 +797,7 @@ const TransactionDetailsModal: React.FC = ({ `${formatBitcoinDisplay(transaction.fee / 1e8, { inSats: showSats, formatted: balanceFormattingEnabled, - })} (${getCurrencySymbol(selectedCurrency)}${getFiatAmount( - transaction.fee / 1e8, - )})`, + })} (${hasFiat && getFiatAmount(transaction.fee / 1e8) != null ? getCurrencySymbol(selectedCurrency) + getFiatAmount(transaction.fee / 1e8) : '—'})`, )} {typeof transaction.size === 'number' && Number.isFinite(transaction.size) && diff --git a/components/TransactionList.tsx b/components/TransactionList.tsx index edad5107..1a45b976 100644 --- a/components/TransactionList.tsx +++ b/components/TransactionList.tsx @@ -14,11 +14,12 @@ import { ActivityIndicator, RefreshControl, Platform, - Image, + Animated, + Easing, } from 'react-native'; import AppPressable from './AppPressable'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import axios from 'axios'; +import mempoolClient from '../services/MempoolClient'; import Toast from 'react-native-toast-message'; import moment from 'moment'; import { @@ -35,14 +36,195 @@ import {COMMON_FONT_CONFIGS} from '../theme/fonts'; import TransactionListSkeleton from './TransactionListSkeleton'; import {WalletService} from '../services/WalletService'; import TransactionDetailsModal from './TransactionDetailsModal'; -import LocalCache from '../services/LocalCache'; +import transactionRepository from '../services/repositories/TransactionRepository'; +import HistoricalPriceService, { + getHistoricalRateKey, +} from '../services/HistoricalPriceService'; // Add icon imports const inIcon = require('../assets/in-icon.png'); const outIcon = require('../assets/out-icon.png'); const consolidateIcon = require('../assets/consolidate-icon.png'); const pendingIcon = require('../assets/pending-icon.png'); + +type AnimationType = 'send' | 'receive' | 'consolidate' | 'rebalance' | 'none'; + +/** Renders a status icon with an appropriate looping animation for pending states. */ +const AnimatedStatusIcon = React.memo( + ({ + source, + style, + animationType, + }: { + source: any; + style: any; + animationType: AnimationType; + }) => { + const anim = React.useRef(new Animated.Value(0)).current; + + React.useEffect(() => { + if (animationType === 'none') { + anim.setValue(0); + return; + } + let loop: Animated.CompositeAnimation; + switch (animationType) { + // Arrow slides up + fades then resets: conveys outgoing motion + case 'send': + loop = Animated.loop( + Animated.sequence([ + Animated.timing(anim, { + toValue: 1, + duration: 700, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(anim, { + toValue: 0, + duration: 300, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }), + Animated.delay(300), + ]), + ); + break; + // Arrow slides down + brightens then resets: conveys incoming motion + case 'receive': + loop = Animated.loop( + Animated.sequence([ + Animated.timing(anim, { + toValue: 1, + duration: 700, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(anim, { + toValue: 0, + duration: 300, + easing: Easing.out(Easing.ease), + useNativeDriver: true, + }), + Animated.delay(300), + ]), + ); + break; + // Slow continuous rotation: conveys merging/gathering + case 'consolidate': + loop = Animated.loop( + Animated.timing(anim, { + toValue: 1, + duration: 1400, + easing: Easing.linear, + useNativeDriver: true, + }), + ); + break; + // Scale pulse: conveys spreading/redistributing + case 'rebalance': + loop = Animated.loop( + Animated.sequence([ + Animated.timing(anim, { + toValue: 1, + duration: 600, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + Animated.timing(anim, { + toValue: 0, + duration: 600, + easing: Easing.inOut(Easing.ease), + useNativeDriver: true, + }), + ]), + ); + break; + default: + return; + } + loop.start(); + return () => loop.stop(); + }, [animationType, anim]); + + let animStyle: object = {}; + switch (animationType) { + case 'send': + animStyle = { + opacity: anim.interpolate({ + inputRange: [0, 0.5, 1], + outputRange: [1, 0.35, 1], + }), + transform: [ + { + translateY: anim.interpolate({ + inputRange: [0, 1], + outputRange: [0, -4], + }), + }, + ], + }; + break; + case 'receive': + animStyle = { + opacity: anim.interpolate({ + inputRange: [0, 0.5, 1], + outputRange: [0.35, 1, 0.35], + }), + transform: [ + { + translateY: anim.interpolate({ + inputRange: [0, 1], + outputRange: [-4, 0], + }), + }, + ], + }; + break; + case 'consolidate': + animStyle = { + transform: [ + { + rotate: anim.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }), + }, + ], + }; + break; + case 'rebalance': + animStyle = { + opacity: anim.interpolate({ + inputRange: [0, 1], + outputRange: [0.4, 1], + }), + transform: [ + { + scale: anim.interpolate({ + inputRange: [0, 1], + outputRange: [0.8, 1.15], + }), + }, + ], + }; + break; + } + + return ( + + ); + }, +); interface TransactionListProps { - address: string; + /** Single address (legacy). Use addresses for multi-address (HD wallet) mode. */ + address?: string; + /** All HD addresses (receive + change) for wallet-level transaction list. */ + addresses?: string[]; + network?: string; + addressType?: string; baseApi: string; onUpdate: (pendingTxs: any[], pending: number) => Promise; initialTransactions?: any[]; @@ -62,22 +244,31 @@ const TransactionList = React.forwardRef< ( { address, + addresses, + network, + addressType, baseApi, onUpdate, initialTransactions = [], selectedCurrency = 'USD', - btcRate = 0, + btcRate: _btcRate = 0, getCurrencySymbol = currency => currency, onPullRefresh, isBlurred = false, }, ref, ) => { + const isMultiAddress = Array.isArray(addresses) && addresses.length > 0; + const effectiveAddress = isMultiAddress ? addresses![0] : address; const [transactions, setTransactions] = useState(initialTransactions); const [loading, setLoading] = useState(false); const [loadingMore, setLoadingMore] = useState(false); const [lastSeenTxId, setLastSeenTxId] = useState(null); + // Per-address cursors for multi-address pagination (null = address exhausted) + const [addressCursors, setAddressCursors] = useState< + Record + >({}); const [isRefreshing, setIsRefreshing] = useState(false); const [hasMoreTransactions, setHasMoreTransactions] = useState(true); const isFetching = useRef(false); @@ -90,43 +281,153 @@ const TransactionList = React.forwardRef< const isMounted = useRef(true); const abortController = useRef(null); const isRefreshingRef = useRef(false); - // Memoized transaction amount calculator - const getTransactionAmounts = useCallback((tx: any, addr: string) => { - if (tx.sentAt) { - const self = - String(tx.from).toLowerCase() === String(tx.to).toLowerCase(); - const sent = self ? 0 : tx.amount; - const chng = self ? sent : 0; - const rcvd = self ? sent : 0; - return { - sent: tx.amount / 1e8, - changeAmount: chng / 1e8, - received: rcvd / 1e8, - }; + const ourAddresses = useMemo( + () => (isMultiAddress ? new Set(addresses!) : null), + [isMultiAddress, addresses], + ); + const isOurAddress = useCallback( + (addr: string) => + ourAddresses ? ourAddresses.has(addr) : addr === effectiveAddress, + [ourAddresses, effectiveAddress], + ); + const [addressPathMap, setAddressPathMap] = useState | null>(null); + /** Historical BTC rate per (currency_timestampDay) for confirmed txs; fiat shown only when present. */ + const [historicalRatesMap, setHistoricalRatesMap] = useState< + Record + >({}); + // Load derivation paths for our HD addresses so we can show path per tx row + useEffect(() => { + let cancelled = false; + const loadPaths = async () => { + if (!network || !addressType) { + setAddressPathMap(null); + return; + } + try { + const list = + await WalletService.getInstance().getHdAddressesWithPaths( + network, + addressType, + ); + if (cancelled) { + return; + } + const map: Record< + string, + {derivationPath: string; chain: 'receive' | 'change'; index: number} + > = {}; + for (const item of list) { + map[item.address] = { + derivationPath: item.derivationPath, + chain: item.chain, + index: item.index, + }; + } + setAddressPathMap(map); + } catch { + if (!cancelled) { + setAddressPathMap(null); + } + } + }; + loadPaths(); + return () => { + cancelled = true; + }; + }, [network, addressType]); + // Fetch historical rates for confirmed txs so we can show fiat at tx-time (not current rate). + useEffect(() => { + if (!baseApi || !selectedCurrency || transactions.length === 0) return; + // Map key → raw block_time so we never have to re-parse the key string. + const keysToFetch = new Map(); + for (const tx of transactions) { + if (tx.sentAt) continue; // pending — will use live rate + const blockTime = tx.status?.block_time; + if (typeof blockTime !== 'number' || !Number.isFinite(blockTime)) + continue; + const key = getHistoricalRateKey(selectedCurrency, blockTime); + keysToFetch.set(key, blockTime); } - const sentAmount = tx.vin.reduce((total: number, input: any) => { - return input.prevout.scriptpubkey_address === addr - ? total + input.prevout.value - : total; - }, 0); - const receivedAmount = tx.vout.reduce((total: number, output: any) => { - return output.scriptpubkey_address === addr - ? total + output.value - : total; - }, 0); - const changeAmount = tx.vout.reduce((total: number, output: any) => { - return sentAmount > 0 && output.scriptpubkey_address === addr - ? total + output.value - : total; - }, 0); - const fee = tx.fee || 0; - const finalSentAmount = Math.max(0, sentAmount - changeAmount - fee); - return { - sent: finalSentAmount / 1e8, - changeAmount: changeAmount / 1e8, - received: receivedAmount / 1e8, + if (selectedTransaction?.status?.block_time && !selectedTransaction.sentAt) { + const bt = selectedTransaction.status.block_time; + keysToFetch.set(getHistoricalRateKey(selectedCurrency, bt), bt); + } + let cancelled = false; + (async () => { + for (const [key, blockTime] of keysToFetch) { + if (cancelled) break; + const rate = await HistoricalPriceService.getHistoricalRate( + selectedCurrency, + blockTime, + baseApi, + ); + if (cancelled) break; + if (rate != null && rate > 0) { + setHistoricalRatesMap(prev => + prev[key] === rate ? prev : {...prev, [key]: rate}, + ); + } + } + })(); + return () => { + cancelled = true; }; - }, []); + }, [ + baseApi, + selectedCurrency, + transactions, + selectedTransaction?.txid, + selectedTransaction?.status?.block_time, + selectedTransaction?.sentAt, + ]); + const getTransactionAmounts = useCallback( + (tx: any, addrOrAddrs?: string | string[]) => { + const checkAddr = (a: string) => + addrOrAddrs + ? Array.isArray(addrOrAddrs) + ? addrOrAddrs.includes(a) + : a === addrOrAddrs + : isOurAddress(a); + if (tx.sentAt) { + const self = + String(tx.from).toLowerCase() === String(tx.to).toLowerCase(); + const sent = self ? 0 : tx.amount; + const chng = self ? sent : 0; + const rcvd = self ? sent : 0; + return { + sent: tx.amount / 1e8, + changeAmount: chng / 1e8, + received: rcvd / 1e8, + }; + } + const sentAmount = tx.vin.reduce((total: number, input: any) => { + return checkAddr(input.prevout?.scriptpubkey_address || '') + ? total + (input.prevout?.value || 0) + : total; + }, 0); + const receivedAmount = tx.vout.reduce((total: number, output: any) => { + return checkAddr(output.scriptpubkey_address || '') + ? total + output.value + : total; + }, 0); + const changeAmount = tx.vout.reduce((total: number, output: any) => { + return sentAmount > 0 && checkAddr(output.scriptpubkey_address || '') + ? total + output.value + : total; + }, 0); + const fee = tx.fee || 0; + const finalSentAmount = Math.max(0, sentAmount - changeAmount - fee); + return { + sent: finalSentAmount / 1e8, + changeAmount: changeAmount / 1e8, + received: receivedAmount / 1e8, + }; + }, + [isOurAddress], + ); // Memoize fetchTransactions to prevent unnecessary re-renders const memoizedFetchTransactions = useCallback( async (url: string | undefined, silent: boolean = false) => { @@ -154,7 +455,14 @@ const TransactionList = React.forwardRef< const loadFromCache = async () => { dbg('Loading from cache...'); const cachedTransactions = - await WalletService.getInstance().transactionsFromCache(address); + isMultiAddress && network && addressType + ? await WalletService.getInstance().transactionsFromCacheForWallet( + network, + addressType, + ) + : await WalletService.getInstance().transactionsFromCache( + address || '', + ); if (isMounted.current) { // No need to update cache when loading from cache setTransactions(cachedTransactions); @@ -195,80 +503,102 @@ const TransactionList = React.forwardRef< a.startsWith('1') || a.startsWith('3') || a.startsWith('bc1') ); }; - if (!addressMatchesNetwork(address, isTestnetApi)) { - dbg('TransactionList: address/baseApi mismatch; skipping fetch', { - address, + const addrToCheck = isMultiAddress ? addresses?.[0] : address; + if ( + !addrToCheck || + !addressMatchesNetwork(addrToCheck, isTestnetApi) + ) { + dbg('TransactionList: address/baseApi mismatch; loading from DB', { + address: addrToCheck, url, }); - if (isMounted.current) { - setTransactions([]); - setHasMoreTransactions(false); - setIsRefreshing(false); - } + // Show cached data rather than blanking the list on a mismatch + await loadFromCache(); return; } dbg( 'TransactionList: Guard passed. Address matches network. Proceeding to fetch.', { - address, + address: addrToCheck, isTestnetApi, + isMultiAddress, }, ); - // Construct proper API URL const cleanBaseApi = url.replace(/\/+$/, '').replace(/\/api\/?$/, ''); - const apiUrl = `${cleanBaseApi}/api/address/${address}/txs`; - dbg('Starting fetch transactions from:', apiUrl); - // Set a timeout to fall back to cache if API takes too long - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('API timeout')), 10000); // Increased timeout to 10s - }); - const response = (await Promise.race([ - axios.get(apiUrl, { + let responseData: any[]; + let multiHasMore = false; + if (isMultiAddress && addresses && addresses.length > 0) { + const result = + await WalletService.getInstance().fetchTransactionsForAddresses( + cleanBaseApi, + addresses, + ); + responseData = result.txs; + // Store per-address cursors so fetchMore can page each address independently + setAddressCursors(result.cursors); + multiHasMore = Object.values(result.cursors).some(c => c !== null); + } else { + const apiUrl = `${cleanBaseApi}/api/address/${address}/txs`; + dbg('Starting fetch transactions from:', apiUrl); + const response = await mempoolClient.get(apiUrl, { signal: abortController.current.signal, - timeout: 10000, // Increased timeout to 10s - }), - timeoutPromise, - ])) as {data: any[]}; + }); + if (!response.ok) { + // HTTP-level error (not a thrown exception) — fall back to DB + dbg('TransactionList: non-ok response, loading from DB'); + await loadFromCache(); + return; + } + responseData = response.data ?? []; + } dbg( 'TransactionList: Received response with', - response.data.length, + responseData.length, 'transactions', ); if (!isMounted.current) { dbg('Component unmounted, skipping state updates'); return; } - const cached = JSON.parse( - (await LocalCache.getItem(`${address}-pendingTxs`)) || '{}', - ); - // Process pending transactions + const cached = (() => { + if (isMultiAddress && addresses!.length > 0) { + const merged: Record = {}; + for (const addr of addresses!) { + const p = transactionRepository.getPendingTxMap(addr, network || 'mainnet'); + Object.assign(merged, p); + } + return merged; + } + return transactionRepository.getPendingTxMap(address!, network || 'mainnet'); + })(); + const addrForAmounts = isMultiAddress ? addresses! : address; let pending = 0; - let pendingTxs = response.data + let pendingTxs = responseData .filter((tx: any) => !tx.status || !tx.status.confirmed) .map((tx: any) => { - const {sent} = getTransactionAmounts(tx, address); + const {sent} = getTransactionAmounts(tx, addrForAmounts); if (!isNaN(sent) && sent > 0) { pending += Number(sent); } return tx; }); - // Update cache - response.data.forEach((tx: any) => { + // Update cache - remove confirmed txs from pending + const addrsToUpdate = isMultiAddress ? addresses! : [address!]; + for (const tx of responseData) { if (cached[tx.txid]) { delete cached[tx.txid]; - LocalCache.setItem( - `${address}-pendingTxs`, - JSON.stringify(cached), - ); + for (const a of addrsToUpdate) { + transactionRepository.removePending(tx.txid, network || 'mainnet'); + } } - }); - // Add cached transactions + } + const workingData = [...responseData]; for (const txID in cached) { const validTxID = /^[a-fA-F0-9]{64}$/.test(txID); if (!validTxID) { delete cached[txID]; } else { - response.data.unshift({ + workingData.unshift({ txid: txID, from: cached[txID].from, to: cached[txID].to, @@ -284,8 +614,7 @@ const TransactionList = React.forwardRef< } } await onUpdate(pendingTxs, pending); - // Filter out duplicates, keeping confirmed transactions over pending ones - const uniqueTransactions = response.data.reduce( + const uniqueTransactions = workingData.reduce( (acc: any[], tx: any) => { const existingTx = acc.find(t => t.txid === tx.txid); if (!existingTx) { @@ -324,19 +653,44 @@ const TransactionList = React.forwardRef< // For confirmed transactions, sort by block height return (b.status.block_height || 0) - (a.status.block_height || 0); }); - WalletService.getInstance().updateTransactionsCache( - address, - newTransactions, - ); + if (isMultiAddress && network && addressType) { + WalletService.getInstance().updateTransactionsCacheForWallet( + network, + addressType, + newTransactions, + ); + } else { + WalletService.getInstance().updateTransactionsCache( + address!, + newTransactions, + ); + } if (isMounted.current) { dbg( - 'TransactionList: Setting', + 'TransactionList: Merging', newTransactions.length, - 'transactions to state', + 'API transactions into state', ); - setTransactions(newTransactions); - setHasMoreTransactions(newTransactions.length > 0); - if (newTransactions.length > 0) { + // Merge API page into existing state — never replace, so historical + // txs loaded via fetchMore are preserved even when the API returns a + // shorter first page. + setTransactions(prev => { + if (prev.length === 0) { + return newTransactions; + } + const merged = new Map( + prev.map((tx: any) => [tx.txid, tx]), + ); + for (const tx of newTransactions) { + // API data takes precedence: updates confirmation status, block height, etc. + merged.set(tx.txid, tx); + } + return sortTxs(Array.from(merged.values())); + }); + setHasMoreTransactions( + isMultiAddress ? multiHasMore : newTransactions.length > 0, + ); + if (!isMultiAddress && newTransactions.length > 0) { setLastSeenTxId(newTransactions[newTransactions.length - 1].txid); } // Clear refresh state on successful API response @@ -372,7 +726,15 @@ const TransactionList = React.forwardRef< } } }, - [address, getTransactionAmounts, onUpdate], + [ + address, + addresses, + isMultiAddress, + network, + addressType, + getTransactionAmounts, + onUpdate, + ], ); // For user pull-to-refresh const handlePullRefresh = useCallback(async () => { @@ -382,6 +744,14 @@ const TransactionList = React.forwardRef< HapticFeedback.medium(); setIsRefreshing(true); onPullRefresh?.(); + // Invalidate cached /address/… responses so the refresh always hits the + // network. Without this, mempoolClient serves the 30-second-old cached + // snapshot (originally populated during discovery's isAddressUsed() calls) + // and the user sees the same stale transaction list on every pull-to-refresh. + if (baseApi) { + const cleanBase = baseApi.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + mempoolClient.invalidate(`${cleanBase}/api/address/`); + } try { await memoizedFetchTransactions(baseApi); } catch { @@ -419,10 +789,11 @@ const TransactionList = React.forwardRef< }, [isRefreshing]); // Fix transaction refresh handling useEffect(() => { - // Skip effect if address or baseApi are not initialized - if (!address || !baseApi || address === '' || baseApi === '') { - dbg('Skipping transaction fetch - address or baseApi not initialized', { + const hasAddress = address || (isMultiAddress && addresses!.length > 0); + if (!hasAddress || !baseApi || baseApi === '') { + dbg('Skipping transaction fetch - address/baseApi not initialized', { address, + addresses: isMultiAddress ? addresses?.length : 0, baseApi, }); setTransactions([]); @@ -432,12 +803,33 @@ const TransactionList = React.forwardRef< setIsRefreshing(false); return; } - // Reset list when address or baseApi changes to prevent showing stale rows - setTransactions([]); + // Pre-populate from DB so cached rows are visible while the live fetch runs. + // This replaces the eager setTransactions([]) that left the list empty on + // address/network change and on first mount while offline. + let mounted = true; + (async () => { + try { + const cached = + isMultiAddress && network && addressType + ? await WalletService.getInstance().transactionsFromCacheForWallet( + network, + addressType, + ) + : await WalletService.getInstance().transactionsFromCache( + address || '', + ); + // Only pre-populate if no live fetch has already set newer data. + if (mounted && cached.length > 0 && !isFetching.current) { + setTransactions(cached); + } + } catch { + // Non-critical — the live fetch will populate state when connectivity + // is available. + } + })(); setHasMoreTransactions(true); setLastSeenTxId(null); - let mounted = true; - let refreshInterval: NodeJS.Timeout | null = null; + const controller = new AbortController(); abortController.current = controller; const fetchData = async (silent: boolean = false) => { @@ -463,18 +855,9 @@ const TransactionList = React.forwardRef< if (!isFetching.current && !isRefreshingRef.current) { fetchData(true); } - // Set up refresh interval - refreshInterval = setInterval(() => { - if (mounted && !isFetching.current && !isRefreshingRef.current) { - fetchData(true); - } - }, 30000); // Refresh every 30 seconds return () => { dbg('Cleaning up fetch effect'); mounted = false; - if (refreshInterval) { - clearInterval(refreshInterval); - } if (abortController.current) { abortController.current.abort(); } @@ -483,16 +866,22 @@ const TransactionList = React.forwardRef< setLoading(false); setIsRefreshing(false); }; - }, [address, baseApi, memoizedFetchTransactions]); + }, [ + address, + addresses, + isMultiAddress, + baseApi, + memoizedFetchTransactions, + ]); // Memoized transaction status checker const getTransactionStatus = useCallback( (tx: any) => { const isSending = !!tx?.sentAt || - !!tx.vin.some( - (input: any) => input.prevout.scriptpubkey_address === address, + !!tx.vin?.some((input: any) => + isOurAddress(input.prevout?.scriptpubkey_address || ''), ); - if (tx.sentAt || !tx.status.confirmed) { + if (tx.sentAt || !tx.status?.confirmed) { return { confirmed: false, text: isSending ? 'Sending' : 'Receiving', @@ -505,24 +894,113 @@ const TransactionList = React.forwardRef< icon: isSending ? outIcon : inIcon, }; }, - [address], + [isOurAddress], + ); + // Shared sort: pending (no block_height) first, then block_height descending. + const sortTxs = useCallback( + (txs: any[]): any[] => + [...txs].sort((a, b) => { + const aPending = !a.status?.block_height; + const bPending = !b.status?.block_height; + if (aPending && !bPending) { + return -1; + } + if (!aPending && bPending) { + return 1; + } + if (aPending && bPending) { + return (b.sentAt || 0) - (a.sentAt || 0); + } + return (b.status.block_height || 0) - (a.status.block_height || 0); + }), + [], ); - // Debounced fetch more implementation + const fetchMore = useCallback(async () => { if (loadingMore || !isMounted.current) { - dbg('Skipping fetch more - conditions not met:', { - loadingMore, - isMounted: isMounted.current, - }); + dbg('Skipping fetchMore — already in flight or unmounted'); + return; + } + + // ── Multi-address path ───────────────────────────────────────────────── + if (isMultiAddress) { + if (!baseApi || !addresses?.length) { + return; + } + const hasOpenCursor = Object.values(addressCursors).some( + c => c !== null, + ); + if (!hasOpenCursor) { + setHasMoreTransactions(false); + return; + } + dbg( + 'fetchMore (multi-address): fetching next page with cursors', + addressCursors, + ); + setLoadingMore(true); + try { + const cleanBaseApi = baseApi + .replace(/\/+$/, '') + .replace(/\/api\/?$/, ''); + const result = + await WalletService.getInstance().fetchMoreTransactionsForAddresses( + cleanBaseApi, + addressCursors, + ); + if (!isMounted.current) { + return; + } + const stillHasMore = Object.values(result.cursors).some( + c => c !== null, + ); + setAddressCursors(result.cursors); + setHasMoreTransactions(stillHasMore); + if (result.txs.length > 0) { + setTransactions(prev => { + const existingIds = new Set(prev.map((tx: any) => tx.txid)); + const newTxs = result.txs.filter( + (tx: any) => !existingIds.has(tx.txid), + ); + if (newTxs.length === 0) { + return prev; + } + const merged = sortTxs([...prev, ...newTxs]); + if (network && addressType) { + WalletService.getInstance().updateTransactionsCacheForWallet( + network, + addressType, + merged, + ); + } + dbg( + 'fetchMore (multi-address): appended', + newTxs.length, + 'new txs, total', + merged.length, + ); + return merged; + }); + } + } catch (error: any) { + if (!isCanceledError(error)) { + dbg('fetchMore (multi-address) error:', error); + Toast.show({ + type: 'error', + text1: 'Error loading more transactions', + }); + } + } finally { + if (isMounted.current) { + setLoadingMore(false); + } + } return; } - // Guard against invalid state + + // ── Single-address path ──────────────────────────────────────────────── if (!lastSeenTxId || !address || !baseApi) { - dbg('Skipping fetch more - invalid state:', { - lastSeenTxId, - address, - baseApi, - }); + dbg('Skipping fetchMore (single):', {lastSeenTxId, address, baseApi}); return; } dbg('Starting fetch more from:', lastSeenTxId); @@ -530,27 +1008,29 @@ const TransactionList = React.forwardRef< try { // Ensure baseApi doesn't end with a slash and add a single slash const cleanBaseApi = baseApi.replace(/\/+$/, ''); - const response = await axios.get( + const response = await mempoolClient.get( `${cleanBaseApi}/address/${address}/txs/chain/${lastSeenTxId}`, - { - signal: abortController.current?.signal, - }, + {signal: abortController.current?.signal}, ); - dbg('Received more transactions:', response.data.length); + if (!response.ok) { + // API error during pagination — leave hasMoreTransactions true so + // the user can retry without losing the ability to paginate. + dbg('fetchMore: non-ok response, keeping pagination state'); + return; + } + const newTransactions = response.data ?? []; + dbg('Received more transactions:', newTransactions.length); if (!isMounted.current) { dbg('Component unmounted during fetch more'); return; } - const newTransactions = response.data; - // Only set hasMoreTransactions to false if we get no new transactions + // Only set hasMoreTransactions to false on a genuine empty page if (newTransactions.length === 0) { dbg('No more transactions to load'); setHasMoreTransactions(false); return; } - const cached = JSON.parse( - (await LocalCache.getItem(`${address}-pendingTxs`)) || '{}', - ); + const cached = transactionRepository.getPendingTxMap(address!, network || 'mainnet'); dbg('Cached transactions for fetch more:', Object.keys(cached).length); setTransactions(prevTransactions => { try { @@ -564,7 +1044,10 @@ const TransactionList = React.forwardRef< let pendingTxs = filteredTransactions .filter((tx: any) => !tx.status || !tx.status.confirmed) .map((tx: any) => { - const {sent} = getTransactionAmounts(tx, address); + const {sent} = getTransactionAmounts( + tx, + isMultiAddress ? addresses : address, + ); if (!isNaN(sent) && sent > 0) { pending += Number(sent); } @@ -576,10 +1059,7 @@ const TransactionList = React.forwardRef< if (cached[tx.txid]) { delete cached[tx.txid]; dbg('delete from cache in fetch more', tx.txid); - LocalCache.setItem( - `${address}-pendingTxs`, - JSON.stringify(cached), - ); + transactionRepository.removePending(tx.txid, network || 'mainnet'); } }); // Add cached transactions @@ -648,9 +1128,15 @@ const TransactionList = React.forwardRef< loadingMore, lastSeenTxId, address, + addresses, baseApi, getTransactionAmounts, onUpdate, + isMultiAddress, + addressCursors, + network, + addressType, + sortTxs, ]); // Add effect to handle initialTransactions changes useEffect(() => { @@ -755,6 +1241,19 @@ const TransactionList = React.forwardRef< color: appTheme.colors.text, opacity: 0.8, }, + pathText: { + fontSize: appTheme.fontSizes?.xs || 11, + fontFamily: appTheme.fontFamilies?.monospace, + color: appTheme.colors.textSecondary, + opacity: 0.8, + marginTop: 2, + }, + pathIndexText: { + fontSize: appTheme.fontSizes?.xs || 11, + fontFamily: appTheme.fontFamilies?.regular, + color: appTheme.colors.textSecondary, + opacity: 0.8, + }, txId: { fontSize: appTheme.fontSizes?.base || 13, fontFamily: appTheme.fontFamilies?.monospaceMedium, @@ -821,14 +1320,10 @@ const TransactionList = React.forwardRef< // Memoized render item with currency support const renderItem = useCallback( ({item}: any) => { - const { - text: status, - confirmed, - icon: statusIcon, - } = getTransactionStatus(item); - const {sent, changeAmount, received} = getTransactionAmounts( + const {text: status, icon: statusIcon} = getTransactionStatus(item); + const {sent, received} = getTransactionAmounts( item, - address, + isMultiAddress ? addresses : address, ); const txTime = item.sentAt || item.status.block_time * 1000; const txConf = item.sentAt ? false : item.status.confirmed; @@ -837,31 +1332,51 @@ const TransactionList = React.forwardRef< ? moment(txTime).fromNow() : 'Recently confirmed' : 'Pending confirmation'; - const shortTxId = `${item.txid.slice(0, 4)}...${item.txid.slice(-4)}`; + const shortTxId = `${item.txid.slice(0, 3)}…${item.txid.slice(-3)}`; // Get the relevant address(es) based on transaction type let relevantAddresses: string[] = []; let relevantAddress: string | null = null; if (status.includes('Sen')) { - // For sent transactions: collect ALL recipient addresses (outputs that aren't the sender's address) + // For sent transactions: collect ALL recipient addresses (outputs that aren't ours) relevantAddresses = item?.vout - ?.filter((output: any) => output.scriptpubkey_address !== address) + ?.filter( + (output: any) => !isOurAddress(output.scriptpubkey_address), + ) .map((output: any) => output.scriptpubkey_address) .filter((addr: string) => addr) || []; - // Remove duplicates relevantAddresses = [...new Set(relevantAddresses)]; relevantAddress = relevantAddresses[0] || null; } else { - // For received transactions: show the first input address that's not the receiver's address + // For received transactions: show the first input address that's not ours (the sender) relevantAddress = item?.vin?.find( - (input: any) => input.prevout.scriptpubkey_address !== address, + (input: any) => + !isOurAddress(input.prevout?.scriptpubkey_address || ''), )?.prevout?.scriptpubkey_address || null; // Set empty array for received transactions (not used in display) relevantAddresses = []; } // Follow global BTC/sats toggle (WalletHome) - let info = status.includes('Sen') + // sent === 0: all outputs landed on our own addresses — self-directed tx. + // Distinguish by number of internal outputs: + // 1 internal output → classic UTXO merge → Consolidation + // 2+ internal outputs → spreading across paths → Rebalancing + const isSelfTransfer = status.includes('Sen') && sent === 0; + const confirmed = item.sentAt ? false : item.status?.confirmed; + const internalOutputCount = isSelfTransfer + ? (item.vout ?? []).filter((o: any) => + isOurAddress(o.scriptpubkey_address || ''), + ).length + : 0; + const isConsolidation = isSelfTransfer && internalOutputCount <= 1; + const isRebalancing = isSelfTransfer && internalOutputCount > 1; + let info = isSelfTransfer + ? `+${formatBitcoinDisplay(received, { + inSats: showSats, + formatted: balanceFormattingEnabled, + })}` + : status.includes('Sen') ? `-${formatBitcoinDisplay(sent, { inSats: showSats, formatted: balanceFormattingEnabled, @@ -870,29 +1385,48 @@ const TransactionList = React.forwardRef< inSats: showSats, formatted: balanceFormattingEnabled, })}`; - let finalStatus = status; - let finalIcon = statusIcon; - if (sent === 0 && received === changeAmount) { - finalStatus = confirmed - ? 'Consolidated UTXOs' - : 'Consolidating UTXOs'; - info = `+${formatBitcoinDisplay(received, { - inSats: showSats, - formatted: balanceFormattingEnabled, - })}`; - finalIcon = confirmed ? consolidateIcon : pendingIcon; - } - // Calculate amount in selected currency with proper formatting + const finalStatus = isConsolidation + ? confirmed + ? 'Consolidated' + : 'Consolidating' + : isRebalancing + ? confirmed + ? 'Rebalanced' + : 'Rebalancing' + : status; + const finalIcon = isSelfTransfer + ? confirmed + ? consolidateIcon + : pendingIcon + : statusIcon; + // Historical rate at tx time for confirmed txs; current live rate for pending/unconfirmed. + const isPendingTx = !!item.sentAt || !item.status?.confirmed; + const blockTime = isPendingTx ? null : item.status?.block_time; + const historicalKey = + typeof blockTime === 'number' && Number.isFinite(blockTime) + ? getHistoricalRateKey(selectedCurrency, blockTime) + : null; + const historicalRate = + historicalKey != null ? historicalRatesMap[historicalKey] ?? null : null; + // Pending/unconfirmed txs fall back to the current live rate from WalletHome. + const effectiveRate = + historicalRate != null && historicalRate > 0 + ? historicalRate + : isPendingTx && _btcRate > 0 + ? _btcRate + : null; const getFiatAmount = (btcAmount: number) => { - if (!btcRate || btcRate <= 0) { - return '0.00'; - } - const amount = btcAmount * btcRate; - return presentFiat(amount); + if (effectiveRate == null || effectiveRate <= 0) return null; + return presentFiat(btcAmount * effectiveRate); }; - const fiatAmount = status.includes('Sen') - ? getFiatAmount(sent) - : getFiatAmount(received); + const fiatAmount = + effectiveRate != null && effectiveRate > 0 + ? isConsolidation + ? getFiatAmount(received) + : status.includes('Sen') + ? getFiatAmount(sent) + : getFiatAmount(received) + : null; return ( [ @@ -912,7 +1446,21 @@ const TransactionList = React.forwardRef< }}> - + {finalStatus} {status.includes('Sen') ? 'To: ' : 'Fr: '} - {relevantAddress.slice(0, 4)}... - {relevantAddress.slice(-4)} + {relevantAddress.slice(0, 3)}…{relevantAddress.slice(-3)} {status.includes('Sen') && relevantAddresses.length > 1 && ( @@ -951,7 +1498,9 @@ const TransactionList = React.forwardRef< {isBlurred ? '***' - : `${getCurrencySymbol(selectedCurrency)}${fiatAmount}`} + : fiatAmount != null + ? `${getCurrencySymbol(selectedCurrency)}${fiatAmount}` + : '—'} )} @@ -971,6 +1520,9 @@ const TransactionList = React.forwardRef< getTransactionStatus, getTransactionAmounts, address, + addresses, + isMultiAddress, + isOurAddress, appTheme.colors.background, appTheme.colors.bitcoinOrange, styles.transactionRow, @@ -992,7 +1544,7 @@ const TransactionList = React.forwardRef< isBlurred, getCurrencySymbol, selectedCurrency, - btcRate, + historicalRatesMap, balanceFormattingEnabled, showSats, ], @@ -1084,34 +1636,62 @@ const TransactionList = React.forwardRef< }} baseApi={baseApi} selectedCurrency={selectedCurrency} - btcRate={btcRate} + historicalRate={(() => { + const selTx = selectedTransaction; + // Confirmed: use historical rate at block time. + if (!selTx?.sentAt && selTx?.status?.block_time != null) { + return ( + historicalRatesMap[ + getHistoricalRateKey( + selectedCurrency, + selTx.status.block_time, + ) + ] ?? null + ); + } + // Pending / unconfirmed: show value at current live rate. + return _btcRate > 0 ? _btcRate : null; + })()} getCurrencySymbol={getCurrencySymbol} - address={address} status={ selectedTransaction ? (() => { - const {text: status, confirmed} = + const {text, confirmed} = getTransactionStatus(selectedTransaction); - const {sent, changeAmount, received} = - getTransactionAmounts(selectedTransaction, address); - let finalStatus = status; - if (sent === 0 && received === changeAmount) { - finalStatus = confirmed - ? 'Consolidated UTXOs' - : 'Consolidating UTXOs'; + const {sent} = getTransactionAmounts( + selectedTransaction, + isMultiAddress ? addresses : address, + ); + const isSelf = text.includes('Sen') && sent === 0; + if (!isSelf) { + return {confirmed, text}; } - return { - confirmed, - text: finalStatus, - }; + const internalOuts = ( + selectedTransaction.vout ?? [] + ).filter((o: any) => + isOurAddress(o.scriptpubkey_address || ''), + ).length; + const label = + internalOuts <= 1 + ? confirmed + ? 'Consolidation' + : 'Consolidating' + : confirmed + ? 'Rebalanced' + : 'Rebalancing'; + return {confirmed, text: label}; })() : null } amounts={ selectedTransaction - ? getTransactionAmounts(selectedTransaction, address) + ? getTransactionAmounts( + selectedTransaction, + isMultiAddress ? addresses : address, + ) : null } + addressPathMap={addressPathMap} isBlurred={isBlurred} /> )} diff --git a/components/TransportModeSelector.tsx b/components/TransportModeSelector.tsx index 1ee72ba2..9ea87715 100644 --- a/components/TransportModeSelector.tsx +++ b/components/TransportModeSelector.tsx @@ -30,6 +30,9 @@ interface TransportModeSelectorProps { fiatAmount?: string; // Fiat amount for display fiatFees?: string; // Fiat fees for display selectedCurrency?: string; // Currency symbol for display + utxosJson?: string | null; // Optional JSON of utxosWithPaths (when multi-path UTXOs were used) + utxoCount?: number; // Optional count of UTXOs in utxosJson + changeAddress?: string | null; // Pre-computed change address (ensures both devices use the same output) } | null; showQRCode?: boolean; // Whether to show QR code (false when data came from scan) } @@ -256,6 +259,13 @@ const TransportModeSelector: React.FC = ({ gap: 10, width: '100%', }, + transportSelectedHintRowWithMargin: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 10, + width: '100%', + marginTop: 8, + }, transportSelectedHintIcon: { width: 20, height: 20, @@ -374,6 +384,8 @@ const TransportModeSelector: React.FC = ({ sendBitcoinData.addressType || '', sendBitcoinData.derivationPath || '', sendBitcoinData.network || '', + sendBitcoinData.utxosJson || '', + sendBitcoinData.changeAddress || '', ); return ( @@ -475,7 +487,7 @@ const TransportModeSelector: React.FC = ({ {/* Selected Transport Hint */} - {selectedTransport && description && description.length > 0 && ( + {selectedTransport && (description?.length > 0 || sendBitcoinData?.utxoCount) && ( = ({ )} + {!!sendBitcoinData?.utxoCount && ( + + + + Using{' '} + + {sendBitcoinData.utxoCount} UTXO + {sendBitcoinData.utxoCount === 1 ? '' : 's'} + {' '} + pre-selected from this wallet for a deterministic spend. + + + )} )} {/* Continue Button */} diff --git a/context/NetworkContext.tsx b/context/NetworkContext.tsx index c53405e0..0e695d75 100644 --- a/context/NetworkContext.tsx +++ b/context/NetworkContext.tsx @@ -1,5 +1,5 @@ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; import { BBMTLibNativeModule } from '../native_modules'; import {dbg, getMainnetAPIList, getTestnetAPIList} from '../utils'; interface NetworkContextType { @@ -17,31 +17,16 @@ export const NetworkProvider: React.FC<{ children: React.ReactNode }> = ({ child const refreshFromCache = useCallback(async () => { try { dbg('=== NetworkContext: Refreshing from cache'); - // Get current network - const net = await LocalCache.getItem('network'); + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK); dbg('NetworkContext: Network from cache:', net); if (net) { setNetwork(net); - dbg('NetworkContext: Network state updated to:', net); - // Try to get network-specific API first, then fallback to general API - let api = await LocalCache.getItem(`api_${net}`); - if (!api) { - api = await LocalCache.getItem('api'); - dbg('NetworkContext: No network-specific API, using general API:', api); - } else { - dbg('NetworkContext: Using network-specific API:', api); - } + let api = appConfigRepository.get(`api_${net}`) || appConfigRepository.get('api'); + dbg('NetworkContext: API from cache:', api); if (api) { setApiBase(api); - dbg('NetworkContext: API state updated to:', api); - // Sync with native module await BBMTLibNativeModule.setAPI(net, api); - dbg('NetworkContext: Native module synced with network:', net, 'API:', api); - } else { - dbg('NetworkContext: No API found in cache'); } - } else { - dbg('NetworkContext: No network found in cache'); } } catch (error) { dbg('NetworkContext: Error refreshing from cache:', error); @@ -51,45 +36,26 @@ export const NetworkProvider: React.FC<{ children: React.ReactNode }> = ({ child const updateNetwork = useCallback(async (newNetwork: string) => { try { dbg('=== NetworkContext: Updating network to:', newNetwork); - // Save current API for the current network before switching - const currentApi = apiBase; - if (currentApi) { - await LocalCache.setItem(`api_${network}`, currentApi); - dbg(`NetworkContext: Saved current API for ${network}:`, currentApi); + if (apiBase) { + appConfigRepository.set(`api_${network}`, apiBase); } - // Cache the new network - await LocalCache.setItem('network', newNetwork); - dbg('NetworkContext: Network cached:', newNetwork); - // Try to get the previously selected API for this network, fallback to default - let api = await LocalCache.getItem(`api_${newNetwork}`); + appConfigRepository.set(CONFIG_KEYS.NETWORK, newNetwork); + let api = appConfigRepository.get(`api_${newNetwork}`); if (!api) { - // Use default API for the network if no cached selection api = newNetwork === 'testnet3' - ? 'https://mempool.space/testnet/api' // TESTNET_APIS[0] - : 'https://mempool.space/api'; // MAINNET_APIS[0] - dbg('NetworkContext: No cached API found, using default for', newNetwork, ':', api); - } else { - dbg('NetworkContext: Using cached API for', newNetwork, ':', api); + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'; } - // Cache the selected API for this network - await LocalCache.setItem(`api_${newNetwork}`, api); - await LocalCache.setItem('api', api); // Also update the current API - dbg('NetworkContext: API cached for network:', newNetwork, 'API:', api); - // Update local state - this should trigger all dependent effects + appConfigRepository.set(`api_${newNetwork}`, api); + appConfigRepository.set('api', api); setNetwork(newNetwork); setApiBase(api); - dbg('NetworkContext: Local state updated with network:', newNetwork, 'API:', api); - // Update native module await BBMTLibNativeModule.setAPI(newNetwork, api); - dbg('NetworkContext: Native module updated with network:', newNetwork, 'API:', api); - // Set fee APIs for the selected network const networkAPIs = newNetwork === 'mainnet' ? await getMainnetAPIList() : await getTestnetAPIList(); - const feeAPIsString = networkAPIs.join(','); - await BBMTLibNativeModule.setFeeAPIs(feeAPIsString); - dbg('NetworkContext: Fee APIs set for', newNetwork + ':', feeAPIsString); - dbg('=== NetworkContext: Network update completed'); + await BBMTLibNativeModule.setFeeAPIs(networkAPIs.join(',')); + dbg('NetworkContext: Network update completed', newNetwork, api); } catch (error) { dbg('NetworkContext: Error updating network:', error); } @@ -97,91 +63,36 @@ export const NetworkProvider: React.FC<{ children: React.ReactNode }> = ({ child // Update API for current network const updateAPI = useCallback(async (newAPI: string) => { try { - dbg('=== NetworkContext: Updating API to:', newAPI); - // Cache the API for the current network - await LocalCache.setItem(`api_${network}`, newAPI); - await LocalCache.setItem('api', newAPI); // Also update the current API - dbg('NetworkContext: API cached for network:', network, 'API:', newAPI); - // Update local state + appConfigRepository.set(`api_${network}`, newAPI); + appConfigRepository.set('api', newAPI); setApiBase(newAPI); - dbg('NetworkContext: API state updated to:', newAPI); - // Update native module await BBMTLibNativeModule.setAPI(network, newAPI); - dbg('NetworkContext: Native module updated with network:', network, 'API:', newAPI); - dbg('=== NetworkContext: API update completed'); + dbg('NetworkContext: API updated to:', newAPI); } catch (error) { dbg('NetworkContext: Error updating API:', error); } }, [network]); - // Initialize from cache on mount + // Initialize from SQLite on mount useEffect(() => { const initializeContext = async () => { try { - dbg('=== NetworkContext: Initializing context'); - // Get current network - const net = await LocalCache.getItem('network'); - dbg('NetworkContext: Network from cache:', net); - if (net) { - setNetwork(net); - dbg('NetworkContext: Network state updated to:', net); - // Try to get network-specific API first, then fallback to general API - let api = await LocalCache.getItem(`api_${net}`); - if (!api) { - api = await LocalCache.getItem('api'); - dbg('NetworkContext: No network-specific API, using general API:', api); - } else { - dbg('NetworkContext: Using network-specific API:', api); - } - if (api) { - setApiBase(api); - dbg('NetworkContext: API state updated to:', api); - // Sync with native module - await BBMTLibNativeModule.setAPI(net, api); - dbg('NetworkContext: Native module synced with network:', net, 'API:', api); - // Set fee APIs for the selected network - const networkAPIs = net === 'mainnet' - ? await getMainnetAPIList() - : await getTestnetAPIList(); - const feeAPIsString = networkAPIs.join(','); - await BBMTLibNativeModule.setFeeAPIs(feeAPIsString); - dbg('NetworkContext: Fee APIs set for', net + ':', feeAPIsString); - } else { - // Set default API if none found - const defaultApi = net === 'testnet3' - ? 'https://mempool.space/testnet/api' - : 'https://mempool.space/api'; - dbg('NetworkContext: No API found, using default:', defaultApi); - setApiBase(defaultApi); - await LocalCache.setItem('api', defaultApi); - await LocalCache.setItem(`api_${net}`, defaultApi); - await BBMTLibNativeModule.setAPI(net, defaultApi); - dbg('NetworkContext: Default API set and cached'); - // Set fee APIs for the selected network - const networkAPIs = net === 'mainnet' - ? await getMainnetAPIList() - : await getTestnetAPIList(); - const feeAPIsString = networkAPIs.join(','); - await BBMTLibNativeModule.setFeeAPIs(feeAPIsString); - dbg('NetworkContext: Fee APIs set for', net + ':', feeAPIsString); - } - } else { - // No network found, set defaults - dbg('NetworkContext: No network found, setting defaults'); - const defaultNetwork = 'mainnet'; - const defaultApi = 'https://mempool.space/api'; - setNetwork(defaultNetwork); - setApiBase(defaultApi); - await LocalCache.setItem('network', defaultNetwork); - await LocalCache.setItem('api', defaultApi); - await LocalCache.setItem(`api_${defaultNetwork}`, defaultApi); - await BBMTLibNativeModule.setAPI(defaultNetwork, defaultApi); - dbg('NetworkContext: Defaults set - network:', defaultNetwork, 'API:', defaultApi); - // Set fee APIs for mainnet (default network) - const networkAPIs = await getMainnetAPIList(); - const feeAPIsString = networkAPIs.join(','); - await BBMTLibNativeModule.setFeeAPIs(feeAPIsString); - dbg('NetworkContext: Fee APIs set for mainnet:', feeAPIsString); + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK) || 'mainnet'; + setNetwork(net); + let api = appConfigRepository.get(`api_${net}`) || appConfigRepository.get('api'); + if (!api) { + api = net === 'testnet3' + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'; + appConfigRepository.set('api', api); + appConfigRepository.set(`api_${net}`, api); } + setApiBase(api); + await BBMTLibNativeModule.setAPI(net, api); + const networkAPIs = net === 'mainnet' + ? await getMainnetAPIList() + : await getTestnetAPIList(); + await BBMTLibNativeModule.setFeeAPIs(networkAPIs.join(',')); + dbg('NetworkContext: initialized', net, api); } catch (error) { dbg('NetworkContext: Error during initialization:', error); } diff --git a/context/UserContext.tsx b/context/UserContext.tsx index 148e66a1..c1083528 100644 --- a/context/UserContext.tsx +++ b/context/UserContext.tsx @@ -7,9 +7,10 @@ import React, { useState, } from 'react'; import EncryptedStorage from 'react-native-encrypted-storage'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; import {BBMTLibNativeModule} from '../native_modules'; -import {getDerivePathForNetwork, isLegacyWallet, dbg} from '../utils'; +import {getReceivePath, isLegacyWallet, dbg} from '../utils'; +import {getExternalIndex} from '../services/HdIndexService'; type AddressType = 'legacy' | 'segwit-native' | 'segwit-compatible'; interface UserContextType { btcPub: string; @@ -161,69 +162,31 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ }; loadBalanceFormatting(); }, []); - // Load mempool playground tab preference (Settings; default off) + // Load tab preferences from SQLite (synchronous reads) useEffect(() => { - const loadShowMempoolPlayground = async () => { - try { - const stored = await LocalCache.getItem('mempool_playground_enabled'); - setShowMempoolPlaygroundState(stored === 'true'); - } catch { - setShowMempoolPlaygroundState(false); - } - }; - loadShowMempoolPlayground(); - }, []); - // Load UTXOs tab preference (Settings; default off) - useEffect(() => { - const loadShowUtxosTab = async () => { - try { - const stored = await LocalCache.getItem('utxos_tab_enabled'); - setShowUtxosTabState(stored === 'true'); - } catch { - setShowUtxosTabState(false); - } - }; - loadShowUtxosTab(); - }, []); - // Load PSBT tab preference (Settings; default off) - useEffect(() => { - const loadShowPsbtTab = async () => { - try { - const stored = await LocalCache.getItem('psbt_tab_enabled'); - setShowPsbtTabState(stored === 'true'); - } catch { - setShowPsbtTabState(false); - } - }; - loadShowPsbtTab(); - }, []); - // Load Wallet tab preference (Settings; default on) - useEffect(() => { - const loadShowWalletTab = async () => { - try { - const stored = await LocalCache.getItem('wallet_tab_enabled'); - setShowWalletTabState(stored !== 'false'); - } catch { - setShowWalletTabState(true); - } - }; - loadShowWalletTab(); + try { + setShowMempoolPlaygroundState(appConfigRepository.getBool(CONFIG_KEYS.TAB_MEMPOOL_ENABLED, false)); + setShowUtxosTabState(appConfigRepository.getBool(CONFIG_KEYS.TAB_UTXOS_ENABLED, false)); + setShowPsbtTabState(appConfigRepository.getBool(CONFIG_KEYS.TAB_PSBT_ENABLED, false)); + const walletEnabled = appConfigRepository.get(CONFIG_KEYS.TAB_WALLET_ENABLED); + setShowWalletTabState(walletEnabled !== 'false'); + } catch { + // defaults already set by useState + } }, []); - // Initialize network/api from cache (migrated from NetworkContext) + // Initialize network/api from SQLite (synchronous reads) useEffect(() => { const initializeNetwork = async () => { try { - const net = (await LocalCache.getItem('network')) || 'mainnet'; + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK) || 'mainnet'; setNetwork(net); - let api = await LocalCache.getItem(`api_${net}`); + let api = appConfigRepository.get(`api_${net}`) || appConfigRepository.get('api'); if (!api) { - api = - (await LocalCache.getItem('api')) || - (net === 'testnet3' - ? 'https://mempool.space/testnet/api' - : 'https://mempool.space/api'); - await LocalCache.setItem('api', api); - await LocalCache.setItem(`api_${net}`, api); + api = net === 'testnet3' + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'; + appConfigRepository.set('api', api); + appConfigRepository.set(`api_${net}`, api); } setApiBase(api); await BBMTLibNativeModule.setAPI(net, api); @@ -302,9 +265,7 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ }); try { // Load address type - const storedType = (await LocalCache.getItem( - 'addressType', - )) as AddressType | null; + const storedType = appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE) as AddressType | null; const currentAddressType = (storedType as AddressType) || 'segwit-native'; dbg(`[UserContext] refresh() - Address type loaded:`, { timestamp: Date.now(), @@ -322,17 +283,20 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ ks = JSON.parse(jks); // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - // Use derivation path that matches the address type (or legacy path for old wallets) - const path = getDerivePathForNetwork( + const externalIndex = await getExternalIndex(network, currentAddressType); + // Use receive path at current external index (HD: no address reuse) + const path = getReceivePath( network, currentAddressType, useLegacyPath, + externalIndex, ); dbg(`[UserContext] refresh() - Deriving btcPub:`, { timestamp: Date.now(), network, currentAddressType, useLegacyPath, + externalIndex, path, }); pub = await BBMTLibNativeModule.derivePubkey( @@ -366,10 +330,12 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ // because btcPub is network-specific (derivation path includes coin type: 0' for mainnet, 1' for testnet) const otherNet = actualNet === 'mainnet' ? 'testnet3' : 'mainnet'; const useLegacyPathOther = isLegacyWallet(ks.created_at); - const otherPath = getDerivePathForNetwork( + const otherExternalIndex = await getExternalIndex(otherNet, currentAddressType); + const otherPath = getReceivePath( otherNet, currentAddressType, useLegacyPathOther, + otherExternalIndex, ); const otherPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, @@ -407,27 +373,21 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ const handleSetActiveNetwork = useCallback( async (newNetwork: string) => { try { - // Save current API for the current network before switching - const currentApi = apiBase; - if (currentApi) { - await LocalCache.setItem(`api_${network}`, currentApi); + if (apiBase) { + appConfigRepository.set(`api_${network}`, apiBase); } - // Cache the new network - await LocalCache.setItem('network', newNetwork); - // Try to get the previously selected API for this network, fallback to default - let nextApi = await LocalCache.getItem(`api_${newNetwork}`); + appConfigRepository.set(CONFIG_KEYS.NETWORK, newNetwork); + let nextApi = appConfigRepository.get(`api_${newNetwork}`); if (!nextApi) { nextApi = newNetwork === 'testnet3' ? 'https://mempool.space/testnet/api' : 'https://mempool.space/api'; } - // Cache and update state - await LocalCache.setItem(`api_${newNetwork}`, nextApi); - await LocalCache.setItem('api', nextApi); + appConfigRepository.set(`api_${newNetwork}`, nextApi); + appConfigRepository.set('api', nextApi); setNetwork(newNetwork); setApiBase(nextApi); - // Update native module await BBMTLibNativeModule.setAPI(newNetwork, nextApi); } catch { // no-op @@ -437,14 +397,12 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ ); const handleSetActiveAddressType = useCallback( async (newType: AddressType) => { - await LocalCache.setItem('addressType', newType); + appConfigRepository.set(CONFIG_KEYS.ADDRESS_TYPE, newType); setActiveAddressTypeState(newType); - // Clear cached btcPub so it will be re-derived with the new address type await EncryptedStorage.removeItem('btcPub'); - // Refresh to derive new addresses with the new address type await refresh(); if (activeAddress) { - await LocalCache.setItem('currentAddress', activeAddress); + appConfigRepository.set(CONFIG_KEYS.CURRENT_ADDRESS, activeAddress); } }, [activeAddress, refresh], @@ -452,8 +410,8 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ const handleSetActiveApiProvider = useCallback( async (newApi: string) => { try { - await LocalCache.setItem(`api_${network}`, newApi); - await LocalCache.setItem('api', newApi); + appConfigRepository.set(`api_${network}`, newApi); + appConfigRepository.set('api', newApi); setApiBase(newApi); await BBMTLibNativeModule.setAPI(network, newApi); } catch { @@ -480,35 +438,19 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({ }, []); const setShowMempoolPlayground = useCallback(async (value: boolean) => { setShowMempoolPlaygroundState(value); - try { - await LocalCache.setItem('mempool_playground_enabled', value ? 'true' : 'false'); - } catch { - // no-op - } + appConfigRepository.setBool(CONFIG_KEYS.TAB_MEMPOOL_ENABLED, value); }, []); const setShowUtxosTab = useCallback(async (value: boolean) => { setShowUtxosTabState(value); - try { - await LocalCache.setItem('utxos_tab_enabled', value ? 'true' : 'false'); - } catch { - // no-op - } + appConfigRepository.setBool(CONFIG_KEYS.TAB_UTXOS_ENABLED, value); }, []); const setShowPsbtTab = useCallback(async (value: boolean) => { setShowPsbtTabState(value); - try { - await LocalCache.setItem('psbt_tab_enabled', value ? 'true' : 'false'); - } catch { - // no-op - } + appConfigRepository.setBool(CONFIG_KEYS.TAB_PSBT_ENABLED, value); }, []); const setShowWalletTab = useCallback(async (value: boolean) => { setShowWalletTabState(value); - try { - await LocalCache.setItem('wallet_tab_enabled', value ? 'true' : 'false'); - } catch { - // no-op - } + appConfigRepository.setBool(CONFIG_KEYS.TAB_WALLET_ENABLED, value); }, []); const value: UserContextType = { btcPub, diff --git a/context/WalletContext.tsx b/context/WalletContext.tsx index e8dca85b..b875a5dd 100644 --- a/context/WalletContext.tsx +++ b/context/WalletContext.tsx @@ -1,8 +1,9 @@ import React, {createContext, useContext, useState, useEffect} from 'react'; import EncryptedStorage from 'react-native-encrypted-storage'; import {NativeModules} from 'react-native'; -import {dbg, getDerivePathForNetwork, isLegacyWallet} from '../utils'; -import LocalCache from '../services/LocalCache'; +import {dbg, getReceivePath, isLegacyWallet} from '../utils'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; +import {getExternalIndex} from '../services/HdIndexService'; const {BBMTLibNativeModule} = NativeModules; interface WalletContextType { address: string; @@ -23,13 +24,11 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({ const handleAddressTypeChange = async (type: string) => { try { dbg('WalletContext: Changing address type to:', type); - await LocalCache.setItem('addressType', type); + appConfigRepository.set(CONFIG_KEYS.ADDRESS_TYPE, type); setAddressType(type); - // Refresh wallet to generate new address await refreshWallet(); } catch (error) { dbg('WalletContext: Error changing address type:', error); - dbg('Error changing address type:', error); } }; const refreshWallet = async () => { @@ -41,20 +40,18 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({ return; } const ks = JSON.parse(jks); - // Get current network - let net = await LocalCache.getItem('network'); - if (!net) { - net = 'mainnet'; - await LocalCache.setItem('network', net); + let net = appConfigRepository.get(CONFIG_KEYS.NETWORK) || 'mainnet'; + if (!appConfigRepository.get(CONFIG_KEYS.NETWORK)) { + appConfigRepository.set(CONFIG_KEYS.NETWORK, net); } dbg('WalletContext: Current network:', net); - // Get current address type for path calculation - const storedAddressType = await LocalCache.getItem('addressType'); + const storedAddressType = appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE); 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); - dbg('WalletContext: Using derivation path:', path); + const externalIndex = await getExternalIndex(net, currentAddressType); + const path = getReceivePath(net, currentAddressType, useLegacyPath, externalIndex); + dbg('WalletContext: Using derivation path (external index ' + externalIndex + '):', path); // Set network in native module first const netParams = await BBMTLibNativeModule.setBtcNetwork(net); net = netParams.split('@')[0]; @@ -79,15 +76,12 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({ // Update state setAddress(btcAddress); setNetwork(net!!); - // Handle API URL let base = netParams.split('@')[1]; - // Ensure base URL doesn't end with a slash if (base.endsWith('/')) { base = base.substring(0, base.length - 1); } - let api = await LocalCache.getItem('api'); + let api = appConfigRepository.get('api'); if (api) { - // Ensure API URL doesn't end with a slash if (api.endsWith('/')) { api = api.substring(0, api.length - 1); } @@ -96,7 +90,7 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({ setBaseApi(api); } else { dbg('WalletContext: Using default API URL:', base); - await LocalCache.setItem('api', base); + appConfigRepository.set('api', base); setBaseApi(base); } dbg('WalletContext: Wallet refresh completed'); diff --git a/ios/BBMTLibNativeModule.m b/ios/BBMTLibNativeModule.m index 8995e0b7..b75db1e8 100644 --- a/ios/BBMTLibNativeModule.m +++ b/ios/BBMTLibNativeModule.m @@ -154,6 +154,14 @@ @interface RCT_EXTERN_MODULE(BBMTLibNativeModule, NSObject) resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +// Call estimateFeeWithUTXOs (multi-path) +RCT_EXTERN_METHOD(estimateFeeWithUTXOs:(NSString *)utxosWithPathsJSON + receiverAddress:(NSString *)receiverAddress + amountSatoshi:(NSString *)amountSatoshi + changeAddress:(NSString *)changeAddress + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + // Call spendingHash RCT_EXTERN_METHOD(spendingHash:(NSString *)senderAddress receiverAddress:(NSString *)receiverAddress @@ -161,6 +169,31 @@ @interface RCT_EXTERN_MODULE(BBMTLibNativeModule, NSObject) resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +// Call spendingHashWithUTXOs (multi-path) +RCT_EXTERN_METHOD(spendingHashWithUTXOs:(NSString *)utxosWithPathsJSON + receiverAddress:(NSString *)receiverAddress + amountSatoshi:(NSString *)amountSatoshi + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +// Call mpcSendBTCWithUTXOs (multi-path) +RCT_EXTERN_METHOD(mpcSendBTCWithUTXOs:(NSString *)server + partyID:(NSString *)partyID + partiesCSV:(NSString *)partiesCSV + sessionID:(NSString *)sessionID + sessionKey:(NSString *)sessionKey + encKey:(NSString *)encKey + decKey:(NSString *)decKey + keyshare:(NSString *)keyshare + publicKey:(NSString *)publicKey + receiverAddress:(NSString *)receiverAddress + amountSatoshi:(NSString *)amountSatoshi + feeSatoshi:(NSString *)feeSatoshi + utxosWithPathsJSON:(NSString *)utxosWithPathsJSON + changeAddress:(NSString *)changeAddress + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + // Call mpcSendBTC RCT_EXTERN_METHOD(mpcSendBTC:(NSString *)server partyID:(NSString *)partyID @@ -208,7 +241,22 @@ @interface RCT_EXTERN_MODULE(BBMTLibNativeModule, NSObject) resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -// Nostr MPC Send BTC +// Nostr MPC Send BTC With UTXOs (multi-path) +RCT_EXTERN_METHOD(nostrMpcSendBTCWithUTXOs:(NSString *)relaysCSV + partyNsec:(NSString *)partyNsec + partiesNpubsCSV:(NSString *)partiesNpubsCSV + npubsSorted:(NSString *)npubsSorted + balanceSats:(NSString *)balanceSats + keyshareJSON:(NSString *)keyshareJSON + receiverAddress:(NSString *)receiverAddress + amountSatoshi:(NSString *)amountSatoshi + estimatedFee:(NSString *)estimatedFee + utxosWithPathsJSON:(NSString *)utxosWithPathsJSON + changeAddress:(NSString *)changeAddress + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +// Nostr MPC Send BTC (changeAddress: optional HD change address; pass empty string for legacy behavior) RCT_EXTERN_METHOD(nostrMpcSendBTC:(NSString *)relaysCSV partyNsec:(NSString *)partyNsec partiesNpubsCSV:(NSString *)partiesNpubsCSV @@ -221,6 +269,7 @@ @interface RCT_EXTERN_MODULE(BBMTLibNativeModule, NSObject) receiverAddress:(NSString *)receiverAddress amountSatoshi:(NSString *)amountSatoshi estimatedFee:(NSString *)estimatedFee + changeAddress:(NSString *)changeAddress resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) @@ -253,4 +302,23 @@ @interface RCT_EXTERN_MODULE(BBMTLibNativeModule, NSObject) resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +// Broadcast a signed raw tx hex; returns txid on success +RCT_EXTERN_METHOD(postTx:(NSString *)rawTxHex + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +// Compute txid from raw tx hex (for filename before broadcast) +RCT_EXTERN_METHOD(computeTxId:(NSString *)rawTxHex + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +// Cancel server-based MPC session (sessionID prefix) +RCT_EXTERN_METHOD(cancelMpcSession:(NSString *)sessionID + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +// Cancel active Nostr MPC operation (best-effort) +RCT_EXTERN_METHOD(cancelNostrMpc:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + @end diff --git a/ios/BBMTLibNativeModule.swift b/ios/BBMTLibNativeModule.swift index a932cb0c..3c49f2e1 100644 --- a/ios/BBMTLibNativeModule.swift +++ b/ios/BBMTLibNativeModule.swift @@ -117,6 +117,24 @@ class BBMTLibNativeModule: RCTEventEmitter, TssGoLogListenerProtocol, TssHookLis } } + @objc func spendingHashWithUTXOs( + _ utxosWithPathsJSON: String, + receiverAddress: String, + amountSatoshi: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { [weak self] in + var error: NSError? + let output = TssSpendingHashWithUTXOs( + utxosWithPathsJSON, + receiverAddress, + amountSatoshi, &error) + self?.sendLogEvent("spendingHashWithUTXOs", output) + resolver(error == nil ? output : error!.localizedDescription) + } + } + @objc func estimateFees( _ senderAddress: String, receiverAddress: String, @@ -135,6 +153,26 @@ class BBMTLibNativeModule: RCTEventEmitter, TssGoLogListenerProtocol, TssHookLis } } + @objc func estimateFeeWithUTXOs( + _ utxosWithPathsJSON: String, + receiverAddress: String, + amountSatoshi: String, + changeAddress: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { [weak self] in + var error: NSError? + let output = TssEstimateFeeWithUTXOs( + utxosWithPathsJSON, + receiverAddress, + amountSatoshi, + changeAddress, &error) + self?.sendLogEvent("estimateFeeWithUTXOs", output) + resolver(error == nil ? output : error!.localizedDescription) + } + } + @objc func mpcSendBTC( /* tss */ _ server: String, @@ -177,6 +215,46 @@ class BBMTLibNativeModule: RCTEventEmitter, TssGoLogListenerProtocol, TssHookLis } } + @objc func mpcSendBTCWithUTXOs( + _ server: String, + partyID: String, + partiesCSV: String, + sessionID: String, + sessionKey: String, + encKey: String, + decKey: String, + keyshare: String, + publicKey: String, + receiverAddress: String, + amountSatoshi: String, + feeSatoshi: String, + utxosWithPathsJSON: String, + changeAddress: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { [weak self] in + var error: NSError? + let output = TssMpcSendBTCWithUTXOs( + server, + partyID, + partiesCSV, + sessionID, + sessionKey, + encKey, + decKey, + keyshare, + publicKey, + receiverAddress, + amountSatoshi, + feeSatoshi, + utxosWithPathsJSON, + changeAddress, &error) + self?.sendLogEvent("mpcSendBTCWithUTXOs", output) + resolver(error == nil ? output : error!.localizedDescription) + } + } + @objc func runRelay( _ port: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock @@ -534,6 +612,7 @@ class BBMTLibNativeModule: RCTEventEmitter, TssGoLogListenerProtocol, TssHookLis _ relaysCSV: String, partyNsec: String, partiesNpubsCSV: String, npubsSorted: String, balanceSats: String, keyshareJSON: String, derivePath: String, publicKey: String, senderAddress: String, receiverAddress: String, amountSatoshi: String, estimatedFee: String, + changeAddress: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) { DispatchQueue.global(qos: .background).async { [weak self] in @@ -541,12 +620,108 @@ class BBMTLibNativeModule: RCTEventEmitter, TssGoLogListenerProtocol, TssHookLis let output = TssNostrMpcSendBTC( relaysCSV, partyNsec, partiesNpubsCSV, npubsSorted, balanceSats, keyshareJSON, derivePath, publicKey, senderAddress, receiverAddress, Int64(amountSatoshi) ?? 0, - Int64(estimatedFee) ?? 0, &error) + Int64(estimatedFee) ?? 0, changeAddress, &error) self?.sendLogEvent("nostrMpcSendBTC", output) resolver(error == nil ? output : error!.localizedDescription) } } + @objc func nostrMpcSendBTCWithUTXOs( + _ relaysCSV: String, + partyNsec: String, + partiesNpubsCSV: String, + npubsSorted: String, + balanceSats: String, + keyshareJSON: String, + receiverAddress: String, + amountSatoshi: String, + estimatedFee: String, + utxosWithPathsJSON: String, + changeAddress: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { [weak self] in + var error: NSError? + let output = TssNostrMpcSendBTCWithUTXOs( + relaysCSV, + partyNsec, + partiesNpubsCSV, + npubsSorted, + balanceSats, + keyshareJSON, + receiverAddress, + amountSatoshi, + estimatedFee, + utxosWithPathsJSON, + changeAddress, &error) + self?.sendLogEvent("nostrMpcSendBTCWithUTXOs", output) + resolver(error == nil ? output : error!.localizedDescription) + } + } + + @objc func postTx( + _ rawTxHex: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { [weak self] in + var error: NSError? + let txid = TssPostTx(rawTxHex, &error) + if error == nil { + self?.sendLogEvent("postTx", txid) + resolver(txid) + } else { + rejecter("POST_TX_ERROR", error!.localizedDescription, error) + } + } + } + + @objc func computeTxId( + _ rawTxHex: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + var error: NSError? + let txid = TssComputeTxId(rawTxHex, &error) + if error == nil { + resolver(txid) + } else { + rejecter("COMPUTE_TXID_ERROR", error!.localizedDescription, error) + } + } + + @objc func cancelMpcSession( + _ sessionID: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { + var error: NSError? + let output = TssCancelMpcSession(sessionID, &error) + if error == nil { + resolver(output) + } else { + rejecter("CANCEL_MPC_ERROR", error!.localizedDescription, error) + } + } + } + + @objc func cancelNostrMpc( + _ resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + DispatchQueue.global(qos: .background).async { + var error: NSError? + let output = TssCancelNostrMpc(&error) + if error == nil { + resolver(output) + } else { + rejecter("CANCEL_NOSTR_MPC_ERROR", error!.localizedDescription, error) + } + } + } + @objc func disableLogging( _ tag: String, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock diff --git a/ios/BoldWallet.xcodeproj/project.pbxproj b/ios/BoldWallet.xcodeproj/project.pbxproj index e99a7a6c..27a681ec 100644 --- a/ios/BoldWallet.xcodeproj/project.pbxproj +++ b/ios/BoldWallet.xcodeproj/project.pbxproj @@ -351,14 +351,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-resources.sh\"\n"; @@ -416,14 +412,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-frameworks.sh\"\n"; @@ -437,14 +429,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BoldWallet/Pods-BoldWallet-frameworks.sh\"\n"; @@ -458,14 +446,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-resources-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-BoldWallet-BoldWalletTests/Pods-BoldWallet-BoldWalletTests-resources.sh\"\n"; @@ -578,7 +562,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 50; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_BITCODE = NO; @@ -592,7 +576,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 3.0.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = ( "$(inherited)", @@ -616,7 +600,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 50; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 2G529K765N; ENABLE_TESTABILITY = NO; @@ -629,7 +613,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.0; + MARKETING_VERSION = 3.0.0; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ( "$(inherited)", @@ -716,7 +700,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -788,7 +775,10 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = "$(inherited) "; + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 65fde2dd..0188bdc0 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -10,15 +10,43 @@ PODS: - hermes-engine (0.82.1): - hermes-engine/Pre-built (= 0.82.1) - hermes-engine/Pre-built (0.82.1) - - lottie-ios (4.5.0) - - lottie-react-native (7.3.4): + - lottie-ios (4.6.0) + - lottie-react-native (7.3.6): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - lottie-ios (= 4.6.0) + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - op-sqlite (15.2.5): - boost - DoubleConversion - fast_float - fmt - glog - hermes-engine - - lottie-ios (= 4.5.0) - RCT-Folly - RCT-Folly/Fabric - RCTRequired @@ -1874,9 +1902,35 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-netinfo (11.4.1): + - react-native-netinfo (11.5.2): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety - React-Core - - react-native-safe-area-context (5.6.1): + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga + - react-native-safe-area-context (5.7.0): - boost - DoubleConversion - fast_float @@ -1894,8 +1948,8 @@ PODS: - React-graphics - React-ImageManager - React-jsi - - react-native-safe-area-context/common (= 5.6.1) - - react-native-safe-area-context/fabric (= 5.6.1) + - react-native-safe-area-context/common (= 5.7.0) + - react-native-safe-area-context/fabric (= 5.7.0) - React-NativeModulesApple - React-RCTFabric - React-renderercss @@ -1906,7 +1960,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/common (5.6.1): + - react-native-safe-area-context/common (5.7.0): - boost - DoubleConversion - fast_float @@ -1934,7 +1988,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - react-native-safe-area-context/fabric (5.6.1): + - react-native-safe-area-context/fabric (5.7.0): - boost - DoubleConversion - fast_float @@ -2536,7 +2590,7 @@ PODS: - React-Core - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.28.0): + - RNGestureHandler (2.30.0): - boost - DoubleConversion - fast_float @@ -2592,7 +2646,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNReanimated (4.2.0): + - RNReanimated (4.2.2): - boost - DoubleConversion - fast_float @@ -2619,11 +2673,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 4.2.0) + - RNReanimated/reanimated (= 4.2.2) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated (4.2.0): + - RNReanimated/reanimated (4.2.2): - boost - DoubleConversion - fast_float @@ -2650,11 +2704,11 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 4.2.0) + - RNReanimated/reanimated/apple (= 4.2.2) - RNWorklets - SocketRocket - Yoga - - RNReanimated/reanimated/apple (4.2.0): + - RNReanimated/reanimated/apple (4.2.2): - boost - DoubleConversion - fast_float @@ -2684,7 +2738,7 @@ PODS: - RNWorklets - SocketRocket - Yoga - - RNScreens (4.17.1): + - RNScreens (4.24.0): - boost - DoubleConversion - fast_float @@ -2711,10 +2765,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNScreens/common (= 4.17.1) + - RNScreens/common (= 4.24.0) - SocketRocket - Yoga - - RNScreens/common (4.17.1): + - RNScreens/common (4.24.0): - boost - DoubleConversion - fast_float @@ -2743,7 +2797,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNShare (12.2.0): + - RNShare (12.2.5): - boost - DoubleConversion - fast_float @@ -2771,7 +2825,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNSVG (15.14.0): + - RNSVG (15.15.3): - boost - DoubleConversion - fast_float @@ -2797,10 +2851,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNSVG/common (= 15.14.0) + - RNSVG/common (= 15.15.3) - SocketRocket - Yoga - - RNSVG/common (15.14.0): + - RNSVG/common (15.15.3): - boost - DoubleConversion - fast_float @@ -2828,7 +2882,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - RNWorklets (0.7.1): + - RNWorklets (0.7.4): - boost - DoubleConversion - fast_float @@ -2855,10 +2909,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets (= 0.7.1) + - RNWorklets/worklets (= 0.7.4) - SocketRocket - Yoga - - RNWorklets/worklets (0.7.1): + - RNWorklets/worklets (0.7.4): - boost - DoubleConversion - fast_float @@ -2885,10 +2939,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNWorklets/worklets/apple (= 0.7.1) + - RNWorklets/worklets/apple (= 0.7.4) - SocketRocket - Yoga - - RNWorklets/worklets/apple (0.7.1): + - RNWorklets/worklets/apple (0.7.4): - boost - DoubleConversion - fast_float @@ -2918,11 +2972,11 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - VisionCamera (4.7.2): - - VisionCamera/Core (= 4.7.2) - - VisionCamera/React (= 4.7.2) - - VisionCamera/Core (4.7.2) - - VisionCamera/React (4.7.2): + - VisionCamera (4.7.3): + - VisionCamera/Core (= 4.7.3) + - VisionCamera/React (= 4.7.3) + - VisionCamera/Core (4.7.3) + - VisionCamera/React (4.7.3): - React-Core - Yoga (0.0.0) @@ -2936,6 +2990,7 @@ DEPENDENCIES: - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) + - "op-sqlite (from `../node_modules/@op-engineering/op-sqlite`)" - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) - RCTRequired (from `../node_modules/react-native/Libraries/Required`) @@ -3048,6 +3103,8 @@ EXTERNAL SOURCES: :tag: hermes-2025-09-01-RNv0.82.0-265ef62ff3eb7289d17e366664ac0da82303e101 lottie-react-native: :path: "../node_modules/lottie-react-native" + op-sqlite: + :path: "../node_modules/@op-engineering/op-sqlite" RCT-Folly: :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -3226,8 +3283,9 @@ SPEC CHECKSUMS: fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 273e30e7fb618279934b0b95ffab60ecedb7acf5 - lottie-ios: a881093fab623c467d3bce374367755c272bdd59 - lottie-react-native: d849be4292d467c0e13fd4ca5042bb352b7d1a61 + lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3 + lottie-react-native: 994a0f1696a7ea01f63443e53a860c735246082d + op-sqlite: bbdfb540be2064a31e466eb0eef2ab014a3a1737 RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f RCTDeprecation: f17e2ebc07876ca9ab8eb6e4b0a4e4647497ae3a RCTRequired: e2c574c1b45231f7efb0834936bd609d75072b63 @@ -3265,8 +3323,8 @@ SPEC CHECKSUMS: react-native-document-picker: 6151275a22fd452b9241855250f574aa2520d1f9 react-native-encrypted-storage: db300a3f2f0aba1e818417c1c0a6be549038deb7 react-native-get-random-values: 9856c21cab42a6d8db71dcb35c3059fdaeac2b16 - react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-safe-area-context: 6d8a7b750e496e37bda47c938320bf2c734d441f + react-native-netinfo: 5e036f49ab00a53569bcc0cbc2954b608b6532c3 + react-native-safe-area-context: 0f4986a88ec555aff660503b483d6e4bd6980a9a react-native-zeroconf: 5b38b434ccc6ca245c8a5ffd64a4501e67f9edcb React-NativeModulesApple: c4bee6aa736092cd347456488a4f97a8e7517604 React-oscompat: 95875e81f5d4b3c7b2c888d5bd2c9d83450d8bdb @@ -3303,15 +3361,15 @@ SPEC CHECKSUMS: RNCClipboard: 962296f7af77f6c039b683e21c2e2255af9c05df RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 310fefa1b72004d0b0a6a361b537a74724f45d5b + RNGestureHandler: 67d0939149fdc7d619ca6bb2de7ecf5b959539ae RNReactNativeHapticFeedback: 63aa39dbd6ef56e9de688210c5761d4007927a7e - RNReanimated: 84ecd7c42f6a7e4d0f7ca498fb84980b6007e7a4 - RNScreens: d3b832c50356686916d18e28d3c5b9107382191c - RNShare: 5d5c5158bc67618ed3f8b5cc008171f1c0607cbe - RNSVG: 870974196fca9fedd72914d560cc4c774f98d170 - RNWorklets: ee480a67d776e180fca0e89a0fcb84a51f1f0c16 + RNReanimated: 03b886a575aaba0fd363ace6ea1e5b654e526e99 + RNScreens: 7179cc1ba31b4e18ed29f10abf20c24a7961cf4c + RNShare: 0e51a2414634610e30be480d26816c260c61b027 + RNSVG: 2008bfa99f51efe1edd86dec50de20d5ac16429a + RNWorklets: d0114f9d0c47b1678c93ae8ed32e5c4948e5cca7 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - VisionCamera: 2230ecfd78a1f6cddd8be0fe4e6589f21a6385e1 + VisionCamera: 3ea10c46a5c612f5f89fb46a54bef4a0de8b58a7 Yoga: 689c8e04277f3ad631e60fe2a08e41d411daf8eb PODFILE CHECKSUM: d3cf1ed985447b7e1d75f6233972019126d31419 diff --git a/ios/Tss.xcframework/Info.plist b/ios/Tss.xcframework/Info.plist index c81fd9d5..0f2e0509 100644 --- a/ios/Tss.xcframework/Info.plist +++ b/ios/Tss.xcframework/Info.plist @@ -6,17 +6,18 @@ BinaryPath - Tss.framework/Tss + Tss.framework/Versions/A/Tss LibraryIdentifier - ios-arm64 + macos-arm64_x86_64 LibraryPath Tss.framework SupportedArchitectures arm64 + x86_64 SupportedPlatform - ios + macos BinaryPath @@ -37,18 +38,17 @@ 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 723ad0f4..1f43263a 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 @@ -26,6 +26,7 @@ @class TssSession; @class TssStatus; @class TssUTXO; +@class TssUTXOWithPath; @protocol TssGoLogListener; @class TssGoLogListener; @protocol TssHookListener; @@ -274,6 +275,23 @@ @end +/** + * UTXOWithPath extends UTXO with derivation path and scriptpubkey for HD wallets (per-input signing). +Scriptpubkey (hex) is optional: when present, FetchUTXODetails is skipped during signing, +removing the last network call from the MPC signing loop. + */ +@interface TssUTXOWithPath : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field UTXOWithPath.UTXO with unsupported type: github.com/BoldBitcoinWallet/BBMTLib/tss.UTXO + +@property (nonatomic) NSString* _Nonnull derivationPath; +@property (nonatomic) NSString* _Nonnull scriptpubkey; +@end + // skipped const MaxUint32 with unsupported type: uint32 @@ -281,8 +299,29 @@ FOUNDATION_EXPORT NSString* _Nonnull TssAesDecrypt(NSString* _Nullable encrypted FOUNDATION_EXPORT NSString* _Nonnull TssAesEncrypt(NSString* _Nullable data, NSString* _Nullable key, NSError* _Nullable* _Nullable error); +/** + * CancelMpcSession requests cancellation for a given base session ID. +It cancels any currently-running derived sessions (prefix match) and ensures +any future derived sessions start cancelled. + +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelMpcSession(NSString* _Nullable sessionID, NSError* _Nullable* _Nullable error); + +/** + * CancelNostrMpc cancels the currently running Nostr MPC operation (best-effort). +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelNostrMpc(NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT void TssClearSessionLog(NSString* _Nullable session); +/** + * ComputeTxId returns the txid (reversed double-SHA256 of serialized tx) for a raw tx hex. +Used by the app to name the shared file before broadcasting. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssComputeTxId(NSString* _Nullable rawTxHex, NSError* _Nullable* _Nullable error); + // skipped function Contains with unsupported parameter or return types @@ -314,6 +353,13 @@ Returns: xpub (mainnet) or tpub (testnet) encoded string at account level m/44/0 */ FOUNDATION_EXPORT NSString* _Nonnull TssEncodeXpub(NSString* _Nullable hexPubKey, NSString* _Nullable hexChainCode, NSString* _Nullable network, NSError* _Nullable* _Nullable error); +/** + * EstimateFeeWithUTXOs estimates fees using a pre-fetched UTXO pool with paths (multi-path send). +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: used for change output size estimation (e.g. next HD change address) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFeeWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFees(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssFetchData(NSString* _Nullable url, NSString* _Nullable decKey, NSString* _Nullable data, NSError* _Nullable* _Nullable error); @@ -384,6 +430,13 @@ FOUNDATION_EXPORT BOOL TssLocalPreParams(NSString* _Nullable ppmFile, long timeo FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTC(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +/** + * MpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTCWithUTXOs(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable publicKey, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + /** * MpcSignPSBT signs a PSBT using MPC (server-based transport) Parameters: @@ -439,8 +492,18 @@ It internally performs pre-agreement to establish sessionID and unified fees. - npubsSorted: Comma-separated sorted list of all party npubs (for sessionFlag calculation) - balanceSats: Balance in satoshis (for sessionFlag calculation) - amountSatoshi: Transaction amount in satoshis (for sessionFlag calculation) + +NostrMpcSendBTC performs a Nostr-based MPC Bitcoin transaction. +changeAddress: when non-empty, change output is sent here (HD internal chain); otherwise to senderAddress. */ -FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + +/** + * NostrMpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTCWithUTXOs(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); /** * NostrMpcSignPSBT signs a PSBT using MPC over Nostr transport @@ -504,6 +567,9 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSecureRandom(long length, NSError* _Null // skipped function SelectUTXOs with unsupported parameter or return types +// skipped function SelectUTXOsWithPaths with unsupported parameter or return types + + FOUNDATION_EXPORT NSString* _Nonnull TssSendBitcoin(NSString* _Nullable wifKey, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t preview, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssSessionLog(NSString* _Nullable session); @@ -523,6 +589,16 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSha256(NSString* _Nullable msg, NSError* FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHash(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); +/** + * SpendingHashWithUTXOs is the multi-path counterpart of SpendingHash. +Instead of fetching UTXOs from a single address, it accepts a pre-fetched +pool (JSON-encoded []utxoWithPathJSON) that covers all HD addresses. +It selects UTXOs using the same "smallest-first" strategy and returns a +deterministic SHA-256 hex over "txid:vout" pairs - identical across +co-signing devices as long as they supply the same UTXO set. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHashWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssStopRelay(NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssTotalUTXO(NSString* _Nullable address, NSError* _Nullable* _Nullable error); diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist index c371d7e8..c1d5ee8a 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.1771221908 + 0.0.1772963071 CFBundleVersion - 0.0.1771221908 + 0.0.1772963071 CFBundlePackageType FMWK diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss index 99938437..fba1f101 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 723ad0f4..1f43263a 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 @@ -26,6 +26,7 @@ @class TssSession; @class TssStatus; @class TssUTXO; +@class TssUTXOWithPath; @protocol TssGoLogListener; @class TssGoLogListener; @protocol TssHookListener; @@ -274,6 +275,23 @@ @end +/** + * UTXOWithPath extends UTXO with derivation path and scriptpubkey for HD wallets (per-input signing). +Scriptpubkey (hex) is optional: when present, FetchUTXODetails is skipped during signing, +removing the last network call from the MPC signing loop. + */ +@interface TssUTXOWithPath : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field UTXOWithPath.UTXO with unsupported type: github.com/BoldBitcoinWallet/BBMTLib/tss.UTXO + +@property (nonatomic) NSString* _Nonnull derivationPath; +@property (nonatomic) NSString* _Nonnull scriptpubkey; +@end + // skipped const MaxUint32 with unsupported type: uint32 @@ -281,8 +299,29 @@ FOUNDATION_EXPORT NSString* _Nonnull TssAesDecrypt(NSString* _Nullable encrypted FOUNDATION_EXPORT NSString* _Nonnull TssAesEncrypt(NSString* _Nullable data, NSString* _Nullable key, NSError* _Nullable* _Nullable error); +/** + * CancelMpcSession requests cancellation for a given base session ID. +It cancels any currently-running derived sessions (prefix match) and ensures +any future derived sessions start cancelled. + +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelMpcSession(NSString* _Nullable sessionID, NSError* _Nullable* _Nullable error); + +/** + * CancelNostrMpc cancels the currently running Nostr MPC operation (best-effort). +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelNostrMpc(NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT void TssClearSessionLog(NSString* _Nullable session); +/** + * ComputeTxId returns the txid (reversed double-SHA256 of serialized tx) for a raw tx hex. +Used by the app to name the shared file before broadcasting. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssComputeTxId(NSString* _Nullable rawTxHex, NSError* _Nullable* _Nullable error); + // skipped function Contains with unsupported parameter or return types @@ -314,6 +353,13 @@ Returns: xpub (mainnet) or tpub (testnet) encoded string at account level m/44/0 */ FOUNDATION_EXPORT NSString* _Nonnull TssEncodeXpub(NSString* _Nullable hexPubKey, NSString* _Nullable hexChainCode, NSString* _Nullable network, NSError* _Nullable* _Nullable error); +/** + * EstimateFeeWithUTXOs estimates fees using a pre-fetched UTXO pool with paths (multi-path send). +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: used for change output size estimation (e.g. next HD change address) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFeeWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFees(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssFetchData(NSString* _Nullable url, NSString* _Nullable decKey, NSString* _Nullable data, NSError* _Nullable* _Nullable error); @@ -384,6 +430,13 @@ FOUNDATION_EXPORT BOOL TssLocalPreParams(NSString* _Nullable ppmFile, long timeo FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTC(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +/** + * MpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTCWithUTXOs(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable publicKey, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + /** * MpcSignPSBT signs a PSBT using MPC (server-based transport) Parameters: @@ -439,8 +492,18 @@ It internally performs pre-agreement to establish sessionID and unified fees. - npubsSorted: Comma-separated sorted list of all party npubs (for sessionFlag calculation) - balanceSats: Balance in satoshis (for sessionFlag calculation) - amountSatoshi: Transaction amount in satoshis (for sessionFlag calculation) + +NostrMpcSendBTC performs a Nostr-based MPC Bitcoin transaction. +changeAddress: when non-empty, change output is sent here (HD internal chain); otherwise to senderAddress. */ -FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + +/** + * NostrMpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTCWithUTXOs(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); /** * NostrMpcSignPSBT signs a PSBT using MPC over Nostr transport @@ -504,6 +567,9 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSecureRandom(long length, NSError* _Null // skipped function SelectUTXOs with unsupported parameter or return types +// skipped function SelectUTXOsWithPaths with unsupported parameter or return types + + FOUNDATION_EXPORT NSString* _Nonnull TssSendBitcoin(NSString* _Nullable wifKey, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t preview, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssSessionLog(NSString* _Nullable session); @@ -523,6 +589,16 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSha256(NSString* _Nullable msg, NSError* FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHash(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); +/** + * SpendingHashWithUTXOs is the multi-path counterpart of SpendingHash. +Instead of fetching UTXOs from a single address, it accepts a pre-fetched +pool (JSON-encoded []utxoWithPathJSON) that covers all HD addresses. +It selects UTXOs using the same "smallest-first" strategy and returns a +deterministic SHA-256 hex over "txid:vout" pairs - identical across +co-signing devices as long as they supply the same UTXO set. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHashWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssStopRelay(NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssTotalUTXO(NSString* _Nullable address, NSError* _Nullable* _Nullable error); 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 c371d7e8..c1d5ee8a 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.1771221908 + 0.0.1772963071 CFBundleVersion - 0.0.1771221908 + 0.0.1772963071 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 ce94ae3a..4fbc4618 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 723ad0f4..1f43263a 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 @@ -26,6 +26,7 @@ @class TssSession; @class TssStatus; @class TssUTXO; +@class TssUTXOWithPath; @protocol TssGoLogListener; @class TssGoLogListener; @protocol TssHookListener; @@ -274,6 +275,23 @@ @end +/** + * UTXOWithPath extends UTXO with derivation path and scriptpubkey for HD wallets (per-input signing). +Scriptpubkey (hex) is optional: when present, FetchUTXODetails is skipped during signing, +removing the last network call from the MPC signing loop. + */ +@interface TssUTXOWithPath : NSObject { +} +@property(strong, readonly) _Nonnull id _ref; + +- (nonnull instancetype)initWithRef:(_Nonnull id)ref; +- (nonnull instancetype)init; +// skipped field UTXOWithPath.UTXO with unsupported type: github.com/BoldBitcoinWallet/BBMTLib/tss.UTXO + +@property (nonatomic) NSString* _Nonnull derivationPath; +@property (nonatomic) NSString* _Nonnull scriptpubkey; +@end + // skipped const MaxUint32 with unsupported type: uint32 @@ -281,8 +299,29 @@ FOUNDATION_EXPORT NSString* _Nonnull TssAesDecrypt(NSString* _Nullable encrypted FOUNDATION_EXPORT NSString* _Nonnull TssAesEncrypt(NSString* _Nullable data, NSString* _Nullable key, NSError* _Nullable* _Nullable error); +/** + * CancelMpcSession requests cancellation for a given base session ID. +It cancels any currently-running derived sessions (prefix match) and ensures +any future derived sessions start cancelled. + +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelMpcSession(NSString* _Nullable sessionID, NSError* _Nullable* _Nullable error); + +/** + * CancelNostrMpc cancels the currently running Nostr MPC operation (best-effort). +Exposed to mobile via gomobile bind. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssCancelNostrMpc(NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT void TssClearSessionLog(NSString* _Nullable session); +/** + * ComputeTxId returns the txid (reversed double-SHA256 of serialized tx) for a raw tx hex. +Used by the app to name the shared file before broadcasting. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssComputeTxId(NSString* _Nullable rawTxHex, NSError* _Nullable* _Nullable error); + // skipped function Contains with unsupported parameter or return types @@ -314,6 +353,13 @@ Returns: xpub (mainnet) or tpub (testnet) encoded string at account level m/44/0 */ FOUNDATION_EXPORT NSString* _Nonnull TssEncodeXpub(NSString* _Nullable hexPubKey, NSString* _Nullable hexChainCode, NSString* _Nullable network, NSError* _Nullable* _Nullable error); +/** + * EstimateFeeWithUTXOs estimates fees using a pre-fetched UTXO pool with paths (multi-path send). +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: used for change output size estimation (e.g. next HD change address) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFeeWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssEstimateFees(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssFetchData(NSString* _Nullable url, NSString* _Nullable decKey, NSString* _Nullable data, NSError* _Nullable* _Nullable error); @@ -384,6 +430,13 @@ FOUNDATION_EXPORT BOOL TssLocalPreParams(NSString* _Nullable ppmFile, long timeo FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTC(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +/** + * MpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssMpcSendBTCWithUTXOs(NSString* _Nullable server, NSString* _Nullable key, NSString* _Nullable partiesCSV, NSString* _Nullable session, NSString* _Nullable sessionKey, NSString* _Nullable encKey, NSString* _Nullable decKey, NSString* _Nullable keyshare, NSString* _Nullable publicKey, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + /** * MpcSignPSBT signs a PSBT using MPC (server-based transport) Parameters: @@ -439,8 +492,18 @@ It internally performs pre-agreement to establish sessionID and unified fees. - npubsSorted: Comma-separated sorted list of all party npubs (for sessionFlag calculation) - balanceSats: Balance in satoshis (for sessionFlag calculation) - amountSatoshi: Transaction amount in satoshis (for sessionFlag calculation) + +NostrMpcSendBTC performs a Nostr-based MPC Bitcoin transaction. +changeAddress: when non-empty, change output is sent here (HD internal chain); otherwise to senderAddress. */ -FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTC(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable derivePath, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, int64_t estimatedFee, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); + +/** + * NostrMpcSendBTCWithUTXOs is the multi-path variant: uses pre-fetched UTXOs with per-input derivation paths. +utxosWithPathsJSON: JSON array of {txid, vout, value, derivation_path or derivationPath} +changeAddress: HD change address for change output (required) + */ +FOUNDATION_EXPORT NSString* _Nonnull TssNostrMpcSendBTCWithUTXOs(NSString* _Nullable relaysCSV, NSString* _Nullable partyNsec, NSString* _Nullable partiesNpubsCSV, NSString* _Nullable npubsSorted, NSString* _Nullable balanceSats, NSString* _Nullable keyshareJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSString* _Nullable estimatedFeeStr, NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable changeAddress, NSError* _Nullable* _Nullable error); /** * NostrMpcSignPSBT signs a PSBT using MPC over Nostr transport @@ -504,6 +567,9 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSecureRandom(long length, NSError* _Null // skipped function SelectUTXOs with unsupported parameter or return types +// skipped function SelectUTXOsWithPaths with unsupported parameter or return types + + FOUNDATION_EXPORT NSString* _Nonnull TssSendBitcoin(NSString* _Nullable wifKey, NSString* _Nullable publicKey, NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t preview, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssSessionLog(NSString* _Nullable session); @@ -523,6 +589,16 @@ FOUNDATION_EXPORT NSString* _Nonnull TssSha256(NSString* _Nullable msg, NSError* FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHash(NSString* _Nullable senderAddress, NSString* _Nullable receiverAddress, int64_t amountSatoshi, NSError* _Nullable* _Nullable error); +/** + * SpendingHashWithUTXOs is the multi-path counterpart of SpendingHash. +Instead of fetching UTXOs from a single address, it accepts a pre-fetched +pool (JSON-encoded []utxoWithPathJSON) that covers all HD addresses. +It selects UTXOs using the same "smallest-first" strategy and returns a +deterministic SHA-256 hex over "txid:vout" pairs - identical across +co-signing devices as long as they supply the same UTXO set. + */ +FOUNDATION_EXPORT NSString* _Nonnull TssSpendingHashWithUTXOs(NSString* _Nullable utxosWithPathsJSON, NSString* _Nullable receiverAddress, NSString* _Nullable amountSatoshiStr, NSError* _Nullable* _Nullable error); + FOUNDATION_EXPORT NSString* _Nonnull TssStopRelay(NSError* _Nullable* _Nullable error); FOUNDATION_EXPORT NSString* _Nonnull TssTotalUTXO(NSString* _Nullable address, NSError* _Nullable* _Nullable error); 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 c371d7e8..c1d5ee8a 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.1771221908 + 0.0.1772963071 CFBundleVersion - 0.0.1771221908 + 0.0.1772963071 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 1e8a0f01..4fcfad59 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 b4dd886e..edc66845 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boldwallet", - "version": "2.2.0", + "version": "3.0.0", "private": true, "scripts": { "android": "react-native run-android", @@ -14,11 +14,12 @@ "dependencies": { "@keystonehq/bc-ur-registry-btc": "^0.1.1", "@ngraveio/bc-ur": "^1.1.13", + "@op-engineering/op-sqlite": "^15.2.5", "@react-native-clipboard/clipboard": "^1.16.0", "@react-native-community/netinfo": "^11.4.1", + "@react-navigation/bottom-tabs": "^7.2.2", "@react-navigation/elements": "^2.6.5", "@react-navigation/native": "^7.1.18", - "@react-navigation/bottom-tabs": "^7.2.2", "@react-navigation/native-stack": "^7.3.28", "axios": "^1.7.9", "big.js": "^6.2.2", diff --git a/screens/MempoolPlaygroundScreen.tsx b/screens/MempoolPlaygroundScreen.tsx index 11cde14e..cd93908c 100644 --- a/screens/MempoolPlaygroundScreen.tsx +++ b/screens/MempoolPlaygroundScreen.tsx @@ -21,7 +21,7 @@ import Animated, { import AppPressable from '../components/AppPressable'; import {useTheme} from '../theme'; import {useUser} from '../context/UserContext'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; import { HeaderPriceButton, HeaderProvider, @@ -218,7 +218,7 @@ const MempoolPlaygroundScreen: React.FC<{navigation: any}> = ({navigation}) => { // Header: same as WalletHome – price (left), provider (center), network (right) useEffect(() => { - LocalCache.getItem('currency').then(c => setSelectedCurrency(c || 'USD')); + setSelectedCurrency(appConfigRepository.get(CONFIG_KEYS.CURRENCY) || 'USD'); }, []); useEffect(() => { if (!apiBase) return; diff --git a/screens/MobileNostrPairing.tsx b/screens/MobileNostrPairing.tsx index c84c08df..1fd47a9c 100644 --- a/screens/MobileNostrPairing.tsx +++ b/screens/MobileNostrPairing.tsx @@ -6,6 +6,7 @@ import { StyleSheet, Alert, Image, + Linking, Modal, TextInput, ScrollView, @@ -32,6 +33,7 @@ import StaticQRCode from '../components/StaticQRCode'; import Clipboard from '@react-native-clipboard/clipboard'; import QRScanner from '../components/QRScanner'; import BackupKeyshareModal from '../components/BackupKeyshareModal'; +import SignedTxBroadcastModal from '../components/SignedTxBroadcastModal'; import * as Progress from 'react-native-progress'; import {CommonActions, RouteProp, useRoute} from '@react-navigation/native'; import {SafeAreaView} from 'react-native-safe-area-context'; @@ -42,10 +44,13 @@ import { getNostrRelays, hexToString, getResetToMainTabsWallet, + shortenAddress, } from '../utils'; import {useTheme} from '../theme'; import {useUser} from '../context/UserContext'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; +import database from '../services/Database'; +import transactionRepository from '../services/repositories/TransactionRepository'; import {WalletService} from '../services/WalletService'; import RNFS from 'react-native-fs'; const {BBMTLibNativeModule} = NativeModules; @@ -78,6 +83,8 @@ type RouteParams = { psbtBase64?: string; // For PSBT signing mode derivationPath?: string; // Derivation path from QR code (ensures same source address) network?: string; // Network from QR code (ensures same network) + utxosJson?: string; // Pre-selected UTXOs from QR (avoids re-fetch on scanner) + changeAddress?: string; // Pre-computed change address from sender (ensures consistency) }; const MobileNostrPairing = ({navigation}: any) => { const route = useRoute>(); @@ -88,7 +95,13 @@ const MobileNostrPairing = ({navigation}: any) => { // In keygen mode, use setupMode const [isTrio, setIsTrio] = useState(setupMode === 'trio'); const {theme} = useTheme(); - const {activeNetwork, showMempoolPlayground, showUtxosTab, showPsbtTab, showWalletTab} = useUser(); + const { + activeNetwork, + showMempoolPlayground, + showUtxosTab, + showPsbtTab, + showWalletTab, + } = useUser(); const showPlay = activeNetwork === 'mainnet' && showMempoolPlayground; const ppmFile = `${RNFS.DocumentDirectoryPath}/ppm.json`; // Nostr Identity @@ -137,10 +150,6 @@ const MobileNostrPairing = ({navigation}: any) => { totalOutput: number; derivePaths?: string[]; } | null>(null); - const [fromAddress, setFromAddress] = useState(''); // Derived address for send transaction - const [currentDerivationPath, setCurrentDerivationPath] = - useState(''); // Derivation path for display - const [currentNetwork, setCurrentNetwork] = useState('mainnet'); // Network for display const [isPreParamsReady, setIsPreParamsReady] = useState(false); const [isPreparing, setIsPreparing] = useState(false); const [isPrepared, setIsPrepared] = useState(false); @@ -175,6 +184,60 @@ const MobileNostrPairing = ({navigation}: any) => { const [isQRModalVisible, setIsQRModalVisible] = useState(false); const [showRelayConfig, setShowRelayConfig] = useState(false); const [showHelpModal, setShowHelpModal] = useState(false); + + // Pre-loaded UTXO preview for the send-BTC confirmation card. + type UTXOPreview = {address: string; value: number; derivationPath: string}; + const [txPreview, setTxPreview] = useState<{ + utxos: UTXOPreview[]; + changeAddress: string; + changeAddressPath: string; + totalInputSats: number; + } | null>(null); + const [_txPreviewLoading, setTxPreviewLoading] = useState(false); + const [signedTxRawHex, setSignedTxRawHex] = useState(null); + const broadcastSuccessPayloadRef = useRef<{ + senderAddress: string; + toAddress: string; + satoshiAmount: number; + satoshiFees: number; + net: string; + addressTypeToUse: string; + showPlay: boolean; + showUtxosTab: boolean; + showPsbtTab: boolean; + showWalletTab: boolean; + originalNetwork?: string; + originalApiUrl?: string; + originalWalletServiceNetwork?: string; + originalWalletServiceApiUrl?: string; + } | null>(null); + const skipRestoreInFinallyRef = useRef(false); + const nostrAbortRef = useRef(false); + + const abortActiveNostrMpc = React.useCallback(() => { + Alert.alert( + 'Abort signing?', + 'This will stop the current Nostr MPC signing flow. You can retry anytime.', + [ + {text: 'Keep signing', style: 'cancel'}, + { + text: 'Abort', + style: 'destructive', + onPress: async () => { + nostrAbortRef.current = true; + setIsPairing(false); + setStatus('Aborted'); + try { + await BBMTLibNativeModule.cancelNostrMpc(); + } catch (e) { + dbg('MobileNostrPairing: cancelNostrMpc failed', e); + } + }, + }, + ], + ); + }, []); + const connectionQrRef = useRef(null); // Connection details for sharing (hex encoded) const connectionDetails = React.useMemo(() => { @@ -217,6 +280,110 @@ const MobileNostrPairing = ({navigation}: any) => { }; loadRelays(); }, []); + + // Pre-fetch UTXOs + change address so the confirmation card can show real inputs. + useEffect(() => { + if (!isSendBitcoin) { + return; + } + let cancelled = false; + const load = async () => { + setTxPreviewLoading(true); + const net = (route.params?.network || 'mainnet').trim(); + const addrType = (route.params?.addressType || 'segwit-native').trim(); + try { + // When QR carries UTXOs, use them directly — no re-fetch needed. + const utxosFromQR = route.params?.utxosJson; + if ( + utxosFromQR && + typeof utxosFromQR === 'string' && + utxosFromQR.trim() !== '' + ) { + const parsed = JSON.parse(utxosFromQR) as Array<{ + txid: string; + vout: number; + value: number; + derivation_path?: string; + derivationPath?: string; + address: string; + }>; + if (Array.isArray(parsed) && parsed.length > 0) { + const totalInputSats = parsed.reduce( + (s, u) => s + (u.value || 0), + 0, + ); + const chgFromParams = route.params?.changeAddress; + let chgAddress = ''; + let chgPath = ''; + if (chgFromParams && chgFromParams.trim() !== '') { + chgAddress = chgFromParams; + try { + const r = await WalletService.getInstance().getNextChangeAddressWithPath(net, addrType); + chgPath = r.path; + } catch {} + } else { + const r = await WalletService.getInstance().getNextChangeAddressWithPath(net, addrType); + chgAddress = r.address; + chgPath = r.path; + } + if (!cancelled) { + setTxPreview({ + utxos: parsed.map(u => ({ + address: u.address, + value: u.value, + derivationPath: u.derivation_path ?? u.derivationPath ?? '', + })), + changeAddress: chgAddress, + changeAddressPath: chgPath, + totalInputSats, + }); + } + return; + } + } + // Fallback: fresh fetch (sender device, or QR has no utxosJson). + const apiUrl = + (appConfigRepository.get(`api_${net}`)) || + (net === 'testnet3' || net === 'testnet' + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'); + const [utxos, chgResult] = await Promise.all([ + WalletService.getInstance().fetchUtxosWithPaths(net, addrType, apiUrl), + WalletService.getInstance().getNextChangeAddressWithPath(net, addrType), + ]); + if (!cancelled) { + const totalInputSats = utxos.reduce((s, u) => s + u.value, 0); + setTxPreview({ + utxos: utxos.map(u => ({ + address: u.address, + value: u.value, + derivationPath: u.derivationPath, + })), + changeAddress: chgResult?.address || '', + changeAddressPath: chgResult?.path || '', + totalInputSats, + }); + } + } catch { + // Non-critical: falls back to generic "HD Wallet" row. + } finally { + if (!cancelled) { + setTxPreviewLoading(false); + } + } + }; + load(); + return () => { + cancelled = true; + }; + }, [ + isSendBitcoin, + route.params?.network, + route.params?.addressType, + route.params?.utxosJson, + route.params?.changeAddress, + ]); + // Update relays when input changes (support both comma and newline separation) useEffect(() => { const parsed = relaysInput @@ -232,16 +399,16 @@ const MobileNostrPairing = ({navigation}: any) => { if (setupMode === 'duo' || setupMode === 'trio') { try { dbg('=== MobileNostrPairing: Clearing all cache for wallet setup'); - // Clear LocalCache - await LocalCache.clear(); - dbg('LocalCache cleared successfully'); + // Clear SQLite wallet data + database.clearWalletData(); + dbg('SQLite wallet data cleared'); // Clear stale EncryptedStorage items (but keep keyshare if it exists for signing) // We clear btcPub as it will be regenerated with the new keyshare await EncryptedStorage.removeItem('btcPub'); dbg('Cleared stale btcPub from EncryptedStorage'); // Clear WalletService cache try { - await LocalCache.removeItem('walletCache'); + // stale key removed; dbg('WalletService cache cleared'); } catch (error) { dbg('Error clearing WalletService cache:', error); @@ -479,7 +646,11 @@ const MobileNostrPairing = ({navigation}: any) => { } const statusDot = msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...'; - setStatus('Processing cryptographic operations' + statusDot); + if (utxoCount > 0 && utxoIndex > 0 && isSendBitcoin) { + setStatus(`Signing input ${utxoIndex}/${utxoCount}${statusDot}`); + } else { + setStatus('Processing cryptographic operations' + statusDot); + } } catch { // If parsing fails, it might be a log message, just log it dbg('TSS log:', message); @@ -498,7 +669,7 @@ const MobileNostrPairing = ({navigation}: any) => { return () => { subscription.remove(); }; - }, [isTrio]); + }, [isTrio, isSendBitcoin]); // Load keyshare and derive device info in send mode useEffect(() => { if (!isSendBitcoin && !isSignPSBT) return; @@ -704,138 +875,6 @@ const MobileNostrPairing = ({navigation}: any) => { } } }, [isSendBitcoin, isSignPSBT, isTrio, sendModeDevices, selectedPeerNpub]); - // Initialize network immediately when component loads (for send Bitcoin mode) - useEffect(() => { - const initializeNetwork = async () => { - if (!isSendBitcoin || !route.params) { - // For non-send modes, use cached network - const cachedNetwork = - (await LocalCache.getItem('network')) || 'mainnet'; - setCurrentNetwork(cachedNetwork); - return; - } - dbg('=== MobileNostrPairing: Received route params ===', { - network: route.params?.network, - derivationPath: route.params?.derivationPath, - addressType: route.params?.addressType, - toAddress: route.params?.toAddress, - satoshiAmount: route.params?.satoshiAmount, - allParams: route.params, - }); - // CRITICAL: In send mode, ALL parameters MUST come from route params (no fallbacks) - if (!route.params.network || route.params.network.trim() === '') { - dbg('ERROR: Network missing from route params in send mode'); - return; - } - // ALWAYS use route params - no fallbacks - const netForNative = route.params.network.trim(); - const netForDisplay = - netForNative === 'testnet3' ? 'testnet' : netForNative; - setCurrentNetwork(netForDisplay); - // Also set derivation path immediately if available from route params - if ( - route.params.derivationPath && - route.params.derivationPath.trim() !== '' - ) { - setCurrentDerivationPath(route.params.derivationPath.trim()); - dbg( - 'MobileNostrPairing: Initialized derivation path from route params:', - route.params.derivationPath, - ); - } - dbg( - 'MobileNostrPairing: Initialized network for display:', - netForDisplay, - '(native format:', - netForNative, - ')', - ); - }; - initializeNetwork(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSendBitcoin, route.params?.network, route.params?.derivationPath]); - // Compute from address for send transactions - useEffect(() => { - const computeFromAddress = async () => { - if (!isSendBitcoin || !route.params) return; - try { - // CRITICAL: In send mode, ALL parameters MUST come from route params (no fallbacks) - // This ensures consistency between devices and prevents mismatches - if (!route.params.network || route.params.network.trim() === '') { - dbg('ERROR: Network missing from route params in send mode'); - setFromAddress(''); - return; - } - if ( - !route.params.addressType || - route.params.addressType.trim() === '' - ) { - dbg('ERROR: Address type missing from route params in send mode'); - setFromAddress(''); - return; - } - if ( - !route.params.derivationPath || - route.params.derivationPath.trim() === '' - ) { - dbg('ERROR: Derivation path missing from route params in send mode'); - setFromAddress(''); - return; - } - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (!keyshareJSON) return; - const keyshare = JSON.parse(keyshareJSON); - // ALWAYS use route params - no fallbacks - const netForNative = route.params.network.trim(); - const addressTypeToUse = route.params.addressType.trim(); - const path = route.params.derivationPath.trim(); - // Normalize for display only: 'testnet3' -> 'testnet' - const netForDisplay = - netForNative === 'testnet3' ? 'testnet' : netForNative; - dbg( - '=== MobileNostrPairing: Using route params ONLY (no fallbacks) ===', - { - network: netForNative, - addressType: addressTypeToUse, - derivationPath: path, - }, - ); - // Derive the public key and address - const publicKey = await BBMTLibNativeModule.derivePubkey( - keyshare.pub_key, - keyshare.chain_code_hex, - path, - ); - // Use original network format for native module (requires 'testnet3' not 'testnet') - const derivedAddress = await BBMTLibNativeModule.btcAddress( - publicKey, - netForNative, - addressTypeToUse, - ); - setFromAddress(derivedAddress); - setCurrentDerivationPath(path); - setCurrentNetwork(netForDisplay); - dbg('=== MobileNostrPairing: Computed from address ===', { - derivationPath: path, - addressType: addressTypeToUse, - fromAddress: derivedAddress, - network: netForNative, - networkForDisplay: netForDisplay, - }); - } catch (error) { - dbg('Error computing from address:', error); - setFromAddress(''); - } - }; - computeFromAddress(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - isSendBitcoin, - route.params?.derivationPath, - route.params?.mode, - route.params?.network, - route.params?.addressType, - ]); const generateLocalKeypair = async () => { try { const keypairJSON = await BBMTLibNativeModule.nostrKeypair(); @@ -1300,7 +1339,7 @@ const MobileNostrPairing = ({navigation}: any) => { // Prepare relays CSV const relaysCSV = relays.join(','); // Save relays to cache - await LocalCache.setItem('nostr_relays', relaysCSV); + appConfigRepository.set('nostr_relays', relaysCSV); // Log detailed info for debugging trio mode dbg('Starting Nostr keygen with:', { relays: relaysCSV, @@ -1421,6 +1460,7 @@ const MobileNostrPairing = ({navigation}: any) => { Alert.alert('Error', 'Missing transaction parameters'); return; } + nostrAbortRef.current = false; setIsPairing(true); setProgress(0); setStatus('Starting transaction signing...'); @@ -1482,8 +1522,8 @@ const MobileNostrPairing = ({navigation}: any) => { satoshiFees, }); // Store original network/API - originalNetwork = (await LocalCache.getItem('network')) || 'mainnet'; - const cachedApi = await LocalCache.getItem(`api_${originalNetwork}`); + originalNetwork = (appConfigRepository.get(CONFIG_KEYS.NETWORK)) || 'mainnet'; + const cachedApi = appConfigRepository.get(`api_${originalNetwork}`); originalApiUrl = cachedApi || ''; if (!originalApiUrl) { originalApiUrl = @@ -1493,7 +1533,7 @@ const MobileNostrPairing = ({navigation}: any) => { } // Set network and API in BBMTLib for this transaction // Use normalized network (native format) for API lookup and BBMTLib - let apiUrl = await LocalCache.getItem(`api_${net}`); + let apiUrl = appConfigRepository.get(`api_${net}`); if (!apiUrl) { apiUrl = net === 'testnet3' || net === 'testnet' @@ -1504,7 +1544,7 @@ const MobileNostrPairing = ({navigation}: any) => { await BBMTLibNativeModule.setAPI(net, apiUrl); // CRITICAL: Update LocalCache 'api' key so WalletService.getWalletBalance uses correct API // This ensures balance fetch uses the network from route params, not device's current network - await LocalCache.setItem('api', apiUrl); + appConfigRepository.set('api', apiUrl); // CRITICAL: Temporarily update WalletService internal state so getWalletBalance uses correct network // This is needed because getWalletBalance uses this.currentNetwork for address validation const walletService = WalletService.getInstance(); @@ -1730,68 +1770,212 @@ const MobileNostrPairing = ({navigation}: any) => { } } const partiesNpubsCSV = allNpubs.sort().join(','); - // Prepare relays CSV const relaysCSV = relays.join(','); - // Call MPC send BTC - const txId = await BBMTLibNativeModule.nostrMpcSendBTC( - relaysCSV, - nsecToUse, - partiesNpubsCSV, - npubsSorted, - balanceSats, - keyshareJSON, - path, - publicKey, + // HD: get next change address so change output goes to internal chain (no address reuse). + // Prefer the change address from route params (pre-computed by sender) to ensure both + // devices use the identical change output in the signed transaction. + let changeAddress = ''; + const changeAddressFromParams = route.params?.changeAddress; + if (changeAddressFromParams && changeAddressFromParams.trim() !== '') { + changeAddress = changeAddressFromParams.trim(); + } else { + try { + changeAddress = + (await WalletService.getInstance().getNextChangeAddress( + net, + addressTypeToUse, + )) || ''; + } catch (e) { + dbg( + 'MobileNostrPairing: getNextChangeAddress failed, using legacy change to sender:', + e, + ); + } + } + + let rawTxHex: string; + try { + let utxosWithPathsJSON: string | null = null; + const utxosJsonFromQR = route.params?.utxosJson; + if ( + utxosJsonFromQR && + typeof utxosJsonFromQR === 'string' && + utxosJsonFromQR.trim() !== '' + ) { + try { + const parsed = JSON.parse(utxosJsonFromQR); + if (Array.isArray(parsed) && parsed.length > 0) { + const first = parsed[0]; + if ( + first && + typeof first.txid === 'string' && + typeof first.vout === 'number' && + typeof first.value === 'number' + ) { + // Map to WalletService shape so enrichUtxosWithScriptpubkey can process them. + const asUtxos = parsed.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivationPath: u.derivation_path ?? u.derivationPath ?? '', + address: u.address, + scriptpubkey: u.scriptpubkey ?? '', + chain: 'receive' as const, + chainIndex: 0, + })); + const needsEnrichment = asUtxos.some(u => !u.scriptpubkey); + const enriched = needsEnrichment + ? await WalletService.getInstance().enrichUtxosWithScriptpubkey( + asUtxos, + apiUrl, + ) + : asUtxos; + const forNative = enriched.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivation_path: u.derivationPath ?? u.derivation_path, + address: u.address, + scriptpubkey: u.scriptpubkey ?? '', + })); + utxosWithPathsJSON = JSON.stringify(forNative); + dbg( + 'MobileNostrPairing: using UTXOs from QR (enriched)', + forNative.length, + ); + } + } + } catch { + dbg( + 'MobileNostrPairing: failed to use utxosJson from QR, will fetch', + ); + } + } + if (!utxosWithPathsJSON) { + const utxosWithPaths = + await WalletService.getInstance().fetchUtxosWithPaths( + net, + addressTypeToUse, + apiUrl, + ); + if (utxosWithPaths.length > 0 && changeAddress) { + const enriched = + await WalletService.getInstance().enrichUtxosWithScriptpubkey( + utxosWithPaths, + apiUrl, + ); + const utxosForNative = enriched.map(u => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivation_path: u.derivationPath, + address: u.address, + scriptpubkey: u.scriptpubkey, + })); + utxosWithPathsJSON = JSON.stringify(utxosForNative); + } + } + if (utxosWithPathsJSON && changeAddress) { + rawTxHex = await BBMTLibNativeModule.nostrMpcSendBTCWithUTXOs( + relaysCSV, + nsecToUse, + partiesNpubsCSV, + npubsSorted, + balanceSats, + keyshareJSON, + toAddress, + satoshiAmount, + satoshiFees, + utxosWithPathsJSON, + changeAddress, + ); + dbg('MobileNostrPairing: multi-path send succeeded'); + } else { + rawTxHex = await BBMTLibNativeModule.nostrMpcSendBTC( + relaysCSV, + nsecToUse, + partiesNpubsCSV, + npubsSorted, + balanceSats, + keyshareJSON, + path, + publicKey, + senderAddress, + toAddress, + satoshiAmount, + satoshiFees, + changeAddress, + ); + } + } catch (multiPathErr) { + dbg( + 'MobileNostrPairing: multi-path failed, trying single-path:', + multiPathErr, + ); + rawTxHex = await BBMTLibNativeModule.nostrMpcSendBTC( + relaysCSV, + nsecToUse, + partiesNpubsCSV, + npubsSorted, + balanceSats, + keyshareJSON, + path, + publicKey, + senderAddress, + toAddress, + satoshiAmount, + satoshiFees, + changeAddress, + ); + } + if ( + !rawTxHex || + typeof rawTxHex !== 'string' || + rawTxHex.length % 2 !== 0 || + !/^[a-fA-F0-9]+$/.test(rawTxHex) + ) { + throw new Error(rawTxHex || 'Invalid signed transaction'); + } + broadcastSuccessPayloadRef.current = { senderAddress, toAddress, - satoshiAmount, - satoshiFees, - ); - // 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(`${senderAddress}-pendingTxs`)) || '{}', - ); - pendingTxs[txId] = { - txid: txId, - from: senderAddress, - to: toAddress, - amount: satoshiAmount, - satoshiAmount: satoshiAmount, - satoshiFees: satoshiFees, - sentAt: Date.now(), - status: { - confirmed: false, - block_height: null, - }, + satoshiAmount: Number(satoshiAmount), + satoshiFees: Number(satoshiFees), + net, + addressTypeToUse, + showPlay, + showUtxosTab, + showPsbtTab, + showWalletTab, + originalNetwork, + originalApiUrl, + originalWalletServiceNetwork, + originalWalletServiceApiUrl, }; - await LocalCache.setItem( - `${senderAddress}-pendingTxs`, - JSON.stringify(pendingTxs), - ); - // Navigate to Wallet tab with txId - navigation.dispatch( - CommonActions.reset(getResetToMainTabsWallet({txId}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })), - ); - setMpcDone(true); + skipRestoreInFinallyRef.current = true; + if (nostrAbortRef.current) { + setIsPairing(false); + return; + } + setSignedTxRawHex(rawTxHex); } catch (error: any) { dbg('Send BTC error:', error); - Alert.alert('Error', error?.message || 'Transaction signing failed'); + if (!nostrAbortRef.current) { + Alert.alert('Error', error?.message || 'Transaction signing failed'); + } setStatus('Transaction signing failed'); } finally { + if (skipRestoreInFinallyRef.current) { + skipRestoreInFinallyRef.current = false; + setIsPairing(false); + return; + } // CRITICAL: Restore original network after transaction completes (success or failure) - // This ensures the device's active network remains unchanged if (originalNetwork && originalApiUrl) { try { await BBMTLibNativeModule.setBtcNetwork(originalNetwork); await BBMTLibNativeModule.setAPI(originalNetwork, originalApiUrl); - // Restore LocalCache 'api' key to original network's API - await LocalCache.setItem('api', originalApiUrl); - // Restore WalletService internal state (in case it wasn't restored earlier due to error) + appConfigRepository.set('api', originalApiUrl); if (originalWalletServiceNetwork && originalWalletServiceApiUrl) { const walletService = WalletService.getInstance(); (walletService as any).currentNetwork = @@ -1830,6 +2014,7 @@ const MobileNostrPairing = ({navigation}: any) => { ); return; } + nostrAbortRef.current = false; setIsPairing(true); setProgress(0); setStatus('Starting PSBT signing...'); @@ -1971,6 +2156,10 @@ const MobileNostrPairing = ({navigation}: any) => { route.params.psbtBase64, ) .then(async (signedPsbt: any) => { + if (nostrAbortRef.current) { + setIsPairing(false); + return; + } if ( !signedPsbt || signedPsbt.includes('error') || @@ -1988,16 +2177,41 @@ const MobileNostrPairing = ({navigation}: any) => { 'PSBT signing complete: Navigating to Wallet tab with signedPsbt', ); navigation.dispatch( - CommonActions.reset(getResetToMainTabsWallet({signedPsbt}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })), + CommonActions.reset( + getResetToMainTabsWallet( + {signedPsbt}, + { + showPlay, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ), ); setMpcDone(true); }) .catch(async (e: any) => { - Alert.alert('Operation Error', `Could not sign PSBT.\n${e?.message}`); + if (!nostrAbortRef.current) { + Alert.alert( + 'Operation Error', + `Could not sign PSBT.\n${e?.message}`, + ); + } dbg(localNpub, 'PSBT signing error', e); try { navigation.dispatch( - CommonActions.reset(getResetToMainTabsWallet({}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })), + CommonActions.reset( + getResetToMainTabsWallet( + {}, + { + showPlay, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ), ); } catch (navError) { dbg('Error navigating after PSBT error:', navError); @@ -2011,7 +2225,19 @@ const MobileNostrPairing = ({navigation}: any) => { Alert.alert('Error', error?.message || 'PSBT signing failed'); setStatus('PSBT signing failed'); try { - navigation.dispatch(CommonActions.reset(getResetToMainTabsWallet({}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab }))); + navigation.dispatch( + CommonActions.reset( + getResetToMainTabsWallet( + {}, + { + showPlay, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ), + ); } catch (navError) { dbg('Error navigating after PSBT error:', navError); } @@ -3032,6 +3258,7 @@ const MobileNostrPairing = ({navigation}: any) => { checkboxLabel: { fontSize: theme.fontSizes?.base || 14, color: theme.colors.text, + marginTop: 6, flex: 1, }, preparingModalContent: { @@ -5298,14 +5525,12 @@ const MobileNostrPairing = ({navigation}: any) => { color: theme.colors.background === '#ffffff' ? theme.colors.primary - : theme.colors.text, // Use text color for better visibility in dark mode + : theme.colors.bitcoinOrange, textTransform: 'uppercase', letterSpacing: 0.5, - marginTop: 4, }}> {(() => { - const net = - route.params?.network || currentNetwork; + const net = route.params?.network || ''; const normalizedNet = net === 'testnet3' ? 'testnet' : net; return normalizedNet === 'testnet' @@ -5315,145 +5540,365 @@ const MobileNostrPairing = ({navigation}: any) => { - {fromAddress && ( - + {/* Transaction Flow */} + {(() => { + const accentColor = + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange; + const totalSats = + Number(route.params?.satoshiAmount) + + Number(route.params?.satoshiFees); + const toAddr = route.params?.toAddress || ''; + const net = route.params?.network || ''; + const isTestnet = + net === 'testnet3' || net === 'testnet'; + const explorerBase = isTestnet + ? 'https://mempool.space/testnet' + : 'https://mempool.space'; + const sectionTitle = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.textSecondary, + textTransform: 'uppercase' as const, + letterSpacing: 0.5, + marginBottom: 6, + }; + const rowBase = { + flexDirection: 'row' as const, + alignItems: 'center' as const, + backgroundColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + '06' + : '#ffffff08', + borderRadius: 8, + padding: 8, + marginBottom: 4, + borderWidth: 1, + borderColor: theme.colors.border, + }; + const rowOurs = { + ...rowBase, + backgroundColor: + theme.colors.background === '#ffffff' + ? accentColor + '12' + : accentColor + '1A', + borderColor: accentColor + '60', + paddingLeft: 11, + overflow: 'hidden' as const, + }; + const iconBase = { + width: 18, + height: 18, + marginRight: 8, + }; + const labelStyle = { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + }; + const labelOurs = { + ...labelStyle, + color: accentColor, + }; + const pathText = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.monospace, + color: theme.colors.textSecondary, + marginTop: 1, + }; + const subLabel = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + fontStyle: 'italic' as const, + marginTop: 1, + }; + const amtBTC = { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + textAlign: 'right' as const, + }; + const amtBTCOurs = { + ...amtBTC, + color: accentColor, + }; + const amtFiat = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + textAlign: 'right' as const, + }; + const changeSats = + txPreview && txPreview.totalInputSats > 0 + ? txPreview.totalInputSats - + Number(route.params?.satoshiAmount) - + Number(route.params?.satoshiFees) + : 0; + const accentBar = ( - - From Address + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 3, + backgroundColor: accentColor, + borderTopLeftRadius: 8, + borderBottomLeftRadius: 8, + }} + /> + ); + return ( + + {/* Inputs */} + + Inputs + {txPreview && txPreview.utxos.length > 0 + ? ` (${txPreview.utxos.length})` + : ''} - {currentDerivationPath && ( + {txPreview && txPreview.utxos.length > 0 ? ( + txPreview.utxos.map((u, idx) => ( + + Linking.openURL( + `${explorerBase}/address/${u.address}`, + ) + }> + {accentBar} + + + + {shortenAddress(u.address)} + + + {u.derivationPath} + + + + + {sat2btcStr(String(u.value))} BTC + + + + )) + ) : ( + + {accentBar} + + + + HD Wallet + + + + + {sat2btcStr(String(totalSats))} BTC + + + + )} + + {/* Hub */} + + + + ↓ + + - {currentDerivationPath} + Transaction - )} - - - - {fromAddress} - + + + {/* Outputs */} + Outputs + {/* Recipient */} + + Linking.openURL( + `${explorerBase}/address/${toAddr}`, + ) + }> + + + + {shortenAddress(toAddr)} + + recipient + + + + {sat2btcStr(route.params?.satoshiAmount)}{' '} + BTC + + + {route.params?.selectedCurrency || ''}{' '} + {formatFiat(route.params?.fiatAmount)} + + + + {/* Connector */} + + {/* Fee */} + + + + Fee + + + + {sat2btcStr(route.params?.satoshiFees)} BTC + + + {route.params?.selectedCurrency || ''}{' '} + {formatFiat(route.params?.fiatFees)} + + + + {/* Change output — only when we know the change address */} + {txPreview && txPreview.changeAddress ? ( + <> + + + Linking.openURL( + `${explorerBase}/address/${txPreview.changeAddress}`, + ) + }> + {accentBar} + + + + {shortenAddress( + txPreview.changeAddress, + )} + + change + {txPreview.changeAddressPath ? ( + + {txPreview.changeAddressPath} + + ) : null} + + + {changeSats > 0 && ( + + {sat2btcStr(String(changeSats))} BTC + + )} + + + + ) : null} - - )} - - - 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)} - - - + ); + })()} )} @@ -5599,6 +6044,18 @@ const MobileNostrPairing = ({navigation}: any) => { Time elapsed: {prepCounter} seconds + {isSendBitcoin && ( + + + Abort + + + )} @@ -5664,6 +6121,18 @@ const MobileNostrPairing = ({navigation}: any) => { Time elapsed: {prepCounter} seconds + {(isSendBitcoin || isSignPSBT) && ( + + + Abort + + + )} @@ -5947,6 +6416,87 @@ const MobileNostrPairing = ({navigation}: any) => { visible={isBackupModalVisible} onClose={() => setIsBackupModalVisible(false)} /> + {/* Signed tx: copy / share / broadcast — on Broadcast success we run post-broadcast logic */} + { + const p = broadcastSuccessPayloadRef.current; + if (!p) { + setSignedTxRawHex(null); + return; + } + try { + try { + await WalletService.getInstance().incrementChangeIndexAfterSend( + p.net, + p.addressTypeToUse, + ); + } catch (e) { + dbg( + 'MobileNostrPairing: incrementChangeIndexAfterSend failed:', + e, + ); + } + const pendingTxs = transactionRepository.getPendingTxMap(p.senderAddress, p.net || 'mainnet'); + pendingTxs[txId] = { + txid: txId, + from: p.senderAddress, + to: p.toAddress, + amount: p.satoshiAmount, + satoshiAmount: p.satoshiAmount, + satoshiFees: p.satoshiFees, + sentAt: Date.now(), + status: {confirmed: false, block_height: null}, + }; + transactionRepository.setPendingTxMap(p.senderAddress, p.net || 'mainnet', pendingTxs); + navigation.dispatch( + CommonActions.reset( + getResetToMainTabsWallet( + {txId}, + { + showPlay: p.showPlay, + showUtxos: p.showUtxosTab, + showPsbt: p.showPsbtTab, + showWallet: p.showWalletTab, + }, + ), + ), + ); + setMpcDone(true); + if (p.originalNetwork && p.originalApiUrl) { + try { + await BBMTLibNativeModule.setBtcNetwork(p.originalNetwork); + await BBMTLibNativeModule.setAPI( + p.originalNetwork, + p.originalApiUrl, + ); + appConfigRepository.set('api', p.originalApiUrl); + if ( + p.originalWalletServiceNetwork && + p.originalWalletServiceApiUrl + ) { + const ws = WalletService.getInstance(); + (ws as any).currentNetwork = p.originalWalletServiceNetwork; + (ws as any).currentApiUrl = p.originalWalletServiceApiUrl; + } + } catch (e) { + dbg( + 'MobileNostrPairing: Error restoring network after broadcast:', + e, + ); + } + } + } finally { + broadcastSuccessPayloadRef.current = null; + setSignedTxRawHex(null); + } + }} + onClose={() => { + broadcastSuccessPayloadRef.current = null; + setSignedTxRawHex(null); + }} + /> ); }; diff --git a/screens/MobilesPairing.tsx b/screens/MobilesPairing.tsx index 48e78109..33919030 100644 --- a/screens/MobilesPairing.tsx +++ b/screens/MobilesPairing.tsx @@ -42,12 +42,21 @@ import { } from '@react-navigation/native'; import {SafeAreaView} from 'react-native-safe-area-context'; import Big from 'big.js'; -import {dbg, getPinnedRemoteIPs, hexToString, getResetToMainTabsWallet} from '../utils'; +import { + dbg, + getPinnedRemoteIPs, + hexToString, + getResetToMainTabsWallet, + shortenAddress, +} from '../utils'; import {useTheme} from '../theme'; import {useUser} from '../context/UserContext'; import {waitMS, WalletService} from '../services/WalletService'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; +import database from '../services/Database'; +import transactionRepository from '../services/repositories/TransactionRepository'; import BackupKeyshareModal from '../components/BackupKeyshareModal'; +import SignedTxBroadcastModal from '../components/SignedTxBroadcastModal'; const {BBMTLibNativeModule} = NativeModules; // Helper component for connection line animation const ConnectionLineAnimatedView: React.FC<{ @@ -111,10 +120,6 @@ const MobilesPairing = ({navigation}: any) => { const [prepCounter, setPrepCounter] = useState(0); const [keypair, setKeypair] = useState(''); const [peerPubkey, setPeerPubkey] = useState(''); - const [fromAddress, setFromAddress] = useState(''); // Derived address for send transaction - const [currentDerivationPath, setCurrentDerivationPath] = - useState(''); // Derivation path for display - const [currentNetwork, setCurrentNetwork] = useState('mainnet'); // Network for display const [peerPubkey2, setPeerPubkey2] = useState(''); const [shareName, setShareName] = useState(''); const [_keyshare, setKeyshare] = useState(''); @@ -129,7 +134,13 @@ const MobilesPairing = ({navigation}: any) => { derivePaths?: string[]; } | null>(null); const {theme} = useTheme(); - const {activeNetwork, showMempoolPlayground, showUtxosTab, showPsbtTab, showWalletTab} = useUser(); + const { + activeNetwork, + showMempoolPlayground, + showUtxosTab, + showPsbtTab, + showWalletTab, + } = useUser(); const showPlay = activeNetwork === 'mainnet' && showMempoolPlayground; // Animation ref for horizontal progress bar const progressAnimation = useSharedValue(0); @@ -146,6 +157,8 @@ const MobilesPairing = ({navigation}: any) => { psbtBase64?: string; // For PSBT signing mode derivationPath?: string; // Derivation path from QR code (ensures same source address) network?: string; // Network from QR code (ensures same network) + utxosJson?: string; // Pre-selected UTXOs from QR (avoids re-fetch on scanner) + changeAddress?: string; // Pre-computed change address from sender (ensures consistency) }; const route = useRoute>(); const isFocused = useIsFocused(); @@ -174,7 +187,71 @@ const MobilesPairing = ({navigation}: any) => { deviceThree: false, }); const [isBackupModalVisible, setIsBackupModalVisible] = useState(false); + + // Pre-loaded UTXO preview for the send-BTC confirmation card. + type UTXOPreview = {address: string; value: number; derivationPath: string}; + const [txPreview, setTxPreview] = useState<{ + utxos: UTXOPreview[]; + changeAddress: string; + changeAddressPath: string; + totalInputSats: number; + } | null>(null); + const [_txPreviewLoading, setTxPreviewLoading] = useState(false); + const [signedTxRawHex, setSignedTxRawHex] = useState(null); + const mpcAbortRef = useRef(false); + const activeMpcSessionIdRef = useRef(null); + const broadcastSuccessPayloadRef = useRef<{ + multiPath: boolean; + pendingKey: string; + toAddress: string; + satoshiAmount: string; + satoshiFees: string; + net: string; + addressTypeToUse: string; + showPlay: boolean; + showUtxosTab: boolean; + showPsbtTab: boolean; + showWalletTab: boolean; + senderAddress: string; + originalNetwork?: string; + originalApiUrl?: string; + isMaster?: boolean; + } | null>(null); + const allChecked = Object.values(checks).every(Boolean); + + const abortActiveMpc = () => { + Alert.alert( + 'Abort signing?', + 'This will stop the current MPC signing flow. You can retry anytime.', + [ + {text: 'Keep signing', style: 'cancel'}, + { + text: 'Abort', + style: 'destructive', + onPress: async () => { + mpcAbortRef.current = true; + setDoingMPC(false); + setIsPairing(false); + setStatus('Aborted'); + const sid = activeMpcSessionIdRef.current; + if (sid) { + try { + await BBMTLibNativeModule.cancelMpcSession(sid); + } catch (e) { + dbg('MobilesPairing: cancelMpcSession failed', e); + } + } + try { + stopRelay(); + } catch { + // ignore + } + }, + }, + ], + ); + }; const allBackupChecked = isTrio ? backupChecks.deviceOne && backupChecks.deviceTwo && @@ -203,16 +280,16 @@ const MobilesPairing = ({navigation}: any) => { if (setupMode === 'duo' || setupMode === 'trio') { try { dbg('=== MobilesPairing: Clearing all cache for wallet setup'); - // Clear LocalCache - await LocalCache.clear(); - dbg('LocalCache cleared successfully'); + // Clear SQLite wallet data + database.clearWalletData(); + dbg('SQLite wallet data cleared'); // Clear stale EncryptedStorage items (but keep keyshare if it exists for signing) // We clear btcPub as it will be regenerated with the new keyshare await EncryptedStorage.removeItem('btcPub'); dbg('Cleared stale btcPub from EncryptedStorage'); // Clear WalletService cache try { - await LocalCache.removeItem('walletCache'); + // stale key removed; dbg('WalletService cache cleared'); } catch (error) { dbg('Error clearing WalletService cache:', error); @@ -225,135 +302,112 @@ const MobilesPairing = ({navigation}: any) => { }; clearCacheForSetup(); }, [setupMode]); - // Initialize network and derivation path immediately when component loads (for send Bitcoin mode) - useEffect(() => { - const initializeNetwork = async () => { - if (!isSendBitcoin || !route.params) { - // For non-send modes, use cached network - const cachedNetwork = - (await LocalCache.getItem('network')) || 'mainnet'; - setCurrentNetwork(cachedNetwork); - return; - } - dbg('=== MobilesPairing: Received route params ===', { - network: route.params?.network, - derivationPath: route.params?.derivationPath, - addressType: route.params?.addressType, - toAddress: route.params?.toAddress, - satoshiAmount: route.params?.satoshiAmount, - allParams: route.params, - }); - // CRITICAL: In send mode, ALL parameters MUST come from route params (no fallbacks) - if (!route.params.network || route.params.network.trim() === '') { - dbg('ERROR: Network missing from route params in send mode'); - return; - } - // ALWAYS use route params - no fallbacks - const netForNative = route.params.network.trim(); - const netForDisplay = - netForNative === 'testnet3' ? 'testnet' : netForNative; - setCurrentNetwork(netForDisplay); - // Also set derivation path immediately if available from route params - if ( - route.params.derivationPath && - route.params.derivationPath.trim() !== '' - ) { - setCurrentDerivationPath(route.params.derivationPath.trim()); - dbg( - 'MobilesPairing: Initialized derivation path from route params:', - route.params.derivationPath, - ); - } - dbg( - 'MobilesPairing: Initialized network for display:', - netForDisplay, - '(native format:', - netForNative, - ')', - ); - }; - initializeNetwork(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSendBitcoin, route.params?.network, route.params?.derivationPath]); - // Compute from address for send transactions + + // Pre-fetch UTXOs + change address so the confirmation card can show real inputs. useEffect(() => { - const computeFromAddress = async () => { - if (!isSendBitcoin || !route.params) return; + if (!isSendBitcoin) { + return; + } + let cancelled = false; + const load = async () => { + setTxPreviewLoading(true); + const net = (route.params?.network || 'mainnet').trim(); + const addrType = (route.params?.addressType || 'segwit-native').trim(); try { - // CRITICAL: In send mode, ALL parameters MUST come from route params (no fallbacks) - // This ensures consistency between devices and prevents mismatches - if (!route.params.network || route.params.network.trim() === '') { - dbg('ERROR: Network missing from route params in send mode'); - setFromAddress(''); - return; - } + // When QR carries UTXOs, use them directly — no re-fetch needed. + const utxosFromQR = route.params?.utxosJson; if ( - !route.params.addressType || - route.params.addressType.trim() === '' + utxosFromQR && + typeof utxosFromQR === 'string' && + utxosFromQR.trim() !== '' ) { - dbg('ERROR: Address type missing from route params in send mode'); - setFromAddress(''); - return; + const parsed = JSON.parse(utxosFromQR) as Array<{ + txid: string; + vout: number; + value: number; + derivation_path?: string; + derivationPath?: string; + address: string; + }>; + if (Array.isArray(parsed) && parsed.length > 0) { + const totalInputSats = parsed.reduce( + (s, u) => s + (u.value || 0), + 0, + ); + const chgFromParams = route.params?.changeAddress; + let chgAddress = ''; + let chgPath = ''; + if (chgFromParams && chgFromParams.trim() !== '') { + chgAddress = chgFromParams; + // derive the path for display from WalletService (index doesn't change, just the path string) + try { + const r = await WalletService.getInstance().getNextChangeAddressWithPath(net, addrType); + chgPath = r.path; + } catch {} + } else { + const r = await WalletService.getInstance().getNextChangeAddressWithPath(net, addrType); + chgAddress = r.address; + chgPath = r.path; + } + if (!cancelled) { + setTxPreview({ + utxos: parsed.map(u => ({ + address: u.address, + value: u.value, + derivationPath: u.derivation_path ?? u.derivationPath ?? '', + })), + changeAddress: chgAddress, + changeAddressPath: chgPath, + totalInputSats, + }); + } + return; + } } - if ( - !route.params.derivationPath || - route.params.derivationPath.trim() === '' - ) { - dbg('ERROR: Derivation path missing from route params in send mode'); - setFromAddress(''); - return; + // Fallback: fresh fetch (sender device, or QR has no utxosJson). + const apiUrl = + (appConfigRepository.get(`api_${net}`)) || + (net === 'testnet3' || net === 'testnet' + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'); + const [utxos, chgResult] = await Promise.all([ + WalletService.getInstance().fetchUtxosWithPaths(net, addrType, apiUrl), + WalletService.getInstance().getNextChangeAddressWithPath(net, addrType), + ]); + if (!cancelled) { + const totalInputSats = utxos.reduce((s, u) => s + u.value, 0); + setTxPreview({ + utxos: utxos.map(u => ({ + address: u.address, + value: u.value, + derivationPath: u.derivationPath, + })), + changeAddress: chgResult?.address || '', + changeAddressPath: chgResult?.path || '', + totalInputSats, + }); + } + } catch { + // Non-critical: falls back to generic "HD Wallet" row. + } finally { + if (!cancelled) { + setTxPreviewLoading(false); } - const jks = await EncryptedStorage.getItem('keyshare'); - if (!jks) return; - const ks = JSON.parse(jks); - // ALWAYS use route params - no fallbacks - const netForNative = route.params.network.trim(); - const addressTypeToUse = route.params.addressType.trim(); - const path = route.params.derivationPath.trim(); - // Normalize for display only: 'testnet3' -> 'testnet' - const netForDisplay = - netForNative === 'testnet3' ? 'testnet' : netForNative; - dbg('=== MobilesPairing: Using route params ONLY (no fallbacks) ===', { - network: netForNative, - addressType: addressTypeToUse, - derivationPath: path, - }); - // Derive the public key and address - const btcPub = await BBMTLibNativeModule.derivePubkey( - ks.pub_key, - ks.chain_code_hex, - path, - ); - // Use original network format for native module (requires 'testnet3' not 'testnet') - const derivedAddress = await BBMTLibNativeModule.btcAddress( - btcPub, - netForNative, - addressTypeToUse, - ); - setFromAddress(derivedAddress); - setCurrentDerivationPath(path); - setCurrentNetwork(netForDisplay); - dbg('=== MobilesPairing: Computed from address ===', { - derivationPath: path, - addressType: addressTypeToUse, - fromAddress: derivedAddress, - network: netForNative, - networkForDisplay: netForDisplay, - }); - } catch (error) { - dbg('Error computing from address:', error); - setFromAddress(''); } }; - computeFromAddress(); - // eslint-disable-next-line react-hooks/exhaustive-deps + load(); + return () => { + cancelled = true; + }; }, [ isSendBitcoin, - route.params?.derivationPath, - route.params?.mode, route.params?.network, route.params?.addressType, + route.params?.utxosJson, + route.params?.changeAddress, ]); + + // Initialize network and derivation path immediately when component loads (for send Bitcoin mode) const stringToHex = (str: string) => { return Array.from(str) .map(char => char.charCodeAt(0).toString(16).padStart(2, '0')) @@ -687,13 +741,13 @@ const MobilesPairing = ({navigation}: any) => { // For PSBT signing, network comes from app state, not route params if (isSignPSBT) { // Get network from LocalCache (app's current network state) - net = (await LocalCache.getItem('network')) || 'mainnet'; + net = (appConfigRepository.get(CONFIG_KEYS.NETWORK)) || 'mainnet'; dbg( 'MobilesPairing: PSBT signing - using network from app state:', net, ); // Set network and API in BBMTLib for this transaction - let apiUrl = await LocalCache.getItem(`api_${net}`); + let apiUrl = appConfigRepository.get(`api_${net}`); if (!apiUrl) { apiUrl = net === 'testnet3' || net === 'testnet' @@ -754,8 +808,8 @@ const MobilesPairing = ({navigation}: any) => { satoshiFees, }); // Store original network/API - originalNetwork = (await LocalCache.getItem('network')) || 'mainnet'; - const cachedApi = await LocalCache.getItem(`api_${originalNetwork}`); + originalNetwork = (appConfigRepository.get(CONFIG_KEYS.NETWORK)) || 'mainnet'; + const cachedApi = appConfigRepository.get(`api_${originalNetwork}`); originalApiUrl = cachedApi || ''; if (!originalApiUrl) { originalApiUrl = @@ -764,7 +818,7 @@ const MobilesPairing = ({navigation}: any) => { : 'https://mempool.space/api'; } // Set network and API in BBMTLib for this transaction - let apiUrl = await LocalCache.getItem(`api_${net}`); + let apiUrl = appConfigRepository.get(`api_${net}`); if (!apiUrl) { apiUrl = net === 'testnet3' || net === 'testnet' @@ -775,13 +829,13 @@ const MobilesPairing = ({navigation}: any) => { await BBMTLibNativeModule.setAPI(net, apiUrl); // CRITICAL: Update LocalCache 'api' key so any balance/UTXO fetches use correct API // This ensures operations use the network from route params, not device's current network - await LocalCache.setItem('api', apiUrl); + appConfigRepository.set('api', apiUrl); dbg('MobilesPairing: Set network and API in BBMTLib:', net, apiUrl); } // Store original network/API (for both PSBT and send BTC modes) if (isSignPSBT) { - originalNetwork = (await LocalCache.getItem('network')) || 'mainnet'; - const cachedApi = await LocalCache.getItem(`api_${originalNetwork}`); + originalNetwork = (appConfigRepository.get(CONFIG_KEYS.NETWORK)) || 'mainnet'; + const cachedApi = appConfigRepository.get(`api_${originalNetwork}`); originalApiUrl = cachedApi || ''; if (!originalApiUrl) { originalApiUrl = @@ -801,6 +855,8 @@ const MobilesPairing = ({navigation}: any) => { } const partiesCSV = allParties.sort().join(','); const sessionID = await BBMTLibNativeModule.sha256(`${data}/${server}`); + activeMpcSessionIdRef.current = sessionID; + mpcAbortRef.current = false; const kp = JSON.parse(keypair); const encKey = peerPubkey; const decKey = kp.privateKey; @@ -853,9 +909,21 @@ const MobilesPairing = ({navigation}: any) => { } else { dbg(partyID, 'PSBT signed successfully'); } - dbg('PSBT signing complete: Navigating to Wallet tab with signedPsbt'); + dbg( + 'PSBT signing complete: Navigating to Wallet tab with signedPsbt', + ); navigation.dispatch( - CommonActions.reset(getResetToMainTabsWallet({signedPsbt}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })), + CommonActions.reset( + getResetToMainTabsWallet( + {signedPsbt}, + { + showPlay, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ), ); setMpcDone(true); }) @@ -875,7 +943,7 @@ const MobilesPairing = ({navigation}: any) => { }); return; // Exit early for PSBT } else { - // Send BTC mode - derive from address using route params + // Send BTC mode - try multi-path first (spend from receive + change addresses) const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -892,100 +960,230 @@ const MobilesPairing = ({navigation}: any) => { if (satoshiAmount !== route.params.satoshiAmount) { throw 'Make sure you\'re sending the "Same Bitcoin" amount from Both Devices'; } - // Call MPC send BTC - await BBMTLibNativeModule.mpcSendBTC( - server, - partyID, - partiesCSV, - sessionID, - sessionKey, - encKey, - decKey, - jks, - path, - btcPub, - senderAddress, - toAddress, - satoshiAmount, - satoshiFees, - ) - .then(async (txId: any) => { - dbg(partyID, 'txID', txId); - const validTxID = /^[a-fA-F0-9]{64}$/.test(txId); - if (!validTxID) { - throw txId; + + const apiUrl = + (appConfigRepository.get(`api_${net}`)) || + (net === 'testnet3' || net === 'testnet' + ? 'https://mempool.space/testnet/api' + : 'https://mempool.space/api'); + + let usedMultiPath = false; + try { + let utxosWithPathsJSON: string | null = null; + let pendingKeyMultiPath = senderAddress; + const utxosJsonFromQR = route.params?.utxosJson; + if ( + utxosJsonFromQR && + typeof utxosJsonFromQR === 'string' && + utxosJsonFromQR.trim() !== '' + ) { + try { + const parsed = JSON.parse(utxosJsonFromQR); + if (Array.isArray(parsed) && parsed.length > 0) { + const first = parsed[0]; + if ( + first && + typeof first.txid === 'string' && + typeof first.vout === 'number' && + typeof first.value === 'number' + ) { + // Map to WalletService shape so enrichUtxosWithScriptpubkey can process them. + const asUtxos = parsed.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivationPath: u.derivation_path ?? u.derivationPath ?? '', + address: u.address, + scriptpubkey: u.scriptpubkey ?? '', + chain: 'receive' as const, + chainIndex: 0, + })); + // Enrich scriptpubkeys if missing (QR omits them to keep QR compact). + const needsEnrichment = asUtxos.some(u => !u.scriptpubkey); + const enriched = needsEnrichment + ? await WalletService.getInstance().enrichUtxosWithScriptpubkey( + asUtxos, + apiUrl, + ) + : asUtxos; + const forNative = enriched.map((u: any) => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivation_path: u.derivationPath ?? u.derivation_path, + address: u.address, + scriptpubkey: u.scriptpubkey ?? '', + })); + utxosWithPathsJSON = JSON.stringify(forNative); + pendingKeyMultiPath = forNative[0]?.address || senderAddress; + dbg( + 'MobilesPairing: using UTXOs from QR (enriched)', + forNative.length, + ); + } + } + } catch { + dbg( + 'MobilesPairing: failed to use utxosJson from QR, will fetch', + ); } - // Save pending transaction - const pendingTxs = JSON.parse( - (await LocalCache.getItem(`${senderAddress}-pendingTxs`)) || '{}', - ); - pendingTxs[txId] = { - txid: txId, - from: senderAddress, - to: toAddress, - amount: satoshiAmount, - satoshiAmount: satoshiAmount, - satoshiFees: satoshiFees, - sentAt: Date.now(), - status: { - confirmed: false, - block_height: null, - }, - }; - await LocalCache.setItem( - `${senderAddress}-pendingTxs`, - JSON.stringify(pendingTxs), - ); - navigation.dispatch( - CommonActions.reset(getResetToMainTabsWallet({txId}, { showPlay, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })), - ); - setMpcDone(true); - }) - .catch((e: any) => { - Alert.alert( - 'Operation Error', - `Could not sign and send transaction.\n${e?.message}`, - ); - dbg(partyID, 'keysign error', e); - }) - .finally(async () => { - // CRITICAL: Restore original network after transaction completes (success or failure) - // This ensures the device's active network remains unchanged - if (originalNetwork && originalApiUrl) { - try { - await BBMTLibNativeModule.setBtcNetwork(originalNetwork); - await BBMTLibNativeModule.setAPI( - originalNetwork, - originalApiUrl, - ); - // Restore LocalCache 'api' key to original network's API - await LocalCache.setItem('api', originalApiUrl); - // Restore WalletService internal state - const walletServiceRestore = WalletService.getInstance(); - (walletServiceRestore as any).currentNetwork = originalNetwork; - (walletServiceRestore as any).currentApiUrl = originalApiUrl; - dbg( - 'MobilesPairing: Restored original network:', - originalNetwork, - 'API:', - originalApiUrl, - ); - } catch (restoreError) { - dbg( - 'MobilesPairing: Error restoring original network:', - restoreError, + } + if (!utxosWithPathsJSON) { + const utxosWithPaths = + await WalletService.getInstance().fetchUtxosWithPaths( + net, + addressTypeToUse, + apiUrl, + ); + const changeAddress = + await WalletService.getInstance().getNextChangeAddress( + net, + addressTypeToUse, + ); + if (utxosWithPaths.length > 0 && changeAddress) { + const enriched = + await WalletService.getInstance().enrichUtxosWithScriptpubkey( + utxosWithPaths, + apiUrl, ); - } + const utxosForNative = enriched.map(u => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivation_path: u.derivationPath, + address: u.address, + scriptpubkey: u.scriptpubkey, + })); + utxosWithPathsJSON = JSON.stringify(utxosForNative); + pendingKeyMultiPath = utxosWithPaths[0]?.address || senderAddress; } - if (isMaster) { - await waitMS(2000); - stopRelay(); + } + // Use change address from route params when available (sender pre-computed it; + // this ensures both devices show and use the identical change output). + const changeAddressFromParams = route.params?.changeAddress; + const changeAddress = utxosWithPathsJSON + ? changeAddressFromParams && changeAddressFromParams.trim() !== '' + ? changeAddressFromParams + : await WalletService.getInstance().getNextChangeAddress( + net, + addressTypeToUse, + ) + : ''; + if (utxosWithPathsJSON && changeAddress) { + const rawTxHex = await BBMTLibNativeModule.mpcSendBTCWithUTXOs( + server, + partyID, + partiesCSV, + sessionID, + sessionKey, + encKey, + decKey, + jks, + btcPub, + toAddress, + satoshiAmount, + satoshiFees, + utxosWithPathsJSON, + changeAddress, + ); + dbg(partyID, 'signed tx (multi-path), len=', rawTxHex?.length); + if ( + !rawTxHex || + typeof rawTxHex !== 'string' || + rawTxHex.length % 2 !== 0 || + !/^[a-fA-F0-9]+$/.test(rawTxHex) + ) { + throw rawTxHex || 'Invalid signed transaction'; } + usedMultiPath = true; + const pendingKey = pendingKeyMultiPath; + broadcastSuccessPayloadRef.current = { + multiPath: true, + pendingKey, + toAddress, + satoshiAmount, + satoshiFees, + net, + addressTypeToUse, + showPlay, + showUtxosTab, + showPsbtTab, + showWalletTab, + senderAddress, + originalNetwork, + originalApiUrl, + isMaster, + }; + if (mpcAbortRef.current) { + setDoingMPC(false); + return; + } + setSignedTxRawHex(rawTxHex); setDoingMPC(false); - }); + } + } catch (multiPathErr) { + dbg( + 'MobilesPairing: multi-path send failed, falling back to single-path:', + multiPathErr, + ); + } + + if (!usedMultiPath) { + // Fallback: single-path (original flow) + const rawTxHexSingle = await BBMTLibNativeModule.mpcSendBTC( + server, + partyID, + partiesCSV, + sessionID, + sessionKey, + encKey, + decKey, + jks, + path, + btcPub, + senderAddress, + toAddress, + satoshiAmount, + satoshiFees, + ); + dbg(partyID, 'signed tx (single-path), len=', rawTxHexSingle?.length); + if ( + !rawTxHexSingle || + typeof rawTxHexSingle !== 'string' || + rawTxHexSingle.length % 2 !== 0 || + !/^[a-fA-F0-9]+$/.test(rawTxHexSingle) + ) { + throw rawTxHexSingle || 'Invalid signed transaction'; + } + broadcastSuccessPayloadRef.current = { + multiPath: false, + pendingKey: senderAddress, + toAddress, + satoshiAmount, + satoshiFees, + net, + addressTypeToUse, + showPlay, + showUtxosTab, + showPsbtTab, + showWalletTab, + senderAddress, + originalNetwork, + originalApiUrl, + isMaster, + }; + if (mpcAbortRef.current) { + setDoingMPC(false); + return; + } + setSignedTxRawHex(rawTxHexSingle); + setDoingMPC(false); + } } } catch (error: any) { - Alert.alert('Operation Error', error?.message || error); + if (!mpcAbortRef.current) { + Alert.alert('Operation Error', error?.message || error); + } dbg(localDevice, 'keysign error', error); // CRITICAL: Restore original network even on error if (originalNetwork && originalApiUrl) { @@ -993,7 +1191,7 @@ const MobilesPairing = ({navigation}: any) => { await BBMTLibNativeModule.setBtcNetwork(originalNetwork); await BBMTLibNativeModule.setAPI(originalNetwork, originalApiUrl); // Restore LocalCache 'api' key to original network's API - await LocalCache.setItem('api', originalApiUrl); + appConfigRepository.set('api', originalApiUrl); // Restore WalletService internal state const walletServiceError = WalletService.getInstance(); (walletServiceError as any).currentNetwork = originalNetwork; @@ -1018,14 +1216,14 @@ const MobilesPairing = ({navigation}: any) => { setDoingMPC(false); } }; - function stopRelay() { + const stopRelay = useCallback(() => { try { BBMTLibNativeModule.stopRelay(localDevice); dbg(localDevice, 'relay stop:'); } catch { dbg(localDevice, 'error stoping relay'); } - } + }, [localDevice]); useEffect(() => { let subscription: EmitterSubscription | undefined; const logEmitter = new NativeEventEmitter(BBMTLibNativeModule); @@ -1103,7 +1301,11 @@ const MobilesPairing = ({navigation}: any) => { } const statusDot = msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...'; - setStatus('Processing cryptographic operations' + statusDot); + if (utxoCount > 0 && utxoIndex > 0 && isSendBitcoin) { + setStatus(`Signing input ${utxoIndex}/${utxoCount}${statusDot}`); + } else { + setStatus('Processing cryptographic operations' + statusDot); + } }; if (Platform.OS === 'android') { subscription = logEmitter.addListener('BBMT_DROID', async log => { @@ -1122,7 +1324,7 @@ const MobilesPairing = ({navigation}: any) => { return () => { subscription?.remove(); }; - }, [isTrio]); + }, [isTrio, isSendBitcoin]); useEffect(() => { if (isPreparing) { const interval = setInterval(() => { @@ -1207,7 +1409,7 @@ const MobilesPairing = ({navigation}: any) => { const deviceName = await DeviceInfo.getDeviceName(); setLocalDevice(deviceName); setStatus('Starting peer discovery...'); - await LocalCache.setItem('peerFound', ''); + appConfigRepository.set('peerFound', ''); const promises = [ listenForPeerPromise( kp, @@ -1233,7 +1435,7 @@ const MobilesPairing = ({navigation}: any) => { let result = await Promise.race(promises); while (!result && Date.now() < until) { dbg('checking peer...'); - result = await LocalCache.getItem('peerFound'); + result = appConfigRepository.get('peerFound'); if (result) { dbg('checking peer ok...'); break; @@ -1250,7 +1452,7 @@ const MobilesPairing = ({navigation}: any) => { const extraWaitUntil = Date.now() + 3000; // wait up to 3s more while (Date.now() < extraWaitUntil && raws.length < 2) { await waitMS(300); - const updated = await LocalCache.getItem('peerFound'); + const updated = appConfigRepository.get('peerFound'); raws = (updated || result || '').split('|').filter(Boolean); } } @@ -1407,7 +1609,7 @@ const MobilesPairing = ({navigation}: any) => { masterHost: resolvedMasterHost, }); await Promise.allSettled(promises).then(() => - LocalCache.removeItem('peerFound'), + appConfigRepository.remove('peerFound'), ); } else { setStatus('Pairing timed out. Please try again.'); @@ -1469,7 +1671,7 @@ const MobilesPairing = ({navigation}: any) => { String(timeout), isTrio ? 'trio' : 'duo', ); - await LocalCache.setItem('peerFound', result); + appConfigRepository.set('peerFound', result); return result; } catch (error) { dbg('ListenForPeer Error:', error); @@ -1503,7 +1705,7 @@ const MobilesPairing = ({navigation}: any) => { }); while (Date.now() < until) { try { - let peerFound = await LocalCache.getItem('peerFound'); + let peerFound = appConfigRepository.get('peerFound'); if (peerFound) { dbg('discoverPeer already found'); return peerFound; @@ -1523,7 +1725,7 @@ const MobilesPairing = ({navigation}: any) => { ); if (result) { dbg('discoverPeer result', result); - await LocalCache.setItem('peerFound', result); + appConfigRepository.set('peerFound', result); return result; } } catch (error) { @@ -1632,7 +1834,10 @@ const MobilesPairing = ({navigation}: any) => { useEffect(() => { if (!isPairing || Platform.OS !== 'android') return undefined; const onBack = () => true; // prevent default (stay on screen) - const subscription = BackHandler.addEventListener('hardwareBackPress', onBack); + const subscription = BackHandler.addEventListener( + 'hardwareBackPress', + onBack, + ); return () => subscription.remove(); }, [isPairing]); const styles = StyleSheet.create({ @@ -2136,6 +2341,7 @@ const MobilesPairing = ({navigation}: any) => { color: theme.colors.text, flex: 1, textAlign: 'left', + marginTop: 6, lineHeight: 18, }, deviceContainer: { @@ -3522,7 +3728,8 @@ const MobilesPairing = ({navigation}: any) => { style={[ styles.checkboxContainer, styles.keepOpenDuringSetupContainer, - isPrepared && styles.enhancedCheckboxContainerChecked, + isPrepared && + styles.enhancedCheckboxContainerChecked, ]} disabled={isPreparing} onPress={() => { @@ -3571,7 +3778,9 @@ const MobilesPairing = ({navigation}: any) => { {} /* non-dismissible: block Android back */}> + onRequestClose={ + () => {} /* non-dismissible: block Android back */ + }> {/* Icon Container */} @@ -3709,7 +3918,9 @@ const MobilesPairing = ({navigation}: any) => { transparent={true} visible={doingMPC} animationType="fade" - onRequestClose={() => {} /* non-dismissible: block Android back */}> + onRequestClose={ + () => {} /* non-dismissible: block Android back */ + }> {/* Icon Container */} @@ -4067,8 +4278,7 @@ const MobilesPairing = ({navigation}: any) => { letterSpacing: 0.5, }}> {(() => { - const net = - route.params?.network || currentNetwork; + const net = route.params?.network || ''; const normalizedNet = net === 'testnet3' ? 'testnet' : net; return normalizedNet === 'testnet' @@ -4078,145 +4288,356 @@ const MobilesPairing = ({navigation}: any) => { - {fromAddress && ( - + {/* Transaction Flow */} + {(() => { + const accentColor = + theme.colors.background === '#ffffff' + ? theme.colors.primary + : theme.colors.bitcoinOrange; + const totalSats = + Number(route.params.satoshiAmount) + + Number(route.params.satoshiFees); + const toAddr = route.params.toAddress || ''; + const net = route.params?.network || ''; + const isTestnet = + net === 'testnet3' || net === 'testnet'; + const explorerBase = isTestnet + ? 'https://mempool.space/testnet' + : 'https://mempool.space'; + const sectionTitle = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.textSecondary, + textTransform: 'uppercase' as const, + letterSpacing: 0.5, + marginBottom: 6, + }; + const rowBase = { + flexDirection: 'row' as const, + alignItems: 'center' as const, + backgroundColor: + theme.colors.background === '#ffffff' + ? theme.colors.primary + '06' + : '#ffffff08', + borderRadius: 8, + padding: 8, + marginBottom: 4, + borderWidth: 1, + borderColor: theme.colors.border, + }; + const rowOurs = { + ...rowBase, + backgroundColor: + theme.colors.background === '#ffffff' + ? accentColor + '12' + : accentColor + '1A', + borderColor: accentColor + '60', + paddingLeft: 11, + overflow: 'hidden' as const, + }; + const iconBase = { + width: 18, + height: 18, + marginRight: 8, + }; + const labelStyle = { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + }; + const labelOurs = { + ...labelStyle, + color: accentColor, + }; + const pathText = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.monospace, + color: theme.colors.textSecondary, + marginTop: 1, + }; + const subLabel = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + fontStyle: 'italic' as const, + marginTop: 1, + }; + const amtBTC = { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: theme.fontFamilies?.bold, + color: theme.colors.text, + textAlign: 'right' as const, + }; + const amtBTCOurs = { + ...amtBTC, + color: accentColor, + }; + const amtFiat = { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.regular, + color: theme.colors.textSecondary, + textAlign: 'right' as const, + }; + const changeSats = + txPreview && txPreview.totalInputSats > 0 + ? txPreview.totalInputSats - + Number(route.params.satoshiAmount) - + Number(route.params.satoshiFees) + : 0; + const accentBar = ( - - From Address + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + width: 3, + backgroundColor: accentColor, + borderTopLeftRadius: 8, + borderBottomLeftRadius: 8, + }} + /> + ); + return ( + + {/* Inputs */} + + Inputs + {txPreview && txPreview.utxos.length > 0 + ? ` (${txPreview.utxos.length})` + : ''} - {currentDerivationPath && ( + {txPreview && txPreview.utxos.length > 0 ? ( + txPreview.utxos.map((u, idx) => ( + + Linking.openURL( + `${explorerBase}/address/${u.address}`, + ) + }> + {accentBar} + + + + {shortenAddress(u.address)} + + + {u.derivationPath} + + + + + {sat2btcStr(String(u.value))} BTC + + + + )) + ) : ( + + {accentBar} + + + + HD Wallet + + + + + {sat2btcStr(String(totalSats))} BTC + + + + )} + + {/* Hub */} + + + + ↓ + + - {currentDerivationPath} + Transaction - )} - - - - {fromAddress} - + + + {/* Outputs */} + Outputs + {/* Recipient */} + + Linking.openURL( + `${explorerBase}/address/${toAddr}`, + ) + }> + + + + {shortenAddress(toAddr)} + + recipient + + + + {sat2btcStr(route.params.satoshiAmount)} BTC + + + {route.params.selectedCurrency}{' '} + {formatFiat(route.params.fiatAmount)} + + + + {/* Connector */} + + {/* Fee */} + + + + Fee + + + + {sat2btcStr(route.params.satoshiFees)} BTC + + + {route.params.selectedCurrency}{' '} + {formatFiat(route.params.fiatFees)} + + + + {/* Change output — only when we know the change address */} + {txPreview && txPreview.changeAddress ? ( + <> + + + Linking.openURL( + `${explorerBase}/address/${txPreview.changeAddress}`, + ) + }> + {accentBar} + + + + {shortenAddress(txPreview.changeAddress)} + + change + {txPreview.changeAddressPath ? ( + + {txPreview.changeAddressPath} + + ) : null} + + + {changeSats > 0 && ( + + {sat2btcStr(String(changeSats))} BTC + + )} + + + + ) : null} - - )} - - - 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)} - - - + ); + })()} )} {isSignPSBT && ( @@ -4430,7 +4851,9 @@ const MobilesPairing = ({navigation}: any) => { transparent={true} visible={doingMPC} animationType="fade" - onRequestClose={() => {} /* non-dismissible: block Android back */}> + onRequestClose={ + () => {} /* non-dismissible: block Android back */ + }> {/* Icon Container */} @@ -4489,6 +4912,18 @@ const MobilesPairing = ({navigation}: any) => { Time elapsed: {prepCounter} seconds + {isSendBitcoin && ( + + + Abort + + + )} @@ -4532,6 +4967,87 @@ const MobilesPairing = ({navigation}: any) => { visible={isBackupModalVisible} onClose={() => setIsBackupModalVisible(false)} /> + {/* Signed tx: copy / share / broadcast — on Broadcast success we run post-broadcast logic */} + { + const p = broadcastSuccessPayloadRef.current; + if (!p) { + setSignedTxRawHex(null); + return; + } + try { + if (p.multiPath) { + try { + await WalletService.getInstance().incrementChangeIndexAfterSend( + p.net, + p.addressTypeToUse, + ); + } catch (e) { + dbg('MobilesPairing: incrementChangeIndexAfterSend failed:', e); + } + } + const pendingTxs = JSON.parse( + JSON.stringify(transactionRepository.getPendingTxMap(p.pendingKey, p.net || 'mainnet')), + ); + pendingTxs[txId] = { + txid: txId, + from: p.pendingKey, + to: p.toAddress, + amount: p.satoshiAmount, + satoshiAmount: p.satoshiAmount, + satoshiFees: p.satoshiFees, + sentAt: Date.now(), + status: {confirmed: false, block_height: null}, + }; + transactionRepository.setPendingTxMap(p.pendingKey, p.net || 'mainnet', pendingTxs); + navigation.dispatch( + CommonActions.reset( + getResetToMainTabsWallet( + {txId}, + { + showPlay: p.showPlay, + showUtxos: p.showUtxosTab, + showPsbt: p.showPsbtTab, + showWallet: p.showWalletTab, + }, + ), + ), + ); + setMpcDone(true); + if (p.originalNetwork && p.originalApiUrl) { + try { + await BBMTLibNativeModule.setBtcNetwork(p.originalNetwork); + await BBMTLibNativeModule.setAPI( + p.originalNetwork, + p.originalApiUrl, + ); + appConfigRepository.set('api', p.originalApiUrl); + const ws = WalletService.getInstance(); + (ws as any).currentNetwork = p.originalNetwork; + (ws as any).currentApiUrl = p.originalApiUrl; + } catch (e) { + dbg( + 'MobilesPairing: Error restoring network after broadcast:', + e, + ); + } + } + if (p.isMaster) { + await waitMS(2000); + stopRelay(); + } + } finally { + broadcastSuccessPayloadRef.current = null; + setSignedTxRawHex(null); + } + }} + onClose={() => { + broadcastSuccessPayloadRef.current = null; + setSignedTxRawHex(null); + }} + /> ); }; diff --git a/screens/PSBTScreen.tsx b/screens/PSBTScreen.tsx index 6f9b36f1..035ca1b9 100644 --- a/screens/PSBTScreen.tsx +++ b/screens/PSBTScreen.tsx @@ -26,7 +26,7 @@ import * as RNFS from 'react-native-fs'; import QRCodeModal from '../components/QRCodeModal'; import SignedPSBTModal from './SignedPSBTModal'; import {WalletService} from '../services/WalletService'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; import CurrencySelector from '../components/CurrencySelector'; import {createStyles as createGlobalStyles} from '../components/Styles'; const {BBMTLibNativeModule} = NativeModules; @@ -313,7 +313,7 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { useEffect(() => { const fetchPrice = async () => { try { - const currency = (await LocalCache.getItem('currency')) || 'USD'; + const currency = appConfigRepository.get(CONFIG_KEYS.CURRENCY) || 'USD'; setSelectedCurrency(currency); const walletService = WalletService.getInstance(); await walletService.initialize(); @@ -333,7 +333,7 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => { }, []); const handleCurrencySelect = async (currency: {code: string}) => { setSelectedCurrency(currency.code); - await LocalCache.setItem('currency', currency.code); + appConfigRepository.set(CONFIG_KEYS.CURRENCY, currency.code); if (priceData[currency.code]) { const formattedPrice = priceData[currency.code].toFixed(2); setBtcPrice(formattedPrice); diff --git a/screens/ReceiveModal.tsx b/screens/ReceiveModal.tsx index d7c80a79..790ecd04 100644 --- a/screens/ReceiveModal.tsx +++ b/screens/ReceiveModal.tsx @@ -17,13 +17,16 @@ import * as RNFS from 'react-native-fs'; import {dbg} from '../utils'; import {useTheme} from '../theme'; import {capitalize} from 'lodash'; +export type ReceivePathInfo = {path: string; index: number; address: string} | null; + const ReceiveModal: React.FC<{ address: string; addressType: string; baseApi: string; network: string; onClose: () => void; -}> = ({address, addressType, baseApi, network, onClose}) => { + receivePathInfo?: ReceivePathInfo; +}> = ({address, addressType, baseApi, network, onClose, receivePathInfo}) => { const qrRef = useRef(null); const {theme} = useTheme(); const [isCopied, setIsCopied] = useState(false); @@ -167,6 +170,30 @@ const ReceiveModal: React.FC<{ fontFamily: theme.fontFamilies?.bold, color: theme.colors.text, // Fix dark mode readability }, + pathChip: { + backgroundColor: + theme.colors.background === '#ffffff' + ? theme.colors.blackOverlay05 + : theme.colors.whiteOverlay10, + borderWidth: 1, + borderColor: theme.colors.border, + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 10, + marginBottom: 16, + alignSelf: 'center', + }, + pathChipText: { + fontSize: theme.fontSizes?.xs || 10, + fontFamily: theme.fontFamilies?.monospaceMedium, + color: theme.colors.textSecondary, + }, + pathChipPathText: { + fontSize: theme.fontSizes?.xs || 10, + marginTop: 2, + fontFamily: theme.fontFamilies?.monospaceMedium, + color: theme.colors.textSecondary, + }, qrContainer: { backgroundColor: 'white', padding: 8, @@ -297,7 +324,7 @@ const ReceiveModal: React.FC<{ onClose(); }} style={styles.closeButton} - android_ripple={{ color: 'rgba(0,0,0,0.1)' }}> + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> ✖️ @@ -306,6 +333,16 @@ const ReceiveModal: React.FC<{ {capitalize(network)} • {formatAddressType(addressType)} + {receivePathInfo && ( + + + Receive #{receivePathInfo.index}:{' '} + + {receivePathInfo.path} + + + + )} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> { copyToClipboard(); }} - android_ripple={{ color: 'rgba(0,0,0,0.1)' }}> + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> { shareQRCode(); }} - android_ripple={{ color: 'rgba(0,0,0,0.1)' }}> + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> void; btcToFiatRate: Big; walletBalance: Big; @@ -61,6 +64,10 @@ const SendBitcoinModal: React.FC = ({ }) => { const isMountedRef = useRef(true); const visibleRef = useRef(visible); + /** Compact UTXOs (no scriptpubkey) captured at fee-estimation time, embedded in the QR. */ + const lastUtxosJsonRef = useRef(null); + /** Change address computed alongside UTXOs; passed through to co-signers via route params. */ + const lastChangeAddressRef = useRef(null); const [address, setAddress] = useState(''); const [btcAmount, setBtcAmount] = useState(Big(0)); const [inBtcAmount, setInBtcAmount] = useState(''); @@ -73,7 +80,13 @@ const SendBitcoinModal: React.FC = ({ const [feeStrategy, setFeeStrategy] = useState('1hr'); const [addressError, setAddressError] = useState(null); const {theme} = useTheme(); - const {showSats, balanceFormattingEnabled, activeNetwork} = useUser(); + const { + showSats, + balanceFormattingEnabled, + activeNetwork, + activeAddressType, + activeApiProvider, + } = useUser(); const isSatsMode = showSats; useEffect(() => { visibleRef.current = visible; @@ -456,15 +469,83 @@ const SendBitcoinModal: React.FC = ({ } setIsCalculatingFee(true); const satoshiAmount = amount.times(1e8).toFixed(0); - BBMTLibNativeModule.spendingHash(walletAddress, addr, satoshiAmount) - .then((hash: string) => { + + // Fetch multi-path UTXOs once and reuse for both spendingHash and fee estimation. + // Falls back to single-address path when no activeApiProvider is set. + let utxosJson: string | null = null; + let changeAddress: string = ''; + if (activeApiProvider) { + try { + const ws = WalletService.getInstance(); + const utxosWithPaths = await ws.fetchUtxosWithPaths( + activeNetwork, + activeAddressType, + activeApiProvider, + ); + dbg('SendBitcoinModal: utxosWithPaths count', utxosWithPaths.length); + utxosJson = JSON.stringify(utxosWithPaths); + // Compact form for QR (no scriptpubkey — enriched during signing) + lastUtxosJsonRef.current = JSON.stringify( + utxosWithPaths.map(u => ({ + txid: u.txid, + vout: u.vout, + value: u.value, + derivation_path: u.derivationPath, + address: u.address, + })), + ); + lastChangeAddressRef.current = changeAddress; + changeAddress = await ws.getNextChangeAddress( + activeNetwork, + activeAddressType, + ); + } catch (e) { + dbg('SendBitcoinModal: UTXO fetch failed, falling back to single-address', e); + } + } + + // Spending hash: prefer multi-path (deterministic across all co-signers), + // fall back to single-address legacy path when UTXOs could not be fetched. + let hashPromise: Promise; + if (utxosJson) { + hashPromise = BBMTLibNativeModule.spendingHashWithUTXOs( + utxosJson, + addr, + satoshiAmount, + ); + } else { + hashPromise = BBMTLibNativeModule.spendingHash( + walletAddress, + addr, + satoshiAmount, + ); + } + + hashPromise + .then(async (hash: string) => { if (!isMountedRef.current || !visibleRef.current) { return; } setSpendingHash(hash); dbg('got spending hash:', hash); - BBMTLibNativeModule.estimateFees(walletAddress, addr, satoshiAmount) - .then((fee: string) => { + + // Fee estimation — reuse already-fetched UTXOs (no second network round-trip). + let feePromise: Promise; + if (utxosJson) { + feePromise = BBMTLibNativeModule.estimateFeeWithUTXOs( + utxosJson, + addr, + satoshiAmount, + changeAddress, + ); + } else { + feePromise = BBMTLibNativeModule.estimateFees( + walletAddress, + addr, + satoshiAmount, + ); + } + feePromise.then((fee: string) => { if (!isMountedRef.current || !visibleRef.current) { return; } @@ -650,12 +731,15 @@ const SendBitcoinModal: React.FC = ({ validateAddressForCurrentNetwork, currentNetworkForValidation, isSatsMode, + activeApiProvider, + activeNetwork, + activeAddressType, ], ); const debouncedGetFee = useMemo(() => debounce(getFee, 1000), [getFee]); useEffect(() => { const initFee = async () => { - const feeOption = await LocalCache.getItem('feeStrategy'); + const feeOption = appConfigRepository.get(CONFIG_KEYS.FEE_STRATEGY); // Always default to 'eco' if no fee strategy is set or if it was 'min' const defaultFee = feeOption && feeOption !== 'min' ? feeOption : 'eco'; setFeeStrategy(defaultFee); @@ -867,7 +951,7 @@ const SendBitcoinModal: React.FC = ({ setFeeStrategy(value); dbg('setting fee strategy to', value); BBMTLibNativeModule.setFeePolicy(value); - LocalCache.setItem('feeStrategy', value); + appConfigRepository.set(CONFIG_KEYS.FEE_STRATEGY, value); // Dismiss keyboard when fee strategy changes (triggers new fee estimation) Keyboard.dismiss(); }; @@ -907,7 +991,7 @@ const SendBitcoinModal: React.FC = ({ } // Normalize to sats for sending const amountSats = btcAmount.times(E8); - onSend(address, amountSats, estimatedFee, spendingHash); + onSend(address, amountSats, estimatedFee, spendingHash, lastUtxosJsonRef.current, lastChangeAddressRef.current); }; // Check if amount exceeds balance const amountExceedsBalance = btcAmount.gt(0) && btcAmount.gt(walletBalance); diff --git a/screens/ShowcaseScreen.tsx b/screens/ShowcaseScreen.tsx index 5b577184..7bd59134 100644 --- a/screens/ShowcaseScreen.tsx +++ b/screens/ShowcaseScreen.tsx @@ -23,7 +23,10 @@ import {useTheme} from '../theme'; import {dbg, isLegacyWallet} from '../utils'; import LegalModal from '../components/LegalModal'; import TransportModeSelector from '../components/TransportModeSelector'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; +import database from '../services/Database'; +import {WalletService} from '../services/WalletService'; +import mempoolClient from '../services/MempoolClient'; import {useUser} from '../context/UserContext'; const {BBMTLibNativeModule} = NativeModules; const ShowcaseScreen = ({navigation}: any) => { @@ -47,28 +50,43 @@ const ShowcaseScreen = ({navigation}: any) => { const fadeAnim = useRef(new Animated.Value(0.6)).current; const connectorAnim = useRef(new Animated.Value(0)).current; const connectorLoopRef = useRef(null as Animated.CompositeAnimation | null); - // Clear all app cache on component mount (wallet import screen) + // Full reset on mount: wipe all wallet DB tables, all in-memory caches, and + // all user-preference EncryptedStorage entries so the import starts from a + // guaranteed clean slate. The keyshare itself is preserved so a user who + // already onboarded can land here without losing their key material. useEffect(() => { const clearAllCache = async () => { try { - dbg('=== ShowcaseScreen: Clearing all cache for wallet import'); - // Clear LocalCache - await LocalCache.clear(); - dbg('LocalCache cleared successfully'); - // Clear stale EncryptedStorage items (but keep keyshare if it exists) - // We clear btcPub as it will be regenerated with the imported keyshare - await EncryptedStorage.removeItem('btcPub'); - dbg('Cleared stale btcPub from EncryptedStorage'); - // Clear WalletService cache - try { - await LocalCache.removeItem('walletCache'); - dbg('WalletService cache cleared'); - } catch (error) { - dbg('Error clearing WalletService cache:', error); - } - dbg('=== ShowcaseScreen: Cache clearing completed'); + dbg('=== ShowcaseScreen: Full reset for wallet import'); + + // 1. Wipe all SQLite wallet tables (UTXOs, balances, transactions, + // HD indexes, sync metadata, pending txs, …). + database.clearWalletData(); + dbg('SQLite wallet data cleared'); + + // 2. Wipe in-memory caches so stale data never leaks into the new + // wallet session. + const ws = WalletService.getInstance(); + ws.invalidateAddressCache(); + mempoolClient.invalidateAll(); + dbg('In-memory caches cleared'); + + // 3. Clear all EncryptedStorage preference items. btcPub will be + // re-derived from the newly imported keyshare; the rest are user + // preferences that should reset to defaults for a fresh wallet. + await Promise.allSettled([ + EncryptedStorage.removeItem('btcPub'), + EncryptedStorage.removeItem('bitcoin_display_sats'), + EncryptedStorage.removeItem('balance_formatting_enabled'), + EncryptedStorage.removeItem('app_icon_preference'), + EncryptedStorage.removeItem('devDebugEnabled'), + EncryptedStorage.removeItem('psbt_mode_first_visit'), + ]); + dbg('EncryptedStorage preferences cleared'); + + dbg('=== ShowcaseScreen: Full reset completed'); } catch (err) { - dbg('Error clearing app cache:', err); + dbg('ShowcaseScreen: Error during full reset:', err); } }; clearAllCache(); @@ -187,10 +205,7 @@ const ShowcaseScreen = ({navigation}: any) => { // Reset legacy wallet modal flag for new wallet // If legacy wallet, set to "no" (show modal); if not legacy, set to "yes" (won't show anyway) const isLegacy = isLegacyWallet(ks.created_at); - await LocalCache.setItem( - 'legacyWalletModalDoNotRemind', - isLegacy ? 'no' : 'yes', - ); + appConfigRepository.set(CONFIG_KEYS.LEGACY_WALLET_DO_NOT_REMIND, isLegacy ? 'no' : 'yes'); // CRITICAL: Always reset network to mainnet on keyshare import // This ensures a clean state and proper address derivation for the new wallet dbg('=== Keyshare imported: Resetting network to mainnet'); @@ -198,15 +213,15 @@ const ShowcaseScreen = ({navigation}: any) => { dbg('=== Network reset to mainnet, UserContext will refresh addresses'); setModalVisible(false); setPassword(''); - dbg('Navigating to User Preferences'); + dbg('Navigating to User Preferences (restore will run after endpoint selection)'); setTimeout(() => { navigation.dispatch( CommonActions.reset({ index: 0, - routes: [{name: 'User Preferences'}], + routes: [{name: 'User Preferences', params: {pendingRestore: true}}], }), ); - }, 1000); + }, 500); } } catch { dbg('Failed to decode as UTF-8. File might be binary.'); diff --git a/screens/UserPreferenceScreen.tsx b/screens/UserPreferenceScreen.tsx index 6154dfbf..a9a752b2 100644 --- a/screens/UserPreferenceScreen.tsx +++ b/screens/UserPreferenceScreen.tsx @@ -9,18 +9,31 @@ import { Platform, Image, } from 'react-native'; +import {useRoute} from '@react-navigation/native'; import AppPressable from '../components/AppPressable'; import AppText from '../components/AppText'; +import RestoringIndexesModal from '../components/RestoringIndexesModal'; import {SafeAreaView} from 'react-native-safe-area-context'; import {useTheme} from '../theme'; import {dbg, getResetToMainTabsWallet} from '../utils'; import {useUser} from '../context/UserContext'; +import {WalletService} from '../services/WalletService'; +import mempoolClient from '../services/MempoolClient'; const UserPreferenceScreen: React.FC<{navigation: any}> = ({navigation}) => { + const route = useRoute(); + const pendingRestore = (route.params as any)?.pendingRestore === true; + const {theme} = useTheme(); - const {setActiveApiProvider, activeNetwork, showMempoolPlayground, showUtxosTab, showPsbtTab, showWalletTab} = useUser(); + const {setActiveApiProvider, activeApiProvider, activeNetwork, showMempoolPlayground, showUtxosTab, showPsbtTab, showWalletTab} = useUser(); const [pendingAPI, setPendingAPI] = useState(''); const [isAPISaving, setIsAPISaving] = useState(false); + const [isRestoringIndexes, setIsRestoringIndexes] = useState(false); + const [restoreProgress, setRestoreProgress] = useState<{ + chain: 'external' | 'internal'; + index: number; + gapIndex: number; + } | null>(null); const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); @@ -96,8 +109,9 @@ const UserPreferenceScreen: React.FC<{navigation: any}> = ({navigation}) => { await setActiveApiProvider(normalizedApi); setPendingAPI(normalizedApi); dbg('=== API saved and propagated successfully:', normalizedApi); - // Proceed to home after successful save - handleProceed(); + // Proceed to home after successful save — pass the resolved API so the + // restore uses the endpoint the user just configured, not the stale state. + handleProceed(normalizedApi); } catch (error) { dbg('Error in saveAPIAndProceed:', error); Alert.alert('Error', 'Failed to save API endpoint. Please try again.'); @@ -125,7 +139,7 @@ const UserPreferenceScreen: React.FC<{navigation: any}> = ({navigation}) => { return styles.apiInputContainer; }; - const handleSkip = () => { + const navigateToHome = () => { navigation.reset( getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, @@ -136,15 +150,56 @@ const UserPreferenceScreen: React.FC<{navigation: any}> = ({navigation}) => { ); }; - const handleProceed = () => { - navigation.reset( - getResetToMainTabsWallet({}, { - showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, - showUtxos: showUtxosTab, - showPsbt: showPsbtTab, - showWallet: showWalletTab, - }), - ); + const runRestoreIfNeeded = async (apiUrl: string) => { + if (!pendingRestore) { + return; + } + dbg('UserPreferenceScreen: Running HD index restore with API:', apiUrl.slice(0, 40)); + setIsRestoringIndexes(true); + setRestoreProgress(null); + try { + const ws = WalletService.getInstance(); + for (const addrType of ['legacy', 'segwit-native', 'segwit-compatible']) { + await ws.discoverHdIndexesForNetwork( + 'mainnet', + addrType, + apiUrl, + (chain, index, gapIndex) => setRestoreProgress({chain, index, gapIndex}), + ); + } + dbg('UserPreferenceScreen: HD index restore complete'); + + // Pre-warm the in-memory HD address cache for the default address type. + // discoverHdIndexesForNetwork sets the indexes but never calls + // getHdAddressesWithPaths, so hdAddressCache is cold when WalletHome loads. + // A cold cache forces sequential native-module derivation (2-5 s), during + // which TransactionList fires in single-address mode and shows stale/partial + // results. Pre-warming here means getHdAddressesWithPaths returns from the + // in-memory Map in microseconds — walletAddresses is ready before the first + // TransactionList render. + await ws.getHdAddressesWithPaths('mainnet', 'segwit-native'); + + // Wipe all HTTP cache entries populated by isAddressUsed() during discovery. + // Those entries share the same /address/{addr}/txs URLs that the transaction + // list fetches. Leaving them causes pull-to-refresh (within the 30 s TTL) + // to serve discovery-era snapshots instead of fresh network data. + mempoolClient.invalidateAll(); + } finally { + setIsRestoringIndexes(false); + setRestoreProgress(null); + } + }; + + const handleSkip = async () => { + const fallbackApi = + activeApiProvider || 'https://mempool.space/api'; + await runRestoreIfNeeded(fallbackApi); + navigateToHome(); + }; + + const handleProceed = async (resolvedApi?: string) => { + await runRestoreIfNeeded(resolvedApi || activeApiProvider || 'https://mempool.space/api'); + navigateToHome(); }; const styles = StyleSheet.create({ @@ -375,13 +430,13 @@ const UserPreferenceScreen: React.FC<{navigation: any}> = ({navigation}) => { { saveAPIAndProceed(pendingAPI); }} - disabled={isAPISaving || pendingAPI.trim() === ''} + disabled={isAPISaving || isRestoringIndexes || pendingAPI.trim() === ''} android_ripple={{ color: 'rgba(0,0,0,0.1)' }}> = ({navigation}) => { Skip for now + ); }; diff --git a/screens/UtxosScreen.tsx b/screens/UtxosScreen.tsx index 980936f8..042fd7dd 100644 --- a/screens/UtxosScreen.tsx +++ b/screens/UtxosScreen.tsx @@ -19,7 +19,8 @@ import { HeaderProvider, HeaderNetwork, } from '../components/Header'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; +import utxoRepository from '../services/repositories/UtxoRepository'; import {WalletService} from '../services/WalletService'; import {presentFiat, getCurrencySymbol} from '../utils'; import AppPressable from '../components/AppPressable'; @@ -38,6 +39,59 @@ type ApiUtxo = { }; }; +/** UTXO with HD context: address, derivation path, and chain (receive vs change). */ +export type UtxoWithPath = ApiUtxo & { + address: string; + derivationPath: string; + chain: 'receive' | 'change'; + chainIndex: number; +}; + +/** + * Convert StoredUtxo rows from the DB into the UtxoWithPath shape used by the UI. + * Chain and index are resolved from the addressesWithPaths lookup first; if the + * address is not found there (e.g. pre-load without fresh derivation), they are + * parsed directly from the stored derivation path (e.g. "m/84'/0'/0'/1/3" → + * chain=change, index=3). + * The result is sorted: receive before change, by chain index, newest confirmed first. + */ +function storedToUtxoWithPath( + stored: ReturnType, + addressesWithPaths: Array<{ + address: string; + derivationPath: string; + chain: 'receive' | 'change'; + index: number; + }>, +): UtxoWithPath[] { + const mapped: UtxoWithPath[] = stored.map(u => { + const info = addressesWithPaths.find(a => a.address === u.address); + const parts = (u.derivationPath ?? '').split('/'); + const chainNum = parseInt(parts.at(-2) ?? '', 10); + const chainIdx = parseInt(parts.at(-1) ?? '', 10); + return { + txid: u.txid, + vout: u.vout, + value: u.valueSats, + status: { + confirmed: u.isConfirmed, + block_height: u.blockHeight ?? undefined, + block_time: u.blockTime ?? undefined, + }, + address: u.address, + derivationPath: u.derivationPath ?? info?.derivationPath ?? '', + chain: info?.chain ?? (chainNum === 1 ? 'change' : 'receive'), + chainIndex: info?.index ?? (Number.isNaN(chainIdx) ? 0 : chainIdx), + }; + }); + mapped.sort((a, b) => { + if (a.chain !== b.chain) return a.chain === 'receive' ? -1 : 1; + if (a.chainIndex !== b.chainIndex) return a.chainIndex - b.chainIndex; + return (b.status?.block_time ?? 0) - (a.status?.block_time ?? 0); + }); + return mapped; +} + function addressMatchesNetwork(addr: string, isTestnetApi: boolean): boolean { if (!addr) return false; if (isTestnetApi) { @@ -56,20 +110,20 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { const { activeApiProvider: apiBase, activeNetwork: network, - activeAddress, + activeAddressType: addressType, } = useUser(); const [btcPrice, setBtcPrice] = useState(''); const [btcRate, setBtcRate] = useState(0); const [selectedCurrency, setSelectedCurrency] = useState('USD'); const [refreshing, setRefreshing] = useState(false); const [loading, setLoading] = useState(true); - const [rawUtxos, setRawUtxos] = useState([]); + const [utxosWithPath, setUtxosWithPath] = useState([]); const [fetchError, setFetchError] = useState(null); const [utxoFetchTimestamp, setUtxoFetchTimestamp] = useState(0); useEffect(() => { const loadCurrency = async () => { - const stored = await LocalCache.getItem('currency'); + const stored = appConfigRepository.get(CONFIG_KEYS.CURRENCY); if (stored) setSelectedCurrency(stored); }; loadCurrency(); @@ -104,59 +158,145 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { }, [selectedCurrency]); const fetchUtxos = useCallback(async () => { - const addr = activeAddress?.trim(); const base = apiBase?.trim(); - if (!addr || !base) { - setRawUtxos([]); - setFetchError(addr ? 'No API configured' : 'No wallet address'); + if (!base) { + setFetchError('No API configured'); setLoading(false); return; } const cleanBase = base.replace(/\/+$/, '').replace(/\/api\/?$/, ''); const apiUrl = `${cleanBase}/api`; const isTestnetApi = /\/testnet(\/|$)/.test(apiUrl); - if (!addressMatchesNetwork(addr, isTestnetApi)) { - setRawUtxos([]); - setFetchError('Address and network mismatch'); + setFetchError(null); + + // Resolve HD addresses once, outside the try block so the catch can use them. + let addressesWithPaths: Awaited< + ReturnType + > = []; + try { + addressesWithPaths = await WalletService.getInstance().getHdAddressesWithPaths( + network, + addressType || 'segwit-native', + ); + } catch { + // Derivation failed — fall through to DB-only path below. + } + + if (addressesWithPaths.length === 0) { + // No addresses derived yet — show whatever the DB has for this network + address type. + const allNetworkUtxos = utxoRepository.getUtxosForNetwork( + network, + addressType || 'segwit-native', + ); + setUtxosWithPath(storedToUtxoWithPath(allNetworkUtxos, [])); + setUtxoFetchTimestamp(Date.now()); setLoading(false); + setRefreshing(false); return; } - setFetchError(null); - const utxoUrl = `${apiUrl}/address/${encodeURIComponent(addr)}/utxo`; + try { const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - const res = await fetch(utxoUrl, {signal: controller.signal}); - clearTimeout(timeoutId); - if (!res.ok) { - setRawUtxos([]); - setFetchError(`API error ${res.status}`); - return; - } - const rawList: ApiUtxo[] = await res.json(); - if (!Array.isArray(rawList)) { - setRawUtxos([]); - return; + const timeoutId = setTimeout(() => controller.abort(), 20000); + for (const {address, derivationPath} of addressesWithPaths) { + if (!addressMatchesNetwork(address, isTestnetApi)) continue; + try { + const utxoUrl = `${apiUrl}/address/${encodeURIComponent(address)}/utxo`; + const res = await fetch(utxoUrl, {signal: controller.signal}); + if (!res.ok) continue; + const rawList: ApiUtxo[] = await res.json(); + if (!Array.isArray(rawList)) continue; + // Persist to DB (full replace per address — mempool always returns the complete UTXO set). + const now = Date.now(); + utxoRepository.replaceUtxosForAddress( + address, + network, + rawList.map(u => ({ + txid: u.txid, + vout: u.vout, + address, + network, + valueSats: u.value, + scriptPubkey: null, + derivationPath, + isConfirmed: u.status?.confirmed ?? true, + blockHeight: u.status?.block_height ?? null, + blockTime: u.status?.block_time ?? null, + fetchedAt: now, + })), + ); + } catch { + // Skip failed address — its existing DB rows are preserved for it. + } } - setRawUtxos(rawList); + clearTimeout(timeoutId); + // DB-first: read the complete truth from DB after all writes. + // Addresses that succeeded have fresh rows; addresses that failed still have + // their previous rows — the DB is always the best available picture. + const allFromDB = utxoRepository.getUtxosForAddresses( + addressesWithPaths.map(a => a.address), + network, + ); + setUtxosWithPath(storedToUtxoWithPath(allFromDB, addressesWithPaths)); setUtxoFetchTimestamp(Date.now()); } catch (e: any) { - if (e?.name === 'AbortError') { - setFetchError('Request timed out'); - } else { - setFetchError(e?.message || 'Failed to load UTXOs'); + // Outer catch: something threw outside the per-address inner catch + // (e.g. unexpected JS error). Read from DB as the safe fallback. + const stored = utxoRepository.getUtxosForAddresses( + addressesWithPaths.map(a => a.address), + network, + ); + setUtxosWithPath(storedToUtxoWithPath(stored, addressesWithPaths)); + if (stored.length === 0) { + setFetchError( + e?.name === 'AbortError' + ? 'Request timed out' + : (e?.message || 'Failed to load UTXOs'), + ); } - setRawUtxos([]); } finally { setLoading(false); setRefreshing(false); } - }, [activeAddress, apiBase]); + }, [apiBase, network, addressType]); useEffect(() => { - setLoading(true); - fetchUtxos(); - }, [fetchUtxos]); + let cancelled = false; + (async () => { + // Phase 1 — read from DB immediately so the list is never blank on launch. + let hadCachedData = false; + try { + const addrs = await WalletService.getInstance().getHdAddressesWithPaths( + network, + addressType || 'segwit-native', + ); + if (!cancelled && addrs.length > 0) { + const stored = utxoRepository.getUtxosForAddresses( + addrs.map(a => a.address), + network, + ); + if (!cancelled && stored.length > 0) { + hadCachedData = true; + setUtxosWithPath(storedToUtxoWithPath(stored, addrs)); + setLoading(false); // cached list visible; API will update in background + } + } + } catch { + // Pre-load failed — API fetch below will still populate the list + } + + // Phase 2 — live API fetch (updates the already-visible cached list). + if (!cancelled) { + if (!hadCachedData) { + setLoading(true); // no cached data yet — show spinner + } + fetchUtxos(); + } + })(); + return () => { + cancelled = true; + }; + }, [fetchUtxos, network, addressType]); const headerLeft = useCallback( () => ( @@ -215,12 +355,56 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { : theme.colors.blackOverlay05 ?? 'rgba(0,0,0,0.06)'; const receivedColor = themes.cryptoVibrant.colors.secondary; + /** Confirmed / unconfirmed breakdown derived from the already-loaded UTXO list. + * Zero extra API calls — same data source as the list below, always in sync. */ + const balanceSummary = useMemo(() => { + let confirmedSats = 0; + let unconfirmedSats = 0; + let confirmedCount = 0; + let unconfirmedCount = 0; + for (const u of utxosWithPath) { + if (u.status?.confirmed) { + confirmedSats += u.value; + confirmedCount++; + } else { + unconfirmedSats += u.value; + unconfirmedCount++; + } + } + const totalSats = confirmedSats + unconfirmedSats; + const fmt = (sats: number) => (sats / 1e8).toFixed(8); + const fiat = (sats: number) => + btcRate > 0 + ? getCurrencySymbol(selectedCurrency || 'USD') + + presentFiat((sats / 1e8) * btcRate) + : null; + return { + confirmedSats, + unconfirmedSats, + totalSats, + confirmedCount, + unconfirmedCount, + fmt, + fiat, + }; + }, [utxosWithPath, btcRate, selectedCurrency]); + const shortTxId = (txid: string) => txid ? `${txid.slice(0, 6)}…${txid.slice(-6)}` : '—'; - const shortAddr = - activeAddress && activeAddress.length > 12 - ? `${activeAddress.slice(0, 6)}…${activeAddress.slice(-6)}` - : activeAddress || '—'; + const shortAddr = (addr: string) => + addr && addr.length > 12 + ? `${addr.slice(0, 6)}…${addr.slice(-6)}` + : addr || '—'; + /** Format path for display: keep last segment visible (e.g. …/0/3). */ + const formatPath = (path: string) => { + if (!path) return '—'; + const parts = path.split('/').filter(Boolean); + if (parts.length >= 2) + return `…/${parts[parts.length - 2]}/${parts[parts.length - 1]}`; + return path; + }; + const chainLabel = (chain: 'receive' | 'change', index: number) => + chain === 'receive' ? `Receive #${index}` : `Change #${index}`; const baseUrl = apiBase?.trim() ? apiBase.replace(/\/+$/, '').replace(/\/api\/?$/, '') @@ -294,6 +478,89 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { fontSize: theme.fontSizes?.xs || 11, opacity: 0.5, }, + chainBadge: { + alignSelf: 'flex-start', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 6, + marginBottom: 6, + }, + chainBadgeReceive: { + backgroundColor: theme.colors.receivedOverlay15, + }, + chainBadgeChange: { + backgroundColor: theme.colors.primary + '20', + }, + chainBadgeText: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: theme.fontFamilies?.bold, + }, + pathRow: { + marginTop: 4, + paddingTop: 4, + borderTopWidth: 1, + borderTopColor: theme.colors.border + '60', + }, + pathLabel: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + opacity: 0.7, + }, + pathFull: { + marginTop: 2, + fontSize: (theme.fontSizes?.xs ?? 11) - 1, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + opacity: 0.7, + }, + summaryCard: { + borderRadius: 12, + borderWidth: 1, + paddingHorizontal: 14, + paddingVertical: 12, + marginHorizontal: 16, + marginTop: 12, + marginBottom: 0, + gap: 6, + }, + summaryRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + summaryDivider: { + height: 1, + marginVertical: 2, + }, + summaryLabel: { + fontSize: theme.fontSizes?.sm || 12, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + opacity: 0.75, + }, + summaryCount: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + opacity: 0.55, + marginLeft: 6, + }, + summaryBtc: { + fontSize: theme.fontSizes?.md || 15, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + letterSpacing: COMMON_FONT_CONFIGS.bitcoinAmountMono.letterSpacing, + }, + summaryFiat: { + fontSize: theme.fontSizes?.xs || 11, + fontFamily: COMMON_FONT_CONFIGS.bitcoinAmountMono.fontFamily, + opacity: 0.55, + marginTop: 1, + textAlign: 'right', + }, + summaryRight: { + alignItems: 'flex-end', + }, + summaryLabelRow: { + flexDirection: 'row', + alignItems: 'center', + }, emptyWrap: { paddingVertical: 32, paddingHorizontal: 24, @@ -333,7 +600,7 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { ); const renderUtxoItem = useCallback( - ({item: u}: {item: ApiUtxo}) => { + ({item: u}: {item: UtxoWithPath}) => { const blockTime = u.status?.block_time != null ? u.status.block_time * 1000 : null; const timestamp = blockTime @@ -346,15 +613,14 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { getCurrencySymbol(selectedCurrency || 'USD') + presentFiat((u.value / 1e8) * btcRate); const openInExplorer = () => { - if (!baseUrl || !u.txid) { - return; - } + if (!baseUrl || !u.txid) return; const vout = u.vout ?? 0; const url = `${baseUrl}/tx/${u.txid}#vout=${vout}`; Linking.openURL(url).catch(() => { Alert.alert('Error', 'Could not open explorer'); }); }; + const isReceive = u.chain === 'receive'; return ( = ({navigation}) => { }} accessible={true} accessibilityRole="button" - accessibilityLabel={`UTXO ${shortTxId(u.txid || '')} vout ${u.vout ?? 0}. Tap to open in explorer.`}> + accessibilityLabel={`${chainLabel( + u.chain, + u.chainIndex, + )} UTXO ${shortTxId(u.txid || '')} vout ${ + u.vout ?? 0 + }. Tap to open in explorer.`}> + + + {chainLabel(u.chain, u.chainIndex)} + + = ({navigation}) => { style={[styles.utxoLeft, {color: theme.colors.text}]} numberOfLines={1} selectable> - Addr: {shortAddr} + Addr: + {shortAddr(u.address)} {timestamp} + + + Path:{' '} + + {formatPath(u.derivationPath)} + + + + {u.derivationPath} + + ); }, @@ -418,7 +724,6 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { styles, cardBg, cardBorder, - shortAddr, selectedCurrency, btcRate, isDarkMode, @@ -468,31 +773,129 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { return ( + {/* Balance summary card — static, sits above the scrollable list */} + + {/* Total row */} + + + Total + + + + {balanceSummary.fmt(balanceSummary.totalSats)} BTC + + {balanceSummary.fiat(balanceSummary.totalSats) && ( + + {balanceSummary.fiat(balanceSummary.totalSats)} + + )} + + + + {/* Divider */} + + + {/* Confirmed row */} + + + + ✓ Confirmed + + + {balanceSummary.confirmedCount}{' '} + {balanceSummary.confirmedCount === 1 ? 'UTXO' : 'UTXOs'} + + + + + {balanceSummary.fmt(balanceSummary.confirmedSats)} BTC + + {balanceSummary.fiat(balanceSummary.confirmedSats) && ( + + {balanceSummary.fiat(balanceSummary.confirmedSats)} + + )} + + + + {/* Pending row — only when there are unconfirmed UTXOs */} + {balanceSummary.unconfirmedCount > 0 && ( + + + + ⏳ Pending + + + {balanceSummary.unconfirmedCount}{' '} + {balanceSummary.unconfirmedCount === 1 ? 'UTXO' : 'UTXOs'} + + + + + +{balanceSummary.fmt(balanceSummary.unconfirmedSats)} BTC + + {balanceSummary.fiat(balanceSummary.unconfirmedSats) && ( + + {balanceSummary.fiat(balanceSummary.unconfirmedSats)} + + )} + + + )} + + `${item.txid}:${item.vout}`} + keyExtractor={item => `${item.txid}:${item.vout}:${item.address}`} ListEmptyComponent={ListEmpty} ListHeaderComponentStyle={styles.listHeader} ListHeaderComponent={ - - 0 && - Date.now() - utxoFetchTimestamp > 60000 - } - /> + + + 0 && + Date.now() - utxoFetchTimestamp > 60000 + } + /> + } ListFooterComponent={ - rawUtxos.length > 0 ? ( + utxosWithPath.length > 0 ? ( No more UTXOs @@ -502,7 +905,7 @@ const UtxosScreen: React.FC<{navigation: any}> = ({navigation}) => { styles.endOfListCount, {color: theme.colors.textSecondary}, ]}> - {rawUtxos.length} in total + {utxosWithPath.length} in total (all addresses) ) : null diff --git a/screens/WalletHome.tsx b/screens/WalletHome.tsx index 448e77a1..4f8a8114 100644 --- a/screens/WalletHome.tsx +++ b/screens/WalletHome.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState, useCallback, useRef} from 'react'; +import React, {useEffect, useState, useCallback, useRef, useMemo} from 'react'; import { View, Text, @@ -15,6 +15,8 @@ import AppPressable from '../components/AppPressable'; import Animated, { useSharedValue, withTiming, + withRepeat, + withSequence, useAnimatedStyle, } from 'react-native-reanimated'; import QRScanner from '../components/QRScanner'; @@ -33,6 +35,7 @@ import TransactionList from '../components/TransactionList'; import {CommonActions} from '@react-navigation/native'; import Big from 'big.js'; import ReceiveModal from './ReceiveModal'; +import RestoringIndexesModal from '../components/RestoringIndexesModal'; import SignedPSBTModal from './SignedPSBTModal'; import LegacyWalletModal from '../components/LegacyWalletModal'; import ExtensionPairingModal from '../components/ExtensionPairingModal'; @@ -45,6 +48,7 @@ import { HapticFeedback, getKeyshareLabel, getDerivePathForNetwork, + getReceivePath, isLegacyWallet, decodeSendBitcoinQR, getResetToMainTabsWallet, @@ -71,7 +75,12 @@ import { HeaderProvider, HeaderNetwork, } from '../components/Header'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; + +import walletRepository from '../services/repositories/WalletRepository'; +import balanceRepository from '../services/repositories/BalanceRepository'; +import {getExternalIndex} from '../services/HdIndexService'; +import syncCoordinator from '../services/sync/SyncCoordinator'; import { parsePairingCodeFromScannedData, computeExtensionBindResponseQr, @@ -94,6 +103,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { amountSats: Big; feeSats: Big; spendingHash: string; + utxosJson?: string | null; + utxoCount?: number; + changeAddress?: string | null; } | null>(null); const [currentDerivationPath, setCurrentDerivationPath] = useState(''); @@ -102,11 +114,23 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const [computedFromAddress, setComputedFromAddress] = useState(''); // Computed from address for send transaction const [btcPrice, setBtcPrice] = useState(''); const [btcRate, setBtcRate] = useState(0); - const [balanceBTC, setBalanceBTC] = useState('0.00000000'); + const [balanceBTC, setBalanceBTC] = useState('-'); const [balanceFiat, setBalanceFiat] = useState('0'); + const [pendingSats, setPendingSats] = useState(0); const [_party, setParty] = useState(''); const [isBlurred, setIsBlurred] = useState(false); const [isReceiveModalVisible, setIsReceiveModalVisible] = useState(false); + const [isRestoringIndexes, setIsRestoringIndexes] = useState(false); + const [restoreProgress, setRestoreProgress] = useState<{ + chain: 'external' | 'internal'; + index: number; + gapIndex: number; + } | null>(null); + const [receivePathInfo, setReceivePathInfo] = useState<{ + path: string; + index: number; + address: string; + } | null>(null); const [isSignedPSBTModalVisible, setIsSignedPSBTModalVisible] = useState(false); const [signedPsbt, setSignedPsbt] = useState(null); @@ -140,6 +164,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { useState(false); const [pendingExtensionPairingCode, setPendingExtensionPairingCode] = useState(null); + const [isReceiveBusy, setIsReceiveBusy] = useState(false); const extensionQrModalStyles = React.useMemo( () => StyleSheet.create({qrPadding: {padding: 16}}), [], @@ -150,38 +175,35 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { }); const extensionBindAlertShownRef = useRef(false); - const proceedWithExtensionBind = useCallback( - async (pairingCode: string) => { - try { - const keyshareJSON = await EncryptedStorage.getItem('keyshare'); - if (!keyshareJSON) { - extensionBindAlertShownRef.current = false; - Alert.alert('Error', 'Keyshare not found.'); - return; - } - const keyshare = JSON.parse(keyshareJSON); - const pubKey = keyshare.pub_key || ''; - const chainCode = keyshare.chain_code_hex || ''; - if (!pubKey || !chainCode) { - extensionBindAlertShownRef.current = false; - Alert.alert('Error', 'Keyshare info is not available.'); - return; - } - const qrDataBase64 = await computeExtensionBindResponseQr( - pairingCode, - pubKey, - chainCode, - ); - setExtensionResponseQrData(qrDataBase64); - setIsExtensionResponseQrVisible(true); - } catch (e) { - dbg('Extension bind from scan failed:', e); + const proceedWithExtensionBind = useCallback(async (pairingCode: string) => { + try { + const keyshareJSON = await EncryptedStorage.getItem('keyshare'); + if (!keyshareJSON) { extensionBindAlertShownRef.current = false; - Alert.alert('Error', 'Failed to generate response QR.'); + Alert.alert('Error', 'Keyshare not found.'); + return; } - }, - [], - ); + const keyshare = JSON.parse(keyshareJSON); + const pubKey = keyshare.pub_key || ''; + const chainCode = keyshare.chain_code_hex || ''; + if (!pubKey || !chainCode) { + extensionBindAlertShownRef.current = false; + Alert.alert('Error', 'Keyshare info is not available.'); + return; + } + const qrDataBase64 = await computeExtensionBindResponseQr( + pairingCode, + pubKey, + chainCode, + ); + setExtensionResponseQrData(qrDataBase64); + setIsExtensionResponseQrVisible(true); + } catch (e) { + dbg('Extension bind from scan failed:', e); + extensionBindAlertShownRef.current = false; + Alert.alert('Error', 'Failed to generate response QR.'); + } + }, []); const [initialSendAddress, setInitialSendAddress] = useState( null, @@ -191,9 +213,16 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const [_segwitCompatibleAddress, setSegwitCompatibleAddress] = React.useState(''); const [initialTransactions, setInitialTransactions] = useState([]); + const [walletAddresses, setWalletAddresses] = useState([]); + // true once getHdAddressesWithPaths has resolved for the first time. + // TransactionList must not fire in single-address mode while HD derivation is + // still running (2-5 s when hdAddressCache is cold), otherwise it fetches only + // the current receive address (likely unused, 0 txs) and caches that stale result. + const [walletAddressesReady, setWalletAddressesReady] = useState(false); // Animation and visual feedback states const [isBalanceLoading, setIsBalanceLoading] = useState(false); const balanceUpdateAnimation = useSharedValue(1); + const shimmerOpacity = useSharedValue(1.0); const [balanceError, setBalanceError] = useState(null); const previousBalanceRef = useRef('0.00000000'); // Helper function for showing error toasts @@ -237,6 +266,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { showUtxosTab, showPsbtTab, showWalletTab, + refresh: refreshUserContext, } = useUser(); // Keep local state in sync with UserContext useEffect(() => { @@ -309,9 +339,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const addr = userActiveAddress || address || - (await LocalCache.getItem('currentAddress')); - const baseApi = apiBase || (await LocalCache.getItem('api')); - const currency = (await LocalCache.getItem('currency')) || 'USD'; + (appConfigRepository.get(CONFIG_KEYS.CURRENT_ADDRESS)); + const baseApi = apiBase || (appConfigRepository.get('api')); + const currency = (appConfigRepository.get(CONFIG_KEYS.CURRENCY)) || 'USD'; dbg(`[WalletHome] fetchData - Using address:`, { timestamp: Date.now(), userActiveAddress: userActiveAddress @@ -348,30 +378,24 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const apiUrl = `${cleanBaseApi}/api`; // Ensure native module has correct settings await BBMTLibNativeModule.setAPI(network, apiUrl); - // Set up timeout for API calls - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - setIsRefreshing(false); - reject(new Error('API refresh timed out')); - }, 5000); // 5 second timeout - }); + let freshData; setIsRefreshing(true); setIsBalanceLoading(true); setBalanceError(null); try { dbg('fetching bitcoin price and wallet balance...'); - freshData = await Promise.race([ - Promise.all([ - WalletService.getInstance().getBitcoinPrice(), - WalletService.getInstance().getWalletBalance( - addr, - btcRate, - _pendingSent, - true, - ), - ]), - timeoutPromise, + const effectiveAddressType = + addressType || userAddressType || 'segwit-native'; + freshData = await Promise.all([ + WalletService.getInstance().getBitcoinPrice(), + WalletService.getInstance().getWalletBalanceAggregate( + network, + effectiveAddressType, + btcRate, + _pendingSent, + true, + ), ]); if (Array.isArray(freshData) && freshData.length === 2) { const [freshPrice, freshBalance] = freshData; @@ -384,9 +408,12 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const normalizedBTC = freshBalance.btc || '0.00000000'; const balanceNum = parseFloat(normalizedBTC); const finalBTC = balanceNum <= 0 ? '0.00000000' : normalizedBTC; + dbg('WalletHome: Final BTC (fresh):', finalBTC); setBalanceBTC(finalBTC); - const fiatBalance = Number(freshBalance.btc * rates[currency]); - setBalanceFiat(Math.max(0, fiatBalance).toFixed(2)); + setPendingSats(freshBalance.pendingSats ?? 0); + const fiatBalance = + Number(freshBalance.btc) * Number(rates[currency] || 0); + setBalanceFiat(Math.max(0, fiatBalance).toFixed(2) || '-'); // Update cache timestamps with fresh data setCacheTimestamps({ price: freshPrice.timestamp, @@ -426,11 +453,18 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Fall back to cached data only if fresh data fetch failed if (!freshData) { const cachedPricePromise = WalletService.getInstance().getCachePrice(); - const cachedBalancePromise = - WalletService.getInstance().getWalletBalance( - addr, - btcRate, - _pendingSent, + const effectiveAddressType = + addressType || userAddressType || 'segwit-native'; + const cachedBalancePromise = WalletService.getInstance() + .getCachedAggregateBalance(network, effectiveAddressType) + .then( + c => + c ?? + WalletService.getInstance().getWalletBalance( + addr, + btcRate, + _pendingSent, + ), ); const cachedResults = await Promise.all([ cachedPricePromise, @@ -447,7 +481,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Normalize balance to ensure no negative zero const normalizedBTC = cachedBalance.btc || '0.00000000'; const balanceNum = parseFloat(normalizedBTC); - const finalBTC = balanceNum <= 0 ? '0.00000000' : normalizedBTC; + const finalBTC = balanceNum <= 0 ? '-' : normalizedBTC; // Animate balance update if it changed if (finalBTC !== previousBalanceRef.current) { // Trigger fade animation @@ -457,10 +491,12 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Haptic feedback on balance update HapticFeedback.light(); } + dbg('WalletHome: Final BTC (cached):', finalBTC); setBalanceBTC(finalBTC); + setPendingSats(cachedBalance.pendingSats ?? 0); const fiatBalance = Number(cachedBalance.btc) * Number(rates[currency]); - setBalanceFiat(Math.max(0, fiatBalance).toFixed(2)); + setBalanceFiat(Math.max(0, fiatBalance).toFixed(2) || '-'); setCacheTimestamps({ price: cachedPrice.timestamp, balance: cachedBalance.timestamp, @@ -472,6 +508,29 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } } } + // Sync walletAddresses with the current HD index state. + // bumpExternalIndexIfCurrentUsed may advance indexes between renders + // but the walletAddresses effect only fires on specific dep changes. + // Refreshing here ensures the TransactionList always covers every address + // the wallet has used, including the one just received on. + try { + const addrType = addressType || userAddressType || 'segwit-native'; + const freshAddrs = + await WalletService.getInstance().getHdAddressesWithPaths( + network, + addrType, + ); + const freshList = freshAddrs.map(a => a.address); + setWalletAddresses(prev => { + const same = + prev.length === freshList.length && + prev.every((a, i) => a === freshList[i]); + return same ? prev : freshList; + }); + setWalletAddressesReady(true); + } catch { + // Non-critical — address list failure does not affect balance display. + } } catch (error: any) { dbg('WalletHome: Error fetching data:', error); let errMsg = 'Unknown error'; @@ -500,11 +559,59 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { btcRate, _pendingSent, showErrorToast, - userActiveAddress, // Use UserContext address as primary source - address, // Keep for backward compatibility + userActiveAddress, + address, apiBase, - // balanceUpdateAnimation is a stable ref, doesn't need to be in deps + addressType, + userAddressType, ]); + // Load HD addresses for multi-address transaction list. + // If discovery has never been run for this (network, addressType), run it first + // so indexes are correct — otherwise getHdAddressesWithPaths returns only 1 receive + 1 change. + // walletAddressesReady is set to true once derivation has settled so TransactionList + // is never given a partial address list. + useEffect(() => { + if (!isInitialized || !network || !(addressType || userAddressType)) return; + const effectiveType = addressType || userAddressType || 'segwit-native'; + setWalletAddressesReady(false); + let cancelled = false; + const load = async () => { + try { + const ws = WalletService.getInstance(); + const restoreDone = + walletRepository.getHdState(network, effectiveType)?.restoreDone === true; + if (!restoreDone) { + dbg( + '[WalletHome] HD restore not done for', + network, + effectiveType, + '- running discovery', + ); + const apiUrl = + (appConfigRepository.get('api')) || + (network === 'mainnet' + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'); + await ws.discoverHdIndexesForNetwork(network, effectiveType, apiUrl); + } + if (cancelled) return; + const arr = await ws.getHdAddressesWithPaths(network, effectiveType); + if (cancelled) return; + setWalletAddresses(arr.map(a => a.address)); + setWalletAddressesReady(true); + } catch (e) { + dbg('[WalletHome] Address list load error', e); + if (!cancelled) { + setWalletAddresses([]); + setWalletAddressesReady(true); + } + } + }; + load(); + return () => { + cancelled = true; + }; + }, [isInitialized, network, addressType, userAddressType]); // Update the ref whenever fetchData changes useEffect(() => { fetchDataRef.current = fetchData; @@ -516,8 +623,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const addr = userActiveAddress || address || - (await LocalCache.getItem('currentAddress')); - const baseApi = apiBase || (await LocalCache.getItem('api')); + (appConfigRepository.get(CONFIG_KEYS.CURRENT_ADDRESS)); + const baseApi = apiBase || (appConfigRepository.get('api')); if (!addr || !baseApi) { dbg('checkBalanceForSend: Missing wallet address or baseApi'); return 0; @@ -533,13 +640,17 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { reject(new Error('Balance check timed out')); }, 5000); }); - // Fetch balance only (force fresh fetch) - const balancePromise = WalletService.getInstance().getWalletBalance( - addr, - btcRate, - _pendingSent, - true, // force fresh fetch - ); + // Fetch aggregate balance (all HD addresses) + const effectiveAddressType = + addressType || userAddressType || 'segwit-native'; + const balancePromise = + WalletService.getInstance().getWalletBalanceAggregate( + network, + effectiveAddressType, + btcRate, + _pendingSent, + true, + ); const balanceResult = await Promise.race([ balancePromise, timeoutPromise, @@ -554,11 +665,12 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Update balance state - normalize to ensure no negative zero const normalizedBTC = (balanceResult as any).btc || '0.00000000'; const balanceNum = parseFloat(normalizedBTC); - const finalBTC = balanceNum <= 0 ? '0.00000000' : normalizedBTC; + const finalBTC = balanceNum <= 0 ? '-' : normalizedBTC; + dbg('checkBalanceForSend: Final BTC:', finalBTC); setBalanceBTC(finalBTC); if (btcRate > 0) { const fiatBalance = Number((balanceResult as any).btc) * btcRate; - setBalanceFiat(Math.max(0, fiatBalance).toFixed(2)); + setBalanceFiat(Math.max(0, fiatBalance).toFixed(2) || '-'); } return newBalance; } @@ -567,7 +679,16 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { dbg('checkBalanceForSend: Error checking balance:', error); return 0; } - }, [userActiveAddress, address, apiBase, network, btcRate, _pendingSent]); + }, [ + userActiveAddress, + address, + apiBase, + network, + btcRate, + _pendingSent, + addressType, + userAddressType, + ]); // Function to update address type modal with new network addresses const updateAddressTypeModal = useCallback( async (newNetwork: string) => { @@ -588,7 +709,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Get current address type for derivation path - use state or cache const currentAddressType = addressType || - (await LocalCache.getItem('addressType')) || + (appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE)) || 'segwit-native'; // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); @@ -655,7 +776,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const currentAddressType = userAddressType || addressType || - (await LocalCache.getItem('addressType')) || + (appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE)) || 'segwit-native'; dbg( 'Using address type:', @@ -677,13 +798,16 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { return; } const ks = JSON.parse(jks); - // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - // Use the same currentAddressType for derivation path to ensure consistency - const path = getDerivePathForNetwork( + const externalIndex = await getExternalIndex( + newNetwork, + currentAddressType, + ); + const path = getReceivePath( newNetwork, currentAddressType, useLegacyPath, + externalIndex, ); btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, @@ -742,8 +866,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { : 'EMPTY', }); setAddress(newAddress); - await LocalCache.setItem('currentAddress', newAddress); - await LocalCache.setItem('currentNetwork', newNetwork); + appConfigRepository.set(CONFIG_KEYS.CURRENT_ADDRESS, newAddress); + appConfigRepository.set(CONFIG_KEYS.NETWORK, newNetwork); // Also update the address type display if needed if (newNetwork === 'testnet3') { dbg('Testnet address generated and cached:', newAddress); @@ -821,32 +945,59 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { setIsInitialized(true); return; } - // Clear existing state + // Reset address slots so stale receive addresses are not shown while + // the new ones are being derived. Do NOT clear balance/price — show + // the last-known DB value instead so the user never sees 0 on unlock. setAddress(''); - setBalanceBTC('0.00000000'); - setBalanceFiat('0'); - setBtcPrice(''); - setBtcRate(0); setLegacyAddress(''); setSegwitAddress(''); setSegwitCompatibleAddress(''); + + // Synchronously preload the aggregate balance from SQLite so the + // balance is visible immediately (before any async work completes). + // This prevents the balance from flashing '-' / 0 on lock-unlock. + { + const earlyAddressType = + appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE) || 'segwit-native'; + const earlyNet = + network || appConfigRepository.get(CONFIG_KEYS.NETWORK) || 'mainnet'; + const earlyAgg = balanceRepository.getBalance( + `aggregate_${earlyNet}_${earlyAddressType}`, + earlyNet, + ); + if (earlyAgg && earlyAgg.balanceSats > 0) { + const earlyBTC = (earlyAgg.balanceSats / 1e8).toFixed(8); + setBalanceBTC(earlyBTC); + setPendingSats(earlyAgg.pendingSats ?? 0); + // Fiat: reuse whatever btcRate is already in state (non-zero after + // first successful fetch); skip if rate not available yet. + if (btcRate > 0) { + const earlyFiat = (earlyAgg.balanceSats / 1e8) * btcRate; + setBalanceFiat(Math.max(0, earlyFiat).toFixed(2)); + } + } + } + // Do NOT clear persistent cache here; we need it for offline startup // Only ensure service is initialized to read existing caches // Initialize WalletService const walletService = WalletService.getInstance(); await walletService.initialize(); const ks = JSON.parse(jks); - // Get current address type for derivation path const currentAddressType = - (await LocalCache.getItem('addressType')) || 'segwit-native'; - // Check if this is a legacy wallet (created before migration timestamp) + (appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE)) || 'segwit-native'; const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork( + const externalIndex = await getExternalIndex( + network, + currentAddressType, + ); + const path = getReceivePath( network, currentAddressType, useLegacyPath, + externalIndex, ); - // Always derive btcPub fresh to ensure it's current + // Always derive btcPub fresh to ensure it's current (HD: at current external index) const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -857,11 +1008,11 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { dbg('btcPub derived and stored during re-initialization'); // Get current network from NetworkContext const net = - network || (await LocalCache.getItem('network')) || 'mainnet'; + network || (appConfigRepository.get(CONFIG_KEYS.NETWORK)) || 'mainnet'; dbg('Re-initializing for network:', net); // Get current address type const addrType = - (await LocalCache.getItem('addressType')) || 'segwit-native'; + (appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE)) || 'segwit-native'; setAddressType(addrType); // Set up network parameters const netParams = await BBMTLibNativeModule.setBtcNetwork(net); @@ -877,9 +1028,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { ), ]); // Store all addresses - await LocalCache.setItem('legacyAddress', legacyAddr); - await LocalCache.setItem('segwitAddress', segwitAddr); - await LocalCache.setItem('segwitCompatibleAddress', segwitCompAddr); + appConfigRepository.set('legacyAddress', legacyAddr); + appConfigRepository.set('segwitAddress', segwitAddr); + appConfigRepository.set('segwitCompatibleAddress', segwitCompAddr); setLegacyAddress(legacyAddr); setSegwitAddress(segwitAddr); setSegwitCompatibleAddress(segwitCompAddr); @@ -908,16 +1059,27 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { )}` : 'EMPTY', }); - await LocalCache.setItem('currentAddress', btcAddress); + appConfigRepository.set(CONFIG_KEYS.CURRENT_ADDRESS, btcAddress); setAddress(btcAddress); - // Preload transactions from cache for this address (offline-friendly) + // Preload transactions from cache (wallet-level for HD, single-addr fallback) try { const cachedTxs = - await WalletService.getInstance().transactionsFromCache(btcAddress); - setInitialTransactions(cachedTxs); - } catch {} + await WalletService.getInstance().transactionsFromCacheForWallet( + actualNet, + addrType, + ); + setInitialTransactions(Array.isArray(cachedTxs) ? cachedTxs : []); + } catch { + try { + const fallback = + await WalletService.getInstance().transactionsFromCache( + btcAddress, + ); + setInitialTransactions(Array.isArray(fallback) ? fallback : []); + } catch {} + } // Set up API URL from NetworkContext - const api = apiBase || (await LocalCache.getItem('api')); + const api = apiBase || (appConfigRepository.get('api')); if (api) { await BBMTLibNativeModule.setAPI(actualNet, api); dbg('API set for network:', actualNet, 'API:', api); @@ -925,12 +1087,20 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Initialize UI directly from persistent wallet cache (exact v1.3.2 analogy) try { const cachedPrice = await WalletService.getInstance().getCachePrice(); - const cachedBal = await WalletService.getInstance().getBal( - btcAddress, - ); - const cachedTxs = - await WalletService.getInstance().transactionsFromCache(btcAddress); - const currency = (await LocalCache.getItem('currency')) || 'USD'; + const cachedAggregate = + await WalletService.getInstance().getCachedAggregateBalance( + actualNet, + addrType, + ); + const cachedBal = + cachedAggregate ?? + (await WalletService.getInstance().getBal(btcAddress)); + const cachedTxs = await WalletService.getInstance() + .transactionsFromCacheForWallet(actualNet, addrType) + .catch(() => + WalletService.getInstance().transactionsFromCache(btcAddress), + ); + const currency = (appConfigRepository.get(CONFIG_KEYS.CURRENCY)) || 'USD'; if (cachedBal.timestamp > 0) { // timestamps setCacheTimestamps({ @@ -951,15 +1121,17 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Normalize balance to ensure no negative zero const normalizedBTC = cachedBal.btc || '0.00000000'; const balanceNum = parseFloat(normalizedBTC); - const finalBTC = balanceNum <= 0 ? '0.00000000' : normalizedBTC; + const finalBTC = balanceNum <= 0 ? '-' : normalizedBTC; + dbg('reinitializeWallet: Final BTC (cached):', finalBTC); setBalanceBTC(finalBTC); + setPendingSats(cachedBal.pendingSats ?? 0); const r = (cachedPrice.rates?.[currency] as number) || (cachedPrice.rate as number) || 0; if (r && Number(cachedBal.btc) >= 0) { const fiatBalance = Number(cachedBal.btc) * r; - setBalanceFiat(Math.max(0, fiatBalance).toFixed(2)); + setBalanceFiat(Math.max(0, fiatBalance).toFixed(2) || '-'); } } // initial transactions @@ -981,6 +1153,11 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { setLoading(false); isReinitInProgressRef.current = false; } + // Fetch live balance/price now that the reinit guard has been cleared. + // Without this, reinitializeWallet only shows cached data and never + // triggers a network refresh, so returning from settings after a + // "clear cache" always shows 0 BTC until the user pulls to refresh. + await fetchDataRef.current?.(); }, [network, apiBase, showErrorToast, address], ); @@ -1085,6 +1262,36 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { sendButtonDisabled: { opacity: 0.6, } as const, + ...StyleSheet.create({ + shimmerBTC: { + height: 38, + width: '60%', + borderRadius: 8, + backgroundColor: 'rgba(255,255,255,0.25)', + alignSelf: 'center', + }, + shimmerFiat: { + height: 22, + width: '38%', + borderRadius: 6, + backgroundColor: 'rgba(255,255,255,0.2)', + alignSelf: 'center', + }, + pendingChip: { + alignSelf: 'center', + marginTop: 6, + paddingHorizontal: 10, + paddingVertical: 3, + borderRadius: 12, + backgroundColor: 'rgba(255,179,0,0.18)', + }, + pendingChipText: { + fontSize: 11, + fontWeight: '500', + color: '#FFB300', + letterSpacing: 0.2, + }, + }), balanceContainer: { ...createStyles(theme).balanceContainer, backgroundColor: isDarkMode @@ -1177,17 +1384,10 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { checkPermission(); }, []); useEffect(() => { - LocalCache.getItem('addressType').then(addrType => { - setAddressType(addrType || 'segwit-native'); - }); - LocalCache.getItem('currency').then(currency => { - setSelectedCurrency(currency || 'USD'); - }); - // Load balance visibility preference - LocalCache.getItem('balanceHidden').then(hidden => { - setIsBlurred(hidden === 'true'); - }); - }); + setAddressType(appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE) || 'segwit-native'); + setSelectedCurrency(appConfigRepository.get(CONFIG_KEYS.CURRENCY) || 'USD'); + setIsBlurred(appConfigRepository.get(CONFIG_KEYS.BALANCE_HIDDEN) === 'true'); + }, []); // Simplified focus effect - just refresh data when screen comes into focus useFocusEffect( useCallback(() => { @@ -1275,7 +1475,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { ); const handleCurrencySelect = async (currency: {code: string}) => { setSelectedCurrency(currency.code); - await LocalCache.setItem('currency', currency.code); + appConfigRepository.set(CONFIG_KEYS.CURRENCY, currency.code); if (priceData[currency.code]) { const formattedPrice = priceData[currency.code].toFixed(2); setBtcPrice(formattedPrice); @@ -1283,7 +1483,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Update fiat balance with new currency rate if (balanceBTC) { const newBalance = Number(balanceBTC) * priceData[currency.code]; - setBalanceFiat(newBalance.toFixed(2)); + setBalanceFiat(newBalance.toFixed(2) || '-'); } } }; @@ -1311,55 +1511,77 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { ks = JSON.parse(jks); } catch (error) { dbg('Error parsing keyshare:', error); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); return; } if (!ks.pub_key || !ks.chain_code_hex || !ks.local_party_key) { dbg('Invalid pub_key or chain_code_hex or local_party_key'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); return; } - // Get current address type for derivation path const currentAddressType = - (await LocalCache.getItem('addressType')) || 'segwit-native'; - // Check if this is a legacy wallet (created before migration timestamp) + (appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE)) || 'segwit-native'; const useLegacyPath = isLegacyWallet(ks.created_at); - const path = getDerivePathForNetwork( + const externalIndex = await getExternalIndex( + network, + currentAddressType, + ); + const path = getReceivePath( network, currentAddressType, useLegacyPath, + externalIndex, ); const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, path, ); - // Store btcPub for later use in address generation await EncryptedStorage.setItem('btcPub', btcPub); dbg('btcPub stored in EncryptedStorage for address generation'); // Set default network if not set - let net = await LocalCache.getItem('network'); + let net = appConfigRepository.get(CONFIG_KEYS.NETWORK); if (!net) { net = 'mainnet'; - await LocalCache.setItem('network', net); + appConfigRepository.set(CONFIG_KEYS.NETWORK, net); dbg('WalletHome: Setting default network to mainnet'); } // Set default address type if not set - let addrType = await LocalCache.getItem('addressType'); + let addrType = appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE); if (!addrType) { addrType = 'segwit-native'; - await LocalCache.setItem('addressType', addrType); + appConfigRepository.set(CONFIG_KEYS.ADDRESS_TYPE, addrType); dbg('WalletHome: Setting default address type to segwit-native'); } // Set default currency if not set - let currency = (await LocalCache.getItem('currency')) || 'USD'; + let currency = (appConfigRepository.get(CONFIG_KEYS.CURRENCY)) || 'USD'; // Get available currencies from price data const priceResponse = await walletService.getBitcoinPrice(); const availableCurrencies = Object.keys(priceResponse.rates); currency = availableCurrencies.includes('USD') ? 'USD' : availableCurrencies[0]; - await LocalCache.setItem('currency', currency); + appConfigRepository.set(CONFIG_KEYS.CURRENCY, currency); dbg('WalletHome: Setting default currency to', currency); const netParams = await BBMTLibNativeModule.setBtcNetwork(net); net = netParams.split('@')[0]; @@ -1380,9 +1602,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { 'segwit-compatible', ); // Store all addresses - await LocalCache.setItem('legacyAddress', legacyAddr); - await LocalCache.setItem('segwitAddress', segwitAddr); - await LocalCache.setItem('segwitCompatibleAddress', segwitCompAddr); + appConfigRepository.set('legacyAddress', legacyAddr); + appConfigRepository.set('segwitAddress', segwitAddr); + appConfigRepository.set('segwitCompatibleAddress', segwitCompAddr); setLegacyAddress(legacyAddr); setSegwitAddress(segwitAddr); setSegwitCompatibleAddress(segwitCompAddr); @@ -1398,21 +1620,21 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { net, addrType, ); - await LocalCache.setItem('currentAddress', btcAddress); + appConfigRepository.set(CONFIG_KEYS.CURRENT_ADDRESS, btcAddress); setAddress(btcAddress); // Set up API URL let base = netParams.split('@')[1]; if (!base.endsWith('/')) { base = `${base}/`; } - let api = await LocalCache.getItem('api'); + let api = appConfigRepository.get('api'); if (api) { if (api.endsWith('/')) { api = api.substring(0, api.length - 1); } BBMTLibNativeModule.setAPI(net, api); } else { - await LocalCache.setItem('api', base); + appConfigRepository.set('api', base); } // Initialize cache timestamps from WalletService (works offline) // Seed UI with cached price/balance immediately (no network needed) @@ -1424,12 +1646,13 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { setBtcRate(r); } } - const cachedBal = await WalletService.getInstance().getBal(address); + const cachedBal = await WalletService.getInstance().getBal(btcAddress); if (cachedBal) { // Normalize balance to ensure no negative zero const normalizedBTC = cachedBal.btc || '0.00000000'; const balanceNum = parseFloat(normalizedBTC); - const finalBTC = balanceNum <= 0 ? '0.00000000' : normalizedBTC; + const finalBTC = balanceNum <= 0 ? '-' : normalizedBTC; + dbg('WalletHome: Final BTC (cachedBal):', finalBTC); setBalanceBTC(finalBTC); const r = (priceResponse?.rates?.[currency] as number) || @@ -1449,9 +1672,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Check if this is a legacy wallet and show migration modal if needed // Modal shows by default unless user checked "do not remind" (flag = "yes") if (useLegacyPath) { - const doNotRemind = await LocalCache.getItem( - 'legacyWalletModalDoNotRemind', - ); + const doNotRemind = appConfigRepository.get(CONFIG_KEYS.LEGACY_WALLET_DO_NOT_REMIND); if (doNotRemind !== 'yes') { // Small delay to ensure UI is ready setTimeout(() => { @@ -1470,7 +1691,18 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } }; init(); - }, [showErrorToast, isInitialized, address, navigation, network, activeNetwork, showMempoolPlayground, showUtxosTab, showPsbtTab, showWalletTab]); + }, [ + showErrorToast, + isInitialized, + address, + navigation, + network, + activeNetwork, + showMempoolPlayground, + showUtxosTab, + showPsbtTab, + showWalletTab, + ]); // Remove the old interval effect since we're handling it in CacheIndicator now // Initial data fetch only when initialized and address is set useEffect(() => { @@ -1489,16 +1721,39 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { mounted = false; }; }, [isInitialized, address, activeNetwork, showMempoolPlayground]); + + // Start background sync once the full HD address set is known. + // SyncCoordinator writes deltas to SQLite; the UI reads from the DB. + useEffect(() => { + if (!walletAddressesReady || !apiBase || !network) { + return; + } + const addrs = walletAddresses.length > 0 ? walletAddresses : address ? [address] : []; + if (addrs.length === 0) { + return; + } + const cleanApi = apiBase.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + syncCoordinator.start({ + addresses: addrs.map(a => ({address: a, network})), + network, + apiBase: `${cleanApi}/api`, + }); + return () => { + syncCoordinator.stop(); + }; + }, [walletAddressesReady, walletAddresses, address, network, apiBase]); const handleBlurred = () => { const blurr = !isBlurred; setIsBlurred(blurr); - LocalCache.setItem('balanceHidden', blurr ? 'true' : 'false'); + appConfigRepository.set(CONFIG_KEYS.BALANCE_HIDDEN, blurr ? 'true' : 'false'); }; const handleSend = async ( to: string, amountSats: Big, feeSats: Big, spendingHash: string, + utxosJson?: string | null, + changeAddress?: string | null, ) => { if (!isSending && amountSats.gt(0) && feeSats.gt(0) && to) { setIsSending(true); @@ -1547,14 +1802,18 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { !currentDerivationPath || currentDerivationPath.trim() === '' ) { - // Compute derivation path inline const useLegacyPath = isLegacyWallet(keyshare.created_at); const normalizedNetwork = network === 'testnet3' ? 'testnet' : network || 'mainnet'; - derivationPathToUse = getDerivePathForNetwork( + const externalIndex = await getExternalIndex( + network || 'mainnet', + addressTypeToUse, + ); + derivationPathToUse = getReceivePath( normalizedNetwork, addressTypeToUse, useLegacyPath, + externalIndex, ); } else { derivationPathToUse = currentDerivationPath; @@ -1649,15 +1908,19 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { const keyshare = JSON.parse(keyshareJSON); const useLegacyPath = isLegacyWallet(keyshare.created_at); const currentAddressType = addressType || 'segwit-native'; - // Normalize network for derivation path computation (getDerivePathForNetwork expects 'testnet' not 'testnet3') const normalizedNetwork = network === 'testnet3' ? 'testnet' : network; - derivationPath = getDerivePathForNetwork( + const externalIndex = await getExternalIndex( + network || 'mainnet', + currentAddressType, + ); + derivationPath = getReceivePath( normalizedNetwork, currentAddressType, useLegacyPath, + externalIndex, ); - // Derive the public key using the computed derivation path + // Derive the public key using the computed derivation path (current receive address) const publicKey = await BBMTLibNativeModule.derivePubkey( keyshare.pub_key, keyshare.chain_code_hex, @@ -1690,8 +1953,20 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } setCurrentDerivationPath(derivationPath); setComputedFromAddress(fromAddress); - // Store params and show transport selector after a brief delay to ensure send modal is closed - setPendingSendParams({to, amountSats, feeSats, spendingHash}); + // Store params and show transport selector after a brief delay + const parsedUtxoCount = (() => { + if (!utxosJson) return undefined; + try { return (JSON.parse(utxosJson) as unknown[]).length; } catch { return undefined; } + })(); + setPendingSendParams({ + to, + amountSats, + feeSats, + spendingHash, + utxosJson: utxosJson ?? null, + utxoCount: parsedUtxoCount, + changeAddress: changeAddress ?? null, + }); setTimeout(() => { setIsTransportModalVisible(true); setIsSending(false); @@ -1748,7 +2023,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { } const routeName = transport === 'local' ? 'Devices Pairing' : 'Nostr Connect'; - const navigationParams = { + const navigationParams: Record = { mode: 'send_btc', addressType: addressTypeToUse.trim(), // MANDATORY: address type from sender or QR toAddress, @@ -1761,6 +2036,12 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { derivationPath: derivationPathToUse.trim(), // MANDATORY: derivation path from sender or QR network: networkToUse.trim(), // MANDATORY: network from sender or QR (native format) }; + if (pendingSendParams.utxosJson && pendingSendParams.utxosJson.trim() !== '') { + navigationParams.utxosJson = pendingSendParams.utxosJson; + } + if (pendingSendParams.changeAddress && pendingSendParams.changeAddress.trim() !== '') { + navigationParams.changeAddress = pendingSendParams.changeAddress; + } dbg('=== WalletHome: Navigating to pairing screen ===', { routeName, transport, @@ -1815,16 +2096,16 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { // Support BIP-21: "bitcoin:
" or "bitcoin:
?amount=..." const addressCandidate = trimmed.startsWith('bitcoin:') - ? trimmed.replace(/^bitcoin:/i, '').split('?')[0].trim() + ? trimmed + .replace(/^bitcoin:/i, '') + .split('?')[0] + .trim() : trimmed; const networkForValidation = - network === 'testnet3' ? 'testnet' : (network || 'mainnet'); + network === 'testnet3' ? 'testnet' : network || 'mainnet'; if ( addressCandidate && - validateBitcoinAddressEnhanced( - addressCandidate, - networkForValidation, - ) + validateBitcoinAddressEnhanced(addressCandidate, networkForValidation) ) { setInitialSendAddress(addressCandidate); setIsSendModalVisible(true); @@ -1846,8 +2127,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { lastInvalidQrRef.current = {data: qrData, time: now}; const currentLabel = networkForValidation === 'mainnet' ? 'mainnet' : 'testnet'; - const addressLabel = - otherNetwork === 'mainnet' ? 'mainnet' : 'testnet'; + const addressLabel = otherNetwork === 'mainnet' ? 'mainnet' : 'testnet'; Alert.alert( 'Wrong network', `This address is for ${addressLabel} but you're on ${currentLabel}. Switch network in Settings or scan an address for ${currentLabel}.`, @@ -1855,90 +2135,110 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { return; } const decoded = decodeSendBitcoinQR(qrData) as { - toAddress: string; - amountSats: string; - feeSats: string; - spendingHash?: string; - addressType?: string; - derivationPath?: string; - network?: string; - } | null; - if ( - !decoded || - !decoded.toAddress || - !decoded.amountSats || - !decoded.feeSats - ) { - const now = Date.now(); + toAddress: string; + amountSats: string; + feeSats: string; + spendingHash?: string; + addressType?: string; + derivationPath?: string; + network?: string; + utxosJson?: string; + changeAddress?: string; + } | null; if ( - lastInvalidQrRef.current.data === qrData && - now - lastInvalidQrRef.current.time < 2000 + !decoded || + !decoded.toAddress || + !decoded.amountSats || + !decoded.feeSats ) { + const now = Date.now(); + if ( + lastInvalidQrRef.current.data === qrData && + now - lastInvalidQrRef.current.time < 2000 + ) { + return; + } + lastInvalidQrRef.current = {data: qrData, time: now}; + 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; } - lastInvalidQrRef.current = {data: qrData, time: now}; - 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 address type, derivation path, and network from QR code if available - // These are critical to ensure the second device uses the same source address and network - dbg('=== WalletHome: Processing scanned QR code data ===', { - decoded: { - toAddress: decoded.toAddress, - amountSats: decoded.amountSats, - feeSats: decoded.feeSats, - spendingHash: decoded.spendingHash, - addressType: decoded.addressType, - derivationPath: decoded.derivationPath, - network: decoded.network, - }, - }); - if (decoded.addressType) { - setScannedAddressType(decoded.addressType); - dbg('WalletHome: Address type from QR code:', decoded.addressType); - } - if (decoded.derivationPath) { - setCurrentDerivationPath(decoded.derivationPath); - dbg('WalletHome: Derivation path from QR code:', decoded.derivationPath); - } - if (decoded.network) { - // Keep native format from QR code (native module requires 'testnet3' not 'testnet') - setScannedNetwork(decoded.network); - dbg('WalletHome: Network from QR code:', decoded.network); - } - // 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); + // 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 address type, derivation path, and network from QR code if available + // These are critical to ensure the second device uses the same source address and network + dbg('=== WalletHome: Processing scanned QR code data ===', { + decoded: { + toAddress: decoded.toAddress, + amountSats: decoded.amountSats, + feeSats: decoded.feeSats, + spendingHash: decoded.spendingHash, + addressType: decoded.addressType, + derivationPath: decoded.derivationPath, + network: decoded.network, + }, + }); + if (decoded.addressType) { + setScannedAddressType(decoded.addressType); + dbg('WalletHome: Address type from QR code:', decoded.addressType); + } + if (decoded.derivationPath) { + setCurrentDerivationPath(decoded.derivationPath); + dbg( + 'WalletHome: Derivation path from QR code:', + decoded.derivationPath, + ); + } + if (decoded.network) { + // Keep native format from QR code (native module requires 'testnet3' not 'testnet') + setScannedNetwork(decoded.network); + dbg('WalletHome: Network from QR code:', decoded.network); + } + // Optional: derive UTXO count when QR carries utxosJson + let utxoCount: number | undefined; + if (decoded.utxosJson) { + try { + const parsed = JSON.parse(decoded.utxosJson); + if (Array.isArray(parsed)) { + utxoCount = parsed.length; + } + } catch (e) { + dbg('WalletHome: Failed to parse utxosJson from QR', e); + } + } + // Store params and mark as scanned from QR + setPendingSendParams({ + to: decoded.toAddress, + amountSats, + feeSats, + spendingHash: decoded.spendingHash || '', + utxosJson: decoded.utxosJson || null, + utxoCount, + changeAddress: decoded.changeAddress || null, + }); + setScannedFromQR(true); + // Show transport selector immediately (no QR code shown since data came from scan) + setTimeout(() => { + setIsTransportModalVisible(true); + }, 300); }, [network], ); @@ -1951,6 +2251,42 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { opacity: balanceUpdateAnimation.value, })); + // Pulse the balance text opacity while loading so the value stays readable + // but the user can see a refresh is in progress. + useEffect(() => { + if (isBalanceLoading) { + shimmerOpacity.value = withRepeat( + withSequence( + withTiming(1.0, {duration: 600}), + withTiming(0.45, {duration: 600}), + ), + -1, + false, + ); + } else { + shimmerOpacity.value = withTiming(1.0, {duration: 200}); + } + }, [isBalanceLoading, shimmerOpacity]); + + const shimmerAnimStyle = useAnimatedStyle(() => ({ + opacity: shimmerOpacity.value, + })); + + /** + * Fiat display value derived from balanceBTC × btcRate so the two rows are + * always consistent. Falls back to the stored balanceFiat only when the + * price has not been loaded yet (btcRate === 0). + */ + const displayFiat = useMemo(() => { + if (balanceFiat === '-') { + return '-'; + } + if (btcRate > 0) { + return (parseFloat(balanceBTC || '0') * btcRate).toFixed(2); + } + return balanceFiat; + }, [balanceBTC, balanceFiat, btcRate]); + if (loading && !isInitialized) { return ; } @@ -1985,7 +2321,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { {balanceError && !isBlurred ? ( - {balanceError} + + {balanceError} + ) : ( <> @@ -2008,13 +2346,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { }`} accessibilityHint="Double tap to toggle balance visibility" accessibilityRole="button"> - {isBalanceLoading && !isBlurred && !isRefreshing ? ( - - ) : ( + = ({navigation}) => { formatted: balanceFormattingEnabled, })} - )} + {btcRate > 0 && ( @@ -2051,7 +2383,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { ? 'hidden' : (() => { const fiatValue = - balanceFiat === '-' ? '0' : balanceFiat; + displayFiat === '-' ? '0' : displayFiat; return balanceFormattingEnabled ? `${getCurrencySymbol( selectedCurrency, @@ -2063,13 +2395,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { }`} accessibilityHint="Double tap to toggle balance visibility" accessibilityRole="button"> - {isBalanceLoading && !isBlurred && !isRefreshing ? ( - - ) : ( + = ({navigation}) => { ? `${getCurrencySymbol(selectedCurrency)} ******` : (() => { const fiatValue = - balanceFiat === '-' ? '0' : balanceFiat; - return balanceFormattingEnabled - ? `${getCurrencySymbol( - selectedCurrency, - )}${presentFiat(fiatValue)}` - : `${getCurrencySymbol( - selectedCurrency, - )}${fiatValue}`; + displayFiat === '-' ? '0' : displayFiat; + const symbol = + getCurrencySymbol(selectedCurrency); + const formattedFiat = balanceFormattingEnabled + ? presentFiat(fiatValue) + : fiatValue; + return isNaN(Number(formattedFiat)) + ? '-' + : symbol + formattedFiat; })()} - )} + )} + {!isBlurred && pendingSats !== 0 && ( + + + {pendingSats > 0 + ? `⏳ +${(pendingSats / 1e8).toFixed(8)} BTC incoming` + : `⏳ ${(pendingSats / 1e8).toFixed(8)} BTC outgoing`} + + + )} )} @@ -2105,9 +2441,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { }} style={styles.balanceUnitToggle} android_ripple={{color: 'rgba(0,0,0,0.1)'}} - accessibilityLabel={`Switch to ${ - showSats ? 'BTC' : 'sats' - }`} + accessibilityLabel={`Switch to ${showSats ? 'BTC' : 'sats'}`} accessibilityRole="button"> {showSats ? '₿' : 'BTC'} @@ -2198,23 +2532,88 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { styles.receiveButton, styles.flexOneMinWidthZero, ]} - onPress={() => { - setIsReceiveModalVisible(true); + disabled={isReceiveBusy || isRestoringIndexes} + onPress={async () => { + if (isReceiveBusy || isRestoringIndexes) { + return; + } + try { + setIsReceiveBusy(true); + const ws = WalletService.getInstance(); + const effectiveAddressType = addressType || 'segwit-native'; + const apiUrl = + apiBase || + (network === 'mainnet' + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'); + const restoreDone = + walletRepository.getHdState(network, effectiveAddressType)?.restoreDone === true; + + // Only run full restore discovery once per (network, addressType) + if (!restoreDone) { + setIsRestoringIndexes(true); + setRestoreProgress(null); + await ws.discoverHdIndexesForNetwork( + network, + effectiveAddressType, + apiUrl, + (chain, index, gapIndex) => + setRestoreProgress({chain, index, gapIndex}), + ); + await refreshUserContext(); + } + + // Lightweight frontier bump: if current receive address is already used, + // advance external index so the next receive shows a fresh address. + await ws.bumpExternalIndexIfCurrentUsed( + network, + effectiveAddressType, + apiUrl, + ); + + const info = await ws.getCurrentReceivePathInfo( + network, + effectiveAddressType, + ); + setReceivePathInfo(info); + dbg('[WalletHome] Receive modal: path info', { + index: info?.index, + path: info?.path, + }); + setIsReceiveModalVisible(true); + } catch (e) { + dbg( + '[WalletHome] Receive modal: discovery or path info error', + e, + ); + setReceivePathInfo(null); + setIsReceiveModalVisible(true); + } finally { + setIsRestoringIndexes(false); + setRestoreProgress(null); + setIsReceiveBusy(false); + } }} android_ripple={{color: 'rgba(0,0,0,0.1)'}} hitSlop={{top: 10, bottom: 10, left: 10, right: 10}} accessibilityLabel="Receive Bitcoin" accessibilityHint="Double tap to view your Bitcoin address and QR code" accessibilityRole="button"> - - - Receive - + {isReceiveBusy || isRestoringIndexes ? ( + + ) : ( + <> + + + Receive + + + )} @@ -2241,7 +2640,18 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { 0 + ? walletAddresses + : undefined + } + network={network} + addressType={addressType || userAddressType} onUpdate={handleTransactionUpdate} initialTransactions={initialTransactions} selectedCurrency={selectedCurrency} @@ -2362,18 +2772,33 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => { .div(1e8) .toFixed(2), selectedCurrency: selectedCurrency, + utxosJson: pendingSendParams.utxosJson || null, + utxoCount: pendingSendParams.utxoCount, + changeAddress: pendingSendParams.changeAddress || null, } : null } showQRCode={!scannedFromQR} // Don't show QR if data came from scan /> + {isReceiveModalVisible && ( setIsReceiveModalVisible(false)} + onClose={() => { + setIsReceiveModalVisible(false); + setReceivePathInfo(null); + // Refresh balance and tx history after closing receive (e.g. after sharing address). + fetchDataRef.current?.(); + }} + receivePathInfo={receivePathInfo} /> )} {/* Signed PSBT Modal */} diff --git a/screens/WalletSettings.tsx b/screens/WalletSettings.tsx index 32dfa296..f386ffd4 100644 --- a/screens/WalletSettings.tsx +++ b/screens/WalletSettings.tsx @@ -44,10 +44,20 @@ import { getResetToMainTabsWallet, } from '../utils'; import {useTheme} from '../theme'; -import {WalletService} from '../services/WalletService'; -import LocalCache from '../services/LocalCache'; +import {waitMS, WalletService} from '../services/WalletService'; +import mempoolClient from '../services/MempoolClient'; +import appConfigRepository, { + CONFIG_KEYS, +} from '../services/repositories/AppConfigRepository'; +import database from '../services/Database'; +import walletRepository from '../services/repositories/WalletRepository'; +import balanceRepository from '../services/repositories/BalanceRepository'; +import balanceSyncer from '../services/sync/BalanceSyncer'; +import transactionSyncer from '../services/sync/TransactionSyncer'; +import utxoSyncer from '../services/sync/UtxoSyncer'; import LegalModal from '../components/LegalModal'; import BackupKeyshareModal from '../components/BackupKeyshareModal'; +import RestoringIndexesModal from '../components/RestoringIndexesModal'; import {fetchDynamicAPIEndpoints, getNostrRelays} from '../utils'; import FontComparisonScreen from '../components/FontComparisonScreen'; import {setDebugLoggingEnabled, isDebugLoggingEnabled} from '../App'; @@ -675,7 +685,8 @@ const SettingsSectionGroup: React.FC = ({ theme, }) => ( - + {title.toUpperCase()} {children} @@ -706,6 +717,7 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { showWalletTab, setShowWalletTab, activeNetwork, + activeApiProvider, } = useUser(); const [selectedIcon, setSelectedIcon] = useState< 'default' | 'alternative' | 'loading' @@ -722,6 +734,14 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { const [nostrRelays, setNostrRelays] = useState(''); const [pendingNostrRelays, setPendingNostrRelays] = useState(''); const [hasNostr, setHasNostr] = useState(false); + const [isRestoringIndexes, setIsRestoringIndexes] = useState(false); + const [restoreProgress, setRestoreProgress] = useState<{ + chain?: 'external' | 'internal'; + index?: number; + gapIndex?: number; + /** Free-text label shown during post-discovery sync phases. */ + phase?: string; + } | null>(null); const [isLegalModalVisible, setIsLegalModalVisible] = useState(false); const [legalModalType, setLegalModalType] = useState<'terms' | 'privacy'>( 'terms', @@ -776,13 +796,160 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { return newState; }); }; + /** + * Atomic wallet reset + HD re-index + full pre-sync for a given + * (network, addressType). + * + * The whole operation is treated as a transaction: + * • If HD index discovery fails the function THROWS so the caller can + * show an error toast and abort navigation. The old hd_state + * (preserved by clearWalletCacheData) remains valid. + * • Only when discovery succeeds does the function continue to the + * balance / transaction / UTXO pre-sync so WalletHome arrives with + * fully populated DB data on first render — no extra refresh needed. + * + * Phase sequence (reflected in the progress modal): + * 1. clearWalletCacheData — wipe stale fetched data, keep hd_state + * 2. Invalidate HTTP + address caches + * 3. discoverHdIndexesForNetwork — gap-limit scan (throws on failure) + * 4. Sync balances — shown as "Syncing balances…" + * 5. Sync transactions — shown as "Syncing transactions…" + * 6. Sync UTXOs — shown as "Syncing UTXOs…" + * 7. Re-persist config + */ + const runRestoreIndexing = useCallback( + async (network: string, addressType: string, resolvedApiUrl?: string) => { + setIsRestoringIndexes(true); + setRestoreProgress(null); + // Yield so the RestoringIndexesModal has time to mount and paint. + await waitMS(250); + + try { + // ── Step 1: clear cached data, preserve hd_state ────────────────── + // clearWalletCacheData() keeps hd_state so the old correct indexes + // survive as prevExternalIndex inside discoverHdIndexesForNetwork. + // If discovery later fails on a slow network the wallet retains the + // last-known correct index instead of collapsing to 0. + database.clearWalletCacheData(); + + const ws = WalletService.getInstance(); + const apiUrl = + resolvedApiUrl || + appConfigRepository.get(`api_${network}`) || + appConfigRepository.get('api') || + (network === 'mainnet' + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'); + + // ── Step 2: flush in-memory caches before discovery ─────────────── + // Every isAddressUsed call inside discoverHdIndexesForNetwork must + // hit the network directly — no stale 5-second mempoolClient entries + // can cause incorrect gap-limit decisions. + mempoolClient.invalidateAll(); + ws.invalidateAddressCache(); + await waitMS(250); + + // ── Step 3: HD index discovery ──────────────────────────────────── + await ws.discoverHdIndexesForNetwork( + network, + addressType, + apiUrl, + (chain, index, gapIndex) => + setRestoreProgress({chain, index, gapIndex}), + ); + + // Guard: if discovery did not complete successfully restoreDone is + // not set. Throw so the caller can show a toast and NOT navigate. + const hdState = walletRepository.getHdState(network, addressType); + if (!hdState?.restoreDone) { + throw new Error( + 'Index discovery incomplete — network may be unreachable. Please try again.', + ); + } + + // ── Step 4: derive + cache HD address list ──────────────────────── + const addressesWithPaths = await ws.getHdAddressesWithPaths( + network, + addressType, + ); + + // ── Step 5: pre-sync balances ───────────────────────────────────── + setRestoreProgress({phase: 'Syncing balances…'}); + await balanceSyncer.syncAddresses( + addressesWithPaths.map(a => ({ + address: a.address, + network, + })), + apiUrl, + ); + + // Compute and persist the aggregate balance immediately so WalletHome + // can read a correct total from getCachedAggregateBalance on first + // render without making another API round-trip. + // getAggregateBalance sums all per-address rows written by balanceSyncer + // above (excluding the virtual aggregate key which doesn't exist yet + // because clearWalletCacheData wiped the table before sync started). + const agg = balanceRepository.getAggregateBalance(network); + balanceRepository.setBalance({ + address: `aggregate_${network}_${addressType}`, + network, + balanceSats: agg.balanceSats, + pendingSats: agg.pendingSats, + hasNonzero: agg.hasNonzero, + fetchedAt: agg.fetchedAt || Date.now(), + }); + + // ── Step 6: pre-sync transactions ───────────────────────────────── + setRestoreProgress({phase: 'Syncing transactions…'}); + for (const {address} of addressesWithPaths) { + await transactionSyncer.syncAddress(address, network, apiUrl); + } + + // ── Step 7: pre-sync UTXOs ──────────────────────────────────────── + setRestoreProgress({phase: 'Syncing UTXOs…'}); + await utxoSyncer.syncAddresses( + addressesWithPaths.map(a => ({ + address: a.address, + network, + derivationPath: a.derivationPath, + })), + apiUrl, + ); + + // ── Step 8: re-persist config ───────────────────────────────────── + appConfigRepository.set(CONFIG_KEYS.NETWORK, network); + appConfigRepository.set('api', apiUrl); + appConfigRepository.set(`api_${network}`, apiUrl); + appConfigRepository.set(CONFIG_KEYS.ADDRESS_TYPE, addressType); + } finally { + setIsRestoringIndexes(false); + setRestoreProgress(null); + } + }, + [], + ); // Expand section when opened from header network button (e.g. expandSection: 'advanced' for Network Providers) useEffect(() => { const section = route.params?.expandSection; const validSections = new Set([ - 'theme', 'haptics', 'displayFormat', 'backup', 'advanced', 'nostr', - 'about', 'legal', 'storage', 'appIcon', 'devicePairing', 'addressType', - 'fontTesting', 'devDebug', 'mempoolPlayground', 'utxos', 'psbt', 'wallet', + 'theme', + 'haptics', + 'displayFormat', + 'backup', + 'advanced', + 'nostr', + 'about', + 'legal', + 'storage', + 'appIcon', + 'devicePairing', + 'addressType', + 'fontTesting', + 'devDebug', + 'mempoolPlayground', + 'utxos', + 'psbt', + 'wallet', ]); if (section && validSections.has(section)) { setExpandedSections(prev => ({...prev, [section]: true})); @@ -792,9 +959,7 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { setAppVersion(DeviceInfo.getVersion()); setBuildNumber(DeviceInfo.getBuildNumber()); setHapticsEnabledState(areHapticsEnabled()); - LocalCache.usageSize().then(size => { - setUsageSize(size); - }); + setUsageSize({fileCount: 0, mb: '0.00 MB'}); // Initialize debug logging state from module-level ref setDebugLoggingEnabledState(isDebugLoggingEnabled()); // Load dev debug enabled preference @@ -855,56 +1020,26 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { dbg('Failed to parse keyshare for settings screen:', error); } }); - // Load network and corresponding cached API - LocalCache.getItem('network').then(async net => { + // Load network and corresponding cached API (synchronous SQLite reads) + (() => { + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK); dbg('=== Loading settings for network:', net); setIsTestnet(net !== 'mainnet'); - // Clear any pending API changes when switching networks setPendingAPI(''); - // Try to get the cached API for this network - const cachedApi = await LocalCache.getItem(`api_${net}`); - dbg(`Cached API for ${net}:`, cachedApi); - if (cachedApi) { - setBaseAPI(cachedApi); - setPendingAPI(cachedApi); // Initialize pending API to current API - // Update the current API cache - await LocalCache.setItem('api', cachedApi); - // Update native module with the cached API - if (net) { - await BBMTLibNativeModule.setAPI(net, cachedApi); - } - dbg(`=== Loaded cached API for ${net}:`, cachedApi); - } else { - // Fallback to current API or default - const currentApi = await LocalCache.getItem('api'); - dbg('Current API (fallback):', currentApi); - if (currentApi) { - setBaseAPI(currentApi); - setPendingAPI(currentApi); // Initialize pending API to current API - // Cache it for this network - await LocalCache.setItem(`api_${net}`, currentApi); - // Update native module - if (net) { - await BBMTLibNativeModule.setAPI(net, currentApi); - } - dbg(`=== Cached current API for ${net}:`, currentApi); - } else { - // Use default API for the network - const defaultApi = - net === 'mainnet' - ? 'https://mempool.space/api' - : 'https://mempool.space/testnet/api'; - setBaseAPI(defaultApi); - setPendingAPI(defaultApi); // Initialize pending API to default API - await LocalCache.setItem('api', defaultApi); - await LocalCache.setItem(`api_${net}`, defaultApi); - if (net) { - await BBMTLibNativeModule.setAPI(net, defaultApi); - } - dbg(`=== Using default API for ${net}:`, defaultApi); - } + let resolvedApi = + appConfigRepository.get(`api_${net}`) || appConfigRepository.get('api'); + if (!resolvedApi) { + resolvedApi = + net === 'mainnet' + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'; + appConfigRepository.set('api', resolvedApi); + if (net) appConfigRepository.set(`api_${net}`, resolvedApi); } - }); + setBaseAPI(resolvedApi); + setPendingAPI(resolvedApi); + if (net) BBMTLibNativeModule.setAPI(net, resolvedApi); + })(); // Load Nostr relays (from cache if available, otherwise fetch dynamically) getNostrRelays(false).then(relays => { const relaysCSV = relays.join(','); @@ -919,8 +1054,33 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { const newNetwork = value ? 'testnet3' : 'mainnet'; const networkName = value ? 'Testnet' : 'Mainnet'; await setActiveNetwork(newNetwork); + try { + await runRestoreIndexing(newNetwork, activeAddressType); + } catch (e) { + dbg('Network toggle: sync failed', e); + Toast.show({ + type: 'error', + text1: 'Sync failed', + text2: + e instanceof Error + ? e.message + : 'Network switch could not complete. Please try again.', + visibilityTime: 5000, + }); + return; + } dbg('Network toggle: Navigating to Wallet tab'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: newNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); // Show brief feedback alert after a brief delay to ensure navigation completes setTimeout(() => { // warn user if test net bitcoin is not real @@ -942,22 +1102,16 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { }; const resetAPI = async () => { dbg('resetAPI called'); - const net = await LocalCache.getItem('network'); + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK); const api = net === 'mainnet' - ? 'https://mempool.space/api' // MAINNET_APIS[0] - : 'https://mempool.space/testnet/api'; // TESTNET_APIS[0] - dbg('Resetting to default API for network:', net, 'API:', api); - // Clear pending API selection and set to new API + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'; setPendingAPI(api); - // Update local state setBaseAPI(api); - dbg('Local state updated with API:', api); - // Cache the API setting for the current network if (net) { - await LocalCache.setItem(`api_${net}`, api); - await LocalCache.setItem('api', api); - dbg(`API cached for network ${net}:`, api); + appConfigRepository.set(`api_${net}`, api); + appConfigRepository.set('api', api); } // Update native module if (net) { @@ -973,7 +1127,17 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { await setActiveApiProvider(api); dbg('API reset and propagated successfully:', api); // Navigate to home after reset - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); // Show success alert after navigation setTimeout(() => { Alert.alert('Success', 'API endpoint reset to default!'); @@ -1058,7 +1222,17 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { Alert.alert('Success', 'API endpoint updated successfully!'); dbg('=== API saved and propagated successfully:', normalizedApi); // Navigate to home after successful save - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); } catch (error) { dbg('Error in saveAPI:', error); Alert.alert('Error', 'Failed to save API endpoint. Please try again.'); @@ -1075,8 +1249,9 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { try { setIsDeleting(true); setIsModalResetVisible(false); - dbg('clearing cache storage...'); - await LocalCache.clear(); + dbg('clearing SQLite wallet data...'); + database.clearWalletData(); + mempoolClient.invalidateAll(); dbg('clearing encrypted storage...'); // Prefer a full clear so we return to true first-launch state. // (If clear() is unavailable on some builds, fall back to removing known keys.) @@ -2331,1033 +2506,1167 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => { scrollEventThrottle={16}> {/* App: Theme, Balance Display, Haptics, Storage */} - toggleSection('theme')} - styles={styles} - theme={theme}> - - Choose your preferred color theme. OS Default follows your system - settings. - - - { - setThemeMode('os'); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - OS Default - - {themeMode === 'os' && ( - - )} - - - { - setThemeMode('light'); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - Light - - {themeMode === 'light' && ( - - )} - - - { - setThemeMode('dark'); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - Dark - - {themeMode === 'dark' && ( - - )} - - - - - toggleSection('displayFormat')} - styles={styles} - theme={theme}> - - Bitcoin uses 8 decimal places{' '} - for full accuracy. - + toggleSection('theme')} + styles={styles} + theme={theme}> + + Choose your preferred color theme. OS Default follows your system + settings. + + + { + setThemeMode('os'); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + OS Default + + {themeMode === 'os' && ( + + )} + + + { + setThemeMode('light'); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + Light + + {themeMode === 'light' && ( + + )} + + + { + setThemeMode('dark'); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + Dark + + {themeMode === 'dark' && ( + + )} + + + + + toggleSection('displayFormat')} + styles={styles} + theme={theme}> + + Bitcoin uses 8 decimal places{' '} + for full accuracy. + - - 1 BTC ={' '} - 100,000,000 satoshis (sats) - + + 1 BTC ={' '} + 100,000,000 satoshis (sats) + - - You can choose how your balance is shown: - + + You can choose how your balance is shown: + - - Formatted: Thousand separators - make large numbers easier to read and verify decimal precision. - Example: 1,234.56,789,010 ₿{' '} - or 123,456,789,010 sats - + + Formatted: Thousand + separators make large numbers easier to read and verify decimal + precision. Example:{' '} + 1,234.56,789,010 ₿ or{' '} + 123,456,789,010 sats + - - Raw Numbers: Exact values - without separators. Example:{' '} - 1234.56789 ₿ or{' '} - 123456789000 sats - - - Raw Numbers - - Formatted - - - toggleSection('haptics')} - styles={styles} - theme={theme}> - - Enable vibration feedback. OS settings may override this. - - - Haptics Off - - Haptics On - - - {/* Storage - inside App */} - toggleSection('storage')} - styles={styles} - theme={theme}> - - Cache Maintenance - - Clear cached balances and history. + + Raw Numbers: Exact values + without separators. Example:{' '} + 1234.56789 ₿ or{' '} + 123456789000 sats - - { - try { - await LocalCache.clear(); - setUsageSize(await LocalCache.usageSize()); - Alert.alert('Cache Cleared', 'Cache cleared successfully.'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); - } catch (e) { - dbg('Error clearing cache', e); - Alert.alert( - 'Error', - 'Failed to clear cache. Please try again.', - ); - } - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - + Raw Numbers + - - Clear Cache ({usageSize.mb}) - + Formatted - - - {/* App Icon - Android Only */} - {Platform.OS === 'android' && ( + toggleSection('appIcon')} + title="Haptics" + isExpanded={expandedSections.haptics} + onToggle={() => toggleSection('haptics')} styles={styles} theme={theme}> - - - - Blend in when you need to. - - - Switch to the calculator icon when you want your wallet to - look like just another app on your home screen. - - - - Change the app's launcher icon on your device. + Enable vibration feedback. OS settings may override this. - Bold Wallet + Haptics Off { - try { - const newIcon = value ? 'alternative' : 'default'; - if (!IconChanger || !IconChanger.changeIcon) { - Alert.alert( - 'Error', - 'Icon switching is not available on this device.', - [{text: 'OK'}], - ); - return; - } - setSelectedIcon(newIcon); - await EncryptedStorage.setItem( - 'app_icon_preference', - newIcon, - ); - await IconChanger.changeIcon(newIcon); - const iconName = - newIcon === 'alternative' ? 'QuickCalc' : 'Bold Wallet'; - Alert.alert( - 'Icon Changed', - `App icon switched to ${iconName}.\n\nYou may need to refresh your launcher to see the change.`, - [{text: 'OK'}], - ); - } catch (error: any) { - dbg('Error changing icon:', error); - setSelectedIcon(value ? 'default' : 'alternative'); - Alert.alert( - 'Error', - error?.message || - 'Failed to change app icon. Please try again.', - [{text: 'OK'}], - ); - } - }} - disabled={selectedIcon === 'loading'} + onValueChange={handleToggleHaptics} + value={hapticsEnabled} /> - QuickCalc + Haptics On - )} - - - {/* Wallet: Security, Address Type, Network providers, Nostr Relays */} - - toggleSection('backup')} - styles={styles} - theme={theme}> - Backup Wallet Keyshare - { - setIsBackupModalVisible(true); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - Backup {party} - - - - Delete Wallet Keyshare - - { - setIsModalResetVisible(true); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - Delete {party} + {/* Storage - inside App */} + toggleSection('storage')} + styles={styles} + theme={theme}> + + Cache Maintenance + + Clear cached balances and history. + - - - {/* Address Type - use address-type-icon */} - toggleSection('addressType')} - styles={styles} - theme={theme}> - - Choose the receive address format. Native SegWit (bech32) is - recommended. Changing this updates your receive address on the - Wallet tab. - - { + dbg('WalletSettings: Clear Cache pressed', { + network: activeNetwork, + addressType: activeAddressType, + }); + const apiUrl = + activeApiProvider || + (activeNetwork === 'mainnet' + ? 'https://mempool.space/api' + : 'https://mempool.space/testnet/api'); try { - await setActiveAddressType('legacy'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); + await runRestoreIndexing( + activeNetwork, + activeAddressType, + apiUrl, + ); } catch (e) { - dbg('Error setting address type:', e); + dbg('Clear cache: sync failed', e); + Toast.show({ + type: 'error', + text1: 'Sync failed', + text2: + e instanceof Error + ? e.message + : 'Cache clear could not complete. Please try again.', + visibilityTime: 5000, + }); + return; } + setUsageSize({fileCount: 0, mb: '0.00 MB'}); + dbg('WalletSettings: Storage clear complete'); + Toast.show({ + type: 'success', + text1: 'Cache cleared', + text2: 'Wallet synced successfully.', + visibilityTime: 3000, + }); + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: + activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); }} android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - Legacy (P2PKH) - {activeAddressType === 'legacy' && ( + - )} + + Clear Cache ({usageSize.mb}) + + + + {/* App Icon - Android Only */} + {Platform.OS === 'android' && ( + toggleSection('appIcon')} + styles={styles} + theme={theme}> + + + + Blend in when you need to. + + + Switch to the calculator icon when you want your wallet to + look like just another app on your home screen. + + + + + Change the app's launcher icon on your device. + + + Bold Wallet + { + try { + const newIcon = value ? 'alternative' : 'default'; + if (!IconChanger || !IconChanger.changeIcon) { + Alert.alert( + 'Error', + 'Icon switching is not available on this device.', + [{text: 'OK'}], + ); + return; + } + setSelectedIcon(newIcon); + await EncryptedStorage.setItem( + 'app_icon_preference', + newIcon, + ); + await IconChanger.changeIcon(newIcon); + const iconName = + newIcon === 'alternative' ? 'QuickCalc' : 'Bold Wallet'; + Alert.alert( + 'Icon Changed', + `App icon switched to ${iconName}.\n\nYou may need to refresh your launcher to see the change.`, + [{text: 'OK'}], + ); + } catch (error: any) { + dbg('Error changing icon:', error); + setSelectedIcon(value ? 'default' : 'alternative'); + Alert.alert( + 'Error', + error?.message || + 'Failed to change app icon. Please try again.', + [{text: 'OK'}], + ); + } + }} + disabled={selectedIcon === 'loading'} + /> + QuickCalc + + + )} + + + {/* Wallet: Security, Address Type, Network providers, Nostr Relays */} + + toggleSection('backup')} + styles={styles} + theme={theme}> + Backup Wallet Keyshare { - try { - await setActiveAddressType('segwit-native'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); - } catch (e) { - dbg('Error setting address type:', e); - } + style={[styles.button, styles.backupButton]} + onPress={() => { + setIsBackupModalVisible(true); }} android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - Native SegWit (bech32) - {activeAddressType === 'segwit-native' && ( + - )} + Backup {party} + + + Delete Wallet Keyshare + { - try { - await setActiveAddressType('segwit-compatible'); - navigation.reset(getResetToMainTabsWallet({}, { showPlay: activeNetwork === 'mainnet' && showMempoolPlayground, showUtxos: showUtxosTab, showPsbt: showPsbtTab, showWallet: showWalletTab })); - } catch (e) { - dbg('Error setting address type:', e); - } + style={[styles.button, styles.deleteButton]} + onPress={() => { + setIsModalResetVisible(true); }} android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - Nested SegWit (P2SH-WPKH) - {activeAddressType === 'segwit-compatible' && ( - - )} - - - - {/* Network Providers */} - toggleSection('advanced')} - styles={styles} - theme={theme}> - - - - Mainnet - - - - - Testnet3 - - - - - + - - {isTestnet ? 'Testnet Mode' : 'Mainnet Mode'} - + Delete {party} - {!isTestnet && ( - { - setIsApiInfoVisible(true); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - Change Provider? - - )} - - - {isTestnet - ? 'Testnet Provider is restricted to mempool.space/testnet' - : 'Mainnet Providers are customizable.'} - - - + + {/* Address Type - use address-type-icon */} + toggleSection('addressType')} styles={styles} - theme={theme} - /> - - {!isTestnet && ( + theme={theme}> + + Choose the receive address format. Native SegWit (bech32) is + recommended. Changing this updates your receive address on the + Wallet tab. + + { - saveAPI(pendingAPI); + onPress={async () => { + await setActiveAddressType('legacy'); + try { + await runRestoreIndexing(activeNetwork, 'legacy'); + } catch (e) { + dbg('Address type switch (legacy): sync failed', e); + Toast.show({ + type: 'error', + text1: 'Sync failed', + text2: + e instanceof Error + ? e.message + : 'Address type switch could not complete. Please try again.', + visibilityTime: 5000, + }); + return; + } + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: + activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); }} - disabled={isAPISaving || !pendingAPI || pendingAPI === baseAPI} android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - + + Legacy (P2PKH) + {activeAddressType === 'legacy' && ( - - {isAPISaving ? 'Verifying...' : 'Verify & Save'} - - + )} - )} - {!isTestnet && ( { - resetAPI(); + onPress={async () => { + await setActiveAddressType('segwit-native'); + try { + await runRestoreIndexing(activeNetwork, 'segwit-native'); + } catch (e) { + dbg('Address type switch (segwit-native): sync failed', e); + Toast.show({ + type: 'error', + text1: 'Sync failed', + text2: + e instanceof Error + ? e.message + : 'Address type switch could not complete. Please try again.', + visibilityTime: 5000, + }); + return; + } + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: + activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); }} android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - + + Native SegWit (bech32) + {activeAddressType === 'segwit-native' && ( - Defaults - + )} - )} - - - {hasNostr && ( - toggleSection('nostr')} - styles={styles} - theme={theme}> - - Nostr Relay Configuration - - Configure Nostr relays for device pairing and transaction - signing. Enter relay URLs, one per line or comma-separated - (wss://...). - - - - { + await setActiveAddressType('segwit-compatible'); try { - const relays = pendingNostrRelays - .split(/[,\n]/) - .map(r => r.trim()) - .filter(Boolean); - if (relays.length === 0) { - Alert.alert( - 'Error', - 'Please enter at least one relay URL', - ); - return; - } - const invalid = relays.find( - r => !r.startsWith('wss://') && !r.startsWith('ws://'), + await runRestoreIndexing( + activeNetwork, + 'segwit-compatible', ); - if (invalid) { - Alert.alert( - 'Error', - `Invalid relay URL: ${invalid}\nRelay URLs must start with wss:// or ws://`, - ); - return; - } - const relaysCSV = relays.join(','); - await LocalCache.setItem('nostr_relays', relaysCSV); - setNostrRelays(relaysCSV); - Alert.alert('Success', 'Nostr relays saved successfully!'); - } catch (error) { - dbg('Error saving Nostr relays:', error); - Alert.alert('Error', 'Failed to save Nostr relays'); + } catch (e) { + dbg( + 'Address type switch (segwit-compatible): sync failed', + e, + ); + Toast.show({ + type: 'error', + text1: 'Sync failed', + text2: + e instanceof Error + ? e.message + : 'Address type switch could not complete. Please try again.', + visibilityTime: 5000, + }); + return; } + navigation.reset( + getResetToMainTabsWallet( + {}, + { + showPlay: + activeNetwork === 'mainnet' && showMempoolPlayground, + showUtxos: showUtxosTab, + showPsbt: showPsbtTab, + showWallet: showWalletTab, + }, + ), + ); }} - disabled={ - !pendingNostrRelays || pendingNostrRelays === nostrRelays - } android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - + + + Nested SegWit (P2SH-WPKH) + + {activeAddressType === 'segwit-compatible' && ( + )} + + + + {/* Network Providers */} + toggleSection('advanced')} + styles={styles} + theme={theme}> + + + + Mainnet + + + + + Testnet3 + + + + + + - Save Relays + {isTestnet ? 'Testnet Mode' : 'Mainnet Mode'} - - { + setIsApiInfoVisible(true); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + Change Provider? + + + )} + + { - const fetchedRelays = await getNostrRelays(true); - const relaysCSV = fetchedRelays.join(','); - const relaysForDisplay = relaysCSV.split(',').join('\n'); - setPendingNostrRelays(relaysForDisplay); - }} - android_ripple={{color: 'rgba(0,0,0,0.1)'}}> - - - Defaults - - + styles.apiNetworkDescription, + isTestnet + ? styles.apiNetworkDescriptionTestnet + : styles.apiNetworkDescriptionMainnet, + ]}> + {isTestnet + ? 'Testnet Provider is restricted to mempool.space/testnet' + : 'Mainnet Providers are customizable.'} + + + + + {!isTestnet && ( + { + saveAPI(pendingAPI); + }} + disabled={ + isAPISaving || !pendingAPI || pendingAPI === baseAPI + } + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + + {isAPISaving ? 'Verifying...' : 'Verify & Save'} + + + + )} + {!isTestnet && ( + { + resetAPI(); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + Defaults + + + )} - )} + {hasNostr && ( + toggleSection('nostr')} + styles={styles} + theme={theme}> + + Nostr Relay Configuration + + Configure Nostr relays for device pairing and transaction + signing. Enter relay URLs, one per line or comma-separated + (wss://...). + + + + + { + try { + const relays = pendingNostrRelays + .split(/[,\n]/) + .map(r => r.trim()) + .filter(Boolean); + if (relays.length === 0) { + Alert.alert( + 'Error', + 'Please enter at least one relay URL', + ); + return; + } + const invalid = relays.find( + r => !r.startsWith('wss://') && !r.startsWith('ws://'), + ); + if (invalid) { + Alert.alert( + 'Error', + `Invalid relay URL: ${invalid}\nRelay URLs must start with wss:// or ws://`, + ); + return; + } + const relaysCSV = relays.join(','); + appConfigRepository.set('nostr_relays', relaysCSV); + setNostrRelays(relaysCSV); + Alert.alert( + 'Success', + 'Nostr relays saved successfully!', + ); + } catch (error) { + dbg('Error saving Nostr relays:', error); + Alert.alert('Error', 'Failed to save Nostr relays'); + } + }} + disabled={ + !pendingNostrRelays || pendingNostrRelays === nostrRelays + } + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + + Save Relays + + + + { + const fetchedRelays = await getNostrRelays(true); + const relaysCSV = fetchedRelays.join(','); + const relaysForDisplay = relaysCSV.split(',').join('\n'); + setPendingNostrRelays(relaysForDisplay); + }} + android_ripple={{color: 'rgba(0,0,0,0.1)'}}> + + + Defaults + + + + + )} {/* Tabs: Wallet, UTXO, PSBT, Playground */} - toggleSection('wallet')} - styles={styles} - theme={theme}> - - Show the Wallet tab in the tab - bar. This tab shows your balance and send/receive. On by default. - - - Hide Wallet tab - setShowWalletTab(value)} - value={showWalletTab} - /> - Show Wallet tab - - - toggleSection('utxos')} - styles={styles} - theme={theme}> - - Show the UTXOs tab in the tab - bar. This tab lists your unspent outputs (date, output, address, - value in BTC and fiat). Off by default. - - - Hide UTXOs tab - setShowUtxosTab(value)} - value={showUtxosTab} - /> - Show UTXOs tab - - - toggleSection('psbt')} - styles={styles} - theme={theme}> - - Show the PSBT tab in the tab - bar. This tab is for signing Partially Signed Bitcoin Transactions. - Off by default. - - - Hide PSBT tab - setShowPsbtTab(value)} - value={showPsbtTab} - /> - Show PSBT tab - - - {activeNetwork === 'mainnet' && ( toggleSection('mempoolPlayground')} + title="Wallet" + isExpanded={expandedSections.wallet} + onToggle={() => toggleSection('wallet')} styles={styles} theme={theme}> - Show the Play tab in the tab - bar. This tab is a mempool playground for utility APIs (mainnet - only). + Show the Wallet tab in the + tab bar. This tab shows your balance and send/receive. On by + default. - Hide Play tab + Hide Wallet tab setShowMempoolPlayground(value)} - value={showMempoolPlayground} + onValueChange={value => setShowWalletTab(value)} + value={showWalletTab} /> - Show Play tab + Show Wallet tab - )} - {/* Dev Debug - Only visible on Android if enabled via build number clicks */} - {Platform.OS === 'android' && devDebugEnabled && ( toggleSection('devDebug')} + title="UTXOs" + isExpanded={expandedSections.utxos} + onToggle={() => toggleSection('utxos')} styles={styles} theme={theme}> - - - ⚠️ Developers Only + + Show the UTXOs tab in the tab + bar. This tab lists your unspent outputs (date, output, address, + value in BTC and fiat). Off by default. + + + Hide UTXOs tab + setShowUtxosTab(value)} + value={showUtxosTab} + /> + Show UTXOs tab + + + toggleSection('psbt')} + styles={styles} + theme={theme}> + + Show the PSBT tab in the tab + bar. This tab is for signing Partially Signed Bitcoin + Transactions. Off by default. + + + Hide PSBT tab + setShowPsbtTab(value)} + value={showPsbtTab} + /> + Show PSBT tab + + + {activeNetwork === 'mainnet' && ( + toggleSection('mempoolPlayground')} + styles={styles} + theme={theme}> + + Show the Play tab in the + tab bar. This tab is a mempool playground for utility APIs + (mainnet only). - + Hide Play tab + setShowMempoolPlayground(value)} + value={showMempoolPlayground} + /> + Show Play tab + + + )} + {/* Dev Debug - Only visible on Android if enabled via build number clicks */} + {Platform.OS === 'android' && devDebugEnabled && ( + toggleSection('devDebug')} + styles={styles} + theme={theme}> + - Debug logs may contain sensitive information. Use only on - trusted devices, and only if you know what you are doing. View - detailed logs in logcat (Android only, requires USB debugging). - Session-only setting - resets on app restart. - - - {/* Enhanced Debug Logging Card */} - - - Debug Logging - - + ⚠️ Developers Only + + + Debug logs may contain sensitive information. Use only on + trusted devices, and only if you know what you are doing. View + detailed logs in logcat (Android only, requires USB + debugging). Session-only setting - resets on app restart. + + + {/* Enhanced Debug Logging Card */} + + + Debug Logging + + + + {debugLoggingEnabled ? 'Active' : 'Inactive'} + + + + + Enable Logging + - - {debugLoggingEnabled ? 'Active' : 'Inactive'} - - - Enable Logging - - - - {/* Enhanced Disable Dev Mode Button */} - { - Alert.alert( - 'Disable Dev Mode', - 'Are you sure you want to hide the Dev Debug section? You can enable it again by clicking the build number 7 times.', - [ - { - text: 'Cancel', - style: 'cancel', - }, - { - text: 'Disable', - style: 'destructive', - onPress: () => { - setDevDebugEnabled(false); - EncryptedStorage.setItem( - 'devDebugEnabled', - 'false', - ).catch(error => { - dbg('Error saving devDebugEnabled:', error); - }); - Toast.show({ - type: 'info', - text1: 'Dev Mode Disabled', - text2: 'Dev Debug section is now hidden', - position: 'top', - }); + {/* Enhanced Disable Dev Mode Button */} + { + Alert.alert( + 'Disable Dev Mode', + 'Are you sure you want to hide the Dev Debug section? You can enable it again by clicking the build number 7 times.', + [ + { + text: 'Cancel', + style: 'cancel', }, - }, - ], - ); - }} - android_ripple={{color: 'rgba(255,255,255,0.2)'}}> - - Disable Dev Mode + { + text: 'Disable', + style: 'destructive', + onPress: () => { + setDevDebugEnabled(false); + EncryptedStorage.setItem( + 'devDebugEnabled', + 'false', + ).catch(error => { + dbg('Error saving devDebugEnabled:', error); + }); + Toast.show({ + type: 'info', + text1: 'Dev Mode Disabled', + text2: 'Dev Debug section is now hidden', + position: 'top', + }); + }, + }, + ], + ); + }} + android_ripple={{color: 'rgba(255,255,255,0.2)'}}> + + Disable Dev Mode + + + + )} + {/* Font Testing - Development Only */} + {__DEV__ && ( + toggleSection('fontTesting')} + styles={styles} + theme={theme}> + + Visual font comparison tool to verify unified fonts across + platforms. This section only appears in development builds. Note + that rendered fonts may differ from the actual fonts on your + device. Also, the font testing section is only visible in + development builds. - - - )} - {/* Font Testing - Development Only */} - {__DEV__ && ( + + + )} + + + {/* Info: Legal, About */} + toggleSection('fontTesting')} + title="Legal" + isExpanded={expandedSections.legal} + onToggle={() => toggleSection('legal')} styles={styles} theme={theme}> - Visual font comparison tool to verify unified fonts across - platforms. This section only appears in development builds. Note - that rendered fonts may differ from the actual fonts on your - device. Also, the font testing section is only visible in - development builds. + Terms of Service and Privacy Policy - - - )} - - - {/* Info: Legal, About */} - - toggleSection('legal')} - styles={styles} - theme={theme}> - - Terms of Service and Privacy Policy - - { - setLegalModalType('terms'); - setIsLegalModalVisible(true); - }}> - Read Terms of Use - - { - setLegalModalType('privacy'); - setIsLegalModalVisible(true); - }}> - Read Privacy Policy - - - toggleSection('about')} - styles={styles} - theme={theme}> - - App Version - {appVersion} - - - Build Number - { - // Only enable on Android (iOS prod builds don't support logs) - if (Platform.OS !== 'android') { - return; - } - - // Check if dev mode is already enabled - if (devDebugEnabled) { - Toast.show({ - type: 'info', - text1: 'Dev Mode Already Enabled', - text2: 'Developer debug section is already visible', - position: 'top', - visibilityTime: 2000, - }); - return; - } - - // Increment click count - buildNumberClickCountRef.current += 1; - const currentCount = buildNumberClickCountRef.current; - setBuildNumberClickCount(currentCount); + setLegalModalType('terms'); + setIsLegalModalVisible(true); + }}> + Read Terms of Use + + { + setLegalModalType('privacy'); + setIsLegalModalVisible(true); + }}> + Read Privacy Policy + + + toggleSection('about')} + styles={styles} + theme={theme}> + + App Version + {appVersion} + + + Build Number + { + // Only enable on Android (iOS prod builds don't support logs) + if (Platform.OS !== 'android') { + return; + } - // Clear existing timeout - if (buildNumberClickTimeoutRef.current) { - clearTimeout(buildNumberClickTimeoutRef.current); - } - // Reset count after 3 seconds of inactivity - buildNumberClickTimeoutRef.current = setTimeout(() => { - buildNumberClickCountRef.current = 0; - setBuildNumberClickCount(0); - }, 3000); + // Check if dev mode is already enabled + if (devDebugEnabled) { + Toast.show({ + type: 'info', + text1: 'Dev Mode Already Enabled', + text2: 'Developer debug section is already visible', + position: 'top', + visibilityTime: 2000, + }); + return; + } - // Show progress toast starting from 2 clicks - if (currentCount >= 2 && currentCount < 7) { - const stepsRemaining = 7 - currentCount; - Toast.show({ - type: 'info', - text1: `You're now ${stepsRemaining} step${ - stepsRemaining > 1 ? 's' : '' - } to enable dev mode`, - position: 'top', - visibilityTime: 2000, - }); - } + // Increment click count + buildNumberClickCountRef.current += 1; + const currentCount = buildNumberClickCountRef.current; + setBuildNumberClickCount(currentCount); - // Check if we've reached 7 clicks - if (currentCount >= 7) { - // Enable dev debug mode - setDevDebugEnabled(true); - EncryptedStorage.setItem('devDebugEnabled', 'true').catch( - error => { - dbg('Error saving devDebugEnabled:', error); - }, - ); - // Open devDebug section and close about section - setExpandedSections(prev => ({ - ...prev, - about: false, - devDebug: true, - })); - // Show toast - Toast.show({ - type: 'success', - text1: 'Dev Mode Enabled', - text2: 'Developer debug section is now visible', - position: 'top', - }); - // Reset counter - buildNumberClickCountRef.current = 0; - setBuildNumberClickCount(0); + // Clear existing timeout if (buildNumberClickTimeoutRef.current) { clearTimeout(buildNumberClickTimeoutRef.current); - buildNumberClickTimeoutRef.current = null; } - } - }} - style={styles.buildNumberContainer}> - {buildNumberClickCount >= 2 ? ( - - {buildNumber} - - ) : ( - {buildNumber} - )} - - - - Make sure that your wallet keyshares devices are running the latest - version for optimal compatibility and security. - - - + // Reset count after 3 seconds of inactivity + buildNumberClickTimeoutRef.current = setTimeout(() => { + buildNumberClickCountRef.current = 0; + setBuildNumberClickCount(0); + }, 3000); + + // Show progress toast starting from 2 clicks + if (currentCount >= 2 && currentCount < 7) { + const stepsRemaining = 7 - currentCount; + Toast.show({ + type: 'info', + text1: `You're now ${stepsRemaining} step${ + stepsRemaining > 1 ? 's' : '' + } to enable dev mode`, + position: 'top', + visibilityTime: 2000, + }); + } + // Check if we've reached 7 clicks + if (currentCount >= 7) { + // Enable dev debug mode + setDevDebugEnabled(true); + EncryptedStorage.setItem('devDebugEnabled', 'true').catch( + error => { + dbg('Error saving devDebugEnabled:', error); + }, + ); + // Open devDebug section and close about section + setExpandedSections(prev => ({ + ...prev, + about: false, + devDebug: true, + })); + // Show toast + Toast.show({ + type: 'success', + text1: 'Dev Mode Enabled', + text2: 'Developer debug section is now visible', + position: 'top', + }); + // Reset counter + buildNumberClickCountRef.current = 0; + setBuildNumberClickCount(0); + if (buildNumberClickTimeoutRef.current) { + clearTimeout(buildNumberClickTimeoutRef.current); + buildNumberClickTimeoutRef.current = null; + } + } + }} + style={styles.buildNumberContainer}> + {buildNumberClickCount >= 2 ? ( + + + {buildNumber} + + + ) : ( + {buildNumber} + )} + + + + Make sure that your wallet keyshares devices are running the + latest version for optimal compatibility and security. + + + {/* Modals */} + setIsBackupModalVisible(false)} diff --git a/services/Database.ts b/services/Database.ts new file mode 100644 index 00000000..6721bdc1 --- /dev/null +++ b/services/Database.ts @@ -0,0 +1,357 @@ +/** + * Database.ts — SQLite singleton for Bold Wallet. + * + * Replaces the file-based LocalCache (react-native-fs) with a single + * WAL-mode SQLite database as the authoritative local store. + * + * EncryptedStorage (keyshare, btcPub, etc.) is NOT touched here. + */ +import {open, type DB, type Scalar} from '@op-engineering/op-sqlite'; +import {dbg} from '../utils'; + +// --------------------------------------------------------------------------- +// DDL — executed once on every open() call (all statements are IF NOT EXISTS) +// --------------------------------------------------------------------------- +const SCHEMA_STATEMENTS = [ + // ── App Configuration (replaces all single-value LocalCache preference keys) ─ + `CREATE TABLE IF NOT EXISTS app_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000) + )`, + + // ── Network Providers (replaces api, api_mainnet, api_testnet3 keys) ───────── + `CREATE TABLE IF NOT EXISTS network_providers ( + network TEXT NOT NULL, + api_url TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL, + PRIMARY KEY (network, api_url) + )`, + + // ── Nostr Relays (replaces nostr_relays CSV key) ────────────────────────────── + `CREATE TABLE IF NOT EXISTS nostr_relays ( + url TEXT PRIMARY KEY, + is_active INTEGER NOT NULL DEFAULT 1, + added_at INTEGER NOT NULL + )`, + + // ── HD Wallet State (replaces hd_*__ keys) ───────────── + `CREATE TABLE IF NOT EXISTS hd_state ( + network TEXT NOT NULL, + address_type TEXT NOT NULL, + external_index INTEGER NOT NULL DEFAULT 0, + change_index INTEGER NOT NULL DEFAULT 0, + max_used_external INTEGER NOT NULL DEFAULT 0, + restore_done INTEGER NOT NULL DEFAULT 0, + discovery_status TEXT, + discovery_last_at INTEGER, + PRIMARY KEY (network, address_type) + )`, + + // ── Wallet Addresses (new — previously recomputed from keyshare every time) ─── + `CREATE TABLE IF NOT EXISTS wallet_addresses ( + network TEXT NOT NULL, + address_type TEXT NOT NULL, + chain INTEGER NOT NULL, + idx INTEGER NOT NULL, + address TEXT NOT NULL, + is_used INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (network, address_type, chain, idx) + )`, + + `CREATE UNIQUE INDEX IF NOT EXISTS idx_wallet_addresses_addr + ON wallet_addresses (address)`, + + // ── Address Balances (replaces wallet_balance_* and aggregate_* keys) ───────── + `CREATE TABLE IF NOT EXISTS address_balances ( + address TEXT NOT NULL, + network TEXT NOT NULL, + balance_sats INTEGER NOT NULL DEFAULT 0, + pending_sats INTEGER NOT NULL DEFAULT 0, + has_nonzero INTEGER NOT NULL DEFAULT 0, + fetched_at INTEGER NOT NULL, + PRIMARY KEY (address, network) + )`, + + // ── UTXOs (new — previously only in MempoolClient in-memory cache) ─────────── + `CREATE TABLE IF NOT EXISTS utxos ( + txid TEXT NOT NULL, + vout INTEGER NOT NULL, + address TEXT NOT NULL, + network TEXT NOT NULL, + value_sats INTEGER NOT NULL, + script_pubkey TEXT, + derivation_path TEXT, + is_confirmed INTEGER NOT NULL DEFAULT 1, + block_height INTEGER, + block_time INTEGER, + fetched_at INTEGER NOT NULL, + PRIMARY KEY (txid, vout) + )`, + + `CREATE INDEX IF NOT EXISTS idx_utxos_address ON utxos (address, network)`, + `CREATE INDEX IF NOT EXISTS idx_utxos_network ON utxos (network)`, + + // ── Transactions (one row per txid per network) ─────────────────────────────── + `CREATE TABLE IF NOT EXISTS transactions ( + txid TEXT NOT NULL, + network TEXT NOT NULL, + block_height INTEGER, + block_hash TEXT, + block_time INTEGER, + is_confirmed INTEGER NOT NULL DEFAULT 0, + fee_sats INTEGER, + size INTEGER, + weight INTEGER, + version INTEGER, + locktime INTEGER, + raw_json TEXT NOT NULL, + fetched_at INTEGER NOT NULL, + PRIMARY KEY (txid, network) + )`, + + `CREATE INDEX IF NOT EXISTS idx_txns_network_time + ON transactions (network, block_time DESC)`, + + // ── Transaction ↔ Address mapping (replaces per-address tx cache keys) ──────── + `CREATE TABLE IF NOT EXISTS transaction_addresses ( + txid TEXT NOT NULL, + network TEXT NOT NULL, + address TEXT NOT NULL, + net_sats INTEGER, + PRIMARY KEY (txid, network, address) + )`, + + `CREATE INDEX IF NOT EXISTS idx_txaddr_address + ON transaction_addresses (address, network)`, + + // ── Transaction Inputs ──────────────────────────────────────────────────────── + `CREATE TABLE IF NOT EXISTS tx_inputs ( + txid TEXT NOT NULL, + network TEXT NOT NULL, + vin_idx INTEGER NOT NULL, + prev_txid TEXT, + prev_vout INTEGER, + address TEXT, + value_sats INTEGER, + sequence INTEGER, + PRIMARY KEY (txid, network, vin_idx) + )`, + + // ── Transaction Outputs ─────────────────────────────────────────────────────── + `CREATE TABLE IF NOT EXISTS tx_outputs ( + txid TEXT NOT NULL, + network TEXT NOT NULL, + vout_idx INTEGER NOT NULL, + address TEXT, + value_sats INTEGER NOT NULL, + script_pubkey TEXT, + is_spent INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (txid, network, vout_idx) + )`, + + // ── Pending (mempool) Transactions ─────────────────────────────────────────── + `CREATE TABLE IF NOT EXISTS pending_transactions ( + txid TEXT NOT NULL, + network TEXT NOT NULL, + address TEXT NOT NULL, + raw_json TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (txid, network, address) + )`, + + // ── Bitcoin Price (replaces 'price' and 'historical_price_*' keys) ─────────── + `CREATE TABLE IF NOT EXISTS price_rates ( + currency TEXT NOT NULL, + day_timestamp INTEGER NOT NULL, + rate REAL NOT NULL, + fetched_at INTEGER NOT NULL, + PRIMARY KEY (currency, day_timestamp) + )`, + + // ── Sync Metadata (new — pagination cursors + per-address sync state) ───────── + `CREATE TABLE IF NOT EXISTS sync_metadata ( + entity_type TEXT NOT NULL, + entity_key TEXT NOT NULL, + cursor TEXT, + last_synced_at INTEGER, + sync_status TEXT, + extra_json TEXT, + PRIMARY KEY (entity_type, entity_key) + )`, +]; + +// --------------------------------------------------------------------------- +// DatabaseService +// --------------------------------------------------------------------------- +export type QueryResult = { + rows: Record[]; + rowsAffected: number; + insertId?: number; +}; + +class DatabaseService { + private _db: DB | null = null; + private _opening: Promise | null = null; + + /** Open (or reuse) the database and run schema migrations. */ + async open(): Promise { + if (this._db) return; + if (this._opening) return this._opening; + + this._opening = (async () => { + try { + dbg('DatabaseService: opening bold_wallet.db'); + this._db = open({name: 'bold_wallet.db'}); + + // Enable WAL for concurrent read/write performance + this._db.executeSync('PRAGMA journal_mode=WAL'); + this._db.executeSync('PRAGMA foreign_keys=ON'); + this._db.executeSync('PRAGMA synchronous=NORMAL'); + + // Run schema (all IF NOT EXISTS — safe on every startup) + for (const stmt of SCHEMA_STATEMENTS) { + this._db.executeSync(stmt); + } + + // Column migrations — ALTER TABLE ADD COLUMN fails silently if the + // column already exists (SQLite throws "duplicate column name"). + // Wrap each one individually so a single failure never aborts the rest. + const COLUMN_MIGRATIONS = [ + // v3.1: store block_time alongside block_height in utxos so the UI + // can display human-readable timestamps for confirmed UTXOs loaded + // from the DB (without this the row always showed "Unconfirmed"). + 'ALTER TABLE utxos ADD COLUMN block_time INTEGER', + ]; + for (const stmt of COLUMN_MIGRATIONS) { + try { + this._db.executeSync(stmt); + } catch { + // Column already present — safe to ignore. + } + } + + dbg('DatabaseService: schema ready'); + } catch (err) { + dbg('DatabaseService: open error — attempting recovery', err); + // If schema migration fails, wipe and recreate. + // Keyshare is in EncryptedStorage so wallet is recoverable. + try { + if (this._db) { + this._db.close(); + this._db = null; + } + this._db = open({name: 'bold_wallet.db'}); + this._db.executeSync('PRAGMA journal_mode=WAL'); + for (const stmt of SCHEMA_STATEMENTS) { + this._db.executeSync(stmt); + } + dbg('DatabaseService: recovery succeeded'); + } catch (recoveryErr) { + dbg('DatabaseService: recovery failed', recoveryErr); + throw recoveryErr; + } + } finally { + this._opening = null; + } + })(); + + return this._opening; + } + + close(): void { + if (this._db) { + this._db.close(); + this._db = null; + } + } + + private get db(): DB { + if (!this._db) { + throw new Error('DatabaseService: not open — call open() first'); + } + return this._db; + } + + /** Run a single SQL statement, returning rows and affected count. */ + execute(sql: string, params: unknown[] = []): QueryResult { + const result = this.db.executeSync(sql, params as Scalar[]); + const rows: Array> = result.rows ?? []; + return { + rows, + rowsAffected: result.rowsAffected ?? 0, + insertId: result.insertId, + }; + } + + /** Run multiple statements in a single atomic transaction. */ + transaction(fn: (svc: DatabaseService) => void): void { + this.db.executeSync('BEGIN'); + try { + fn(this); + this.db.executeSync('COMMIT'); + } catch (err) { + this.db.executeSync('ROLLBACK'); + throw err; + } + } + + /** + * Clear all wallet-derived data tables (called on keyshare import/reset). + * Preserves app_config, network_providers, nostr_relays. + */ + clearWalletData(): void { + this.transaction(tx => { + for (const table of [ + 'hd_state', + 'wallet_addresses', + 'address_balances', + 'utxos', + 'transactions', + 'transaction_addresses', + 'tx_inputs', + 'tx_outputs', + 'pending_transactions', + 'sync_metadata', + ]) { + tx.execute(`DELETE FROM ${table}`); + } + }); + dbg('DatabaseService: wallet data cleared'); + } + + /** + * Clear fetched/cached wallet data while preserving HD discovery state. + * + * Used for network-switch, address-type switch, and "Clear Cache" so that: + * • hd_state (externalIndex, changeIndex, restoreDone) is kept as a + * baseline for the next discoverHdIndexesForNetwork run. If discovery + * fails the old correct indexes are still in DB, not replaced by 0. + * • wallet_addresses is kept so WalletHome can derive the current receive + * address immediately while the fresh API fetch completes. + * + * For full wallet reset (delete / new import) use clearWalletData() instead. + */ + clearWalletCacheData(): void { + this.transaction(tx => { + for (const table of [ + 'address_balances', + 'utxos', + 'transactions', + 'transaction_addresses', + 'tx_inputs', + 'tx_outputs', + 'pending_transactions', + 'sync_metadata', + ]) { + tx.execute(`DELETE FROM ${table}`); + } + }); + dbg('DatabaseService: wallet cache data cleared (hd_state preserved)'); + } +} + +// Singleton export +const database = new DatabaseService(); +export default database; diff --git a/services/HdIndexService.ts b/services/HdIndexService.ts new file mode 100644 index 00000000..1f33b9b3 --- /dev/null +++ b/services/HdIndexService.ts @@ -0,0 +1,77 @@ +/** + * HD wallet index persistence: external (receive) and internal (change) chain indexes. + * + * Keys are scoped by network and addressType so switching network/type keeps separate indexes. + * + * Migration: previously backed by LocalCache (file-based). Now backed by SQLite via + * WalletRepository. The public API is synchronous to avoid cascading async changes + * throughout the call-sites. WalletRepository.execute() is synchronous (op-sqlite). + * + * NOTE: All functions remain async-compatible (returning Promise) so existing call-sites + * that use `await` require no changes. + */ +import walletRepository from './repositories/WalletRepository'; +import {dbg} from '../utils'; + +export function getExternalIndex( + network: string, + addressType: string, +): Promise { + return Promise.resolve(walletRepository.getExternalIndex(network, addressType)); +} + +export function setExternalIndex( + network: string, + addressType: string, + value: number, +): Promise { + walletRepository.setExternalIndex(network, addressType, value); + dbg('HdIndexService: setExternalIndex', {network, addressType, value}); + return Promise.resolve(); +} + +export function getChangeIndex( + network: string, + addressType: string, +): Promise { + return Promise.resolve(walletRepository.getChangeIndex(network, addressType)); +} + +export function setChangeIndex( + network: string, + addressType: string, + value: number, +): Promise { + walletRepository.setChangeIndex(network, addressType, value); + dbg('HdIndexService: setChangeIndex', {network, addressType, value}); + return Promise.resolve(); +} + +export function getMaxUsedExternal( + network: string, + addressType: string, +): Promise { + return Promise.resolve(walletRepository.getMaxUsedExternal(network, addressType)); +} + +export function setMaxUsedExternal( + network: string, + addressType: string, + value: number, +): Promise { + walletRepository.setMaxUsedExternal(network, addressType, value); + return Promise.resolve(); +} + +/** + * Call only after a send transaction has been successfully broadcast. + * Increments the change index so the next send uses a fresh internal address. + */ +export function incrementChangeIndexAfterSend( + network: string, + addressType: string, +): Promise { + const next = walletRepository.incrementChangeIndex(network, addressType); + dbg('HdIndexService: incrementChangeIndexAfterSend', {network, addressType, next}); + return Promise.resolve(); +} diff --git a/services/HistoricalPriceService.ts b/services/HistoricalPriceService.ts new file mode 100644 index 00000000..9746492a --- /dev/null +++ b/services/HistoricalPriceService.ts @@ -0,0 +1,118 @@ +/** + * HistoricalPriceService — persistent cache + fetch for BTC price at a given timestamp. + * Used by transaction list and details to show fiat at the time of the tx (not current rate). + * + * - Cache key: currency + timestamp rounded to UTC day (one rate per day per currency). + * - In-memory map for fast lookups; PriceRepository (SQLite) for persistence. + * - Fetches via MempoolClient (GET /api/v1/historical-price?currency=...×tamp=...). + */ + +import mempoolClient from './MempoolClient'; +import priceRepository, {toDayTimestamp} from './repositories/PriceRepository'; +import {dbg} from '../utils'; + +/** In-memory cache: "_" -> rate */ +const memoryCache = new Map(); + +function memKey(currency: string, timestampUnixSec: number): string { + return `${currency}_${toDayTimestamp(timestampUnixSec)}`; +} + +export interface HistoricalPriceResponse { + prices?: Array<{time?: number; [currency: string]: number | undefined}>; +} + +class HistoricalPriceService { + private static _instance: HistoricalPriceService; + + static getInstance(): HistoricalPriceService { + if (!HistoricalPriceService._instance) { + HistoricalPriceService._instance = new HistoricalPriceService(); + } + return HistoricalPriceService._instance; + } + + /** + * Returns the BTC rate (fiat per 1 BTC) at the given timestamp, or null if unavailable. + * Uses in-memory cache → SQLite → API. + */ + async getHistoricalRate( + currency: string, + timestampUnixSec: number, + baseApi: string, + ): Promise { + const key = memKey(currency, timestampUnixSec); + + // 1. In-memory cache + const mem = memoryCache.get(key); + if (mem != null && mem > 0) { + return mem; + } + + // 2. SQLite persistence + const persisted = priceRepository.getHistoricalRate(currency, timestampUnixSec); + if (persisted != null && persisted > 0) { + memoryCache.set(key, persisted); + return persisted; + } + + // 3. Fetch from API + const base = baseApi.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + const url = `${base}/api/v1/historical-price?currency=${encodeURIComponent( + currency, + )}×tamp=${Math.floor(timestampUnixSec)}`; + try { + const res = await mempoolClient.get(url); + if (!res.ok || !res.data?.prices?.length) { + return null; + } + const first = res.data.prices[0]; + const rate = first[currency] ?? first.USD ?? first.EUR; + if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) { + return null; + } + memoryCache.set(key, rate); + priceRepository.setHistoricalRate(currency, timestampUnixSec, rate); + return rate; + } catch (e) { + dbg('HistoricalPriceService: fetch failed', url.slice(-60), e); + return null; + } + } + + /** + * Sync get from in-memory cache only. Returns null if not cached. + */ + getCachedRateSync(currency: string, timestampUnixSec: number): number | null { + const key = memKey(currency, timestampUnixSec); + // Try memory first, then SQLite synchronously + const mem = memoryCache.get(key); + if (mem != null) return mem; + const db = priceRepository.getHistoricalRate(currency, timestampUnixSec); + if (db != null) { + memoryCache.set(key, db); + } + return db; + } + + /** Pre-warm in-memory cache from SQLite for the given currency × timestamps. */ + hydrateKeys(currency: string, timestampsUnixSec: number[]): void { + for (const ts of timestampsUnixSec) { + const key = memKey(currency, ts); + if (memoryCache.has(key)) continue; + const rate = priceRepository.getHistoricalRate(currency, ts); + if (rate != null && rate > 0) { + memoryCache.set(key, rate); + } + } + } +} + +export function getHistoricalRateKey( + currency: string, + timestampUnixSec: number, +): string { + return memKey(currency, timestampUnixSec); +} + +export default HistoricalPriceService.getInstance(); diff --git a/services/LocalCacheMigration.ts b/services/LocalCacheMigration.ts new file mode 100644 index 00000000..6eaca917 --- /dev/null +++ b/services/LocalCacheMigration.ts @@ -0,0 +1,264 @@ +/** + * LocalCacheMigration — one-time migration from the file-based LocalCache + * to the SQLite database on the first launch after the SQLite update. + * + * Strategy: + * 1. Check app_config for 'sqlite_migration_done'. If already set, return early. + * 2. For every known LocalCache key, read the value and insert it into the + * appropriate SQLite table using INSERT OR IGNORE / ON CONFLICT. + * 3. Write app_config 'sqlite_migration_done' = 'v1' when done. + * 4. LocalCache files are NOT deleted here (rollback safety during transition). + * Deletion happens in Phase 5 of the migration plan. + * + * This migration is idempotent — it can be run multiple times safely. + */ +import LocalCache from './LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from './repositories/AppConfigRepository'; +import walletRepository from './repositories/WalletRepository'; +import balanceRepository from './repositories/BalanceRepository'; +import transactionRepository, {type PendingTxData} from './repositories/TransactionRepository'; +import priceRepository from './repositories/PriceRepository'; +import database from './Database'; +import {dbg} from '../utils'; + +const MIGRATION_VERSION = 'v1'; + +// All network+addressType combinations to scan for HD index keys +const NETWORKS = ['mainnet', 'testnet3']; +const ADDRESS_TYPES = ['legacy', 'segwit-native', 'segwit-compatible']; + +// Mapping from old LocalCache key → new app_config key +const SIMPLE_KEY_MAP: Record = { + themeMode: CONFIG_KEYS.THEME_MODE, + currency: CONFIG_KEYS.CURRENCY, + balanceHidden: CONFIG_KEYS.BALANCE_HIDDEN, + hapticsEnabled: CONFIG_KEYS.HAPTICS_ENABLED, + feeStrategy: CONFIG_KEYS.FEE_STRATEGY, + addressType: CONFIG_KEYS.ADDRESS_TYPE, + currentAddress: CONFIG_KEYS.CURRENT_ADDRESS, + network: CONFIG_KEYS.NETWORK, + legacyWalletModalDoNotRemind: CONFIG_KEYS.LEGACY_WALLET_DO_NOT_REMIND, + mempool_playground_enabled: CONFIG_KEYS.TAB_MEMPOOL_ENABLED, + utxos_tab_enabled: CONFIG_KEYS.TAB_UTXOS_ENABLED, + psbt_tab_enabled: CONFIG_KEYS.TAB_PSBT_ENABLED, + wallet_tab_enabled: CONFIG_KEYS.TAB_WALLET_ENABLED, +}; + +export async function runMigrationIfNeeded(): Promise { + try { + // Already migrated? + const done = appConfigRepository.get(CONFIG_KEYS.SQLITE_MIGRATION_DONE); + if (done === MIGRATION_VERSION) { + dbg('LocalCacheMigration: already done, skipping'); + return; + } + + dbg('LocalCacheMigration: starting v1 migration from LocalCache → SQLite'); + const start = Date.now(); + + // ── 1. Simple preference keys → app_config ─────────────────────────── + for (const [oldKey, newKey] of Object.entries(SIMPLE_KEY_MAP)) { + try { + const value = await LocalCache.getItem(oldKey); + if (value != null) { + appConfigRepository.set(newKey, value); + dbg('LocalCacheMigration: migrated', oldKey, '→', newKey, '=', value); + } + } catch { + // non-fatal — continue with rest + } + } + + // ── 2. Network/API provider keys ───────────────────────────────────── + for (const net of NETWORKS) { + try { + const api = await LocalCache.getItem(`api_${net}`); + if (api) { + const isActive = (appConfigRepository.get(CONFIG_KEYS.NETWORK) ?? 'mainnet') === net ? 1 : 0; + database.execute( + `INSERT OR IGNORE INTO network_providers (network, api_url, is_active, updated_at) + VALUES (?, ?, ?, ?)`, + [net, api, isActive, Date.now()], + ); + dbg('LocalCacheMigration: migrated api_' + net, '=', api); + } + } catch { + // non-fatal + } + } + + // ── 3. Nostr relays (CSV string → nostr_relays table) ──────────────── + try { + const relaysCsv = await LocalCache.getItem('nostr_relays'); + if (relaysCsv) { + const relays = relaysCsv.split(',').map(r => r.trim()).filter(Boolean); + const now = Date.now(); + database.transaction(tx => { + for (const url of relays) { + tx.execute( + `INSERT OR IGNORE INTO nostr_relays (url, is_active, added_at) + VALUES (?, 1, ?)`, + [url, now], + ); + } + }); + dbg('LocalCacheMigration: migrated', relays.length, 'nostr relay(s)'); + } + } catch { + // non-fatal + } + + // ── 4. HD index keys → hd_state ────────────────────────────────────── + for (const net of NETWORKS) { + for (const addrType of ADDRESS_TYPES) { + try { + const extRaw = await LocalCache.getItem(`hd_external_index_${net}_${addrType}`); + const chgRaw = await LocalCache.getItem(`hd_change_index_${net}_${addrType}`); + const maxRaw = await LocalCache.getItem(`hd_max_used_external_${net}_${addrType}`); + const doneRaw = await LocalCache.getItem(`hd_restore_done_${net}_${addrType}`); + const statusRaw = await LocalCache.getItem(`hd_discovery_status_${net}_${addrType}`); + const lastAtRaw = await LocalCache.getItem(`hd_discovery_last_at_${net}_${addrType}`); + + if (extRaw != null || chgRaw != null || maxRaw != null) { + const externalIndex = extRaw != null ? parseInt(extRaw, 10) || 0 : 0; + const changeIndex = chgRaw != null ? parseInt(chgRaw, 10) || 0 : 0; + const maxUsed = maxRaw != null ? parseInt(maxRaw, 10) || 0 : 0; + const restoreDone = doneRaw === 'true' ? 1 : 0; + const discoveryStatus = statusRaw ?? null; + const discoveryLastAt = lastAtRaw != null ? parseInt(lastAtRaw, 10) || null : null; + + walletRepository.setHdState({ + network: net, + addressType: addrType, + externalIndex, + changeIndex, + maxUsedExternal: maxUsed, + restoreDone: restoreDone === 1, + discoveryStatus, + discoveryLastAt, + }); + dbg('LocalCacheMigration: migrated hd_state', net, addrType, { + externalIndex, changeIndex, maxUsed, + }); + } + } catch { + // non-fatal + } + } + } + + // ── 5. Per-address balance/transaction caches ───────────────────────── + // We attempt to enumerate by reading a likely set of address cache keys. + // In practice the migration only imports the current address' cache since + // we don't have a list of all known addresses at migration time. + // The sync layer will re-fetch and populate the rest on first launch. + try { + const currentAddr = appConfigRepository.get(CONFIG_KEYS.CURRENT_ADDRESS); + const net = appConfigRepository.get(CONFIG_KEYS.NETWORK) ?? 'mainnet'; + + if (currentAddr) { + // Balance + const balRaw = await LocalCache.getItem(`wallet_balance_${currentAddr}`); + if (balRaw) { + try { + const cached = JSON.parse(balRaw); + const sats = Math.round((parseFloat(cached.btc) || 0) * 100_000_000); + balanceRepository.setBalance({ + address: currentAddr, + network: net, + balanceSats: sats, + pendingSats: cached.pendingSats ?? 0, + hasNonzero: sats > 0, + fetchedAt: cached.timestamp ?? Date.now(), + }); + dbg('LocalCacheMigration: migrated balance for', currentAddr); + } catch { + // malformed cache — skip + } + } + + // Transactions + const txRaw = await LocalCache.getItem(`wallet_transactions_${currentAddr}`); + if (txRaw) { + try { + const cached: {transactions: unknown[]; timestamp: number} = JSON.parse(txRaw); + const fetchedAt = cached.timestamp ?? Date.now(); + for (const apiTx of (cached.transactions ?? [])) { + const tx = apiTx as Record; + const txid = tx.txid as string; + if (!txid) continue; + const status = tx.status as Record ?? {}; + transactionRepository.upsertTransaction( + { + txid, + network: net, + blockHeight: (status.block_height as number) ?? null, + blockHash: (status.block_hash as string) ?? null, + blockTime: (status.block_time as number) ?? null, + isConfirmed: status.confirmed === true, + feeSats: (tx.fee as number) ?? null, + size: (tx.size as number) ?? null, + weight: (tx.weight as number) ?? null, + version: (tx.version as number) ?? null, + locktime: (tx.locktime as number) ?? null, + rawJson: JSON.stringify(tx), + fetchedAt, + }, + [{txid, network: net, address: currentAddr, netSats: null}], + ); + } + dbg( + 'LocalCacheMigration: migrated', + (cached.transactions ?? []).length, + 'transactions for', + currentAddr, + ); + } catch { + // malformed cache — skip + } + } + + // Pending transactions + const pendingRaw = await LocalCache.getItem(`${currentAddr}-pendingTxs`); + if (pendingRaw) { + try { + const map = JSON.parse(pendingRaw) as Record; + transactionRepository.setPendingTxMap(currentAddr, net, map); + dbg('LocalCacheMigration: migrated pending txs for', currentAddr); + } catch { + // malformed cache — skip + } + } + } + } catch { + // non-fatal — network data will be re-fetched + } + + // ── 6. Current price → price_rates ─────────────────────────────────── + try { + const priceRaw = await LocalCache.getItem('price'); + if (priceRaw) { + const cached: {rates?: Record; rate?: number; price?: string; timestamp?: number} = + JSON.parse(priceRaw); + const rates = cached.rates ?? {}; + if (cached.rate) { + rates.USD = cached.rate; + } + if (Object.keys(rates).length) { + priceRepository.setCurrentRates(rates); + dbg('LocalCacheMigration: migrated price rates'); + } + } + } catch { + // non-fatal + } + + // ── Mark migration complete ─────────────────────────────────────────── + appConfigRepository.set(CONFIG_KEYS.SQLITE_MIGRATION_DONE, MIGRATION_VERSION); + dbg('LocalCacheMigration: completed in', Date.now() - start, 'ms'); + } catch (err) { + // Migration failure is non-fatal — app continues with empty SQLite tables + // and the sync layer will re-populate from the network. + dbg('LocalCacheMigration: unexpected error (non-fatal)', err); + } +} diff --git a/services/MempoolClient.ts b/services/MempoolClient.ts new file mode 100644 index 00000000..8dc39075 --- /dev/null +++ b/services/MempoolClient.ts @@ -0,0 +1,307 @@ +/** + * MempoolClient — centralized HTTP cache + request deduplication layer. + * + * All mempool.space (and compatible) API calls should go through this module + * instead of calling fetch() directly. The two guarantees it provides: + * + * 1. CACHE — A successful (HTTP 200) response is reused for subsequent + * identical requests within the TTL window. Failed responses are never + * cached, so a transient error never poisons the cache. + * + * 2. DEDUP — If two callers request the same URL while the first request is + * still in-flight, both await the same Promise. Only one HTTP request + * is made. + * + * TTL defaults: + * - /address/… 5 s (balance, UTXOs, transactions — default) + * - /v1/fees/recommended 30 s (updates ~every block, no need to hammer) + * - /v1/prices 60 s (price ticks slowly relative to UI refresh) + * - /tx/{txid} 5 min (confirmed tx content is immutable) + * - /v1/historical-price 7 d (past-date price is immutable) + * + * Usage: + * import mempoolClient from './MempoolClient'; + * const res = await mempoolClient.get(url); + * if (!res.ok) { ... handle error ... } + * const utxos = res.data; + */ + +import {dbg} from '../utils'; + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +/** Hard cap applied to every outgoing HTTP request. */ +const FETCH_TIMEOUT_MS = 10_000; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Returns an AbortSignal that fires when EITHER of the two input signals fires. + * Falls back gracefully when one of them is undefined. + * + * React Native does not yet expose AbortSignal.any(), so we wire them manually. + */ +function combineSignals( + callerSignal: AbortSignal | undefined, + timeoutSignal: AbortSignal, +): AbortSignal { + if (!callerSignal) { + return timeoutSignal; + } + const combined = new AbortController(); + + if (callerSignal.aborted || timeoutSignal.aborted) { + combined.abort(); + return combined.signal; + } + + callerSignal.addEventListener('abort', () => combined.abort(), {once: true}); + timeoutSignal.addEventListener('abort', () => combined.abort(), {once: true}); + return combined.signal; +} + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface MempoolResponse { + /** true when the HTTP response was 2xx (or served from cache). */ + ok: boolean; + /** HTTP status code of the original response (200 for cache hits). */ + status: number; + /** Parsed JSON body. Only meaningful when ok === true. */ + data: T; +} + +interface CacheEntry { + data: unknown; + expiresAt: number; // epoch ms +} + +// --------------------------------------------------------------------------- +// Per-endpoint TTL configuration +// --------------------------------------------------------------------------- + +/** Default TTL — applies to balance, UTXO, and transaction endpoints. */ +const DEFAULT_TTL_MS = 5_000; // 5 s + +/** + * URL pattern → TTL overrides (evaluated in order, first match wins). + * Keep this list short and specific. + */ +const TTL_RULES: ReadonlyArray<[RegExp, number]> = [ + // Confirmed tx content is immutable — long TTL, rare re-fetch. + [/\/tx\/[a-fA-F0-9]{64}$/, 300_000], + // Fee rates refresh on each block (~10 min); 30 s is responsive without hammering. + [/\/v1\/fees\/recommended/, 30_000], + // BTC price endpoint — slow-moving relative to UI refresh rate. + [/\/v1\/prices/, 60_000], + // Historical price is immutable (past date); cache 7 days. + [/\/v1\/historical-price\?/, 7 * 24 * 60 * 60 * 1000], +]; + +function ttlForUrl(url: string): number { + for (const [pattern, ttl] of TTL_RULES) { + if (pattern.test(url)) { + return ttl; + } + } + return DEFAULT_TTL_MS; +} + +// --------------------------------------------------------------------------- +// Cache key +// --------------------------------------------------------------------------- + +/** + * Builds a stable string key from URL + optional request body. + * Query parameters are already part of the URL string. + */ +function buildKey(url: string, body?: string): string { + return body ? `${url}\x00${body}` : url; +} + +// --------------------------------------------------------------------------- +// MempoolClient +// --------------------------------------------------------------------------- + +class MempoolClient { + private static _instance: MempoolClient; + + /** Successful response cache, keyed by buildKey(). */ + private readonly _cache = new Map(); + + /** + * In-flight request deduplication. + * A key present here means an HTTP request is already running for that URL. + * New callers receive the same Promise, so only one network round-trip runs. + */ + private readonly _inflight = new Map>>(); + + private constructor() {} + + static getInstance(): MempoolClient { + if (!MempoolClient._instance) { + MempoolClient._instance = new MempoolClient(); + } + return MempoolClient._instance; + } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + /** + * Performs a GET (or POST) request with caching and in-flight deduplication. + * + * @param url Full URL including query parameters. + * @param init Standard RequestInit; pass `signal` for abort support. + * Optionally pass `ttl` (ms) to override the default TTL. + * @returns MempoolResponse — never throws for HTTP-level errors; + * rejects only on network-level failures (offline, abort). + */ + async get( + url: string, + init?: RequestInit & {ttl?: number}, + ): Promise> { + const bodyStr = + init?.body != null ? String(init.body) : undefined; + const key = buildKey(url, bodyStr); + const now = Date.now(); + + // 1. Serve from cache if still fresh ----------------------------------- + const cached = this._cache.get(key); + if (cached && cached.expiresAt > now) { + dbg('MempoolClient: cache hit', url.slice(-80)); + return {ok: true, status: 200, data: cached.data as T}; + } + + // 2. Attach to an in-flight request if one exists ---------------------- + const existing = this._inflight.get(key); + if (existing) { + dbg('MempoolClient: dedup in-flight', url.slice(-80)); + return existing as Promise>; + } + + // 3. Issue a new request ----------------------------------------------- + const ttl = init?.ttl ?? ttlForUrl(url); + + // Strip the custom `ttl` field so it is not forwarded to fetch(). + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {ttl: _ttl, signal: callerSignal, ...restInit} = (init ?? {}) as RequestInit & {ttl?: number}; + + const promise = (async (): Promise> => { + // Create a per-request timeout controller and combine with caller's signal. + const timeoutController = new AbortController(); + const timeoutId = setTimeout( + () => timeoutController.abort(), + FETCH_TIMEOUT_MS, + ); + const signal = combineSignals(callerSignal as AbortSignal | undefined, timeoutController.signal); + + try { + const res = await fetch(url, {...restInit, signal}); + + if (res.ok) { + const data = (await res.json()) as unknown; + this._cache.set(key, {data, expiresAt: Date.now() + ttl}); + dbg( + 'MempoolClient: cached', + url.slice(-80), + `(ttl ${ttl / 1000}s)`, + ); + return {ok: true, status: res.status, data}; + } + + // Non-2xx: do NOT cache — transient errors must not persist. + dbg('MempoolClient: non-ok response', res.status, url.slice(-80)); + return {ok: false, status: res.status, data: null as unknown}; + } catch (err) { + // Network error, timeout, or abort: propagate so callers can handle it. + dbg('MempoolClient: fetch error', url.slice(-80), err); + throw err; + } finally { + clearTimeout(timeoutId); + // Always remove the in-flight entry so future callers get a fresh attempt. + this._inflight.delete(key); + } + })(); + + this._inflight.set(key, promise); + return promise as Promise>; + } + + // ------------------------------------------------------------------------- + // Cache management + // ------------------------------------------------------------------------- + + /** + * Removes all cache entries whose key begins with `urlPrefix`. + * Useful after a transaction is broadcast to immediately allow fresh UTXO + * and balance data for a specific address. + * + * Example: + * mempoolClient.invalidate(`${apiBase}/api/address/${address}`); + */ + invalidate(urlPrefix: string): void { + let count = 0; + for (const key of this._cache.keys()) { + if (key.startsWith(urlPrefix)) { + this._cache.delete(key); + count++; + } + } + if (count > 0) { + dbg('MempoolClient: invalidated', count, 'entries matching', urlPrefix); + } + } + + /** + * Clears the entire response cache. + * Call on network switch, address-type change, or full wallet restore. + */ + invalidateAll(): void { + const count = this._cache.size; + this._cache.clear(); + dbg('MempoolClient: full cache clear —', count, 'entries removed'); + } + + /** + * Removes entries whose TTL has elapsed. + * The cache self-serves stale entries only within TTL, so calling prune() + * is optional — its sole purpose is to release Map memory in long sessions. + * Call on app foreground or after a large batch of address scans. + */ + prune(): void { + const now = Date.now(); + let count = 0; + for (const [key, entry] of this._cache) { + if (entry.expiresAt <= now) { + this._cache.delete(key); + count++; + } + } + if (count > 0) { + dbg('MempoolClient: pruned', count, 'expired entries'); + } + } + + // ------------------------------------------------------------------------- + // Diagnostics + // ------------------------------------------------------------------------- + + get cacheSize(): number { + return this._cache.size; + } + + get inflightCount(): number { + return this._inflight.size; + } +} + +export const mempoolClient = MempoolClient.getInstance(); +export default mempoolClient; diff --git a/services/WalletService.ts b/services/WalletService.ts index 69774745..48927891 100644 --- a/services/WalletService.ts +++ b/services/WalletService.ts @@ -1,15 +1,70 @@ import Big from 'big.js'; import {BBMTLibNativeModule} from '../native_modules'; -import {dbg, getDerivePathForNetwork, getMainnetAPIList, isLegacyWallet} from '../utils'; -import LocalCache from './LocalCache'; +import { + dbg, + getChangePath, + getMainnetAPIList, + getReceivePath, + GAP_LIMIT, + isLegacyWallet, +} from '../utils'; +import mempoolClient from './MempoolClient'; +import appConfigRepository, {CONFIG_KEYS} from './repositories/AppConfigRepository'; +import balanceRepository from './repositories/BalanceRepository'; +import transactionRepository from './repositories/TransactionRepository'; +import priceRepository from './repositories/PriceRepository'; +import walletRepository from './repositories/WalletRepository'; import EncryptedStorage from 'react-native-encrypted-storage'; -import { validate as validateBitcoinAddress } from 'bitcoin-address-validation'; +import { + getChangeIndex, + getExternalIndex, + getMaxUsedExternal, + incrementChangeIndexAfterSend as hdIncrementChangeIndexAfterSend, + setChangeIndex, + setExternalIndex, + setMaxUsedExternal, +} from './HdIndexService'; +import {validate as validateBitcoinAddress} from 'bitcoin-address-validation'; export interface WalletBalance { btc: string; usd: string; hasNonZeroBalance: boolean; timestamp: number; + /** Net mempool balance in satoshis across all HD addresses. + * Positive = incoming unconfirmed. Negative = outgoing unconfirmed. + * Absent on legacy cached entries — treat as 0. */ + pendingSats?: number; } + +/** In-scope HD address with derivation path and chain (for UTXO tab and multi-address send). */ +export interface HdAddressWithPath { + address: string; + derivationPath: string; + chain: 'receive' | 'change'; + index: number; +} + +/** Mempool.space UTXO item: txid, vout, value (sats), status. */ +export interface ApiUtxo { + txid: string; + vout: number; + value: number; + status?: { + confirmed: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + }; +} + +/** UTXO with HD context: address, derivation path, and chain (receive vs change). */ +export interface UtxoWithPath extends ApiUtxo { + address: string; + derivationPath: string; + chain: 'receive' | 'change'; + chainIndex: number; +} + export interface Transaction { txid: string; timestamp?: number; @@ -43,9 +98,14 @@ interface CachedTransactionData { export const waitMS = (ms = 2000) => new Promise(resolve => setTimeout(resolve, ms)); // Add validation functions (exported for address-for-network checks, e.g. QR scan) -export const validateBitcoinAddressEnhanced = (address: string, network: string = 'mainnet'): boolean => { +export const validateBitcoinAddressEnhanced = ( + address: string, + network: string = 'mainnet', +): boolean => { if (!address || typeof address !== 'string') { - dbg('WalletService: Bitcoin address validation failed - empty or invalid type'); + dbg( + 'WalletService: Bitcoin address validation failed - empty or invalid type', + ); return false; } try { @@ -60,19 +120,40 @@ export const validateBitcoinAddressEnhanced = (address: string, network: string // Check address type based on network if (isTestnet) { // Testnet addresses: m, n, 2, tb1 prefixes - if (!(address.startsWith('m') || address.startsWith('n') || - address.startsWith('2') || address.startsWith('tb1'))) { - dbg('WalletService: Bitcoin address validation failed - testnet address expected'); + if ( + !( + address.startsWith('m') || + address.startsWith('n') || + address.startsWith('2') || + address.startsWith('tb1') + ) + ) { + dbg( + 'WalletService: Bitcoin address validation failed - testnet address expected', + ); return false; } } else { // Mainnet addresses: 1, 3, bc1 prefixes - if (!(address.startsWith('1') || address.startsWith('3') || address.startsWith('bc1'))) { - dbg('WalletService: Bitcoin address validation failed - mainnet address expected'); + if ( + !( + address.startsWith('1') || + address.startsWith('3') || + address.startsWith('bc1') + ) + ) { + dbg( + 'WalletService: Bitcoin address validation failed - mainnet address expected', + ); return false; } } - dbg('WalletService: Bitcoin address validation passed:', address, 'for network:', network); + dbg( + 'WalletService: Bitcoin address validation passed:', + address, + 'for network:', + network, + ); return true; } catch (error) { dbg('WalletService: Bitcoin address validation error:', error); @@ -114,6 +195,15 @@ export class WalletService { private currentApiUrl: string = 'https://mempool.space/api'; private fetchInProgress: {[key: string]: boolean} = {}; private fetchTimeout: {[key: string]: NodeJS.Timeout} = {}; + // In-memory cache for derived HD address lists. Keyed by + // "___". + // Invalidated explicitly via invalidateAddressCache() whenever indexes change. + private hdAddressCache: Map = new Map(); + // Per-address UTXO result cache. Keyed by address string. + // Only empty results are used to short-circuit future fetches (TTL-gated). + // Addresses with UTXOs are always re-fetched so spent coins are detected. + private readonly UTXO_EMPTY_CACHE_TTL_MS = 30_000; // 30 s + private utxoEmptyCache: Map = new Map(); // address → fetchedAt timestamp private constructor() { // Don't auto-initialize, wait for explicit initialize call } @@ -134,37 +224,82 @@ export class WalletService { } } private async setBal(address: string, balance: WalletBalance) { - await LocalCache.setItem( - `wallet_balance_${address}`, - JSON.stringify({...balance, timestamp: balance.timestamp ?? Date.now()}), - ); + const sats = Math.round((parseFloat(balance.btc) || 0) * 100_000_000); + balanceRepository.setBalance({ + address, + network: this.currentNetwork, + balanceSats: sats, + pendingSats: balance.pendingSats ?? 0, + hasNonzero: balance.hasNonZeroBalance, + fetchedAt: balance.timestamp ?? Date.now(), + }); } private async setTxs(address: string, transactions: Transaction[]) { - await LocalCache.setItem( - `wallet_transactions_${address}`, - JSON.stringify({transactions, timestamp: Date.now()}), - ); + const now = Date.now(); + for (const tx of transactions) { + const status = tx.status ?? {}; + transactionRepository.upsertTransaction( + { + txid: tx.txid, + network: this.currentNetwork, + blockHeight: status.block_height ?? null, + blockHash: null, + blockTime: status.block_time ?? tx.timestamp ?? null, + isConfirmed: status.confirmed === true, + feeSats: tx.fee ?? null, + size: null, + weight: null, + version: null, + locktime: null, + rawJson: JSON.stringify(tx), + fetchedAt: now, + }, + [{txid: tx.txid, network: this.currentNetwork, address, netSats: null}], + ); + } } public async getBal(address: string): Promise { - const balance = await LocalCache.getItem(`wallet_balance_${address}`); - return JSON.parse( - balance || - '{"btc":"0.00000000","usd":"$0.00","hasNonZeroBalance":false,"timestamp":0}', - ); + const stored = balanceRepository.getBalance(address, this.currentNetwork); + if (!stored) { + return {btc: '0.00000000', usd: '$0.00', hasNonZeroBalance: false, timestamp: 0}; + } + return { + btc: (stored.balanceSats / 1e8).toFixed(8), + usd: '$0.00', + hasNonZeroBalance: stored.hasNonzero, + timestamp: stored.fetchedAt, + pendingSats: stored.pendingSats, + }; } public async getTxs(address: string): Promise { - const txs = await LocalCache.getItem(`wallet_transactions_${address}`); - return txs ? JSON.parse(txs) : {transactions: [], timestamp: 0}; + const txs = transactionRepository.getTransactionsForAddress(address, this.currentNetwork); + if (!txs.length) return {transactions: [], timestamp: 0}; + const transactions: Transaction[] = txs.map(r => { + try { + return JSON.parse(r.rawJson) as Transaction; + } catch { + return { + txid: r.txid, + timestamp: r.blockTime ?? undefined, + amount: 0, + fee: r.feeSats ?? 0, + status: {confirmed: r.isConfirmed, block_height: r.blockHeight ?? undefined, block_time: r.blockTime ?? undefined}, + type: 'receive', + address, + }; + } + }); + return {transactions, timestamp: Date.now()}; } private async setPrice(price: { price: string; rate: number; rates: {[key: string]: number}; }) { - await LocalCache.setItem( - 'price', - JSON.stringify({...price, timestamp: Date.now()}), - ); + priceRepository.setCurrentRates(price.rates); + if (!price.rates.USD && price.rate) { + priceRepository.setCurrentRate('USD', price.rate); + } } public async getCachePrice(): Promise<{ price: string; @@ -172,23 +307,26 @@ export class WalletService { rates: {[key: string]: number}; timestamp: number; }> { - const price = await LocalCache.getItem('price'); - return price - ? JSON.parse(price) + const cached = priceRepository.getCachedPrice('USD'); + return cached + ? cached : {price: '$0.00', rate: 0, rates: {}, timestamp: 0}; } private async getStoredState() { try { - const network = (await LocalCache.getItem('network')) || 'mainnet'; - const addressType = (await LocalCache.getItem('addressType')) || 'legacy'; - let api = await LocalCache.getItem('api'); + const network = appConfigRepository.get(CONFIG_KEYS.NETWORK) || 'mainnet'; + const addressType = appConfigRepository.get(CONFIG_KEYS.ADDRESS_TYPE) || 'legacy'; + let api = appConfigRepository.get(`api_${network}`); + if (!api) { + api = appConfigRepository.get('api'); + } if (!api) { api = network === 'mainnet' ? 'https://mempool.space/api' : 'https://mempool.space/testnet/api'; } - const address = await LocalCache.getItem('currentAddress'); + const address = appConfigRepository.get(CONFIG_KEYS.CURRENT_ADDRESS); return { network, addressType, @@ -208,16 +346,16 @@ export class WalletService { }) { try { if (state.network) { - await LocalCache.setItem('network', state.network); + appConfigRepository.set(CONFIG_KEYS.NETWORK, state.network); } if (state.addressType) { - await LocalCache.setItem('addressType', state.addressType); + appConfigRepository.set(CONFIG_KEYS.ADDRESS_TYPE, state.addressType); } if (state.api) { - await LocalCache.setItem('api', state.api); + appConfigRepository.set('api', state.api); } if (state.address) { - await LocalCache.setItem('currentAddress', state.address); + appConfigRepository.set(CONFIG_KEYS.CURRENT_ADDRESS, state.address); } dbg('WalletService: Saved state to storage:', state); } catch (error) { @@ -249,6 +387,651 @@ export class WalletService { } return WalletService.instance; } + + /** + * Returns the next change (internal chain) address for the given network and address type. + * Does not increment the change index; call incrementChangeIndexAfterSend() after successful broadcast. + */ + public async getNextChangeAddress( + network: string, + addressType: string, + ): Promise { + return (await this.getNextChangeAddressWithPath(network, addressType)).address; + } + + public async getNextChangeAddressWithPath( + network: string, + addressType: string, + ): Promise<{address: string; path: string}> { + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) throw new Error('No keyshare found'); + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + const changeIdx = await getChangeIndex(network, addressType); + const path = getChangePath(network, addressType, useLegacyPath, changeIdx); + const btcPub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const address = await BBMTLibNativeModule.btcAddress( + btcPub, + network, + addressType, + ); + dbg('WalletService: getNextChangeAddressWithPath', { + network, + addressType, + changeIdx, + address: address?.slice(0, 12) + '...', + path, + }); + return {address, path}; + } + + /** Call after a send has been successfully broadcast to advance the change index. */ + public async incrementChangeIndexAfterSend( + network: string, + addressType: string, + ): Promise { + await hdIncrementChangeIndexAfterSend(network, addressType); + this.invalidateAddressCache(network, addressType); + } + + /** + * Derive the next receive (external) address and persist it as current. + * Call when user requests "Get new address" to avoid address reuse. + */ + public async getNextReceiveAddress( + network: string, + addressType: string, + ): Promise { + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) throw new Error('No keyshare found'); + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + const nextIndex = (await getExternalIndex(network, addressType)) + 1; + await setExternalIndex(network, addressType, nextIndex); + const path = getReceivePath(network, addressType, useLegacyPath, nextIndex); + const btcPub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const newAddress = await BBMTLibNativeModule.btcAddress( + btcPub, + network, + addressType, + ); + await this.saveStoredState({address: newAddress}); + this.currentAddress = newAddress; + dbg('WalletService: getNextReceiveAddress', { + network, + addressType, + nextIndex, + }); + return newAddress; + } + + /** + * Fetches UTXOs for all in-scope HD addresses (receive + change) and tags each with its derivation path. + * Used by multi-path send flow and can be reused by UtxosScreen. + * @param network - 'mainnet' | 'testnet' + * @param addressType - e.g. 'segwit-native' + * @param apiUrl - base API URL (e.g. https://mempool.space/api) + * @param signal - optional AbortSignal for cancellation + */ + public async fetchUtxosWithPaths( + network: string, + addressType: string, + apiUrl: string, + signal?: AbortSignal, + ): Promise { + const baseUrl = apiUrl.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + const fullApiUrl = `${baseUrl}/api`; + const isTestnetApi = /\/testnet(\/|$)/.test(fullApiUrl); + + const addressesWithPaths = await this.getHdAddressesWithPaths( + network, + addressType || 'segwit-native', + ); + if (addressesWithPaths.length === 0) return []; + + const merged: UtxoWithPath[] = []; + const controller = signal ? undefined : new AbortController(); + const fetchSignal = signal ?? controller?.signal; + if (controller) { + setTimeout(() => controller.abort(), 20000); + } + + // Fetch UTXOs sequentially, address-by-address, to avoid hammering + // mempool.space and to keep behavior deterministic under slow networks. + for (const {address, derivationPath, chain, index} of addressesWithPaths) { + if (!this.addressMatchesNetwork(address, isTestnetApi)) { + continue; + } + if (fetchSignal?.aborted) { + dbg('WalletService: fetchUtxosWithPaths aborted', { + address: address.slice(0, 12), + }); + break; + } + // Skip addresses that returned empty UTXOs recently — no need to ask + // the API again while the user is typing/estimating fees. + // Addresses with UTXOs are always re-fetched so spent coins are detected. + const emptyAt = this.utxoEmptyCache.get(address); + if (emptyAt && Date.now() - emptyAt < this.UTXO_EMPTY_CACHE_TTL_MS) { + continue; + } + + try { + const utxoUrl = `${fullApiUrl}/address/${encodeURIComponent( + address, + )}/utxo`; + const res = await this.withTimeout( + `utxo-${address.slice(0, 12)}`, + mempoolClient.get(utxoUrl, {signal: fetchSignal}), + 8000, + ); + if (!res.ok) { + continue; + } + const rawList: ApiUtxo[] = res.data; + if (!Array.isArray(rawList)) { + continue; + } + if (rawList.length === 0) { + // Cache this empty result so we skip it for the next TTL window + this.utxoEmptyCache.set(address, Date.now()); + continue; + } + for (const u of rawList) { + merged.push({ + ...u, + address, + derivationPath, + chain, + chainIndex: index, + }); + } + } catch (e) { + dbg('WalletService: fetchUtxosWithPaths failed for address', { + address: address.slice(0, 12), + error: e, + }); + // skip failed address, continue with next + } + } + + // Sort: receive first, then change; by chain index; then by block_time desc (newest first) + merged.sort((a, b) => { + if (a.chain !== b.chain) return a.chain === 'receive' ? -1 : 1; + if (a.chainIndex !== b.chainIndex) return a.chainIndex - b.chainIndex; + const ta = a.status?.block_time ?? 0; + const tb = b.status?.block_time ?? 0; + return tb - ta; + }); + + return merged; + } + + private addressMatchesNetwork(addr: string, isTestnetApi: boolean): boolean { + if (!addr) return false; + if (isTestnetApi) { + return ( + ['m', 'n', '2', 't'].some(p => addr.startsWith(p)) || + addr.startsWith('tb1') + ); + } + return ( + ['1', '3', 'b'].some(p => addr.startsWith(p)) || addr.startsWith('bc1') + ); + } + + /** + * Clears the in-memory HD address cache for the given network+addressType combination. + * Call this after any index advance (e.g. after a send, after bumpExternalIndexIfCurrentUsed). + */ + public invalidateAddressCache(network?: string, addressType?: string): void { + if (!network && !addressType) { + this.hdAddressCache.clear(); + this.utxoEmptyCache.clear(); + return; + } + const prefix = `${network}_${addressType}_`; + for (const key of this.hdAddressCache.keys()) { + if (key.startsWith(prefix)) { + this.hdAddressCache.delete(key); + } + } + // Also clear the UTXO empty-skip cache so newly-active addresses are not skipped + this.utxoEmptyCache.clear(); + } + + /** + * Returns all in-scope HD addresses with derivation path and chain (receive vs change). + * Results are memoized in-memory for the lifetime of the app session — re-derivation only + * happens when the indexes change (i.e. after the cache is explicitly invalidated). + */ + public async getHdAddressesWithPaths( + network: string, + addressType: string, + ): Promise { + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) return []; + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + const externalIdx = await getExternalIndex(network, addressType); + const maxUsedExternal = await getMaxUsedExternal(network, addressType); + const changeIdx = await getChangeIndex(network, addressType); + // Runtime range: only addresses that are known to have been issued or used. + // GAP_LIMIT and MIN_SCAN_INDEX are discovery-time concerns (discoverHdIndexesForNetwork), + // not runtime concerns — we already know the wallet state from completed discovery. + const externalEnd = Math.max(externalIdx, maxUsedExternal); + const internalEnd = changeIdx; + + const cacheKey = `${network}_${addressType}_${externalEnd}_${internalEnd}`; + const cached = this.hdAddressCache.get(cacheKey); + if (cached) { + dbg( + 'WalletService: getHdAddressesWithPaths cache hit', + cacheKey, + cached.length, + 'addresses', + ); + return cached; + } + + dbg( + 'WalletService: getHdAddressesWithPaths deriving', + externalEnd + 1, + 'receive +', + internalEnd + 1, + 'change addresses', + ); + const results: HdAddressWithPath[] = []; + for (let i = 0; i <= externalEnd; i++) { + const path = getReceivePath(network, addressType, useLegacyPath, i); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const address = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + results.push({address, derivationPath: path, chain: 'receive', index: i}); + } + for (let i = 0; i <= internalEnd; i++) { + const path = getChangePath(network, addressType, useLegacyPath, i); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const address = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + results.push({address, derivationPath: path, chain: 'change', index: i}); + } + + this.hdAddressCache.set(cacheKey, results); + return results; + } + + /** + * Returns the current receive address, derivation path, and index for display (e.g. ReceiveModal). + * + * All three values are derived together in one call so the returned object is + * always internally consistent — the address is guaranteed to match the path + * and index, eliminating the stale-state flicker that arises when address and + * path info are fetched from separate state variables. + */ + public async getCurrentReceivePathInfo( + network: string, + addressType: string, + ): Promise<{path: string; index: number; address: string} | null> { + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) { + dbg('WalletService: getCurrentReceivePathInfo - no keyshare'); + return null; + } + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + const index = await getExternalIndex(network, addressType); + const path = getReceivePath(network, addressType, useLegacyPath, index); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const address = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + dbg('WalletService: getCurrentReceivePathInfo', { + network, + addressType, + index, + path, + address: address.slice(0, 12), + }); + return {path, index, address}; + } + + /** + * If the current external (receive) address has ever been used (confirmed or mempool), + * advance externalIndex to the next index and update maxUsedExternal accordingly. + * This is a lightweight frontier bump, not a full restore scan. + */ + public async bumpExternalIndexIfCurrentUsed( + network: string, + addressType: string, + apiUrl: string, + ): Promise { + try { + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) { + dbg('WalletService: bumpExternalIndexIfCurrentUsed - no keyshare'); + return; + } + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + const currentIndex = await getExternalIndex(network, addressType); + const path = getReceivePath( + network, + addressType, + useLegacyPath, + currentIndex, + ); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const addr = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + const used = await this.isAddressUsed(addr, apiUrl); + if (!used) { + dbg( + 'WalletService: bumpExternalIndexIfCurrentUsed - current address not used', + {network, addressType, currentIndex, addr: addr.slice(0, 12)}, + ); + return; + } + const prevMaxUsed = await getMaxUsedExternal(network, addressType); + const newMaxUsed = Math.max(prevMaxUsed, currentIndex, 0); + await setMaxUsedExternal(network, addressType, newMaxUsed); + const newIndex = newMaxUsed + 1; + await setExternalIndex(network, addressType, newIndex); + dbg('WalletService: bumpExternalIndexIfCurrentUsed advanced index', { + network, + addressType, + currentIndex, + newIndex, + newMaxUsed, + addr: addr.slice(0, 12), + }); + this.invalidateAddressCache(network, addressType); + } catch (error) { + dbg('WalletService: bumpExternalIndexIfCurrentUsed error', { + network, + addressType, + error, + }); + } + } + + /** + * HD rule: An address is USED if it has EVER appeared in transaction history (confirmed or mempool), + * regardless of current UTXO state. Used for restore discovery to prevent address reuse. + */ + private async isAddressUsed( + address: string, + apiUrl: string, + ): Promise { + const baseUrl = apiUrl.replace(/\/+$/, ''); + const url = `${baseUrl}/address/${address}/txs`; + try { + const response = await this.withTimeout( + `txs-${address.slice(0, 12)}`, + mempoolClient.get(url, {signal: this.abortController.signal}), + 5000, + ); + if (!response.ok) { + dbg('WalletService: isAddressUsed fetch failed', { + address: address.slice(0, 12), + status: response.status, + }); + throw new Error(`isAddressUsed HTTP ${response.status}`); + } + const data = response.data as unknown; + const hasTxs = Array.isArray(data) && data.length > 0; + dbg('WalletService: isAddressUsed', { + address: address.slice(0, 12), + hasTxs, + }); + return hasTxs; + } catch (error) { + dbg('WalletService: isAddressUsed error', { + address: address.slice(0, 12), + error, + }); + throw error; + } + } + + /** + * Restore discovery: scan external and internal chains until GAP_LIMIT consecutive + * unused addresses, then set externalIndex, maxUsedExternal, and changeIndex from chain state. + * Uses transaction history (not UTXO-only) per HD rule: address is used if it has EVER appeared in a tx. + * Call after LocalCache.clear() (storage clear) or keyshare import. + */ + /** Progress: chain, current index, consecutive unused count */ + public async discoverHdIndexesForNetwork( + network: string, + addressType: string, + apiUrl: string, + onProgress?: ( + chain: 'external' | 'internal', + index: number, + gapIndex: number, + ) => void, + ): Promise { + dbg('WalletService: discoverHdIndexesForNetwork START', { + network, + addressType, + apiUrl: apiUrl?.slice(0, 40) + '...', + }); + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) { + dbg('WalletService: No keyshare, skipping restore discovery'); + return; + } + const ks = JSON.parse(jks); + const useLegacyPath = isLegacyWallet(ks.created_at); + BBMTLibNativeModule.setAPI(network, apiUrl); + + const prevExternalIndex = await getExternalIndex(network, addressType); + const prevMaxUsedExternal = await getMaxUsedExternal(network, addressType); + const prevChangeIndex = await getChangeIndex(network, addressType); + + let discoveredMaxUsedExternal = -1; + let discoveredMaxUsedChange = -1; + let discoveryStatus: 'ok' | 'partial' | 'failed' = 'ok'; + const startedAt = Date.now(); + + dbg('WalletService: Restore discovery - scanning external chain', { + network, + addressType, + useLegacyPath, + }); + + // External chain: scan until GAP_LIMIT consecutive unused + try { + let consecutiveUnused = 0; + for (let i = 0; consecutiveUnused < GAP_LIMIT; i++) { + const path = getReceivePath(network, addressType, useLegacyPath, i); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const addr = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + try { + onProgress?.('external', i, consecutiveUnused); + const used = await this.isAddressUsed(addr, apiUrl); + if (used) { + discoveredMaxUsedExternal = i; + consecutiveUnused = 0; + } else { + consecutiveUnused++; + } + } catch (error) { + dbg('WalletService: Restore discovery external error', { + network, + addressType, + index: i, + error, + }); + discoveryStatus = 'partial'; + break; + } + } + } catch (error) { + dbg('WalletService: Restore discovery external FAILED', { + network, + addressType, + error, + }); + discoveryStatus = 'failed'; + } + + // Internal (change) chain: only scan if external completed successfully + if (discoveryStatus === 'ok') { + dbg( + 'WalletService: Restore discovery - scanning internal (change) chain', + { + network, + addressType, + }, + ); + try { + let consecutiveUnused = 0; + for (let i = 0; consecutiveUnused < GAP_LIMIT; i++) { + const path = getChangePath(network, addressType, useLegacyPath, i); + const pub = await BBMTLibNativeModule.derivePubkey( + ks.pub_key, + ks.chain_code_hex, + path, + ); + const addr = await BBMTLibNativeModule.btcAddress( + pub, + network, + addressType, + ); + try { + onProgress?.('internal', i, consecutiveUnused); + const used = await this.isAddressUsed(addr, apiUrl); + if (used) { + discoveredMaxUsedChange = i; + consecutiveUnused = 0; + } else { + consecutiveUnused++; + } + } catch (error) { + dbg('WalletService: Restore discovery internal error', { + network, + addressType, + index: i, + error, + }); + discoveryStatus = 'partial'; + break; + } + } + } catch (error) { + dbg('WalletService: Restore discovery internal FAILED', { + network, + addressType, + error, + }); + discoveryStatus = 'failed'; + } + } + + const durationMs = Date.now() - startedAt; + + if (discoveryStatus === 'ok') { + const externalNext = Math.max( + 0, + discoveredMaxUsedExternal + 1, + prevExternalIndex, + ); + await setExternalIndex(network, addressType, externalNext); + if (discoveredMaxUsedExternal >= 0) { + await setMaxUsedExternal( + network, + addressType, + Math.max(prevMaxUsedExternal, discoveredMaxUsedExternal, 0), + ); + } + const changeNext = Math.max( + 0, + discoveredMaxUsedChange + 1, + prevChangeIndex, + ); + await setChangeIndex(network, addressType, changeNext); + dbg('WalletService: Restore discovery DONE (committed indexes)', { + network, + addressType, + discoveredMaxUsedExternal, + externalNext, + discoveredMaxUsedChange, + changeNext, + durationMs, + }); + // Mark restore discovery as completed for this (network, addressType) pair + walletRepository.setRestoreDone(network, addressType, true); + walletRepository.setDiscoveryStatus(network, addressType, 'ok', Date.now()); + } else { + dbg( + 'WalletService: Restore discovery aborted, keeping previous HD indexes', + { + network, + addressType, + discoveryStatus, + prevExternalIndex, + prevMaxUsedExternal, + prevChangeIndex, + durationMs, + }, + ); + walletRepository.setDiscoveryStatus(network, addressType, discoveryStatus, Date.now()); + } + + dbg('WalletService: discoverHdIndexesForNetwork COMPLETE', { + network, + addressType, + discoveryStatus, + durationMs, + }); + // Indexes may have changed — drop the cached address list + this.invalidateAddressCache(network, addressType); + } + // Add method to cancel ongoing fetches private cancelOngoingFetches(key: string) { if (this.fetchInProgress[key]) { @@ -295,25 +1078,39 @@ export class WalletService { try { // Get the list of mainnet API endpoints const apiEndpoints = await getMainnetAPIList(); - dbg('WalletService: Attempting to fetch BTC price using round-robin from APIs:', apiEndpoints); + dbg( + 'WalletService: Attempting to fetch BTC price using round-robin from APIs:', + apiEndpoints, + ); let lastError: any = null; // Try each API endpoint in sequence until one succeeds for (const baseApiUrl of apiEndpoints) { try { // Always use mainnet price endpoint (remove any testnet suffix) - const priceUrl = baseApiUrl.replace(/\/testnet\/?$/, '') + '/v1/prices'; + const priceUrl = + baseApiUrl.replace(/\/testnet\/?$/, '') + '/v1/prices'; dbg('WalletService: Trying price API URL:', priceUrl); const response = await this.withTimeout( 'price', - fetch(priceUrl, {signal: this.abortController.signal}), + mempoolClient.get(priceUrl, {signal: this.abortController.signal}), ); if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`); + throw new Error(`HTTP ${response.status}`); } - const data = await response.json(); - dbg('WalletService: Raw price data received from', priceUrl, ':', data); + const data = response.data as any; + dbg( + 'WalletService: Raw price data received from', + priceUrl, + ':', + data, + ); if (!data || !data.USD || !validateNumber(data.USD)) { - dbg('WalletService: Invalid price data received from', priceUrl, ':', data); + dbg( + 'WalletService: Invalid price data received from', + priceUrl, + ':', + data, + ); throw new Error('Invalid price data received'); } const rate = parseFloat(data.USD); @@ -323,7 +1120,14 @@ export class WalletService { throw new Error('Invalid rate value'); } const price = this.formatUSD(data.USD); - dbg('WalletService: New price fetched from', priceUrl, '- Rate:', rate, 'Price:', price); + dbg( + 'WalletService: New price fetched from', + priceUrl, + '- Rate:', + rate, + 'Price:', + price, + ); // Use all available rates from the API response const rates: {[key: string]: number} = {}; Object.entries(data).forEach(([currency, value]) => { @@ -368,19 +1172,25 @@ export class WalletService { this.fetchTimeout = {}; // Clear persistent storage try { - await LocalCache.removeItem('walletCache'); - dbg('WalletService: Cleared persistent cache'); + dbg('WalletService: Cleared persistent cache (walletCache key deprecated)'); } catch (error) { dbg('WalletService: Error clearing persistent cache:', error); } - // Generate new address for the current network + // Generate address for the current network at current external index (HD) try { const jks = await EncryptedStorage.getItem('keyshare'); const ks = JSON.parse(jks || '{}'); - // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - // Use derivation path that matches the address type (or legacy path for old wallets) - const path = getDerivePathForNetwork(network, state.addressType, useLegacyPath); + const externalIndex = await getExternalIndex( + network, + state.addressType, + ); + const path = getReceivePath( + network, + state.addressType, + useLegacyPath, + externalIndex, + ); const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -424,15 +1234,17 @@ export class WalletService { public async handleAddressTypeChange(addressType: string) { dbg('WalletService: Address type changed to:', addressType); try { - // Get current state const state = await this.getStoredState(); - // Generate new address for current network and type const jks = await EncryptedStorage.getItem('keyshare'); const ks = JSON.parse(jks || '{}'); - // Check if this is a legacy wallet (created before migration timestamp) const useLegacyPath = isLegacyWallet(ks.created_at); - // Use derivation path that matches the address type (or legacy path for old wallets) - const path = getDerivePathForNetwork(state.network, addressType, useLegacyPath); + const externalIndex = await getExternalIndex(state.network, addressType); + const path = getReceivePath( + state.network, + addressType, + useLegacyPath, + externalIndex, + ); const btcPub = await BBMTLibNativeModule.derivePubkey( ks.pub_key, ks.chain_code_hex, @@ -476,9 +1288,15 @@ export class WalletService { force, ); // Normalize network parameter for validation (testnet3 -> testnet) - const normalizedNetwork = this.currentNetwork === 'testnet3' ? 'testnet' : this.currentNetwork; + const normalizedNetwork = + this.currentNetwork === 'testnet3' ? 'testnet' : this.currentNetwork; if (!validateBitcoinAddressEnhanced(address, normalizedNetwork)) { - dbg('WalletService: Invalid Bitcoin address format:', address, 'for network:', this.currentNetwork); + dbg( + 'WalletService: Invalid Bitcoin address format:', + address, + 'for network:', + this.currentNetwork, + ); throw new Error('Invalid Bitcoin address'); } if (!validateNumber(btcRate)) { @@ -490,7 +1308,7 @@ export class WalletService { throw new Error('Invalid pending amount'); } BBMTLibNativeModule.setAPI(this.currentNetwork, this.currentApiUrl); - const api = await LocalCache.getItem('api'); + const api = appConfigRepository.get('api') || this.currentApiUrl; if (!api) { dbg('WalletService: No API URL found'); throw new Error('No API URL found'); @@ -513,7 +1331,9 @@ export class WalletService { dbg('WalletService: Raw balance in satoshis:', balance.toString()); // Calculate balance after pending sent, ensuring it's never negative const balanceAfterPending = balance.sub(pendingSent); - const finalBalance = balanceAfterPending.gte(0) ? balanceAfterPending : new Big(0); + const finalBalance = balanceAfterPending.gte(0) + ? balanceAfterPending + : new Big(0); const newBalance = finalBalance.div(1e8).toFixed(8); dbg('WalletService: Balance after pending subtraction:', newBalance); const hasNonZeroBalance = Number(newBalance) > 0; @@ -545,6 +1365,190 @@ export class WalletService { return await this.getBal(address); } } + + /** + * Aggregated balance over all HD addresses (external chain 0..maxUsed+GAP, internal 0..changeIndex+GAP). + * + * Uses GET /api/address/{addr} instead of /api/address/{addr}/utxo so that: + * - Responses are ~50× smaller (6 integers vs a full UTXO array) + * - All calls go through mempoolClient (30 s cache + in-flight dedup) + * - The formula is equivalent: confirmed + unconfirmed funded minus spent + * + * balance_sats = (chain_stats.funded_txo_sum - chain_stats.spent_txo_sum) + * + (mempool_stats.funded_txo_sum - mempool_stats.spent_txo_sum) + * + * The UTXO list for transaction construction is fetched separately via + * fetchUtxosWithPaths (also mempoolClient-cached) and is not affected here. + */ + public async getWalletBalanceAggregate( + network: string, + addressType: string, + btcRate: number, + pendingSent: number = 0, + _force: boolean = false, + ): Promise { + try { + dbg('WalletService: getWalletBalanceAggregate', { + network, + addressType, + btcRate, + pendingSent, + _force, + }); + + const jks = await EncryptedStorage.getItem('keyshare'); + if (!jks) { + dbg('WalletService: No keyshare for aggregate balance'); + return { + btc: '0.00000000', + usd: '$0.00', + hasNonZeroBalance: false, + timestamp: Date.now(), + }; + } + const api = appConfigRepository.get('api') || this.currentApiUrl; + if (!api) throw new Error('No API URL found'); + const cleanApi = api.replace(/\/+$/, ''); + + // Reuse the cached address list — no re-derivation if indexes haven't changed. + const addressesWithPaths = await this.getHdAddressesWithPaths( + network, + addressType, + ); + const addresses = addressesWithPaths.map(a => a.address); + + let confirmedSats = new Big(0); + let mempoolSats = new Big(0); + let successCount = 0; + + const applyAddrCache = (addr: string) => { + // Use last-known per-address balance from DB when the API call fails. + const cached = balanceRepository.getBalance(addr, network); + if (!cached || cached.fetchedAt === 0) return; + const confirmedPart = cached.balanceSats - cached.pendingSats; + if (confirmedPart > 0) confirmedSats = confirmedSats.add(confirmedPart); + if (cached.pendingSats !== 0) mempoolSats = mempoolSats.add(cached.pendingSats); + }; + + for (const addr of addresses) { + try { + const res = await mempoolClient.get<{ + chain_stats: {funded_txo_sum: number; spent_txo_sum: number}; + mempool_stats: {funded_txo_sum: number; spent_txo_sum: number}; + }>(`${cleanApi}/address/${encodeURIComponent(addr)}`); + + if (!res.ok) { + // Transient HTTP error — fall back to this address's DB balance so + // its sats are not silently dropped from the aggregate. + applyAddrCache(addr); + continue; + } + successCount++; + const {chain_stats, mempool_stats} = res.data; + const addrConfirmed = + chain_stats.funded_txo_sum - chain_stats.spent_txo_sum; + const addrMempool = + mempool_stats.funded_txo_sum - mempool_stats.spent_txo_sum; + if (Number.isFinite(addrConfirmed) && addrConfirmed > 0) { + confirmedSats = confirmedSats.add(addrConfirmed); + } + if (Number.isFinite(addrMempool) && addrMempool !== 0) { + mempoolSats = mempoolSats.add(addrMempool); + } + // Persist fresh per-address balance so future refreshes have a fallback. + balanceRepository.setBalance({ + address: addr, + network, + balanceSats: Math.max(0, addrConfirmed) + Math.max(0, addrMempool), + pendingSats: addrMempool, + hasNonzero: addrConfirmed > 0 || addrMempool > 0, + fetchedAt: Date.now(), + }); + } catch { + // Network error — fall back to DB for this address. + applyAddrCache(addr); + } + } + + // If every single address failed (no API response at all) and we have + // a prior aggregate stored, return it rather than showing 0. + if (successCount === 0 && addresses.length > 0) { + const cached = await this.getCachedAggregateBalance(network, addressType); + if (cached) { + dbg('WalletService: All addresses failed — returning cached aggregate balance'); + return cached; + } + } + + const totalSats = confirmedSats.add(mempoolSats); + const balanceAfterPending = totalSats.sub(pendingSent); + const finalBalance = balanceAfterPending.gte(0) + ? balanceAfterPending + : new Big(0); + const newBalance = finalBalance.div(1e8).toFixed(8); + const hasNonZeroBalance = Number(newBalance) > 0; + let usdAmount = ''; + if (btcRate > 0) { + usdAmount = this.formatUSD(totalSats.mul(btcRate).div(1e8).toNumber()); + } + const pendingSatsValue = mempoolSats.toNumber(); + dbg('WalletService: getWalletBalanceAggregate', { + addresses: addresses.length, + confirmedSats: confirmedSats.toFixed(0), + pendingSats: pendingSatsValue, + newBalance, + }); + const result: WalletBalance = { + btc: newBalance, + usd: usdAmount, + hasNonZeroBalance, + timestamp: Date.now(), + pendingSats: pendingSatsValue, + }; + // Persist aggregate under a virtual address key for the HD wallet + const aggAddress = `aggregate_${network}_${addressType}`; + balanceRepository.setBalance({ + address: aggAddress, + network, + balanceSats: Math.round(Number(newBalance) * 1e8), + pendingSats: pendingSatsValue, + hasNonzero: hasNonZeroBalance, + fetchedAt: result.timestamp, + }); + dbg('WalletService: getWalletBalanceAggregate result:', result); + return result; + } catch (error) { + dbg('WalletService: getWalletBalanceAggregate error:', error); + const cached = await this.getCachedAggregateBalance(network, addressType); + dbg('WalletService: getWalletBalanceAggregate cached:', cached); + return ( + cached ?? { + btc: '0.00000000', + usd: '$0.00', + hasNonZeroBalance: false, + timestamp: Date.now(), + } + ); + } + } + + /** Returns cached aggregate balance for HD wallet (network + addressType). */ + public async getCachedAggregateBalance( + network: string, + addressType: string, + ): Promise { + const aggAddress = `aggregate_${network}_${addressType}`; + const stored = balanceRepository.getBalance(aggAddress, network); + if (!stored) return null; + return { + btc: (stored.balanceSats / 1e8).toFixed(8), + usd: '$0.00', + hasNonZeroBalance: stored.hasNonzero, + timestamp: stored.fetchedAt, + pendingSats: stored.pendingSats, + }; + } + public getTransactionDetails( tx: any, address: string, @@ -650,4 +1654,239 @@ export class WalletService { dbg('found cached txs:', txs.transactions.length); return txs.transactions; } + + /** + * Fetches transactions for multiple HD addresses, merges by txid, dedupes, sorts by block_time desc. + * Used for wallet-level transaction list (all receive + change addresses). + */ + /** + * mempool.space returns at most 25 txs per /txs call. + * When exactly PAGE_SIZE are returned there may be more; store the last txid + * as a cursor so callers can page with /txs/chain/{cursor}. + */ + private static readonly TX_PAGE_SIZE = 25; + + /** Pending txs (no block_height) sort to the top; confirmed sort by block_height desc. */ + private static txSortKey(tx: any): number { + if (!tx.status?.block_height) { + return Number.MAX_SAFE_INTEGER; // pending → top + } + return tx.status.block_height; + } + + /** + * Initial fetch — calls /txs for every address sequentially. + * Returns merged + deduped transactions sorted newest-first, plus a per-address + * cursor map (null = address exhausted, string = last txid for next page). + */ + public async fetchTransactionsForAddresses( + apiBase: string, + addresses: string[], + ): Promise<{txs: any[]; cursors: Record}> { + if (addresses.length === 0) return {txs: [], cursors: {}}; + const cleanBase = apiBase.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + const seen = new Set(); + const merged: any[] = []; + const cursors: Record = {}; + // Fetch sequentially to avoid rate-limiting mempool.space. + for (const addr of addresses) { + try { + const url = `${cleanBase}/api/address/${encodeURIComponent(addr)}/txs`; + const res = await this.withTimeout( + `txs-${addr.slice(0, 12)}`, + mempoolClient.get(url), + 8000, + ); + if (!res.ok) { + cursors[addr] = null; + continue; + } + const data = res.data as any[]; + if (!Array.isArray(data)) { + cursors[addr] = null; + continue; + } + // Persist to DB immediately with the real Bitcoin address as the key. + // This ensures loadFromCache() can serve offline reads keyed by address, + // and aligns with TransactionSyncer's namespace. + if (data.length > 0) { + await this.setTxs(addr, data); + } + for (const tx of data) { + if (!seen.has(tx.txid)) { + seen.add(tx.txid); + merged.push(tx); + } + } + // Exactly PAGE_SIZE returned → there may be a next page + cursors[addr] = + data.length >= WalletService.TX_PAGE_SIZE + ? data[data.length - 1].txid + : null; + } catch (e) { + dbg( + 'WalletService: fetchTransactionsForAddresses failed for', + addr.slice(0, 12), + e, + ); + cursors[addr] = null; + } + } + merged.sort( + (a, b) => WalletService.txSortKey(b) - WalletService.txSortKey(a), + ); + dbg( + 'WalletService: fetchTransactionsForAddresses merged', + merged.length, + 'txs from', + addresses.length, + 'addresses', + ); + return {txs: merged, cursors}; + } + + /** + * Paginated fetch — calls /txs/chain/{cursor} for every address whose cursor + * is non-null. Returns only the NEW transactions for this page plus updated + * cursors (null out exhausted addresses). + */ + public async fetchMoreTransactionsForAddresses( + apiBase: string, + cursors: Record, + ): Promise<{txs: any[]; cursors: Record}> { + const cleanBase = apiBase.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + const seen = new Set(); + const merged: any[] = []; + const updatedCursors: Record = {...cursors}; + for (const [addr, cursor] of Object.entries(cursors)) { + if (!cursor) continue; // already exhausted + try { + const url = `${cleanBase}/api/address/${encodeURIComponent( + addr, + )}/txs/chain/${cursor}`; + const res = await this.withTimeout( + `txs-more-${addr.slice(0, 12)}`, + mempoolClient.get(url), + 8000, + ); + if (!res.ok) { + updatedCursors[addr] = null; + continue; + } + const data = res.data as any[]; + if (!Array.isArray(data) || data.length === 0) { + updatedCursors[addr] = null; + continue; + } + // Persist each page to DB with the real Bitcoin address key. + await this.setTxs(addr, data); + for (const tx of data) { + if (!seen.has(tx.txid)) { + seen.add(tx.txid); + merged.push(tx); + } + } + updatedCursors[addr] = + data.length >= WalletService.TX_PAGE_SIZE + ? data[data.length - 1].txid + : null; + } catch (e) { + dbg( + 'WalletService: fetchMoreTransactionsForAddresses failed for', + addr.slice(0, 12), + e, + ); + updatedCursors[addr] = null; + } + } + merged.sort( + (a, b) => WalletService.txSortKey(b) - WalletService.txSortKey(a), + ); + dbg( + 'WalletService: fetchMoreTransactionsForAddresses page yielded', + merged.length, + 'new txs', + ); + return {txs: merged, cursors: updatedCursors}; + } + + /** + * Enriches a UTXO list with the scriptpubkey (hex locking script) for each output. + * Fetches /tx/{txid} for each unique txid and reads vout[n].scriptpubkey. + * Called before passing UTXOs to the native bridge so Go's signing loop needs + * no network calls (FetchUTXODetails is skipped when scriptpubkey is present). + * + * UTXOs for which the fetch fails get an empty scriptpubkey string; Go will + * fall back to FetchUTXODetails for those inputs (safe, backward-compatible). + */ + public async enrichUtxosWithScriptpubkey( + utxos: UtxoWithPath[], + apiUrl: string, + ): Promise<(UtxoWithPath & {scriptpubkey: string})[]> { + const base = apiUrl.replace(/\/+$/, '').replace(/\/api\/?$/, ''); + // mempoolClient deduplicates concurrent fetches for the same txid and caches + // the result for 5 min (immutable confirmed tx content), so no local txCache needed. + const results: (UtxoWithPath & {scriptpubkey: string})[] = []; + for (const u of utxos) { + let scriptpubkey = ''; + try { + const res = await mempoolClient.get(`${base}/api/tx/${u.txid}`); + const txData = res.ok ? res.data : undefined; + scriptpubkey = txData?.vout?.[u.vout]?.scriptpubkey ?? ''; + } catch (e) { + dbg('WalletService: enrichUtxosWithScriptpubkey failed for', u.txid, e); + } + results.push({...u, scriptpubkey}); + } + return results; + } + + /** Cache key for wallet-level (multi-address) transactions. */ + private walletTxsCacheKey(network: string, addressType: string) { + return `wallet_txs_${network}_${addressType}`; + } + + public async transactionsFromCacheForWallet( + network: string, + addressType: string, + ): Promise { + // Primary path: synthetic wallet-level key written by updateTransactionsCacheForWallet. + const cacheKey = this.walletTxsCacheKey(network, addressType); + const txs = await this.getTxs(cacheKey); + if (txs.transactions.length > 0) { + return txs.transactions; + } + // Fallback: look up by real Bitcoin addresses (written by fetchTransactionsForAddresses + // with real-address keys, and by TransactionSyncer). Requires keyshare to derive addresses. + try { + const addrs = await this.getHdAddressesWithPaths(network, addressType); + if (addrs.length === 0) return []; + const rows = transactionRepository.getTransactionsForAddresses( + addrs.map(a => a.address), + network, + ); + if (rows.length === 0) return []; + return rows + .map(r => { + try { + return JSON.parse(r.rawJson); + } catch { + return null; + } + }) + .filter(Boolean); + } catch { + return []; + } + } + + public async updateTransactionsCacheForWallet( + network: string, + addressType: string, + txs: any[], + ) { + const cacheKey = this.walletTxsCacheKey(network, addressType); + await this.setTxs(cacheKey, txs); + dbg('WalletService: wallet txs cache updated', cacheKey, txs.length); + } } diff --git a/services/repositories/AppConfigRepository.ts b/services/repositories/AppConfigRepository.ts new file mode 100644 index 00000000..c3acb6d2 --- /dev/null +++ b/services/repositories/AppConfigRepository.ts @@ -0,0 +1,122 @@ +/** + * AppConfigRepository — replaces all single-value LocalCache preference keys. + * + * Covers: network, address_type, current_address, currency, balance_hidden, + * haptics_enabled, theme_mode, fee_strategy, legacy_wallet_do_not_remind, + * tab_wallet_enabled, tab_psbt_enabled, tab_utxos_enabled, tab_mempool_enabled, + * sqlite_migration_done, and any future string-typed preferences. + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +// Canonical key names (use these constants instead of raw strings) +export const CONFIG_KEYS = { + NETWORK: 'network', + ADDRESS_TYPE: 'address_type', + CURRENT_ADDRESS: 'current_address', + CURRENCY: 'currency', + BALANCE_HIDDEN: 'balance_hidden', + HAPTICS_ENABLED: 'haptics_enabled', + THEME_MODE: 'theme_mode', + FEE_STRATEGY: 'fee_strategy', + LEGACY_WALLET_DO_NOT_REMIND: 'legacy_wallet_do_not_remind', + TAB_WALLET_ENABLED: 'tab_wallet_enabled', + TAB_PSBT_ENABLED: 'tab_psbt_enabled', + TAB_UTXOS_ENABLED: 'tab_utxos_enabled', + TAB_MEMPOOL_ENABLED: 'tab_mempool_enabled', + SQLITE_MIGRATION_DONE: 'sqlite_migration_done', +} as const; + +export type ConfigKey = (typeof CONFIG_KEYS)[keyof typeof CONFIG_KEYS]; + +class AppConfigRepository { + /** Read one preference; returns null when absent. */ + get(key: string): string | null { + try { + const {rows} = database.execute( + 'SELECT value FROM app_config WHERE key = ?', + [key], + ); + return rows.length > 0 ? (rows[0].value as string) : null; + } catch (err) { + dbg('AppConfigRepository.get error', key, err); + return null; + } + } + + /** Write one preference (upsert). */ + set(key: string, value: string): void { + try { + database.execute( + `INSERT INTO app_config (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at`, + [key, value, Date.now()], + ); + } catch (err) { + dbg('AppConfigRepository.set error', key, err); + } + } + + /** Delete one preference. */ + remove(key: string): void { + try { + database.execute('DELETE FROM app_config WHERE key = ?', [key]); + } catch (err) { + dbg('AppConfigRepository.remove error', key, err); + } + } + + /** Load all preferences as a plain object — useful for context hydration. */ + getAll(): Record { + try { + const {rows} = database.execute('SELECT key, value FROM app_config'); + const result: Record = {}; + for (const row of rows) { + result[row.key as string] = row.value as string; + } + return result; + } catch (err) { + dbg('AppConfigRepository.getAll error', err); + return {}; + } + } + + /** Convenience: read boolean preference (default false). */ + getBool(key: string, defaultValue = false): boolean { + const v = this.get(key); + if (v === null) return defaultValue; + return v === 'true'; + } + + /** Convenience: write boolean preference. */ + setBool(key: string, value: boolean): void { + this.set(key, value ? 'true' : 'false'); + } + + /** Batch-write multiple key/value pairs in one transaction. */ + setMany(entries: Record): void { + try { + const now = Date.now(); + database.transaction(tx => { + for (const [key, value] of Object.entries(entries)) { + tx.execute( + `INSERT INTO app_config (key, value, updated_at) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value, + updated_at = excluded.updated_at`, + [key, value, now], + ); + } + }); + } catch (err) { + dbg('AppConfigRepository.setMany error', err); + } + } +} + +const appConfigRepository = new AppConfigRepository(); +export default appConfigRepository; diff --git a/services/repositories/BalanceRepository.ts b/services/repositories/BalanceRepository.ts new file mode 100644 index 00000000..22925d63 --- /dev/null +++ b/services/repositories/BalanceRepository.ts @@ -0,0 +1,157 @@ +/** + * BalanceRepository — per-address and aggregate wallet balances. + * + * Replaces LocalCache keys: + * wallet_balance_
+ * wallet_balance_aggregate__ + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export interface AddressBalance { + address: string; + network: string; + balanceSats: number; + pendingSats: number; + hasNonzero: boolean; + fetchedAt: number; +} + +class BalanceRepository { + getBalance(address: string, network: string): AddressBalance | null { + try { + const {rows} = database.execute( + 'SELECT * FROM address_balances WHERE address = ? AND network = ?', + [address, network], + ); + if (!rows.length) return null; + const r = rows[0]; + return { + address: r.address as string, + network: r.network as string, + balanceSats: r.balance_sats as number, + pendingSats: r.pending_sats as number, + hasNonzero: (r.has_nonzero as number) === 1, + fetchedAt: r.fetched_at as number, + }; + } catch (err) { + dbg('BalanceRepository.getBalance error', err); + return null; + } + } + + setBalance(bal: AddressBalance): void { + try { + database.execute( + `INSERT INTO address_balances + (address, network, balance_sats, pending_sats, has_nonzero, fetched_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(address, network) DO UPDATE SET + balance_sats = excluded.balance_sats, + pending_sats = excluded.pending_sats, + has_nonzero = excluded.has_nonzero, + fetched_at = excluded.fetched_at`, + [ + bal.address, + bal.network, + bal.balanceSats, + bal.pendingSats, + bal.hasNonzero ? 1 : 0, + bal.fetchedAt, + ], + ); + } catch (err) { + dbg('BalanceRepository.setBalance error', err); + } + } + + /** Get all balances for a given network (used for aggregate calculation). */ + getBalancesForNetwork(network: string): AddressBalance[] { + try { + const {rows} = database.execute( + 'SELECT * FROM address_balances WHERE network = ?', + [network], + ); + return rows.map(r => ({ + address: r.address as string, + network: r.network as string, + balanceSats: r.balance_sats as number, + pendingSats: r.pending_sats as number, + hasNonzero: (r.has_nonzero as number) === 1, + fetchedAt: r.fetched_at as number, + })); + } catch (err) { + dbg('BalanceRepository.getBalancesForNetwork error', err); + return []; + } + } + + /** Sum all confirmed + pending balances across all addresses for a network. */ + getAggregateBalance(network: string): { + balanceSats: number; + pendingSats: number; + hasNonzero: boolean; + fetchedAt: number; + } { + try { + const {rows} = database.execute( + `SELECT + COALESCE(SUM(balance_sats), 0) AS total_confirmed, + COALESCE(SUM(pending_sats), 0) AS total_pending, + MAX(fetched_at) AS newest_fetch + FROM address_balances + WHERE network = ?`, + [network], + ); + if (!rows.length) { + return {balanceSats: 0, pendingSats: 0, hasNonzero: false, fetchedAt: 0}; + } + const r = rows[0]; + const confirmed = r.total_confirmed as number; + const pending = r.total_pending as number; + return { + balanceSats: confirmed, + pendingSats: pending, + hasNonzero: confirmed > 0 || pending > 0, + fetchedAt: (r.newest_fetch as number) ?? 0, + }; + } catch (err) { + dbg('BalanceRepository.getAggregateBalance error', err); + return {balanceSats: 0, pendingSats: 0, hasNonzero: false, fetchedAt: 0}; + } + } + + /** Batch-write balances in one transaction. */ + setBalances(bals: AddressBalance[]): void { + if (!bals.length) return; + try { + database.transaction(tx => { + for (const bal of bals) { + tx.execute( + `INSERT INTO address_balances + (address, network, balance_sats, pending_sats, has_nonzero, fetched_at) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(address, network) DO UPDATE SET + balance_sats = excluded.balance_sats, + pending_sats = excluded.pending_sats, + has_nonzero = excluded.has_nonzero, + fetched_at = excluded.fetched_at`, + [ + bal.address, + bal.network, + bal.balanceSats, + bal.pendingSats, + bal.hasNonzero ? 1 : 0, + bal.fetchedAt, + ], + ); + } + }); + } catch (err) { + dbg('BalanceRepository.setBalances error', err); + } + } +} + +const balanceRepository = new BalanceRepository(); +export default balanceRepository; diff --git a/services/repositories/PriceRepository.ts b/services/repositories/PriceRepository.ts new file mode 100644 index 00000000..843e4f35 --- /dev/null +++ b/services/repositories/PriceRepository.ts @@ -0,0 +1,161 @@ +/** + * PriceRepository — current BTC price and historical per-day rates. + * + * Replaces LocalCache keys: + * price (current snapshot) + * historical_price__ (per-day historical rates) + * + * Convention: day_timestamp = 0 means "live / current price". + * day_timestamp > 0 means UTC day start (Unix seconds). + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export interface PriceRate { + currency: string; + dayTimestamp: number; // 0 = current + rate: number; + fetchedAt: number; +} + +/** Shape stored under the old LocalCache 'price' key. */ +export interface CachedPrice { + price: string; + rate: number; + rates: Record; + timestamp: number; +} + +const LIVE_DAY_TS = 0; +const SEC_PER_DAY = 86400; + +/** Round a Unix-seconds timestamp down to the UTC day boundary. */ +export function toDayTimestamp(unixSec: number): number { + return Math.floor(unixSec / SEC_PER_DAY) * SEC_PER_DAY; +} + +class PriceRepository { + // ── Current / live price ───────────────────────────────────────────────── + + getCurrentRate(currency: string): number | null { + try { + const {rows} = database.execute( + 'SELECT rate FROM price_rates WHERE currency = ? AND day_timestamp = ?', + [currency, LIVE_DAY_TS], + ); + return rows.length ? (rows[0].rate as number) : null; + } catch (err) { + dbg('PriceRepository.getCurrentRate error', err); + return null; + } + } + + setCurrentRate(currency: string, rate: number): void { + try { + database.execute( + `INSERT INTO price_rates (currency, day_timestamp, rate, fetched_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(currency, day_timestamp) DO UPDATE SET + rate = excluded.rate, + fetched_at = excluded.fetched_at`, + [currency, LIVE_DAY_TS, rate, Date.now()], + ); + } catch (err) { + dbg('PriceRepository.setCurrentRate error', err); + } + } + + /** Write all currency rates from a single API response in one transaction. */ + setCurrentRates(rates: Record): void { + try { + const now = Date.now(); + database.transaction(tx => { + for (const [currency, rate] of Object.entries(rates)) { + tx.execute( + `INSERT INTO price_rates (currency, day_timestamp, rate, fetched_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(currency, day_timestamp) DO UPDATE SET + rate = excluded.rate, + fetched_at = excluded.fetched_at`, + [currency, LIVE_DAY_TS, rate, now], + ); + } + }); + } catch (err) { + dbg('PriceRepository.setCurrentRates error', err); + } + } + + /** + * Returns a legacy CachedPrice object compatible with WalletService consumers. + * If no rate exists for the primary currency, returns null. + */ + getCachedPrice(primaryCurrency: string): CachedPrice | null { + try { + const {rows} = database.execute( + 'SELECT currency, rate, fetched_at FROM price_rates WHERE day_timestamp = ?', + [LIVE_DAY_TS], + ); + if (!rows.length) return null; + + const rateMap: Record = {}; + let primaryRate = 0; + let fetchedAt = 0; + + for (const r of rows) { + rateMap[r.currency as string] = r.rate as number; + if (r.currency === primaryCurrency) { + primaryRate = r.rate as number; + fetchedAt = r.fetched_at as number; + } + } + + if (!primaryRate) return null; + + return { + price: primaryRate.toFixed(2), + rate: primaryRate, + rates: rateMap, + timestamp: fetchedAt, + }; + } catch (err) { + dbg('PriceRepository.getCachedPrice error', err); + return null; + } + } + + // ── Historical rates ───────────────────────────────────────────────────── + + getHistoricalRate(currency: string, unixSec: number): number | null { + try { + const day = toDayTimestamp(unixSec); + const {rows} = database.execute( + 'SELECT rate FROM price_rates WHERE currency = ? AND day_timestamp = ?', + [currency, day], + ); + return rows.length ? (rows[0].rate as number) : null; + } catch (err) { + dbg('PriceRepository.getHistoricalRate error', err); + return null; + } + } + + setHistoricalRate(currency: string, unixSec: number, rate: number): void { + try { + const day = toDayTimestamp(unixSec); + database.execute( + `INSERT INTO price_rates (currency, day_timestamp, rate, fetched_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(currency, day_timestamp) DO UPDATE SET + rate = excluded.rate, + fetched_at = excluded.fetched_at`, + [currency, day, rate, Date.now()], + ); + } catch (err) { + dbg('PriceRepository.setHistoricalRate error', err); + } + } +} + +const priceRepository = new PriceRepository(); +export default priceRepository; diff --git a/services/repositories/SyncRepository.ts b/services/repositories/SyncRepository.ts new file mode 100644 index 00000000..52d1cb31 --- /dev/null +++ b/services/repositories/SyncRepository.ts @@ -0,0 +1,140 @@ +/** + * SyncRepository — pagination cursors and per-entity sync state. + * + * entity_type: 'balance' | 'transactions' | 'utxos' | 'discovery' + * entity_key: address string, or "_" for aggregate entries + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export type SyncStatus = 'ok' | 'partial' | 'failed'; +export type EntityType = 'balance' | 'transactions' | 'utxos' | 'discovery'; + +export interface SyncMeta { + entityType: EntityType; + entityKey: string; + cursor: string | null; + lastSyncedAt: number | null; + syncStatus: SyncStatus | null; + extraJson: string | null; +} + +class SyncRepository { + get(entityType: EntityType, entityKey: string): SyncMeta | null { + try { + const {rows} = database.execute( + 'SELECT * FROM sync_metadata WHERE entity_type = ? AND entity_key = ?', + [entityType, entityKey], + ); + if (!rows.length) return null; + return this._rowToMeta(rows[0]); + } catch (err) { + dbg('SyncRepository.get error', err); + return null; + } + } + + set(meta: SyncMeta): void { + try { + database.execute( + `INSERT INTO sync_metadata + (entity_type, entity_key, cursor, last_synced_at, sync_status, extra_json) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(entity_type, entity_key) DO UPDATE SET + cursor = excluded.cursor, + last_synced_at = excluded.last_synced_at, + sync_status = excluded.sync_status, + extra_json = excluded.extra_json`, + [ + meta.entityType, + meta.entityKey, + meta.cursor ?? null, + meta.lastSyncedAt ?? null, + meta.syncStatus ?? null, + meta.extraJson ?? null, + ], + ); + } catch (err) { + dbg('SyncRepository.set error', err); + } + } + + /** Force immediate re-sync on next SyncCoordinator tick. */ + invalidate(entityType: EntityType, entityKey: string): void { + try { + database.execute( + `UPDATE sync_metadata + SET last_synced_at = 0, sync_status = 'failed' + WHERE entity_type = ? AND entity_key = ?`, + [entityType, entityKey], + ); + } catch (err) { + dbg('SyncRepository.invalidate error', err); + } + } + + /** Retrieve the stored cursor for paginated transaction fetching. */ + getCursor(entityType: EntityType, entityKey: string): string | null { + try { + const {rows} = database.execute( + 'SELECT cursor FROM sync_metadata WHERE entity_type = ? AND entity_key = ?', + [entityType, entityKey], + ); + return rows.length ? ((rows[0].cursor as string) ?? null) : null; + } catch (err) { + dbg('SyncRepository.getCursor error', err); + return null; + } + } + + updateCursor( + entityType: EntityType, + entityKey: string, + cursor: string | null, + status: SyncStatus = 'ok', + ): void { + try { + // Upsert — creates row if absent + database.execute( + `INSERT INTO sync_metadata + (entity_type, entity_key, cursor, last_synced_at, sync_status) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(entity_type, entity_key) DO UPDATE SET + cursor = excluded.cursor, + last_synced_at = excluded.last_synced_at, + sync_status = excluded.sync_status`, + [entityType, entityKey, cursor, Date.now(), status], + ); + } catch (err) { + dbg('SyncRepository.updateCursor error', err); + } + } + + /** Return the timestamp of the last successful sync, or 0 if never synced. */ + getLastSyncedAt(entityType: EntityType, entityKey: string): number { + try { + const {rows} = database.execute( + `SELECT last_synced_at FROM sync_metadata + WHERE entity_type = ? AND entity_key = ? AND sync_status = 'ok'`, + [entityType, entityKey], + ); + return rows.length ? ((rows[0].last_synced_at as number) ?? 0) : 0; + } catch { + return 0; + } + } + + private _rowToMeta(r: Record): SyncMeta { + return { + entityType: r.entity_type as EntityType, + entityKey: r.entity_key as string, + cursor: (r.cursor as string) ?? null, + lastSyncedAt: (r.last_synced_at as number) ?? null, + syncStatus: (r.sync_status as SyncStatus) ?? null, + extraJson: (r.extra_json as string) ?? null, + }; + } +} + +const syncRepository = new SyncRepository(); +export default syncRepository; diff --git a/services/repositories/TransactionRepository.ts b/services/repositories/TransactionRepository.ts new file mode 100644 index 00000000..1cb79296 --- /dev/null +++ b/services/repositories/TransactionRepository.ts @@ -0,0 +1,366 @@ +/** + * TransactionRepository — transaction history, inputs, outputs, and pending tx tracking. + * + * Replaces LocalCache keys: + * wallet_transactions_
+ * wallet_txs__ + *
-pendingTxs + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export interface TxRecord { + txid: string; + network: string; + blockHeight: number | null; + blockHash: string | null; + blockTime: number | null; + isConfirmed: boolean; + feeSats: number | null; + size: number | null; + weight: number | null; + version: number | null; + locktime: number | null; + rawJson: string; + fetchedAt: number; +} + +export interface TxAddressMapping { + txid: string; + network: string; + address: string; + netSats: number | null; +} + +export interface PendingTx { + txid: string; + network: string; + address: string; + rawJson: string; + createdAt: number; +} + +export interface PendingTxData { + txid: string; + from?: string; + to?: string; + satoshiAmount?: number; + satoshiFees?: number; + sentAt?: number; + [key: string]: unknown; +} + +class TransactionRepository { + // ── Confirmed / history transactions ──────────────────────────────────── + + upsertTransaction(tx: TxRecord, addresses: TxAddressMapping[]): void { + try { + database.transaction(svc => { + svc.execute( + `INSERT INTO transactions + (txid, network, block_height, block_hash, block_time, + is_confirmed, fee_sats, size, weight, version, locktime, + raw_json, fetched_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(txid, network) DO UPDATE SET + block_height = excluded.block_height, + block_hash = excluded.block_hash, + block_time = excluded.block_time, + is_confirmed = excluded.is_confirmed, + fee_sats = excluded.fee_sats, + size = excluded.size, + weight = excluded.weight, + raw_json = excluded.raw_json, + fetched_at = excluded.fetched_at`, + [ + tx.txid, tx.network, + tx.blockHeight ?? null, tx.blockHash ?? null, tx.blockTime ?? null, + tx.isConfirmed ? 1 : 0, + tx.feeSats ?? null, tx.size ?? null, tx.weight ?? null, + tx.version ?? null, tx.locktime ?? null, + tx.rawJson, tx.fetchedAt, + ], + ); + + for (const mapping of addresses) { + svc.execute( + `INSERT OR IGNORE INTO transaction_addresses + (txid, network, address, net_sats) + VALUES (?, ?, ?, ?)`, + [mapping.txid, mapping.network, mapping.address, mapping.netSats ?? null], + ); + } + }); + } catch (err) { + dbg('TransactionRepository.upsertTransaction error', err); + } + } + + /** + * Batch upsert — used by TransactionSyncer for large page fetches. + * All records written in one SQLite transaction. + */ + upsertTransactionBatch( + txs: Array<{tx: TxRecord; addresses: TxAddressMapping[]}>, + ): void { + if (!txs.length) return; + try { + database.transaction(svc => { + for (const {tx, addresses} of txs) { + svc.execute( + `INSERT INTO transactions + (txid, network, block_height, block_hash, block_time, + is_confirmed, fee_sats, size, weight, version, locktime, + raw_json, fetched_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(txid, network) DO UPDATE SET + block_height = excluded.block_height, + block_hash = excluded.block_hash, + block_time = excluded.block_time, + is_confirmed = excluded.is_confirmed, + fee_sats = excluded.fee_sats, + size = excluded.size, + weight = excluded.weight, + raw_json = excluded.raw_json, + fetched_at = excluded.fetched_at`, + [ + tx.txid, tx.network, + tx.blockHeight ?? null, tx.blockHash ?? null, tx.blockTime ?? null, + tx.isConfirmed ? 1 : 0, + tx.feeSats ?? null, tx.size ?? null, tx.weight ?? null, + tx.version ?? null, tx.locktime ?? null, + tx.rawJson, tx.fetchedAt, + ], + ); + + for (const m of addresses) { + svc.execute( + `INSERT OR IGNORE INTO transaction_addresses + (txid, network, address, net_sats) + VALUES (?, ?, ?, ?)`, + [m.txid, m.network, m.address, m.netSats ?? null], + ); + } + } + }); + } catch (err) { + dbg('TransactionRepository.upsertTransactionBatch error', err); + } + } + + /** Get all transactions touching a given address, ordered newest first. */ + getTransactionsForAddress( + address: string, + network: string, + limit = 50, + ): TxRecord[] { + try { + const {rows} = database.execute( + `SELECT t.* FROM transactions t + JOIN transaction_addresses ta + ON t.txid = ta.txid AND t.network = ta.network + WHERE ta.address = ? AND t.network = ? + ORDER BY t.block_time DESC NULLS FIRST, t.fetched_at DESC + LIMIT ?`, + [address, network, limit], + ); + return rows.map(this._rowToTx); + } catch (err) { + dbg('TransactionRepository.getTransactionsForAddress error', err); + return []; + } + } + + /** + * Get all transactions for a set of wallet addresses (HD wallet view). + * Addresses must all belong to the same network. + */ + getTransactionsForAddresses( + addresses: string[], + network: string, + limit = 200, + ): TxRecord[] { + if (!addresses.length) return []; + try { + const placeholders = addresses.map(() => '?').join(','); + const {rows} = database.execute( + `SELECT DISTINCT t.* FROM transactions t + JOIN transaction_addresses ta + ON t.txid = ta.txid AND t.network = ta.network + WHERE ta.address IN (${placeholders}) AND t.network = ? + ORDER BY t.block_time DESC NULLS FIRST, t.fetched_at DESC + LIMIT ?`, + [...addresses, network, limit], + ); + return rows.map(this._rowToTx); + } catch (err) { + dbg('TransactionRepository.getTransactionsForAddresses error', err); + return []; + } + } + + /** Mark a previously-unconfirmed transaction as confirmed. */ + markConfirmed( + txid: string, + network: string, + blockHeight: number, + blockTime: number, + blockHash?: string, + ): void { + try { + database.execute( + `UPDATE transactions + SET is_confirmed = 1, block_height = ?, block_time = ?, block_hash = ? + WHERE txid = ? AND network = ?`, + [blockHeight, blockTime, blockHash ?? null, txid, network], + ); + } catch (err) { + dbg('TransactionRepository.markConfirmed error', err); + } + } + + /** Check whether a txid is already stored. */ + hasTx(txid: string, network: string): boolean { + try { + const {rows} = database.execute( + 'SELECT 1 FROM transactions WHERE txid = ? AND network = ? LIMIT 1', + [txid, network], + ); + return rows.length > 0; + } catch { + return false; + } + } + + /** Return set of known txids for a network (for delta computation). */ + getKnownTxids(network: string): Set { + try { + const {rows} = database.execute( + 'SELECT txid FROM transactions WHERE network = ?', + [network], + ); + return new Set(rows.map(r => r.txid as string)); + } catch (err) { + dbg('TransactionRepository.getKnownTxids error', err); + return new Set(); + } + } + + // ── Pending transactions ───────────────────────────────────────────────── + + addPending(pending: PendingTx): void { + try { + database.execute( + `INSERT OR REPLACE INTO pending_transactions + (txid, network, address, raw_json, created_at) + VALUES (?, ?, ?, ?, ?)`, + [ + pending.txid, + pending.network, + pending.address, + pending.rawJson, + pending.createdAt, + ], + ); + } catch (err) { + dbg('TransactionRepository.addPending error', err); + } + } + + removePending(txid: string, network: string): void { + try { + database.execute( + 'DELETE FROM pending_transactions WHERE txid = ? AND network = ?', + [txid, network], + ); + } catch (err) { + dbg('TransactionRepository.removePending error', err); + } + } + + getPendingForAddress(address: string, network: string): PendingTx[] { + try { + const {rows} = database.execute( + `SELECT * FROM pending_transactions + WHERE address = ? AND network = ? + ORDER BY created_at DESC`, + [address, network], + ); + return rows.map(r => ({ + txid: r.txid as string, + network: r.network as string, + address: r.address as string, + rawJson: r.raw_json as string, + createdAt: r.created_at as number, + })); + } catch (err) { + dbg('TransactionRepository.getPendingForAddress error', err); + return []; + } + } + + getPendingTxMap( + address: string, + network: string, + ): Record { + const pending = this.getPendingForAddress(address, network); + const result: Record = {}; + for (const p of pending) { + try { + result[p.txid] = JSON.parse(p.rawJson) as PendingTxData; + } catch { + result[p.txid] = {txid: p.txid}; + } + } + return result; + } + + setPendingTxMap( + address: string, + network: string, + map: Record, + ): void { + try { + database.transaction(tx => { + tx.execute( + 'DELETE FROM pending_transactions WHERE address = ? AND network = ?', + [address, network], + ); + const now = Date.now(); + for (const [txid, data] of Object.entries(map)) { + tx.execute( + `INSERT OR REPLACE INTO pending_transactions + (txid, network, address, raw_json, created_at) + VALUES (?, ?, ?, ?, ?)`, + [txid, network, address, JSON.stringify(data), now], + ); + } + }); + } catch (err) { + dbg('TransactionRepository.setPendingTxMap error', err); + } + } + + // ── Helpers ────────────────────────────────────────────────────────────── + + private _rowToTx(r: Record): TxRecord { + return { + txid: r.txid as string, + network: r.network as string, + blockHeight: (r.block_height as number) ?? null, + blockHash: (r.block_hash as string) ?? null, + blockTime: (r.block_time as number) ?? null, + isConfirmed: (r.is_confirmed as number) === 1, + feeSats: (r.fee_sats as number) ?? null, + size: (r.size as number) ?? null, + weight: (r.weight as number) ?? null, + version: (r.version as number) ?? null, + locktime: (r.locktime as number) ?? null, + rawJson: r.raw_json as string, + fetchedAt: r.fetched_at as number, + }; + } +} + +const transactionRepository = new TransactionRepository(); +export default transactionRepository; diff --git a/services/repositories/UtxoRepository.ts b/services/repositories/UtxoRepository.ts new file mode 100644 index 00000000..a52e6300 --- /dev/null +++ b/services/repositories/UtxoRepository.ts @@ -0,0 +1,174 @@ +/** + * UtxoRepository — UTXO persistence. + * + * Previously UTXOs were only in MempoolClient's in-memory cache and lost on restart. + * This repository persists them to SQLite so the app can show the UTXO list offline + * and use them immediately for fee estimation without a network round-trip. + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export interface StoredUtxo { + txid: string; + vout: number; + address: string; + network: string; + valueSats: number; + scriptPubkey: string | null; + derivationPath: string | null; + isConfirmed: boolean; + blockHeight: number | null; + blockTime: number | null; + fetchedAt: number; +} + +class UtxoRepository { + /** + * Replace the entire UTXO set for an address atomically. + * mempool.space always returns the full current UTXO set so a full replace is correct. + */ + replaceUtxosForAddress( + address: string, + network: string, + utxos: StoredUtxo[], + ): void { + try { + database.transaction(tx => { + tx.execute( + 'DELETE FROM utxos WHERE address = ? AND network = ?', + [address, network], + ); + for (const u of utxos) { + tx.execute( + `INSERT INTO utxos + (txid, vout, address, network, value_sats, script_pubkey, + derivation_path, is_confirmed, block_height, block_time, fetched_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + u.txid, u.vout, u.address, u.network, + u.valueSats, u.scriptPubkey ?? null, + u.derivationPath ?? null, + u.isConfirmed ? 1 : 0, + u.blockHeight ?? null, + u.blockTime ?? null, + u.fetchedAt, + ], + ); + } + }); + } catch (err) { + dbg('UtxoRepository.replaceUtxosForAddress error', err); + } + } + + getUtxosForAddress(address: string, network: string): StoredUtxo[] { + try { + const {rows} = database.execute( + 'SELECT * FROM utxos WHERE address = ? AND network = ? ORDER BY fetched_at DESC', + [address, network], + ); + return rows.map(this._rowToUtxo); + } catch (err) { + dbg('UtxoRepository.getUtxosForAddress error', err); + return []; + } + } + + /** Get all UTXOs for a list of addresses (HD wallet spend flow). */ + getUtxosForAddresses(addresses: string[], network: string): StoredUtxo[] { + if (!addresses.length) return []; + try { + const placeholders = addresses.map(() => '?').join(','); + const {rows} = database.execute( + `SELECT * FROM utxos + WHERE address IN (${placeholders}) AND network = ? + ORDER BY value_sats DESC`, + [...addresses, network], + ); + return rows.map(this._rowToUtxo); + } catch (err) { + dbg('UtxoRepository.getUtxosForAddresses error', err); + return []; + } + } + + /** Delete all UTXOs for an address (used to force re-sync before a send). */ + invalidateAddress(address: string, network: string): void { + try { + database.execute( + 'DELETE FROM utxos WHERE address = ? AND network = ?', + [address, network], + ); + } catch (err) { + dbg('UtxoRepository.invalidateAddress error', err); + } + } + + /** + * Get every UTXO stored for a network, optionally scoped to a specific HD address type. + * Used when HD addresses have not been derived yet (e.g. key not yet loaded). + * + * Address type maps to BIP-purpose derivation path prefix: + * segwit-native → m/84' (P2WPKH, bech32) + * segwit → m/49' (P2SH-P2WPKH, wrapped segwit) + * legacy → m/44' (P2PKH) + * When omitted all stored UTXOs for the network are returned. + */ + getUtxosForNetwork(network: string, addressType?: string): StoredUtxo[] { + const purposeByType: Record = { + 'segwit-native': "m/84'", + segwit: "m/49'", + legacy: "m/44'", + }; + const pathPrefix = addressType ? purposeByType[addressType] : undefined; + try { + const {rows} = pathPrefix + ? database.execute( + `SELECT * FROM utxos + WHERE network = ? AND derivation_path LIKE ? + ORDER BY value_sats DESC`, + [network, `${pathPrefix}%`], + ) + : database.execute( + 'SELECT * FROM utxos WHERE network = ? ORDER BY value_sats DESC', + [network], + ); + return rows.map(this._rowToUtxo); + } catch (err) { + dbg('UtxoRepository.getUtxosForNetwork error', err); + return []; + } + } + + /** Total confirmed satoshis across all addresses for a network. */ + getTotalSats(network: string): number { + try { + const {rows} = database.execute( + 'SELECT COALESCE(SUM(value_sats), 0) AS total FROM utxos WHERE network = ? AND is_confirmed = 1', + [network], + ); + return rows.length ? (rows[0].total as number) : 0; + } catch { + return 0; + } + } + + private _rowToUtxo(r: Record): StoredUtxo { + return { + txid: r.txid as string, + vout: r.vout as number, + address: r.address as string, + network: r.network as string, + valueSats: r.value_sats as number, + scriptPubkey: (r.script_pubkey as string) ?? null, + derivationPath: (r.derivation_path as string) ?? null, + isConfirmed: (r.is_confirmed as number) === 1, + blockHeight: (r.block_height as number) ?? null, + blockTime: (r.block_time as number) ?? null, + fetchedAt: r.fetched_at as number, + }; + } +} + +const utxoRepository = new UtxoRepository(); +export default utxoRepository; diff --git a/services/repositories/WalletRepository.ts b/services/repositories/WalletRepository.ts new file mode 100644 index 00000000..9a96b83f --- /dev/null +++ b/services/repositories/WalletRepository.ts @@ -0,0 +1,324 @@ +/** + * WalletRepository — HD wallet state and address cache. + * + * Replaces: + * LocalCache keys: hd_external_index_*, hd_change_index_*, hd_max_used_external_*, + * hd_restore_done_*, hd_discovery_status_*, hd_discovery_last_at_* + * + * Also owns the new wallet_addresses table (previously recomputed every launch). + */ +import database from '../Database'; +import {dbg} from '../../utils'; + +export interface HdState { + network: string; + addressType: string; + externalIndex: number; + changeIndex: number; + maxUsedExternal: number; + restoreDone: boolean; + discoveryStatus: string | null; + discoveryLastAt: number | null; +} + +export interface WalletAddress { + network: string; + addressType: string; + chain: number; // 0 = receive, 1 = change + idx: number; + address: string; + isUsed: boolean; +} + +class WalletRepository { + // ── HD State ────────────────────────────────────────────────────────────── + + getHdState(network: string, addressType: string): HdState | null { + try { + const {rows} = database.execute( + 'SELECT * FROM hd_state WHERE network = ? AND address_type = ?', + [network, addressType], + ); + if (!rows.length) return null; + const r = rows[0]; + return { + network: r.network as string, + addressType: r.address_type as string, + externalIndex: r.external_index as number, + changeIndex: r.change_index as number, + maxUsedExternal: r.max_used_external as number, + restoreDone: (r.restore_done as number) === 1, + discoveryStatus: (r.discovery_status as string) ?? null, + discoveryLastAt: (r.discovery_last_at as number) ?? null, + }; + } catch (err) { + dbg('WalletRepository.getHdState error', err); + return null; + } + } + + /** Upsert the full HD state row for a network+addressType. */ + setHdState(state: HdState): void { + try { + database.execute( + `INSERT INTO hd_state + (network, address_type, external_index, change_index, + max_used_external, restore_done, discovery_status, discovery_last_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(network, address_type) DO UPDATE SET + external_index = excluded.external_index, + change_index = excluded.change_index, + max_used_external = excluded.max_used_external, + restore_done = excluded.restore_done, + discovery_status = excluded.discovery_status, + discovery_last_at = excluded.discovery_last_at`, + [ + state.network, + state.addressType, + state.externalIndex, + state.changeIndex, + state.maxUsedExternal, + state.restoreDone ? 1 : 0, + state.discoveryStatus ?? null, + state.discoveryLastAt ?? null, + ], + ); + } catch (err) { + dbg('WalletRepository.setHdState error', err); + } + } + + /** Convenience: read just the external (receive) index. */ + getExternalIndex(network: string, addressType: string): number { + try { + const {rows} = database.execute( + 'SELECT external_index FROM hd_state WHERE network = ? AND address_type = ?', + [network, addressType], + ); + return rows.length > 0 ? (rows[0].external_index as number) : 0; + } catch (err) { + dbg('WalletRepository.getExternalIndex error', err); + return 0; + } + } + + /** Convenience: read just the change index. */ + getChangeIndex(network: string, addressType: string): number { + try { + const {rows} = database.execute( + 'SELECT change_index FROM hd_state WHERE network = ? AND address_type = ?', + [network, addressType], + ); + return rows.length > 0 ? (rows[0].change_index as number) : 0; + } catch (err) { + dbg('WalletRepository.getChangeIndex error', err); + return 0; + } + } + + /** Convenience: read max_used_external. */ + getMaxUsedExternal(network: string, addressType: string): number { + try { + const {rows} = database.execute( + 'SELECT max_used_external FROM hd_state WHERE network = ? AND address_type = ?', + [network, addressType], + ); + return rows.length > 0 ? (rows[0].max_used_external as number) : 0; + } catch (err) { + dbg('WalletRepository.getMaxUsedExternal error', err); + return 0; + } + } + + /** Patch a single integer field in hd_state, creating the row if absent. */ + private patchHdField( + network: string, + addressType: string, + field: string, + value: number | string | null, + ): void { + // Ensure row exists first + database.execute( + `INSERT OR IGNORE INTO hd_state (network, address_type) VALUES (?, ?)`, + [network, addressType], + ); + database.execute( + `UPDATE hd_state SET ${field} = ? WHERE network = ? AND address_type = ?`, + [value, network, addressType], + ); + } + + setExternalIndex(network: string, addressType: string, value: number): void { + try { + this.patchHdField(network, addressType, 'external_index', Math.max(0, Math.floor(value))); + dbg('WalletRepository: setExternalIndex', {network, addressType, value}); + } catch (err) { + dbg('WalletRepository.setExternalIndex error', err); + } + } + + setChangeIndex(network: string, addressType: string, value: number): void { + try { + this.patchHdField(network, addressType, 'change_index', Math.max(0, Math.floor(value))); + dbg('WalletRepository: setChangeIndex', {network, addressType, value}); + } catch (err) { + dbg('WalletRepository.setChangeIndex error', err); + } + } + + setMaxUsedExternal(network: string, addressType: string, value: number): void { + try { + this.patchHdField(network, addressType, 'max_used_external', Math.max(0, Math.floor(value))); + } catch (err) { + dbg('WalletRepository.setMaxUsedExternal error', err); + } + } + + incrementChangeIndex(network: string, addressType: string): number { + try { + const current = this.getChangeIndex(network, addressType); + const next = current + 1; + this.setChangeIndex(network, addressType, next); + dbg('WalletRepository: incrementChangeIndex', {network, addressType, next}); + return next; + } catch (err) { + dbg('WalletRepository.incrementChangeIndex error', err); + return 0; + } + } + + setRestoreDone(network: string, addressType: string, done: boolean): void { + try { + this.patchHdField(network, addressType, 'restore_done', done ? 1 : 0); + } catch (err) { + dbg('WalletRepository.setRestoreDone error', err); + } + } + + setDiscoveryStatus( + network: string, + addressType: string, + status: string, + lastAt?: number, + ): void { + try { + database.execute( + `INSERT OR IGNORE INTO hd_state (network, address_type) VALUES (?, ?)`, + [network, addressType], + ); + database.execute( + `UPDATE hd_state + SET discovery_status = ?, discovery_last_at = ? + WHERE network = ? AND address_type = ?`, + [status, lastAt ?? Date.now(), network, addressType], + ); + } catch (err) { + dbg('WalletRepository.setDiscoveryStatus error', err); + } + } + + // ── Wallet Addresses ────────────────────────────────────────────────────── + + upsertAddress(addr: WalletAddress): void { + try { + database.execute( + `INSERT INTO wallet_addresses + (network, address_type, chain, idx, address, is_used) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(network, address_type, chain, idx) DO UPDATE SET + address = excluded.address, + is_used = excluded.is_used`, + [ + addr.network, + addr.addressType, + addr.chain, + addr.idx, + addr.address, + addr.isUsed ? 1 : 0, + ], + ); + } catch (err) { + dbg('WalletRepository.upsertAddress error', err); + } + } + + upsertAddressBatch(addrs: WalletAddress[]): void { + if (!addrs.length) return; + try { + database.transaction(tx => { + for (const addr of addrs) { + tx.execute( + `INSERT INTO wallet_addresses + (network, address_type, chain, idx, address, is_used) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(network, address_type, chain, idx) DO UPDATE SET + address = excluded.address, + is_used = excluded.is_used`, + [ + addr.network, + addr.addressType, + addr.chain, + addr.idx, + addr.address, + addr.isUsed ? 1 : 0, + ], + ); + } + }); + } catch (err) { + dbg('WalletRepository.upsertAddressBatch error', err); + } + } + + getAddresses( + network: string, + addressType: string, + chain: number, + ): WalletAddress[] { + try { + const {rows} = database.execute( + `SELECT * FROM wallet_addresses + WHERE network = ? AND address_type = ? AND chain = ? + ORDER BY idx ASC`, + [network, addressType, chain], + ); + return rows.map(r => ({ + network: r.network as string, + addressType: r.address_type as string, + chain: r.chain as number, + idx: r.idx as number, + address: r.address as string, + isUsed: (r.is_used as number) === 1, + })); + } catch (err) { + dbg('WalletRepository.getAddresses error', err); + return []; + } + } + + markAddressUsed(address: string): void { + try { + database.execute( + 'UPDATE wallet_addresses SET is_used = 1 WHERE address = ?', + [address], + ); + } catch (err) { + dbg('WalletRepository.markAddressUsed error', err); + } + } + + isAddressKnown(address: string): boolean { + try { + const {rows} = database.execute( + 'SELECT 1 FROM wallet_addresses WHERE address = ? LIMIT 1', + [address], + ); + return rows.length > 0; + } catch { + return false; + } + } +} + +const walletRepository = new WalletRepository(); +export default walletRepository; diff --git a/services/sync/BalanceSyncer.ts b/services/sync/BalanceSyncer.ts new file mode 100644 index 00000000..31d935b7 --- /dev/null +++ b/services/sync/BalanceSyncer.ts @@ -0,0 +1,71 @@ +/** + * BalanceSyncer — fetches per-address confirmed + mempool balances and writes to SQLite. + * + * Sync schedule: on app foreground + every 30 s. + * Sequential per address to avoid mempool.space rate limits. + */ +import mempoolClient from '../MempoolClient'; +import balanceRepository from '../repositories/BalanceRepository'; +import syncRepository from '../repositories/SyncRepository'; +import {dbg} from '../../utils'; + +export interface AddressEntry { + address: string; + network: string; +} + +interface MempoolAddressResponse { + chain_stats: {funded_txo_sum: number; spent_txo_sum: number}; + mempool_stats: {funded_txo_sum: number; spent_txo_sum: number}; +} + +class BalanceSyncer { + async syncAddresses( + addresses: AddressEntry[], + apiBase: string, + ): Promise { + if (!addresses.length) return; + const cleanApi = apiBase.replace(/\/+$/, ''); + + for (const {address, network} of addresses) { + try { + const url = `${cleanApi}/address/${encodeURIComponent(address)}`; + const res = await mempoolClient.get(url); + if (!res.ok || !res.data) continue; + + const {chain_stats, mempool_stats} = res.data; + const confirmedSats = + (chain_stats.funded_txo_sum ?? 0) - + (chain_stats.spent_txo_sum ?? 0); + const pendingSats = + (mempool_stats.funded_txo_sum ?? 0) - + (mempool_stats.spent_txo_sum ?? 0); + const balanceSats = Math.max(0, confirmedSats); + const now = Date.now(); + + balanceRepository.setBalance({ + address, + network, + balanceSats, + pendingSats, + hasNonzero: balanceSats > 0 || pendingSats > 0, + fetchedAt: now, + }); + + syncRepository.updateCursor('balance', `${address}_${network}`, null, 'ok'); + dbg('BalanceSyncer: synced', address.slice(0, 10), balanceSats, 'sats'); + } catch (err) { + dbg('BalanceSyncer: error for', address.slice(0, 10), err); + syncRepository.updateCursor( + 'balance', + `${address}_${network}`, + null, + 'failed', + ); + } + } + } +} + +const balanceSyncer = new BalanceSyncer(); +export default balanceSyncer; diff --git a/services/sync/PriceSyncer.ts b/services/sync/PriceSyncer.ts new file mode 100644 index 00000000..7439ae70 --- /dev/null +++ b/services/sync/PriceSyncer.ts @@ -0,0 +1,38 @@ +/** + * PriceSyncer — fetches live BTC/fiat rates from mempool.space. + * + * Sync schedule: every 60 s. + * Endpoint: GET /v1/prices + */ +import mempoolClient from '../MempoolClient'; +import priceRepository from '../repositories/PriceRepository'; +import {dbg} from '../../utils'; + +class PriceSyncer { + async syncCurrentPrice(apiBase: string): Promise { + // Always use mainnet price endpoint + const base = apiBase.replace(/\/+$/, '').replace(/\/testnet\/?/, ''); + const url = `${base}/v1/prices`; + try { + const res = await mempoolClient.get>(url); + if (!res.ok || !res.data) return; + + const rates: Record = {}; + for (const [currency, value] of Object.entries(res.data)) { + if (typeof value === 'number' && isFinite(value) && value > 0) { + rates[currency] = value; + } + } + + if (Object.keys(rates).length) { + priceRepository.setCurrentRates(rates); + dbg('PriceSyncer: rates updated', Object.keys(rates)); + } + } catch (err) { + dbg('PriceSyncer: error fetching prices', err); + } + } +} + +const priceSyncer = new PriceSyncer(); +export default priceSyncer; diff --git a/services/sync/SyncCoordinator.ts b/services/sync/SyncCoordinator.ts new file mode 100644 index 00000000..a770978f --- /dev/null +++ b/services/sync/SyncCoordinator.ts @@ -0,0 +1,161 @@ +/** + * SyncCoordinator — orchestrates all background sync workers. + * + * Architecture: + * - UI reads exclusively from SQLite repositories (zero direct API calls from UI layer). + * - SyncCoordinator runs on app foreground and on a timer, fetching from mempool.space + * and writing deltas to SQLite. + * - Sync failures are silent (sync_metadata.sync_status = 'failed') and retried + * on the next foreground event. + * + * Usage: + * import syncCoordinator from './services/sync/SyncCoordinator'; + * + * // Call once when app foregrounds (e.g. AppState 'active'): + * syncCoordinator.start(addresses, network, apiBase); + * + * // Optional: stop all timers (e.g. when app backgrounds fully): + * syncCoordinator.stop(); + */ +import {AppState, type AppStateStatus} from 'react-native'; +import balanceSyncer, {type AddressEntry as BalanceEntry} from './BalanceSyncer'; +import transactionSyncer from './TransactionSyncer'; +import utxoSyncer, {type AddressEntry as UtxoEntry} from './UtxoSyncer'; +import priceSyncer from './PriceSyncer'; +import {dbg} from '../../utils'; + +const BALANCE_INTERVAL_MS = 30_000; +const PRICE_INTERVAL_MS = 60_000; +const UTXO_INTERVAL_MS = 60_000; +const TX_INTERVAL_MS = 120_000; + +export interface SyncConfig { + addresses: Array<{address: string; network: string; derivationPath?: string}>; + network: string; + apiBase: string; + /** Optional callback — called after each sync cycle completes. */ + onSyncComplete?: () => void; +} + +class SyncCoordinator { + private _config: SyncConfig | null = null; + private _balanceTimer: ReturnType | null = null; + private _txTimer: ReturnType | null = null; + private _utxoTimer: ReturnType | null = null; + private _priceTimer: ReturnType | null = null; + private _appStateSubscription: ReturnType | null = null; + private _running = false; + + /** + * Start background sync with the given config. + * Safe to call multiple times — stops previous timers first. + */ + start(config: SyncConfig): void { + this.stop(); + this._config = config; + this._running = true; + + // Immediate first sync + this._syncAll(); + + // Schedule periodic syncs + this._balanceTimer = setInterval(() => this._syncBalances(), BALANCE_INTERVAL_MS); + this._priceTimer = setInterval(() => this._syncPrice(), PRICE_INTERVAL_MS); + this._utxoTimer = setInterval(() => this._syncUtxos(), UTXO_INTERVAL_MS); + this._txTimer = setInterval(() => this._syncTxs(), TX_INTERVAL_MS); + + // Resume sync when app comes to foreground + this._appStateSubscription = AppState.addEventListener( + 'change', + (state: AppStateStatus) => { + if (state === 'active' && this._running) { + dbg('SyncCoordinator: app foregrounded — syncing'); + this._syncAll(); + } + }, + ); + + dbg('SyncCoordinator: started', config.addresses.length, 'addresses on', config.network); + } + + /** Update the config without restarting timers (e.g. addresses changed). */ + updateConfig(config: SyncConfig): void { + this._config = config; + } + + /** Stop all timers and release listeners. */ + stop(): void { + this._running = false; + if (this._balanceTimer) { clearInterval(this._balanceTimer); this._balanceTimer = null; } + if (this._txTimer) { clearInterval(this._txTimer); this._txTimer = null; } + if (this._utxoTimer) { clearInterval(this._utxoTimer); this._utxoTimer = null; } + if (this._priceTimer) { clearInterval(this._priceTimer); this._priceTimer = null; } + if (this._appStateSubscription) { + this._appStateSubscription.remove(); + this._appStateSubscription = null; + } + dbg('SyncCoordinator: stopped'); + } + + // ── Private helpers ──────────────────────────────────────────────────────── + + private async _syncAll(): Promise { + await Promise.all([ + this._syncBalances(), + this._syncTxs(), + this._syncUtxos(), + this._syncPrice(), + ]); + this._config?.onSyncComplete?.(); + } + + private async _syncBalances(): Promise { + if (!this._config) return; + try { + const entries: BalanceEntry[] = this._config.addresses.map(a => ({ + address: a.address, + network: a.network, + })); + await balanceSyncer.syncAddresses(entries, this._config.apiBase); + } catch (err) { + dbg('SyncCoordinator: balance sync error', err); + } + } + + private async _syncTxs(): Promise { + if (!this._config) return; + try { + for (const {address, network} of this._config.addresses) { + await transactionSyncer.syncAddress(address, network, this._config.apiBase); + } + } catch (err) { + dbg('SyncCoordinator: tx sync error', err); + } + } + + private async _syncUtxos(): Promise { + if (!this._config) return; + try { + const entries: UtxoEntry[] = this._config.addresses.map(a => ({ + address: a.address, + network: a.network, + derivationPath: a.derivationPath, + })); + await utxoSyncer.syncAddresses(entries, this._config.apiBase); + } catch (err) { + dbg('SyncCoordinator: utxo sync error', err); + } + } + + private async _syncPrice(): Promise { + if (!this._config) return; + try { + await priceSyncer.syncCurrentPrice(this._config.apiBase); + } catch (err) { + dbg('SyncCoordinator: price sync error', err); + } + } +} + +const syncCoordinator = new SyncCoordinator(); +export default syncCoordinator; diff --git a/services/sync/TransactionSyncer.ts b/services/sync/TransactionSyncer.ts new file mode 100644 index 00000000..8e69325e --- /dev/null +++ b/services/sync/TransactionSyncer.ts @@ -0,0 +1,159 @@ +/** + * TransactionSyncer — incremental transaction history fetch using mempool.space chain cursors. + * + * The mempool.space API supports paginated fetch: + * GET /address/:addr/txs/chain/:last_txid + * The cursor stored in sync_metadata is the txid of the last confirmed tx in the + * previous page. Pass it to get the next page of 25 txs. + * + * Sync schedule: on app foreground (incremental, only new txs). + * Full re-fetch only after keyshare import (restore_done=0). + */ +import mempoolClient from '../MempoolClient'; +import transactionRepository from '../repositories/TransactionRepository'; +import syncRepository from '../repositories/SyncRepository'; +import {dbg} from '../../utils'; +import type {TxRecord, TxAddressMapping} from '../repositories/TransactionRepository'; + +const PAGE_SIZE = 25; + +interface ApiTx { + txid: string; + status?: { + confirmed?: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + }; + fee?: number; + size?: number; + weight?: number; + version?: number; + locktime?: number; + vin?: Array<{prevout?: {scriptpubkey_address?: string; value?: number}}>; + vout?: Array<{scriptpubkey_address?: string; value?: number}>; +} + +class TransactionSyncer { + /** + * Incrementally fetch new transactions for an address. + * Stops when a page returns no new txids (delta = empty). + */ + async syncAddress( + address: string, + network: string, + apiBase: string, + ): Promise { + const cleanApi = apiBase.replace(/\/+$/, ''); + const entityKey = `${address}_${network}`; + let cursor = syncRepository.getCursor('transactions', entityKey); + const knownTxids = transactionRepository.getKnownTxids(network); + let newCount = 0; + let pages = 0; + const maxPages = 20; // safety limit per sync cycle + + try { + while (pages < maxPages) { + const url = cursor + ? `${cleanApi}/address/${encodeURIComponent(address)}/txs/chain/${cursor}` + : `${cleanApi}/address/${encodeURIComponent(address)}/txs`; + + const res = await mempoolClient.get(url); + if (!res.ok || !Array.isArray(res.data) || !res.data.length) break; + + const page = res.data as ApiTx[]; + const batch: Array<{tx: TxRecord; addresses: TxAddressMapping[]}> = []; + let hasNew = false; + + for (const apiTx of page) { + if (!apiTx.txid) continue; + if (knownTxids.has(apiTx.txid)) { + // If already known and now confirmed, update confirmation status + if ( + apiTx.status?.confirmed && + apiTx.status.block_height && + apiTx.status.block_time + ) { + transactionRepository.markConfirmed( + apiTx.txid, + network, + apiTx.status.block_height, + apiTx.status.block_time, + apiTx.status.block_hash, + ); + } + continue; + } + + hasNew = true; + knownTxids.add(apiTx.txid); + newCount++; + + const netSats = this._computeNetSats(apiTx, address); + const txRecord: TxRecord = { + txid: apiTx.txid, + network, + blockHeight: apiTx.status?.block_height ?? null, + blockHash: apiTx.status?.block_hash ?? null, + blockTime: apiTx.status?.block_time ?? null, + isConfirmed: apiTx.status?.confirmed ?? false, + feeSats: apiTx.fee ?? null, + size: apiTx.size ?? null, + weight: apiTx.weight ?? null, + version: apiTx.version ?? null, + locktime: apiTx.locktime ?? null, + rawJson: JSON.stringify(apiTx), + fetchedAt: Date.now(), + }; + batch.push({ + tx: txRecord, + addresses: [{txid: apiTx.txid, network, address, netSats}], + }); + } + + if (batch.length) { + transactionRepository.upsertTransactionBatch(batch); + } + + // Advance cursor to last confirmed txid in the page + const lastConfirmed = [...page] + .reverse() + .find(tx => tx.status?.confirmed); + if (lastConfirmed) { + cursor = lastConfirmed.txid; + syncRepository.updateCursor('transactions', entityKey, cursor, 'partial'); + } + + pages++; + if (!hasNew || page.length < PAGE_SIZE) break; + } + + syncRepository.updateCursor('transactions', entityKey, cursor ?? null, 'ok'); + dbg('TransactionSyncer: synced', address.slice(0, 10), newCount, 'new txs'); + } catch (err) { + dbg('TransactionSyncer: error for', address.slice(0, 10), err); + syncRepository.updateCursor('transactions', entityKey, cursor ?? null, 'failed'); + } + } + + private _computeNetSats(apiTx: ApiTx, address: string): number | null { + try { + const sent = (apiTx.vin ?? []).reduce((sum, vin) => { + return vin.prevout?.scriptpubkey_address === address + ? sum + (vin.prevout?.value ?? 0) + : sum; + }, 0); + const received = (apiTx.vout ?? []).reduce((sum, vout) => { + return vout.scriptpubkey_address === address + ? sum + (vout.value ?? 0) + : sum; + }, 0); + return received - sent; + } catch { + return null; + } + } +} + +const transactionSyncer = new TransactionSyncer(); +export default transactionSyncer; diff --git a/services/sync/UtxoSyncer.ts b/services/sync/UtxoSyncer.ts new file mode 100644 index 00000000..fca93692 --- /dev/null +++ b/services/sync/UtxoSyncer.ts @@ -0,0 +1,78 @@ +/** + * UtxoSyncer — full replace UTXO set for each address from mempool.space. + * + * mempool.space always returns the complete current UTXO set for an address, + * so a full delete + insert is the correct strategy. + * + * Sync schedule: on app foreground + before any send operation. + */ +import mempoolClient from '../MempoolClient'; +import utxoRepository from '../repositories/UtxoRepository'; +import syncRepository from '../repositories/SyncRepository'; +import {dbg} from '../../utils'; +import type {StoredUtxo} from '../repositories/UtxoRepository'; + +interface ApiUtxo { + txid: string; + vout: number; + value: number; + status?: { + confirmed?: boolean; + block_height?: number; + block_hash?: string; + block_time?: number; + }; +} + +export interface AddressEntry { + address: string; + network: string; + derivationPath?: string; +} + +class UtxoSyncer { + async syncAddresses( + addresses: AddressEntry[], + apiBase: string, + ): Promise { + if (!addresses.length) return; + const cleanApi = apiBase.replace(/\/+$/, ''); + + for (const {address, network, derivationPath} of addresses) { + try { + const url = `${cleanApi}/address/${encodeURIComponent(address)}/utxo`; + const res = await mempoolClient.get(url); + if (!res.ok || !Array.isArray(res.data)) continue; + + const now = Date.now(); + const utxos: StoredUtxo[] = res.data.map(u => ({ + txid: u.txid, + vout: u.vout, + address, + network, + valueSats: u.value, + scriptPubkey: null, + derivationPath: derivationPath ?? null, + isConfirmed: u.status?.confirmed ?? true, + blockHeight: u.status?.block_height ?? null, + fetchedAt: now, + })); + + utxoRepository.replaceUtxosForAddress(address, network, utxos); + syncRepository.updateCursor('utxos', `${address}_${network}`, null, 'ok'); + dbg('UtxoSyncer: synced', address.slice(0, 10), utxos.length, 'UTXOs'); + } catch (err) { + dbg('UtxoSyncer: error for', address.slice(0, 10), err); + syncRepository.updateCursor( + 'utxos', + `${address}_${network}`, + null, + 'failed', + ); + } + } + } +} + +const utxoSyncer = new UtxoSyncer(); +export default utxoSyncer; diff --git a/theme/context.tsx b/theme/context.tsx index 06bcca41..2c116374 100644 --- a/theme/context.tsx +++ b/theme/context.tsx @@ -12,7 +12,7 @@ import React, { ReactNode, } from 'react'; import {Appearance, ColorSchemeName} from 'react-native'; -import LocalCache from '../services/LocalCache'; +import appConfigRepository, {CONFIG_KEYS} from '../services/repositories/AppConfigRepository'; import {dbg} from '../utils'; import type {Theme, ThemeMode, ThemeContextValue} from './types'; import {lightTheme, darkTheme} from './themes'; @@ -74,9 +74,9 @@ export const ThemeProvider: React.FC = ({children}) => { ); // Load theme mode from storage IMMEDIATELY and update theme right away useEffect(() => { - const loadThemeMode = async () => { + const loadThemeMode = () => { try { - const storedMode = await LocalCache.getItem('themeMode'); + const storedMode = appConfigRepository.get(CONFIG_KEYS.THEME_MODE); if ( storedMode === 'os' || storedMode === 'light' || @@ -84,24 +84,15 @@ export const ThemeProvider: React.FC = ({children}) => { ) { const mode = storedMode as ThemeMode; setThemeModeState(mode); - // Immediately update theme when mode is loaded const effectiveTheme = getEffectiveTheme(mode, initialSystemScheme); setTheme(effectiveTheme); dbg('Theme mode loaded:', mode); } else { - // Check for legacy theme preference - const storedTheme = await LocalCache.getItem('theme'); - if (storedTheme === 'cryptoVibrant') { - setThemeModeState('light'); - setTheme(lightTheme); - dbg('Migrated from legacy cryptoVibrant theme'); - } else { - setThemeModeState('os'); - const osTheme = - initialSystemScheme === 'dark' ? darkTheme : lightTheme; - setTheme(osTheme); - dbg('Using default OS theme mode'); - } + setThemeModeState('os'); + const osTheme = + initialSystemScheme === 'dark' ? darkTheme : lightTheme; + setTheme(osTheme); + dbg('Using default OS theme mode'); } } catch (error) { dbg('Error loading theme mode:', error); @@ -111,7 +102,6 @@ export const ThemeProvider: React.FC = ({children}) => { setTheme(osTheme); } }; - // Load immediately, don't wait loadThemeMode(); }, [getEffectiveTheme, initialSystemScheme]); // Update theme when mode or system color scheme changes @@ -125,7 +115,7 @@ export const ThemeProvider: React.FC = ({children}) => { async (mode: ThemeMode) => { setThemeModeState(mode); try { - await LocalCache.setItem('themeMode', mode); + appConfigRepository.set(CONFIG_KEYS.THEME_MODE, mode); dbg('Theme mode saved:', mode); } catch (error) { dbg('Error saving theme mode:', error); diff --git a/utils.js b/utils.js index d84c4f38..8da5d340 100644 --- a/utils.js +++ b/utils.js @@ -116,6 +116,12 @@ export const dbg = (message, ...optionalParams) => { export const shorten = (x, y = 12) => `${x.slice(0, y)}...${x.slice(-y)}`; +/** Shorten a Bitcoin address for display: first 4 + ... + last 4. */ +export const shortenAddress = (addr) => + typeof addr === 'string' && addr.length > 8 + ? `${addr.slice(0, 4)}...${addr.slice(-4)}` + : addr || ''; + /** * Generate all output descriptors (legacy, segwit-native, segwit-compatible) * @param {Object} nativeModule - The BBMTLibNativeModule instance @@ -267,6 +273,32 @@ export const getDerivePathForNetwork = (network, addressType = 'legacy', useLega return `m/${bipPath}/${coinType}/${account}'/${change}/${index}`; }; +/** Standard BIP44/BIP84/BIP49 gap limit for address discovery (e.g. restore). */ +export const GAP_LIMIT = 5; + +/** Minimum indices to always scan for both receive and change chains. Ensures path 0 and old paths are never missed. */ +export const MIN_SCAN_INDEX = 20; + +/** + * Derivation path for receive (external) address at given index. + * @param {string} network - 'mainnet' or 'testnet3' + * @param {string} addressType - 'legacy' | 'segwit-native' | 'segwit-compatible' + * @param {boolean} useLegacyPath - legacy wallet path + * @param {number} index - address index (default 0) + */ +export const getReceivePath = (network, addressType, useLegacyPath, index = 0) => + getDerivePathForNetwork(network, addressType, useLegacyPath, 0, 0, index); + +/** + * Derivation path for change (internal) address at given index. + * @param {string} network - 'mainnet' or 'testnet3' + * @param {string} addressType - 'legacy' | 'segwit-native' | 'segwit-compatible' + * @param {boolean} useLegacyPath - legacy wallet path + * @param {number} index - change address index (default 0) + */ +export const getChangePath = (network, addressType, useLegacyPath, index = 0) => + getDerivePathForNetwork(network, addressType, useLegacyPath, 0, 1, index); + /** * Convert a hex string to a regular string * @param {string} hex - The hex string to convert @@ -824,26 +856,41 @@ export const getKeyshareLabel = keyshare => { /** * Encode send bitcoin data into QR code format - * 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) * @param {string} addressType - Address type (e.g., 'segwit-native', 'legacy', 'segwit-compatible') * @param {string} derivationPath - Derivation path (e.g., "m/84'/0'/0'/0/0") + * @param {string} network - Network identifier (e.g., 'mainnet', 'testnet3') + * @param {string} utxosJson - Optional JSON string of utxosWithPaths used for spending/fee (may be large) + * @param {string} changeAddress - Optional pre-computed change address (ensures both devices use the same change output) * @returns {string} - Encoded QR data string */ -export const encodeSendBitcoinQR = (toAddress, amountSats, feeSats, spendingHash = '', addressType = '', derivationPath = '', network = '') => { +export const encodeSendBitcoinQR = ( + toAddress, + amountSats, + feeSats, + spendingHash = '', + addressType = '', + derivationPath = '', + network = '', + utxosJson = '', + changeAddress = '', +) => { const amount = typeof amountSats === 'string' ? amountSats : amountSats.toString(); const fee = typeof feeSats === 'string' ? feeSats : feeSats.toString(); - return `${toAddress}|${amount}|${fee}|${spendingHash || ''}|${addressType || ''}|${derivationPath || ''}|${network || ''}`; + return `${toAddress}|${amount}|${fee}|${spendingHash || ''}|${addressType || ''}|${derivationPath || ''}|${network || ''}|${utxosJson || ''}|${changeAddress || ''}`; }; /** * Decode send bitcoin data from QR code format - * Format (newest): |||||| - * Format (new): ||||| - * Format (old): ||| + * Format (v5): |||||||| + * Format (v4): ||||||| + * Format (v3): |||||| + * Format (v2): ||||| + * Format (v1): ||| * @param {string} qrData - QR code data string * @returns {Object|null} - Decoded data object or null if invalid */ @@ -853,15 +900,22 @@ export const decodeSendBitcoinQR = (qrData) => { } const parts = qrData.split('|'); - // Support old format (3-4 parts), new format (6 parts), and newest format (7 parts) - if (parts.length < 3 || parts.length > 7) { + // Support all versions (3–9 parts) + if (parts.length < 3 || parts.length > 9) { return null; } - // Old format: ||| - // New format: ||||| - // Newest format: |||||| - const [toAddress, amountSats, feeSats, spendingHash = '', addressType = '', derivationPath = '', network = ''] = parts; + const [ + toAddress, + amountSats, + feeSats, + spendingHash = '', + addressType = '', + derivationPath = '', + network = '', + utxosJson = '', + changeAddress = '', + ] = parts; // Validate address is not empty if (!toAddress || toAddress.trim() === '') { @@ -883,5 +937,7 @@ export const decodeSendBitcoinQR = (qrData) => { addressType: addressType || '', derivationPath: derivationPath || '', network: network || '', + utxosJson: utxosJson || '', + changeAddress: changeAddress || '', }; }; diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..429b03b4 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7390 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.28.6", "@babel/code-frame@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.29.0.tgz#7cd7a59f15b3cc0dcd803038f7792712a7d0b15c" + integrity sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw== + dependencies: + "@babel/helper-validator-identifier" "^7.28.5" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.28.6", "@babel/compat-data@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.29.0.tgz#00d03e8c0ac24dd9be942c5370990cbe1f17d88d" + integrity sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9", "@babel/core@^7.25.2": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.29.0.tgz#5286ad785df7f79d656e88ce86e650d16ca5f322" + integrity sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helpers" "^7.28.6" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/remapping" "^2.3.5" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/eslint-parser@^7.25.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.28.6.tgz#6a294a4add732ebe7ded8a8d2792dd03dd81dc3f" + integrity sha512-QGmsKi2PBO/MHSQk+AAgA9R6OHQr+VqnniFE0eMWZcVcfBZoA2dKn2hUsl3Csg/Plt9opRUWdY7//VXsrIlEiA== + dependencies: + "@nicolo-ribaudo/eslint-scope-5-internals" "5.1.1-v1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.1" + +"@babel/generator@^7.29.0", "@babel/generator@^7.29.1", "@babel/generator@^7.7.2": + version "7.29.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.29.1.tgz#d09876290111abbb00ef962a7b83a5307fba0d50" + integrity sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw== + dependencies: + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== + dependencies: + "@babel/types" "^7.27.3" + +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2", "@babel/helper-compilation-targets@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz#32c4a3f41f12ed1532179b108a4d746e105c2b25" + integrity sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz#611ff5482da9ef0db6291bcd24303400bca170fb" + integrity sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1", "@babel/helper-create-regexp-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz#7c1ddd64b2065c7f78034b25b43346a7e19ed997" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.5", "@babel/helper-define-polyfill-provider@^0.6.7": + version "0.6.7" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz#8d01cba97de419115ad3497573a476db15dc6c6a" + integrity sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + debug "^4.4.3" + lodash.debounce "^4.0.8" + resolve "^1.22.11" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz#b9430df2aa4e17bc28665eadeae8aa1d985e6674" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz#f3e07a10be37ed7a63461c63e6929575945a6150" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz#60632cbd6ffb70b22823187201116762a03e2d5c" + integrity sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw== + dependencies: + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz#9312d9d9e56edc35aeb6e95c25d4106b50b9eb1e" + integrity sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.6" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.28.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz#6f13ea251b68c8532e985fd532f28741a8af9ac8" + integrity sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1", "@babel/helper-replace-supers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz#94aa9a1d7423a00aead3f204f78834ce7d53fe44" + integrity sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.28.6" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz#4e349ff9222dab69a93a019cc296cdd8442e279a" + integrity sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ== + dependencies: + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/helpers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.28.6.tgz#fca903a313ae675617936e8998b814c415cbf5d7" + integrity sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw== + dependencies: + "@babel/template" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.3", "@babel/parser@^7.28.6", "@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== + dependencies: + "@babel/types" "^7.29.0" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz#fbde57974707bbfa0376d34d425ff4fa6c732421" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz#0e8289cec28baaf05d54fd08d81ae3676065f69f" + integrity sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-proposal-export-default-from@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz#59b050b0e5fdc366162ab01af4fcbac06ea40919" + integrity sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-default-from@^7.24.7": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz#8e19047560a8a48b11f1f5b46881f445f8692830" + integrity sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-flow@^7.12.1", "@babel/plugin-syntax-flow@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz#447559a225e66c4cd477a3ffb1a74d8c1fe25a62" + integrity sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-assertions@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz#ae9bc1923a6ba527b70104dd2191b0cd872c8507" + integrity sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-attributes@^7.24.7", "@babel/plugin-syntax-import-attributes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz#b71d5914665f60124e133696f17cd7669062c503" + integrity sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.27.1", "@babel/plugin-syntax-jsx@^7.28.6", "@babel/plugin-syntax-jsx@^7.7.2": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz#f8ca28bbd84883b5fea0e447c635b81ba73997ee" + integrity sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.28.6", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz#c7b2ddf1d0a811145b1de800d1abd146af92e3a2" + integrity sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@7.27.1", "@babel/plugin-transform-arrow-functions@^7.24.7", "@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.25.4", "@babel/plugin-transform-async-generator-functions@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz#63ed829820298f0bf143d5a4a68fb8c06ffd742f" + integrity sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-async-to-generator@^7.24.7", "@babel/plugin-transform-async-to-generator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz#bd97b42237b2d1bc90d74bcb486c39be5b4d7e77" + integrity sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.25.0", "@babel/plugin-transform-block-scoping@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz#e1ef5633448c24e76346125c2534eeb359699a99" + integrity sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-properties@7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-class-properties@^7.25.4", "@babel/plugin-transform-class-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz#d274a4478b6e782d9ea987fda09bdb6d28d66b72" + integrity sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-class-static-block@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz#1257491e8259c6d125ac4d9a6f39f9d2bf3dba70" + integrity sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-classes@7.28.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz#75d66175486788c56728a73424d67cbc7473495c" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.28.4" + +"@babel/plugin-transform-classes@^7.25.4", "@babel/plugin-transform-classes@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz#8f6fb79ba3703978e701ce2a97e373aae7dda4b7" + integrity sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-replace-supers" "^7.28.6" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-computed-properties@^7.24.7", "@babel/plugin-transform-computed-properties@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz#936824fc71c26cb5c433485776d79c8e7b0202d2" + integrity sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/template" "^7.28.6" + +"@babel/plugin-transform-destructuring@^7.24.8", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz#b8402764df96179a2070bb7b501a1586cf8ad7a7" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-transform-dotall-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz#def31ed84e0fb6e25c71e53c124e7b76a4ab8e61" + integrity sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz#8014b8a6cfd0e7b92762724443bf0d2400f26df1" + integrity sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-explicit-resource-management@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz#dd6788f982c8b77e86779d1d029591e39d9d8be7" + integrity sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + +"@babel/plugin-transform-exponentiation-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz#5e477eb7eafaf2ab5537a04aaafcf37e2d7f1091" + integrity sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-flow-strip-types@^7.25.2": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz#5def3e1e7730f008d683144fb79b724f92c5cdf9" + integrity sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-flow" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.24.7", "@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.25.1", "@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-json-strings@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz#4c8c15b2dc49e285d110a4cf3dac52fd2dfc3038" + integrity sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-literals@^7.25.2", "@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.24.7", "@babel/plugin-transform-logical-assignment-operators@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz#53028a3d77e33c50ef30a8fce5ca17065936e605" + integrity sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-commonjs@^7.24.8", "@babel/plugin-transform-modules-commonjs@^7.27.1", "@babel/plugin-transform-modules-commonjs@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz#c0232e0dfe66a734cc4ad0d5e75fc3321b6fdef1" + integrity sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-modules-systemjs@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz#e458a95a17807c415924106a3ff188a3b8dee964" + integrity sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ== + dependencies: + "@babel/helper-module-transforms" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.29.0" + +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.24.7", "@babel/plugin-transform-named-capturing-groups-regex@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz#a26cd51e09c4718588fc4cce1c5d1c0152102d6a" + integrity sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.24.7", "@babel/plugin-transform-nullish-coalescing-operator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz#9bc62096e90ab7a887f3ca9c469f6adec5679757" + integrity sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-numeric-separator@^7.24.7", "@babel/plugin-transform-numeric-separator@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz#1310b0292762e7a4a335df5f580c3320ee7d9e9f" + integrity sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-object-rest-spread@^7.24.7", "@babel/plugin-transform-object-rest-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz#fdd4bc2d72480db6ca42aed5c051f148d7b067f7" + integrity sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA== + dependencies: + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.6" + +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + +"@babel/plugin-transform-optional-catch-binding@^7.24.7", "@babel/plugin-transform-optional-catch-binding@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz#75107be14c78385978201a49c86414a150a20b4c" + integrity sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-optional-chaining@7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-optional-chaining@^7.24.8", "@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz#926cf150bd421fc8362753e911b4a1b1ce4356cd" + integrity sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.24.7", "@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz#1fd2febb7c74e7d21cf3b05f7aebc907940af53a" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.24.7", "@babel/plugin-transform-private-methods@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz#c76fbfef3b86c775db7f7c106fff544610bdb411" + integrity sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-private-property-in-object@^7.24.7", "@babel/plugin-transform-private-property-in-object@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz#4fafef1e13129d79f1d75ac180c52aafefdb2811" + integrity sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-display-name@^7.24.7": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz#6f20a7295fea7df42eb42fed8f896813f5b934de" + integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-self@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92" + integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx-source@^7.24.7": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0" + integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-react-jsx@^7.25.2": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz#f51cb70a90b9529fbb71ee1f75ea27b7078eed62" + integrity sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/plugin-syntax-jsx" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/plugin-transform-regenerator@^7.24.7", "@babel/plugin-transform-regenerator@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz#dec237cec1b93330876d6da9992c4abd42c9d18b" + integrity sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-regexp-modifiers@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz#7ef0163bd8b4a610481b2509c58cf217f065290b" + integrity sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-runtime@^7.24.7": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz#a5fded13cc656700804bfd6e5ebd7fffd5266803" + integrity sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w== + dependencies: + "@babel/helper-module-imports" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@7.27.1", "@babel/plugin-transform-shorthand-properties@^7.24.7", "@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.24.7", "@babel/plugin-transform-spread@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz#40a2b423f6db7b70f043ad027a58bcb44a9757b6" + integrity sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA== + dependencies: + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.24.7", "@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-template-literals@7.27.1", "@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typescript@^7.25.2", "@babel/plugin-transform-typescript@^7.27.1": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz#1e93d96da8adbefdfdade1d4956f73afa201a158" + integrity sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.28.6" + +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-property-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz#63a7a6c21a0e75dae9b1861454111ea5caa22821" + integrity sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/plugin-transform-unicode-regex@7.27.1", "@babel/plugin-transform-unicode-regex@^7.24.7", "@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-sets-regex@^7.28.6": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz#924912914e5df9fe615ec472f88ff4788ce04d4e" + integrity sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.28.6" + +"@babel/preset-env@^7.25.3": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.29.0.tgz#c55db400c515a303662faaefd2d87e796efa08d0" + integrity sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w== + dependencies: + "@babel/compat-data" "^7.29.0" + "@babel/helper-compilation-targets" "^7.28.6" + "@babel/helper-plugin-utils" "^7.28.6" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.6" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.28.6" + "@babel/plugin-syntax-import-attributes" "^7.28.6" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.29.0" + "@babel/plugin-transform-async-to-generator" "^7.28.6" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.6" + "@babel/plugin-transform-class-properties" "^7.28.6" + "@babel/plugin-transform-class-static-block" "^7.28.6" + "@babel/plugin-transform-classes" "^7.28.6" + "@babel/plugin-transform-computed-properties" "^7.28.6" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-dotall-regex" "^7.28.6" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.6" + "@babel/plugin-transform-exponentiation-operator" "^7.28.6" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.28.6" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.6" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.28.6" + "@babel/plugin-transform-modules-systemjs" "^7.29.0" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.29.0" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.28.6" + "@babel/plugin-transform-numeric-separator" "^7.28.6" + "@babel/plugin-transform-object-rest-spread" "^7.28.6" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.28.6" + "@babel/plugin-transform-optional-chaining" "^7.28.6" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.28.6" + "@babel/plugin-transform-private-property-in-object" "^7.28.6" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.29.0" + "@babel/plugin-transform-regexp-modifiers" "^7.28.6" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.28.6" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.28.6" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.28.6" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.15" + babel-plugin-polyfill-corejs3 "^0.14.0" + babel-plugin-polyfill-regenerator "^0.6.6" + core-js-compat "^3.48.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-typescript@7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz#190742a6428d282306648a55b0529b561484f912" + integrity sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.27.1" + +"@babel/runtime@^7.25.0": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b" + integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA== + +"@babel/template@^7.25.0", "@babel/template@^7.28.6", "@babel/template@^7.3.3": + version "7.28.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.28.6.tgz#0e7e56ecedb78aeef66ce7972b082fce76a23e57" + integrity sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ== + dependencies: + "@babel/code-frame" "^7.28.6" + "@babel/parser" "^7.28.6" + "@babel/types" "^7.28.6" + +"@babel/traverse@^7.25.3", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5", "@babel/traverse@^7.28.6", "@babel/traverse@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.29.0.tgz#f323d05001440253eead3c9c858adbe00b90310a" + integrity sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/generator" "^7.29.0" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/types" "^7.29.0" + debug "^4.3.1" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6", "@babel/types@^7.29.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@egjs/hammerjs@^2.0.17": + version "2.0.17" + resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124" + integrity sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A== + dependencies: + "@types/hammerjs" "^2.0.36" + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.6.1": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/ttlcache@^1.4.1": + version "1.4.1" + resolved "https://registry.yarnpkg.com/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz#21fb23db34e9b6220c6ba023a0118a2dd3461ea2" + integrity sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/create-cache-key-function@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz#793be38148fab78e65f40ae30c36785f4ad859f0" + integrity sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA== + dependencies: + "@jest/types" "^29.6.3" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/remapping/-/remapping-2.3.5.tgz#375c476d1972947851ba1e15ae8f123047445aa1" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": + version "0.3.31" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@keystonehq/alias-sampling@^0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@keystonehq/alias-sampling/-/alias-sampling-0.1.2.tgz#63af931ffe6500aef4c0d87775a5b279189abf8d" + integrity sha512-5ukLB3bcgltgaFfQfYKYwHDUbwHicekYo53fSEa7xhVkAEqsA74kxdIwoBIURmGUtXe3EVIRm4SYlgcrt2Ri0w== + +"@keystonehq/bc-ur-registry-btc@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry-btc/-/bc-ur-registry-btc-0.1.1.tgz#5363961c2d0c529b01080eb278ff60de6bf5d181" + integrity sha512-LdYqItY1Y/M6fWJNE6L0HYZbKL8CGVP6OigG7T/gJ+SWnOGgYXj3at02aV7b9qZ7iNwJPkNrqsIDN5eajQcZjQ== + dependencies: + "@keystonehq/bc-ur-registry" "^0.6.4" + uuid "^8.3.2" + +"@keystonehq/bc-ur-registry@^0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@keystonehq/bc-ur-registry/-/bc-ur-registry-0.6.4.tgz#9c57ff9687cafdc0d2bbd04dc36676d3a38c1485" + integrity sha512-j8Uy44DuAkvYkbf0jMxRY3UizJfn8wsEQr7GS3miRF44vcq7k0/yemVkftbn3jQ+0JYaUXf5wY7lVpLhAeW5nQ== + dependencies: + "@ngraveio/bc-ur" "^1.1.5" + bs58check "^2.1.2" + tslib "^2.3.0" + +"@ngraveio/bc-ur@^1.1.13", "@ngraveio/bc-ur@^1.1.5": + version "1.1.13" + resolved "https://registry.yarnpkg.com/@ngraveio/bc-ur/-/bc-ur-1.1.13.tgz#27719fd3e745ccdbe97a7950905edcd1fed4844b" + integrity sha512-j73akJMV4+vLR2yQ4AphPIT5HZmxVjn/LxpL7YHoINnXoH6ccc90Zzck6/n6a3bCXOVZwBxq+YHwbAKRV+P8Zg== + dependencies: + "@keystonehq/alias-sampling" "^0.1.1" + assert "^2.0.0" + bignumber.js "^9.0.1" + cbor-sync "^1.0.4" + crc "^3.8.0" + jsbi "^3.1.5" + sha.js "^2.4.11" + +"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": + version "5.1.1-v1" + resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" + integrity sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg== + dependencies: + eslint-scope "5.1.1" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@op-engineering/op-sqlite@^15.2.5": + version "15.2.5" + resolved "https://registry.yarnpkg.com/@op-engineering/op-sqlite/-/op-sqlite-15.2.5.tgz#7393626b2eab9439e5035994cd27330c139aca05" + integrity sha512-Vmgwt0AzY7qoge3X6EONhsb5NlM2yoQUF0/lseUWBelfc9BUili7/DFsFsS73cvtYWlwPpqeTGOoce5mzHozBw== + +"@react-native-clipboard/clipboard@^1.16.0": + version "1.16.3" + resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz#7807a90fd9984bf4d3a96faf2eee20457984a9bd" + integrity sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ== + +"@react-native-community/cli-clean@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-20.1.2.tgz#dd95bc88d749745214623ed535dbbbaaeda4104c" + integrity sha512-XcNlmFnYOVDjvHQQn0qreI4FPLEUx8p43EdOmKbtzqwqc9T/EdBdzUnUc5wWQPO1CN7BdMfxW8fyvosz8NIlrg== + dependencies: + "@react-native-community/cli-tools" "20.1.2" + execa "^5.0.0" + fast-glob "^3.3.2" + picocolors "^1.1.1" + +"@react-native-community/cli-config-android@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config-android/-/cli-config-android-20.1.2.tgz#f029d0e0facef19d27655e82456adf7d53d900d5" + integrity sha512-W0Qx+lW8pbQMz8x3Rlf/H7D2j2u8O+u9HnrZnKzDl1DaXgaOQqL484lTZlMEQofjq7eLXdmzWxuZdqS6K1QfmQ== + dependencies: + "@react-native-community/cli-tools" "20.1.2" + fast-glob "^3.3.2" + fast-xml-parser "^5.3.6" + picocolors "^1.1.1" + +"@react-native-community/cli-config-apple@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config-apple/-/cli-config-apple-20.1.2.tgz#3a7d789eb1997d8c01f3007ec490625ce21842f6" + integrity sha512-Dhi1N1EoMMmJ4dnDlmNWCrJggfv7X/kl3l8uax72uaxepQI/CfohJP2rBdG2mWis+vzrCIk14z2keY0ixxsN8g== + dependencies: + "@react-native-community/cli-tools" "20.1.2" + execa "^5.0.0" + fast-glob "^3.3.2" + picocolors "^1.1.1" + +"@react-native-community/cli-config@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-config/-/cli-config-20.1.2.tgz#46bcdf662d7ba0c1a8fd4ed7fe8542266b8a3840" + integrity sha512-7aPE14QA8aXpfuQ1jmfiBfjC/N6gdbg6PpBTwb3cl8c/KaeVm+OQYoC2kn2b3XS0NPgw5Ix/VxVaX6AAUQRFuA== + dependencies: + "@react-native-community/cli-tools" "20.1.2" + cosmiconfig "^9.0.0" + deepmerge "^4.3.0" + fast-glob "^3.3.2" + joi "^17.2.1" + picocolors "^1.1.1" + +"@react-native-community/cli-doctor@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-doctor/-/cli-doctor-20.1.2.tgz#d9cc0c35b67195cefab9d503204093819707277c" + integrity sha512-bbT1EhomvHz5ZtzxY2czA4/JMXhP4aIAxRDsqiW6wfZA9A9/HXqA4uv6CxP0wZUUmovmPNRl3kW/LWXrRmdv3A== + dependencies: + "@react-native-community/cli-config" "20.1.2" + "@react-native-community/cli-platform-android" "20.1.2" + "@react-native-community/cli-platform-apple" "20.1.2" + "@react-native-community/cli-platform-ios" "20.1.2" + "@react-native-community/cli-tools" "20.1.2" + command-exists "^1.2.8" + deepmerge "^4.3.0" + envinfo "^7.13.0" + execa "^5.0.0" + node-stream-zip "^1.9.1" + ora "^5.4.1" + picocolors "^1.1.1" + semver "^7.5.2" + wcwidth "^1.0.1" + yaml "^2.2.1" + +"@react-native-community/cli-platform-android@20.1.2", "@react-native-community/cli-platform-android@^20.0.0": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-android/-/cli-platform-android-20.1.2.tgz#ebd5668c9c729b38c2f18d058842eb254eef9f2f" + integrity sha512-1iHB8cTTJpMyEKhxWTRYsxhBBsiOq3tVguGX/HtBdHRzhlCeDpanE6U+aKxWfMFetMcFL+HLe5nQPcJXf9EtKg== + dependencies: + "@react-native-community/cli-config-android" "20.1.2" + "@react-native-community/cli-tools" "20.1.2" + execa "^5.0.0" + logkitty "^0.7.1" + picocolors "^1.1.1" + +"@react-native-community/cli-platform-apple@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-apple/-/cli-platform-apple-20.1.2.tgz#1608dd2af22ce625fdd24bfb1e42c40a1f1ecba7" + integrity sha512-UvzjcRGotO3E2xaty8YWE2XMGkkDDaXRtQtNRjzmtcoNY40C+y4iMHxd0o3xbD0bzYM/PO79tXye9MxTWdyVkg== + dependencies: + "@react-native-community/cli-config-apple" "20.1.2" + "@react-native-community/cli-tools" "20.1.2" + execa "^5.0.0" + fast-xml-parser "^5.3.6" + picocolors "^1.1.1" + +"@react-native-community/cli-platform-ios@20.1.2", "@react-native-community/cli-platform-ios@^20.0.0": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-platform-ios/-/cli-platform-ios-20.1.2.tgz#64d519e89894c8d40151e5eb3d63dd49bb140984" + integrity sha512-ZzdLwJMt7ehjO0iy/rQGPgH6uZqMYXeS5uXzSi1DeLYwurV1wOqFc0SLm4TAz5FKYQmHpwBXlMiI12rUmkZxcg== + dependencies: + "@react-native-community/cli-platform-apple" "20.1.2" + +"@react-native-community/cli-server-api@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-server-api/-/cli-server-api-20.1.2.tgz#6a03b6a35d0729250a69f654fc74631eaa7263bb" + integrity sha512-ZlINtIYoDAwSemwTU9OavI1IixCCmAPPw1s3Mp0cOvrddFSZ0hx1N1IR+imLyo4lhFfM8OO3rUe9oVJj1SHUCA== + dependencies: + "@react-native-community/cli-tools" "20.1.2" + body-parser "^2.2.2" + compression "^1.7.1" + connect "^3.6.5" + errorhandler "^1.5.1" + nocache "^3.0.1" + open "^6.2.0" + pretty-format "^29.7.0" + serve-static "^1.13.1" + strict-url-sanitise "0.0.1" + ws "^6.2.3" + +"@react-native-community/cli-tools@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-tools/-/cli-tools-20.1.2.tgz#9caa7f516cf2722c425c767964e961e30c9bc829" + integrity sha512-on2VUBZb68RlMxvIrEdK6+NiOEYu/z+t/cz746yGtxn49fwW6Wafzmh1QNZj8HPAuZ8+Ds61LiXbwoDDkzNSSA== + dependencies: + "@vscode/sudo-prompt" "^9.0.0" + appdirsjs "^1.2.4" + execa "^5.0.0" + find-up "^5.0.0" + launch-editor "^2.9.1" + mime "^2.4.1" + ora "^5.4.1" + picocolors "^1.1.1" + prompts "^2.4.2" + semver "^7.5.2" + +"@react-native-community/cli-types@20.1.2": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli-types/-/cli-types-20.1.2.tgz#f234ef172559a383ef885ec0108af09d31ba91be" + integrity sha512-WYK98VdcJE+lRuyRzigE/GQAbaJZOKkjpaLwhmMMItXVTqMmIccfGu9b4pRoQOVfs1aLq87DuwUOi9sxz6OG1g== + dependencies: + joi "^17.2.1" + +"@react-native-community/cli@^20.0.0": + version "20.1.2" + resolved "https://registry.yarnpkg.com/@react-native-community/cli/-/cli-20.1.2.tgz#bf029e077850ea72740f0352259d7682425b00d1" + integrity sha512-48GRnGfm1+4ueV8ESNJmKAKW01QdbB8H6qrVxCqpHYvuRkeFBaPpwRY2bEjSwqruw3Pn9ppzGfpAxYQDiUWQxQ== + dependencies: + "@react-native-community/cli-clean" "20.1.2" + "@react-native-community/cli-config" "20.1.2" + "@react-native-community/cli-doctor" "20.1.2" + "@react-native-community/cli-server-api" "20.1.2" + "@react-native-community/cli-tools" "20.1.2" + "@react-native-community/cli-types" "20.1.2" + commander "^9.4.1" + deepmerge "^4.3.0" + execa "^5.0.0" + find-up "^5.0.0" + fs-extra "^8.1.0" + graceful-fs "^4.1.3" + picocolors "^1.1.1" + prompts "^2.4.2" + semver "^7.5.2" + +"@react-native-community/netinfo@^11.4.1": + version "11.5.2" + resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-11.5.2.tgz#daf3bc1b5cf6e9fc6725600c9e95875b5e50e694" + integrity sha512-/g0m65BtX9HU+bPiCH2517bOHpEIUsGrWFXDzi1a5nNKn5KujQgm04WhL7/OSXWKHyrT8VVtUoJA0XKRxueBpQ== + +"@react-native/assets-registry@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/assets-registry/-/assets-registry-0.82.1.tgz#834058f9391fa7aa85404f833ece2ab70754a332" + integrity sha512-B1SRwpntaAcckiatxbjzylvNK562Ayza05gdJCjDQHTiDafa1OABmyB5LHt7qWDOpNkaluD+w11vHF7pBmTpzQ== + +"@react-native/babel-plugin-codegen@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.82.1.tgz#11cee8a38b4f4c5d1c1eace59473c8c046eeed26" + integrity sha512-wzmEz/RlR4SekqmaqeQjdMVh4LsnL9e62mrOikOOkHDQ3QN0nrKLuUDzXyYptVbxQ0IRua4pTm3efJLymDBoEg== + dependencies: + "@babel/traverse" "^7.25.3" + "@react-native/codegen" "0.82.1" + +"@react-native/babel-preset@0.82.1", "@react-native/babel-preset@^0.82.0": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/babel-preset/-/babel-preset-0.82.1.tgz#9b09e1445862e8a6e562b7085fa294ff9b0f2186" + integrity sha512-Olj7p4XIsUWLKjlW46CqijaXt45PZT9Lbvv/Hz698FXTenPKk4k7sy6RGRGZPWO2TCBBfcb73dus1iNHRFSq7g== + dependencies: + "@babel/core" "^7.25.2" + "@babel/plugin-proposal-export-default-from" "^7.24.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-default-from" "^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-transform-arrow-functions" "^7.24.7" + "@babel/plugin-transform-async-generator-functions" "^7.25.4" + "@babel/plugin-transform-async-to-generator" "^7.24.7" + "@babel/plugin-transform-block-scoping" "^7.25.0" + "@babel/plugin-transform-class-properties" "^7.25.4" + "@babel/plugin-transform-classes" "^7.25.4" + "@babel/plugin-transform-computed-properties" "^7.24.7" + "@babel/plugin-transform-destructuring" "^7.24.8" + "@babel/plugin-transform-flow-strip-types" "^7.25.2" + "@babel/plugin-transform-for-of" "^7.24.7" + "@babel/plugin-transform-function-name" "^7.25.1" + "@babel/plugin-transform-literals" "^7.25.2" + "@babel/plugin-transform-logical-assignment-operators" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.24.7" + "@babel/plugin-transform-numeric-separator" "^7.24.7" + "@babel/plugin-transform-object-rest-spread" "^7.24.7" + "@babel/plugin-transform-optional-catch-binding" "^7.24.7" + "@babel/plugin-transform-optional-chaining" "^7.24.8" + "@babel/plugin-transform-parameters" "^7.24.7" + "@babel/plugin-transform-private-methods" "^7.24.7" + "@babel/plugin-transform-private-property-in-object" "^7.24.7" + "@babel/plugin-transform-react-display-name" "^7.24.7" + "@babel/plugin-transform-react-jsx" "^7.25.2" + "@babel/plugin-transform-react-jsx-self" "^7.24.7" + "@babel/plugin-transform-react-jsx-source" "^7.24.7" + "@babel/plugin-transform-regenerator" "^7.24.7" + "@babel/plugin-transform-runtime" "^7.24.7" + "@babel/plugin-transform-shorthand-properties" "^7.24.7" + "@babel/plugin-transform-spread" "^7.24.7" + "@babel/plugin-transform-sticky-regex" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.25.2" + "@babel/plugin-transform-unicode-regex" "^7.24.7" + "@babel/template" "^7.25.0" + "@react-native/babel-plugin-codegen" "0.82.1" + babel-plugin-syntax-hermes-parser "0.32.0" + babel-plugin-transform-flow-enums "^0.0.2" + react-refresh "^0.14.0" + +"@react-native/codegen@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/codegen/-/codegen-0.82.1.tgz#d51fae22e0ae488be011526cb1bf07e403d50832" + integrity sha512-ezXTN70ygVm9l2m0i+pAlct0RntoV4afftWMGUIeAWLgaca9qItQ54uOt32I/9dBJvzBibT33luIR/pBG0dQvg== + dependencies: + "@babel/core" "^7.25.2" + "@babel/parser" "^7.25.3" + glob "^7.1.1" + hermes-parser "0.32.0" + invariant "^2.2.4" + nullthrows "^1.1.1" + yargs "^17.6.2" + +"@react-native/community-cli-plugin@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/community-cli-plugin/-/community-cli-plugin-0.82.1.tgz#4ed6545fe4b4daa445df6f5903e237fbc4bd1e77" + integrity sha512-H/eMdtOy9nEeX7YVeEG1N2vyCoifw3dr9OV8++xfUElNYV7LtSmJ6AqxZUUfxGJRDFPQvaU/8enmJlM/l11VxQ== + dependencies: + "@react-native/dev-middleware" "0.82.1" + debug "^4.4.0" + invariant "^2.2.4" + metro "^0.83.1" + metro-config "^0.83.1" + metro-core "^0.83.1" + semver "^7.1.3" + +"@react-native/debugger-frontend@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/debugger-frontend/-/debugger-frontend-0.82.1.tgz#4b9dca39806b43e60029d1a0352dd71de910e86f" + integrity sha512-a2O6M7/OZ2V9rdavOHyCQ+10z54JX8+B+apYKCQ6a9zoEChGTxUMG2YzzJ8zZJVvYf1ByWSNxv9Se0dca1hO9A== + +"@react-native/debugger-shell@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/debugger-shell/-/debugger-shell-0.82.1.tgz#0224c75afd135cc755a51c929e59a423f71804d4" + integrity sha512-fdRHAeqqPT93bSrxfX+JHPpCXHApfDUdrXMXhoxlPgSzgXQXJDykIViKhtpu0M6slX6xU/+duq+AtP/qWJRpBw== + dependencies: + cross-spawn "^7.0.6" + fb-dotslash "0.5.8" + +"@react-native/dev-middleware@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/dev-middleware/-/dev-middleware-0.82.1.tgz#105d0f7dd4891d9cae2bac9a7e0c3100ed8ef35c" + integrity sha512-wuOIzms/Qg5raBV6Ctf2LmgzEOCqdP3p1AYN4zdhMT110c39TVMbunpBaJxm0Kbt2HQ762MQViF9naxk7SBo4w== + dependencies: + "@isaacs/ttlcache" "^1.4.1" + "@react-native/debugger-frontend" "0.82.1" + "@react-native/debugger-shell" "0.82.1" + chrome-launcher "^0.15.2" + chromium-edge-launcher "^0.2.0" + connect "^3.6.5" + debug "^4.4.0" + invariant "^2.2.4" + nullthrows "^1.1.1" + open "^7.0.3" + serve-static "^1.16.2" + ws "^6.2.3" + +"@react-native/eslint-config@0.82.0": + version "0.82.0" + resolved "https://registry.yarnpkg.com/@react-native/eslint-config/-/eslint-config-0.82.0.tgz#de4d6ef493ff6a5787460995a5f88d33180c5793" + integrity sha512-a6O5sbI2FmFSgYIvXLrl+pjWMQHy+/uQaXBuwkfglVT5jBtP5y1ouA/3vfafYLJtnHBEoutJL9KW5o6yPlU/xQ== + dependencies: + "@babel/core" "^7.25.2" + "@babel/eslint-parser" "^7.25.1" + "@react-native/eslint-plugin" "0.82.0" + "@typescript-eslint/eslint-plugin" "^8.36.0" + "@typescript-eslint/parser" "^8.36.0" + eslint-config-prettier "^8.5.0" + eslint-plugin-eslint-comments "^3.2.0" + eslint-plugin-ft-flow "^2.0.1" + eslint-plugin-jest "^29.0.1" + eslint-plugin-react "^7.30.1" + eslint-plugin-react-hooks "^5.2.0" + eslint-plugin-react-native "^4.0.0" + +"@react-native/eslint-plugin@0.82.0": + version "0.82.0" + resolved "https://registry.yarnpkg.com/@react-native/eslint-plugin/-/eslint-plugin-0.82.0.tgz#5bdc8ac412176518985a3261563e83614fa5c1b7" + integrity sha512-kSZvt008PemdyDUDEYwTEM3ar1UcES74yEN74ogTnMThWeHx/SADOex10yqdzeHwVmjl+N9q0R96Rg49B4h6Vw== + +"@react-native/gradle-plugin@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/gradle-plugin/-/gradle-plugin-0.82.1.tgz#a747810a37f5ce652e6e2f0aa54cff7275d9ced7" + integrity sha512-KkF/2T1NSn6EJ5ALNT/gx0MHlrntFHv8YdooH9OOGl9HQn5NM0ZmQSr86o5utJsGc7ME3R6p3SaQuzlsFDrn8Q== + +"@react-native/js-polyfills@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/js-polyfills/-/js-polyfills-0.82.1.tgz#f707c1de572b8e46084c4b0bf65f9891f093f416" + integrity sha512-tf70X7pUodslOBdLN37J57JmDPB/yiZcNDzS2m+4bbQzo8fhx3eG9QEBv5n4fmzqfGAgSB4BWRHgDMXmmlDSVA== + +"@react-native/metro-babel-transformer@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.82.1.tgz#80ec7c165ea1c62cb6b0fa2e1c4e8c92a5e87132" + integrity sha512-kVQyYxYe1Da7cr7uGK9c44O6vTzM8YY3KW9CSLhhV1CGw7jmohU1HfLaUxDEmYfFZMc4Kj3JsIEbdUlaHMtprQ== + dependencies: + "@babel/core" "^7.25.2" + "@react-native/babel-preset" "0.82.1" + hermes-parser "0.32.0" + nullthrows "^1.1.1" + +"@react-native/metro-config@^0.82.0": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/metro-config/-/metro-config-0.82.1.tgz#872607175af3a8b7bc852a65e2b63cc95a36c5dd" + integrity sha512-mAY6R3xnDMlmDOrUCAtLTjIkli26DZt4LNVuAjDEdnlv5sHANOr5x4qpMn7ea1p9Q/tpfHLalPQUQeJ8CZH4gA== + dependencies: + "@react-native/js-polyfills" "0.82.1" + "@react-native/metro-babel-transformer" "0.82.1" + metro-config "^0.83.1" + metro-runtime "^0.83.1" + +"@react-native/normalize-colors@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.82.1.tgz#be49d4f9f56f1a9b3d09cf6e391bb67e51103807" + integrity sha512-CCfTR1uX+Z7zJTdt3DNX9LUXr2zWXsNOyLbwupW2wmRzrxlHRYfmLgTABzRL/cKhh0Ubuwn15o72MQChvCRaHw== + +"@react-native/typescript-config@0.82.0": + version "0.82.0" + resolved "https://registry.yarnpkg.com/@react-native/typescript-config/-/typescript-config-0.82.0.tgz#9cb60978d3af5b235a111d5cb659e72bc8ffde66" + integrity sha512-L/pZLFh52NUZ7sPAs8IAkQXz/fepuduceqkog6j9YYvbSqS9SQbLSCV9ss/v3XZHaTtRMQD1eq+8WJA0HzqhzA== + +"@react-native/virtualized-lists@0.82.1": + version "0.82.1" + resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.82.1.tgz#7a38adfc7d42353a99a225bdd45199384f2e0ec7" + integrity sha512-f5zpJg9gzh7JtCbsIwV+4kP3eI0QBuA93JGmwFRd4onQ3DnCjV2J5pYqdWtM95sjSKK1dyik59Gj01lLeKqs1Q== + dependencies: + invariant "^2.2.4" + nullthrows "^1.1.1" + +"@react-navigation/bottom-tabs@^7.2.2": + version "7.15.5" + resolved "https://registry.yarnpkg.com/@react-navigation/bottom-tabs/-/bottom-tabs-7.15.5.tgz#a858b88d0d23f0f57eac6204752adfdbb16380cc" + integrity sha512-wQHredlCrRmShWQ1vF4HUcLdaiJ8fUgnbaeQH7BJ7MQVQh4mdzab0IOY/4QSmUyNRB350oyu1biTycyQ5FKWMQ== + dependencies: + "@react-navigation/elements" "^2.9.10" + color "^4.2.3" + sf-symbols-typescript "^2.1.0" + +"@react-navigation/core@^7.16.1": + version "7.16.1" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-7.16.1.tgz#a8f6799a7b18f6d5c45616cbf792ec8f08d1aadc" + integrity sha512-xhquoyhKdqDfiL7LuupbwYnmauUGfVFGDEJO34m26k8zSN1eDjQ2stBZcHN8ILOI1PrG9885nf8ZmfaQxPS0ww== + dependencies: + "@react-navigation/routers" "^7.5.3" + escape-string-regexp "^4.0.0" + fast-deep-equal "^3.1.3" + nanoid "^3.3.11" + query-string "^7.1.3" + react-is "^19.1.0" + use-latest-callback "^0.2.4" + use-sync-external-store "^1.5.0" + +"@react-navigation/elements@^2.6.5", "@react-navigation/elements@^2.9.10": + version "2.9.10" + resolved "https://registry.yarnpkg.com/@react-navigation/elements/-/elements-2.9.10.tgz#8cde92a7b4d088a46efb931e6edb2644eddd630c" + integrity sha512-N8tuBekzTRb0pkMHFJGvmC6Q5OisSbt6gzvw7RHMnp4NDo5auVllT12sWFaTXf8mTduaLKNSrD/NZNaOqThCBg== + dependencies: + color "^4.2.3" + use-latest-callback "^0.2.4" + use-sync-external-store "^1.5.0" + +"@react-navigation/native-stack@^7.3.28": + version "7.14.4" + resolved "https://registry.yarnpkg.com/@react-navigation/native-stack/-/native-stack-7.14.4.tgz#c575117a2da14257ef13f5111cf31a6a8025bbba" + integrity sha512-HFEnM5Q7JY3FmmiolD/zvgY+9sxZAyVGPZJoz7BdTvJmi1VHOdplf24YiH45mqeitlGnaOlvNT55rH4abHJ5eA== + dependencies: + "@react-navigation/elements" "^2.9.10" + color "^4.2.3" + sf-symbols-typescript "^2.1.0" + warn-once "^0.1.1" + +"@react-navigation/native@^7.1.18": + version "7.1.33" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-7.1.33.tgz#a9910b51b42373e8977cf8d76ac960077ad4a2fc" + integrity sha512-DpFdWGcgLajKZ1TuIvDNQsblN2QaUFWpTQaB8v7WRP9Mix8H/6TFoIrZd93pbymI2hybd6UYrD+lI408eWVcfw== + dependencies: + "@react-navigation/core" "^7.16.1" + escape-string-regexp "^4.0.0" + fast-deep-equal "^3.1.3" + nanoid "^3.3.11" + use-latest-callback "^0.2.4" + +"@react-navigation/routers@^7.5.3": + version "7.5.3" + resolved "https://registry.yarnpkg.com/@react-navigation/routers/-/routers-7.5.3.tgz#8002930ef5f62351be2475d0dffde3ffaee174d7" + integrity sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg== + dependencies: + nanoid "^3.3.11" + +"@sideway/address@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" + integrity sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.10" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6" + integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA== + +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@types/babel__core@^7.1.14": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9" + integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.28.0" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74" + integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q== + dependencies: + "@babel/types" "^7.28.2" + +"@types/big.js@^6.2.2": + version "6.2.2" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-6.2.2.tgz#69422ec9ef59df1330ccfde2106d9e1159a083c3" + integrity sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA== + +"@types/graceful-fs@^4.1.3": + version "4.1.9" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" + integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== + dependencies: + "@types/node" "*" + +"@types/hammerjs@^2.0.36": + version "2.0.46" + resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.46.tgz#381daaca1360ff8a7c8dff63f32e69745b9fb1e1" + integrity sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" + integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== + +"@types/istanbul-lib-report@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz#53047614ae72e19fc0401d872de3ae2b4ce350bf" + integrity sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" + integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.13": + version "29.5.14" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" + integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/lodash@^4.17.14": + version "4.17.24" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f" + integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ== + +"@types/node@*": + version "25.3.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.3.5.tgz#beccb5915561f7a9970ace547ad44d6cdbf39b46" + integrity sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA== + dependencies: + undici-types "~7.18.0" + +"@types/prop-types@*": + version "15.7.15" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7" + integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw== + +"@types/react-native-zeroconf@^0.13.1": + version "0.13.1" + resolved "https://registry.yarnpkg.com/@types/react-native-zeroconf/-/react-native-zeroconf-0.13.1.tgz#59ae87bf2b86054a7056beec2c2a31f9e713bd4e" + integrity sha512-edAbIS+1/o9OTYU6S7stNli9ATHlJA2z2jOv6ZC+n38Q2qyUKqzycuD+hLqRLIxe1BibYYcpOoJtuoBkLSsdYg== + dependencies: + "@types/node" "*" + +"@types/react-test-renderer@^18.0.0": + version "18.3.1" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.3.1.tgz#225bfe8d4ad7ee3b04c2fa27642bb74274a5961d" + integrity sha512-vAhnk0tG2eGa37lkU9+s5SoroCsRI08xnsWFiAXOuPH2jqzMbcXvKExXViPi1P5fIklDeCvXqyrdmipFaSkZrA== + dependencies: + "@types/react" "^18" + +"@types/react@^18": + version "18.3.28" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.28.tgz#0a85b1a7243b4258d9f626f43797ba18eb5f8781" + integrity sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw== + dependencies: + "@types/prop-types" "*" + csstype "^3.2.2" + +"@types/react@^19.1.1": + version "19.2.14" + resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" + integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== + dependencies: + csstype "^3.2.2" + +"@types/stack-utils@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" + integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== + +"@types/yargs-parser@*": + version "21.0.3" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" + integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== + +"@types/yargs@^17.0.8": + version "17.0.35" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.35.tgz#07013e46aa4d7d7d50a49e15604c1c5340d4eb24" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^8.36.0": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz#b1ce606d87221daec571e293009675992f0aae76" + integrity sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.56.1" + "@typescript-eslint/type-utils" "8.56.1" + "@typescript-eslint/utils" "8.56.1" + "@typescript-eslint/visitor-keys" "8.56.1" + ignore "^7.0.5" + natural-compare "^1.4.0" + ts-api-utils "^2.4.0" + +"@typescript-eslint/parser@^8.36.0": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.56.1.tgz#21d13b3d456ffb08614c1d68bb9a4f8d9237cdc7" + integrity sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg== + dependencies: + "@typescript-eslint/scope-manager" "8.56.1" + "@typescript-eslint/types" "8.56.1" + "@typescript-eslint/typescript-estree" "8.56.1" + "@typescript-eslint/visitor-keys" "8.56.1" + debug "^4.4.3" + +"@typescript-eslint/project-service@8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.56.1.tgz#65c8d645f028b927bfc4928593b54e2ecd809244" + integrity sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.56.1" + "@typescript-eslint/types" "^8.56.1" + debug "^4.4.3" + +"@typescript-eslint/scope-manager@8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz#254df93b5789a871351335dd23e20bc164060f24" + integrity sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w== + dependencies: + "@typescript-eslint/types" "8.56.1" + "@typescript-eslint/visitor-keys" "8.56.1" + +"@typescript-eslint/tsconfig-utils@8.56.1", "@typescript-eslint/tsconfig-utils@^8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz#1afa830b0fada5865ddcabdc993b790114a879b7" + integrity sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ== + +"@typescript-eslint/type-utils@8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz#7a6c4fabf225d674644931e004302cbbdd2f2e24" + integrity sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg== + dependencies: + "@typescript-eslint/types" "8.56.1" + "@typescript-eslint/typescript-estree" "8.56.1" + "@typescript-eslint/utils" "8.56.1" + debug "^4.4.3" + ts-api-utils "^2.4.0" + +"@typescript-eslint/types@8.56.1", "@typescript-eslint/types@^8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.56.1.tgz#975e5942bf54895291337c91b9191f6eb0632ab9" + integrity sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw== + +"@typescript-eslint/typescript-estree@8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz#3b9e57d8129a860c50864c42188f761bdef3eab0" + integrity sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg== + dependencies: + "@typescript-eslint/project-service" "8.56.1" + "@typescript-eslint/tsconfig-utils" "8.56.1" + "@typescript-eslint/types" "8.56.1" + "@typescript-eslint/visitor-keys" "8.56.1" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.4.0" + +"@typescript-eslint/utils@8.56.1", "@typescript-eslint/utils@^8.0.0": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.56.1.tgz#5a86acaf9f1b4c4a85a42effb217f73059f6deb7" + integrity sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA== + dependencies: + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.56.1" + "@typescript-eslint/types" "8.56.1" + "@typescript-eslint/typescript-estree" "8.56.1" + +"@typescript-eslint/visitor-keys@8.56.1": + version "8.56.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz#50e03475c33a42d123dc99e63acf1841c0231f87" + integrity sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw== + dependencies: + "@typescript-eslint/types" "8.56.1" + eslint-visitor-keys "^5.0.0" + +"@ungap/structured-clone@^1.2.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@vscode/sudo-prompt@^9.0.0": + version "9.3.2" + resolved "https://registry.yarnpkg.com/@vscode/sudo-prompt/-/sudo-prompt-9.3.2.tgz#692ba38df40bd3502ccc4e9f099fbbaedbd5f04e" + integrity sha512-gcXoCN00METUNFeQOFJ+C9xUI0DKB+0EGMVg7wbVYRHBw2Eq3fKisDZOkRdOz3kqXRKOENMfShPOmypw1/8nOw== + +"@yarnpkg/lockfile@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895" + integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng== + dependencies: + mime-types "^3.0.0" + negotiator "^1.0.0" + +accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.15.0, acorn@^8.9.0: + version "8.16.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" + integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== + +agent-base@^7.1.2: + version "7.1.4" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8" + integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== + +ajv@^6.12.4: + version "6.14.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" + integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +anser@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" + integrity sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww== + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-fragments@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/ansi-fragments/-/ansi-fragments-0.2.1.tgz#24409c56c4cc37817c3d7caa99d8969e2de5a05e" + integrity sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w== + dependencies: + colorette "^1.0.7" + slice-ansi "^2.0.0" + strip-ansi "^5.0.0" + +ansi-regex@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== + +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +appdirsjs@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/appdirsjs/-/appdirsjs-1.2.7.tgz#50b4b7948a26ba6090d4aede2ae2dc2b051be3b3" + integrity sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.9" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.9.tgz#1f0ccaa08e90cdbc3eb433210f903ad0f17c3f3a" + integrity sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.0" + es-object-atoms "^1.1.1" + get-intrinsic "^1.3.0" + is-string "^1.1.1" + math-intrinsics "^1.1.0" + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz#534aaf9e6e8dd79fb6b9a9917f839ef1ec63afe5" + integrity sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.flatmap@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz#712cc792ae70370ae40586264629e33aab5dd38b" + integrity sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-shim-unscopables "^1.0.2" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== + +assert@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-2.1.0.tgz#6d92a238d05dc02e7427c881fb8be81c8448b2dd" + integrity sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw== + dependencies: + call-bind "^1.0.2" + is-nan "^1.3.2" + object-is "^1.1.5" + object.assign "^4.1.4" + util "^0.12.5" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axios@^1.7.9: + version "1.13.6" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.6.tgz#c3f92da917dc209a15dd29936d20d5089b6b6c98" + integrity sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ== + dependencies: + follow-redirects "^1.15.11" + form-data "^4.0.5" + proxy-from-env "^1.1.0" + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-polyfill-corejs2@^0.4.14, babel-plugin-polyfill-corejs2@^0.4.15: + version "0.4.16" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz#a1321145f6cde738b0a412616b6bcf77f143ab36" + integrity sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw== + dependencies: + "@babel/compat-data" "^7.28.6" + "@babel/helper-define-polyfill-provider" "^0.6.7" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz#bb7f6aeef7addff17f7602a08a6d19a128c30164" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" + +babel-plugin-polyfill-corejs3@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz#75fb533a1c23c0a976f189cba1d035199705b8ad" + integrity sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.7" + core-js-compat "^3.48.0" + +babel-plugin-polyfill-regenerator@^0.6.5, babel-plugin-polyfill-regenerator@^0.6.6: + version "0.6.7" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz#eca723d67ef87b798881ad00546db1b6dd72e1ef" + integrity sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.7" + +babel-plugin-syntax-hermes-parser@0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz#06f7452bf91adf6cafd7c98e7467404d4eb65cec" + integrity sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg== + dependencies: + hermes-parser "0.32.0" + +babel-plugin-transform-flow-enums@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz#d1d0cc9bdc799c850ca110d0ddc9f21b9ec3ef25" + integrity sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ== + dependencies: + "@babel/plugin-syntax-flow" "^7.12.1" + +babel-preset-current-node-syntax@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz#20730d6cdc7dda5d89401cab10ac6a32067acde6" + integrity sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-import-attributes" "^7.24.7" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +balanced-match@^4.0.2: + version "4.0.4" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.4.tgz#bfb10662feed8196a2c62e7c68e17720c274179a" + integrity sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== + +base-64@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" + integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== + +base-x@^3.0.2: + version "3.0.11" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.11.tgz#40d80e2a1aeacba29792ccc6c5354806421287ff" + integrity sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA== + dependencies: + safe-buffer "^5.0.1" + +base58-js@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/base58-js/-/base58-js-3.0.3.tgz#a753af5e0c484f1c73906e3883baa34805bf0e10" + integrity sha512-3hf42BysHnUqmZO7mK6e5X/hs1AvyEJIhdVLbG/Mxn/fhFnhGxOO37mWbMHg1RT4TxqcPKXgqj9/bp1YG0GBXA== + +base64-js@^1.3.1, base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.18: + version "2.10.0" + resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz#5b09935025bf8a80e29130251e337c6a7fc8cbb9" + integrity sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA== + +bech32@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + +big.js@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-6.2.2.tgz#be3bb9ac834558b53b099deef2a1d06ac6368e1a" + integrity sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ== + +bignumber.js@^9.0.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.3.1.tgz#759c5aaddf2ffdc4f154f7b493e1c8770f88c4d7" + integrity sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ== + +bitcoin-address-validation@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bitcoin-address-validation/-/bitcoin-address-validation-3.0.0.tgz#a94ce7c73f9122d6849db992a180dba54227019a" + integrity sha512-R1X1c9EdgjgjTpjshjk5e16TbgF7HYasxBcu7l5ScWMxVs53845vMUg5PvnQ/R/3h8Grly6Y52DgH6/77gazLQ== + dependencies: + base58-js "^3.0.2" + bech32 "^2.0.0" + sha256-uint8array "^0.10.3" + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.2.tgz#1a32cdb966beaf68de50a9dfbe5b58f83cb8890c" + integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA== + dependencies: + bytes "^3.1.2" + content-type "^1.0.5" + debug "^4.4.3" + http-errors "^2.0.0" + iconv-lite "^0.7.0" + on-finished "^2.4.1" + qs "^6.14.1" + raw-body "^3.0.1" + type-is "^2.0.1" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +brace-expansion@^1.1.7: + version "1.1.12" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.12.tgz#ab9b454466e5a8cc3a187beaad580412a9c5b843" + integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^5.0.2: + version "5.0.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" + integrity sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg== + dependencies: + balanced-match "^4.0.2" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, browserslist@^4.28.1: + version "4.28.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.28.1.tgz#7f534594628c53c63101079e27e40de490456a95" + integrity sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA== + dependencies: + baseline-browser-mapping "^2.9.0" + caniuse-lite "^1.0.30001759" + electron-to-chromium "^1.5.263" + node-releases "^2.0.27" + update-browserslist-db "^1.2.0" + +bs58@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a" + integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw== + dependencies: + base-x "^3.0.2" + +bs58check@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/bs58check/-/bs58check-2.1.2.tgz#53b018291228d82a5aa08e7d796fdafda54aebfc" + integrity sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA== + dependencies: + bs58 "^4.0.0" + create-hash "^1.1.0" + safe-buffer "^5.1.2" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.1.0, buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +bytes@3.1.2, bytes@^3.1.2, bytes@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001759: + version "1.0.30001777" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz#028f21e4b2718d138b55e692583e6810ccf60691" + integrity sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ== + +cbor-sync@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cbor-sync/-/cbor-sync-1.0.4.tgz#5a11a1ab75c2a14d1af1b237fd84aa8c1593662f" + integrity sha512-GWlXN4wiz0vdWWXBU71Dvc1q3aBo0HytqwAZnXF1wOwjqNnDWA1vZ1gDMFLlqohak31VQzmhiYfiCX5QSSfagA== + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +chrome-launcher@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.15.2.tgz#4e6404e32200095fdce7f6a1e1004f9bd36fa5da" + integrity sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + +chromium-edge-launcher@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz#0c378f28c99aefc360705fa155de0113997f62fc" + integrity sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg== + dependencies: + "@types/node" "*" + escape-string-regexp "^4.0.0" + is-wsl "^2.2.0" + lighthouse-logger "^1.0.0" + mkdirp "^1.0.4" + rimraf "^3.0.2" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0, ci-info@^3.7.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cipher-base@^1.0.1: + version "1.0.7" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.7.tgz#bd094bfef42634ccfd9e13b9fc73274997111e39" + integrity sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.2" + +cjs-module-lexer@^1.0.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz#0f79731eb8cfe1ec72acd4066efac9d61991b00d" + integrity sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz#cc1f01eb8d02298cbc9a437c74c70ab4e5210b80" + integrity sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@^1.0.0, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + +colorette@^1.0.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" + integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +command-exists@^1.2.8: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + +commander@^12.0.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^9.4.1: + version "9.5.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +compressible@~2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.8.1.tgz#4a45d909ac16509195a9a28bd91094889c180d79" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== + dependencies: + bytes "3.1.2" + compressible "~2.0.18" + debug "2.6.9" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect@^3.6.5: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@2.0.0, convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +core-js-compat@^3.43.0, core-js-compat@^3.48.0: + version "3.48.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.48.0.tgz#7efbe1fc1cbad44008190462217cc5558adaeaa6" + integrity sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q== + dependencies: + browserslist "^4.28.1" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz#df110631a8547b5d1a98915271986f06e3011379" + integrity sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ== + dependencies: + env-paths "^2.2.1" + import-fresh "^3.3.0" + js-yaml "^4.1.0" + parse-json "^5.2.0" + +crc@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6" + integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ== + dependencies: + buffer "^5.1.0" + +create-hash@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +css-select@^5.1.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.2.2.tgz#01b6e8d163637bb2dd6c982ca4ed65863682786e" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.1.0: + version "6.2.2" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.2.2.tgz#cdcc8f9b6977719fdfbd1de7aec24abf756b9dea" + integrity sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA== + +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== + +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +date-fns@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" + integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== + +dayjs@^1.8.15: + version "1.11.19" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.19.tgz#15dc98e854bb43917f12021806af897c58ae2938" + integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw== + +debug@2.6.9, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +decode-uri-component@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +dedent@^1.0.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.7.2.tgz#34e2264ab538301e27cf7b07bf2369c19baa8dd9" + integrity sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2, deepmerge@^4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.2.2" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.263: + version "1.5.307" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz#09f8973100c39fb0d003b890393cd1d58932b1c8" + integrity sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg== + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +encodeurl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58" + integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg== + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +env-paths@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.13.0: + version "7.21.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.21.0.tgz#04a251be79f92548541f37d13c8b6f22940c3bae" + integrity sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow== + +error-ex@^1.3.1: + version "1.3.4" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.6: + version "2.1.4" + resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.1.4.tgz#229cb01cdbfa84440bfa91876285b94680188286" + integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== + dependencies: + stackframe "^1.3.4" + +errorhandler@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/errorhandler/-/errorhandler-1.5.2.tgz#dd0aa3952eca44aff7c2985e7d246c5932d70444" + integrity sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw== + dependencies: + accepts "~1.3.8" + escape-html "~1.0.3" + +es-abstract@^1.17.5, es-abstract@^1.23.2, es-abstract@^1.23.3, es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9, es-abstract@^1.24.0, es-abstract@^1.24.1: + version "1.24.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.24.1.tgz#f0c131ed5ea1bb2411134a8dd94def09c46c7899" + integrity sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.3.0" + get-proto "^1.0.1" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-negative-zero "^2.0.3" + is-regex "^1.2.1" + is-set "^2.0.3" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.1" + math-intrinsics "^1.1.0" + object-inspect "^1.13.4" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.4" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + stop-iteration-iterator "^1.1.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.19" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz#d979a9f686e2b0b72f88dbead7229924544720bc" + integrity sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-abstract "^1.24.1" + es-errors "^1.3.0" + es-set-tostringtag "^2.1.0" + function-bind "^1.1.2" + get-intrinsic "^1.3.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + iterator.prototype "^1.1.5" + safe-array-concat "^1.1.3" + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-shim-unscopables@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" + integrity sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== + dependencies: + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + +escalade@^3.1.1, escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.5.0: + version "8.10.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz#0642e53625ebc62c31c24726b0f050df6bd97a2e" + integrity sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A== + +eslint-plugin-eslint-comments@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa" + integrity sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ== + dependencies: + escape-string-regexp "^1.0.5" + ignore "^5.0.5" + +eslint-plugin-ft-flow@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-ft-flow/-/eslint-plugin-ft-flow-2.0.3.tgz#3b3c113c41902bcbacf0e22b536debcfc3c819e8" + integrity sha512-Vbsd/b+LYA99jUbsL6viEUWShFaYQt2YQs3QN3f+aeszOhh2sgdcU0mjzDyD4yyBvMc8qy2uwvBBWfMzEX06tg== + dependencies: + lodash "^4.17.21" + string-natural-compare "^3.0.1" + +eslint-plugin-jest@^29.0.1: + version "29.15.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-29.15.0.tgz#58a5917a88244f7536ae10c68b5bd58d407896f0" + integrity sha512-ZCGr7vTH2WSo2hrK5oM2RULFmMruQ7W3cX7YfwoTiPfzTGTFBMmrVIz45jZHd++cGKj/kWf02li/RhTGcANJSA== + dependencies: + "@typescript-eslint/utils" "^8.0.0" + +eslint-plugin-react-hooks@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz#1be0080901e6ac31ce7971beed3d3ec0a423d9e3" + integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg== + +eslint-plugin-react-native-globals@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-native-globals/-/eslint-plugin-react-native-globals-0.1.2.tgz#ee1348bc2ceb912303ce6bdbd22e2f045ea86ea2" + integrity sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g== + +eslint-plugin-react-native@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-native/-/eslint-plugin-react-native-4.1.0.tgz#5343acd3b2246bc1b857ac38be708f070d18809f" + integrity sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q== + dependencies: + eslint-plugin-react-native-globals "^0.1.1" + +eslint-plugin-react@^7.30.1: + version "7.37.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065" + integrity sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.3" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.2.1" + estraverse "^5.3.0" + hasown "^2.0.2" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.9" + object.fromentries "^2.0.8" + object.values "^1.2.1" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.12" + string.prototype.repeat "^1.0.0" + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + +eslint@^8.19.0: + version "8.57.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" + integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.0.0, events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +exponential-backoff@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6" + integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA== + +fast-base64-decode@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" + integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fast-xml-builder@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz#a485d7e8381f1db983cf006f849d1066e2935241" + integrity sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ== + +fast-xml-parser@^5.3.6: + version "5.4.2" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.4.2.tgz#7fc66463b59260b0c5fd57edf46148a418bde68b" + integrity sha512-pw/6pIl4k0CSpElPEJhDppLzaixDEuWui2CUQQBH/ECDf7+y6YwA4Gf7Tyb0Rfe4DIMuZipYj4AEL0nACKglvQ== + dependencies: + fast-xml-builder "^1.0.0" + strnum "^2.1.2" + +fastq@^1.6.0: + version "1.20.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.20.1.tgz#ca750a10dc925bc8b18839fd203e3ef4b3ced675" + integrity sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== + dependencies: + reusify "^1.0.4" + +fb-dotslash@0.5.8: + version "0.5.8" + resolved "https://registry.yarnpkg.com/fb-dotslash/-/fb-dotslash-0.5.8.tgz#c5ef3dacd75e1ddb2197c367052464ddde0115f5" + integrity sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA== + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +finalhandler@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-yarn-workspace-root@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd" + integrity sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ== + dependencies: + micromatch "^4.0.2" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.4.0" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.0.tgz#92ab2efec9b272eb85a3a25a106c3afbbc990d8b" + integrity sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw== + +flow-enums-runtime@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz#5bb0cd1b0a3e471330f4d109039b7eba5cb3e787" + integrity sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw== + +follow-redirects@^1.15.11: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== + +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + +form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + hasown "^2.0.2" + mime-types "^2.1.12" + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +generator-function@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/generator-function/-/generator-function-2.0.1.tgz#0e75dd410d1243687a0ba2e951b94eedb8f737a2" + integrity sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.1, get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.1, glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.11, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hash-base@^3.0.0, hash-base@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.2.tgz#79d72def7611c3f6e3c3b5730652638001b10a74" + integrity sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg== + dependencies: + inherits "^2.0.4" + readable-stream "^2.3.8" + safe-buffer "^5.2.1" + to-buffer "^1.2.1" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hermes-compiler@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/hermes-compiler/-/hermes-compiler-0.0.0.tgz#8d9f6a0b2740ce34d71258fec684e7b6bfd97efa" + integrity sha512-boVFutx6ME/Km2mB6vvsQcdnazEYYI/jV1pomx1wcFUG/EVqTkr5CU0CW9bKipOA/8Hyu3NYwW3THg2Q1kNCfA== + +hermes-estree@0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.32.0.tgz#bb7da6613ab8e67e334a1854ea1e209f487d307b" + integrity sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ== + +hermes-estree@0.33.3: + version "0.33.3" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.33.3.tgz#6d6b593d4b471119772c82bdb0212dfadabb6f17" + integrity sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg== + +hermes-parser@0.32.0: + version "0.32.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.32.0.tgz#7916984ef6fdce62e7415d354cf35392061cd303" + integrity sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw== + dependencies: + hermes-estree "0.32.0" + +hermes-parser@0.33.3: + version "0.33.3" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.33.3.tgz#da50ababb7a5ab636d339e7b2f6e3848e217e09d" + integrity sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA== + dependencies: + hermes-estree "0.33.3" + +hoist-non-react-statics@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +http-errors@^2.0.0, http-errors@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b" + integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ== + dependencies: + depd "~2.0.0" + inherits "~2.0.4" + setprototypeof "~1.2.0" + statuses "~2.0.2" + toidentifier "~1.0.1" + +https-proxy-agent@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@^0.7.0, iconv-lite@~0.7.0: + version "0.7.2" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e" + integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.0.5, ignore@^5.2.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +ignore@^7.0.5: + version "7.0.5" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" + integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== + +image-size@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.2.1.tgz#ee118aedfe666db1a6ee12bed5821cde3740276d" + integrity sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw== + dependencies: + queue "6.0.2" + +import-fresh@^3.2.1, import-fresh@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.1.tgz#9cecb56503c0ada1f2741dbbd6546e4b13b57ccf" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-arguments@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.2.0.tgz#ad58c6aecf563b78ef2bf04df540da8f5d7d8e1b" + integrity sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-arrayish@^0.3.1: + version "0.3.4" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" + integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== + +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-docker@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-generator-function@^1.0.10, is-generator-function@^1.0.7: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.2.tgz#ae3b61e3d5ea4e4839b90bad22b02335051a17d5" + integrity sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== + dependencies: + call-bound "^1.0.4" + generator-function "^2.0.0" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-nan@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15, is-typed-array@^1.1.3: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== + +is-wsl@^2.1.1, is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" + integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" + integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== + dependencies: + "@babel/core" "^7.23.9" + "@babel/parser" "^7.23.9" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz#cb4535162b5784aa623cee21a7252cf2c807ac93" + integrity sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +iterator.prototype@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz#12c959a29de32de0aa3bbbb801f4d777066dae39" + integrity sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== + dependencies: + define-data-property "^1.1.4" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + get-proto "^1.0.0" + has-symbols "^1.1.0" + set-function-name "^2.0.2" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.2.1: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +joi@^17.2.1: + version "17.13.3" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.13.3.tgz#0f5cc1169c999b30d344366d384b12d92558bcec" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== + dependencies: + "@hapi/hoek" "^9.3.0" + "@hapi/topo" "^5.1.0" + "@sideway/address" "^4.1.5" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.2" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.2.tgz#77485ce1dd7f33c061fd1b16ecea23b55fcb04b0" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== + dependencies: + argparse "^2.0.1" + +jsbi@^3.1.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.2.5.tgz#b37bb90e0e5c2814c1c2a1bcd8c729888a2e37d6" + integrity sha512-aBE4n43IPvjaddScbvWRA2YlTzKEynHzu7MqOyTipdHucf/VxS63ViCjxYRg86M8Rxwbt/GfzHl1kKERkt45fQ== + +jsc-safe-url@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz#141c14fbb43791e88d5dc64e85a374575a83477a" + integrity sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q== + +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stable-stringify@^1.0.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz#8903cfac42ea1a0f97f35d63a4ce0518f0cc6a70" + integrity sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + isarray "^2.0.5" + jsonify "^0.0.1" + object-keys "^1.1.1" + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + +jsonfile@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.2.0.tgz#7c265bd1b65de6977478300087c99f1c84383f62" + integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonify@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.1.tgz#2aa3111dae3d34a0f151c63f3a45d995d9420978" + integrity sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +klaw-sync@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c" + integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ== + dependencies: + graceful-fs "^4.1.11" + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +launch-editor@^2.9.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.13.1.tgz#d96ae376a282011661a112479a4bc2b8c1d914be" + integrity sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA== + dependencies: + picocolors "^1.1.1" + shell-quote "^1.8.3" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lighthouse-logger@^1.0.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz#aef90f9e97cd81db367c7634292ee22079280aaa" + integrity sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g== + dependencies: + debug "^2.6.9" + marky "^1.2.2" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +lodash@^4.17.21: + version "4.17.23" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" + integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +logkitty@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/logkitty/-/logkitty-0.7.1.tgz#8e8d62f4085a826e8d38987722570234e33c6aa7" + integrity sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ== + dependencies: + ansi-fragments "^0.2.1" + dayjs "^1.8.15" + yargs "^15.1.0" + +loose-envify@^1.0.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lottie-react-native@^7.3.4: + version "7.3.6" + resolved "https://registry.yarnpkg.com/lottie-react-native/-/lottie-react-native-7.3.6.tgz#b5ab46bff3ec9e48d5efa8d31166dd53758a1aa9" + integrity sha512-TevFHRvFURh6GlaqLKrSNXuKAxvBvFCiXfS7FXQI1K/ikOStgAwWLFPGjW0i1qB2/VzPACKmRs+535VjHUZZZQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +marky@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/marky/-/marky-1.3.0.tgz#422b63b0baf65022f02eda61a238eccdbbc14997" + integrity sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +media-typer@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561" + integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== + +memoize-one@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +metro-babel-transformer@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-babel-transformer/-/metro-babel-transformer-0.83.5.tgz#91f3fa269171ad5189ebba625f1f0aa124ce06ea" + integrity sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA== + dependencies: + "@babel/core" "^7.25.2" + flow-enums-runtime "^0.0.6" + hermes-parser "0.33.3" + nullthrows "^1.1.1" + +metro-cache-key@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-cache-key/-/metro-cache-key-0.83.5.tgz#96896a1768f0494a375e1d5957b7ad487e508a4c" + integrity sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-cache@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-cache/-/metro-cache-0.83.5.tgz#5675f4ad56905aa78fff3dec1b6bf213e0b6c86d" + integrity sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng== + dependencies: + exponential-backoff "^3.1.1" + flow-enums-runtime "^0.0.6" + https-proxy-agent "^7.0.5" + metro-core "0.83.5" + +metro-config@0.83.5, metro-config@^0.83.1: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-config/-/metro-config-0.83.5.tgz#a3dd20fc5d5582aa4ad3704678e52abcf4d46b2b" + integrity sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w== + dependencies: + connect "^3.6.5" + flow-enums-runtime "^0.0.6" + jest-validate "^29.7.0" + metro "0.83.5" + metro-cache "0.83.5" + metro-core "0.83.5" + metro-runtime "0.83.5" + yaml "^2.6.1" + +metro-core@0.83.5, metro-core@^0.83.1: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-core/-/metro-core-0.83.5.tgz#1592033633034feb5d368d22bf18e38052146970" + integrity sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ== + dependencies: + flow-enums-runtime "^0.0.6" + lodash.throttle "^4.1.1" + metro-resolver "0.83.5" + +metro-file-map@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-file-map/-/metro-file-map-0.83.5.tgz#394aa61d54b3822f10e68c18cbd1318f18865d20" + integrity sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ== + dependencies: + debug "^4.4.0" + fb-watchman "^2.0.0" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + invariant "^2.2.4" + jest-worker "^29.7.0" + micromatch "^4.0.4" + nullthrows "^1.1.1" + walker "^1.0.7" + +metro-minify-terser@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-minify-terser/-/metro-minify-terser-0.83.5.tgz#ee43a11a9d3442760781434c599d45eb1274e6fd" + integrity sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q== + dependencies: + flow-enums-runtime "^0.0.6" + terser "^5.15.0" + +metro-resolver@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-resolver/-/metro-resolver-0.83.5.tgz#72340ca8071941eafe92ff2dcb8e33c581870ef7" + integrity sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A== + dependencies: + flow-enums-runtime "^0.0.6" + +metro-runtime@0.83.5, metro-runtime@^0.83.1: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-runtime/-/metro-runtime-0.83.5.tgz#52c1edafc6cc82e57729cc9c21700ab1e53a1777" + integrity sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA== + dependencies: + "@babel/runtime" "^7.25.0" + flow-enums-runtime "^0.0.6" + +metro-source-map@0.83.5, metro-source-map@^0.83.1: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-source-map/-/metro-source-map-0.83.5.tgz#384f311f83fa2bf51cbec08d77210aa951bf9ee3" + integrity sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ== + dependencies: + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-symbolicate "0.83.5" + nullthrows "^1.1.1" + ob1 "0.83.5" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-symbolicate@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-symbolicate/-/metro-symbolicate-0.83.5.tgz#62167db423be6c68b4b9f39935c9cb7330cc9526" + integrity sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA== + dependencies: + flow-enums-runtime "^0.0.6" + invariant "^2.2.4" + metro-source-map "0.83.5" + nullthrows "^1.1.1" + source-map "^0.5.6" + vlq "^1.0.0" + +metro-transform-plugins@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-transform-plugins/-/metro-transform-plugins-0.83.5.tgz#ba21c6a5fa9bf6c5c2c222e2c8e7a668ffb3d341" + integrity sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + flow-enums-runtime "^0.0.6" + nullthrows "^1.1.1" + +metro-transform-worker@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro-transform-worker/-/metro-transform-worker-0.83.5.tgz#8616b54282e727027fdb5c475aade719394a8e8a" + integrity sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA== + dependencies: + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/parser" "^7.29.0" + "@babel/types" "^7.29.0" + flow-enums-runtime "^0.0.6" + metro "0.83.5" + metro-babel-transformer "0.83.5" + metro-cache "0.83.5" + metro-cache-key "0.83.5" + metro-minify-terser "0.83.5" + metro-source-map "0.83.5" + metro-transform-plugins "0.83.5" + nullthrows "^1.1.1" + +metro@0.83.5, metro@^0.83.1: + version "0.83.5" + resolved "https://registry.yarnpkg.com/metro/-/metro-0.83.5.tgz#f5441075d5211c980ac8c79109e9e6fa2df68924" + integrity sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ== + dependencies: + "@babel/code-frame" "^7.29.0" + "@babel/core" "^7.25.2" + "@babel/generator" "^7.29.1" + "@babel/parser" "^7.29.0" + "@babel/template" "^7.28.6" + "@babel/traverse" "^7.29.0" + "@babel/types" "^7.29.0" + accepts "^2.0.0" + chalk "^4.0.0" + ci-info "^2.0.0" + connect "^3.6.5" + debug "^4.4.0" + error-stack-parser "^2.0.6" + flow-enums-runtime "^0.0.6" + graceful-fs "^4.2.4" + hermes-parser "0.33.3" + image-size "^1.0.2" + invariant "^2.2.4" + jest-worker "^29.7.0" + jsc-safe-url "^0.2.2" + lodash.throttle "^4.1.1" + metro-babel-transformer "0.83.5" + metro-cache "0.83.5" + metro-cache-key "0.83.5" + metro-config "0.83.5" + metro-core "0.83.5" + metro-file-map "0.83.5" + metro-resolver "0.83.5" + metro-runtime "0.83.5" + metro-source-map "0.83.5" + metro-symbolicate "0.83.5" + metro-transform-plugins "0.83.5" + metro-transform-worker "0.83.5" + mime-types "^3.0.1" + nullthrows "^1.1.1" + serialize-error "^2.1.0" + source-map "^0.5.6" + throat "^5.0.0" + ws "^7.5.10" + yargs "^17.6.2" + +micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2", mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^2.1.12, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime-types@^3.0.0, mime-types@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@^10.2.2: + version "10.2.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" + integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg== + dependencies: + brace-expansion "^5.0.2" + +minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" + integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +moment@^2.30.1: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.3, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.11: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a" + integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg== + +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + +nocache@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79" + integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw== + +node-exports-info@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/node-exports-info/-/node-exports-info-1.6.0.tgz#1aedafb01a966059c9a5e791a94a94d93f5c2a13" + integrity sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw== + dependencies: + array.prototype.flatmap "^1.3.3" + es-errors "^1.3.0" + object.entries "^1.1.9" + semver "^6.3.1" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.27: + version "2.0.36" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.36.tgz#99fd6552aaeda9e17c4713b57a63964a2e325e9d" + integrity sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA== + +node-stream-zip@^1.9.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" + integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +nullthrows@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/nullthrows/-/nullthrows-1.1.1.tgz#7818258843856ae971eae4208ad7d7eb19a431b1" + integrity sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw== + +ob1@0.83.5: + version "0.83.5" + resolved "https://registry.yarnpkg.com/ob1/-/ob1-0.83.5.tgz#f9c289d759142b76577948eea7fd1f07d36f825f" + integrity sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg== + dependencies: + flow-enums-runtime "^0.0.6" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.3, object-inspect@^1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +object.entries@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.9.tgz#e4770a6a1444afb61bd39f984018b5bede25f8b3" + integrity sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.4" + define-properties "^1.2.1" + es-object-atoms "^1.1.1" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.1.tgz#deed520a50809ff7f75a7cfd4bc64c7a038c6216" + integrity sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +on-finished@^2.4.1, on-finished@~2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.1.0.tgz#59da4f91c45f5f989c6e4bcedc5a3b0aed70ff65" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^6.2.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" + integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== + dependencies: + is-wsl "^1.1.0" + +open@^7.0.3, open@^7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" + integrity sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q== + dependencies: + is-docker "^2.0.0" + is-wsl "^2.1.1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +patch-package@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-8.0.1.tgz#79d02f953f711e06d1f8949c8a13e5d3d7ba1a60" + integrity sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw== + dependencies: + "@yarnpkg/lockfile" "^1.1.0" + chalk "^4.1.2" + ci-info "^3.7.0" + cross-spawn "^7.0.3" + find-yarn-workspace-root "^2.0.0" + fs-extra "^10.0.0" + json-stable-stringify "^1.0.2" + klaw-sync "^6.0.0" + minimist "^1.2.6" + open "^7.4.2" + semver "^7.5.3" + slash "^2.0.0" + tmp "^0.2.4" + yaml "^2.2.2" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + +pirates@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22" + integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier@2.8.8: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +promise@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" + integrity sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg== + dependencies: + asap "~2.0.6" + +prompts@^2.0.1, prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +prop-types@^15.7.2, prop-types@^15.8.0, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +pure-rand@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" + integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== + +qrcode@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + +qs@^6.14.1: + version "6.15.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3" + integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ== + dependencies: + side-channel "^1.1.0" + +query-string@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" + integrity sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg== + dependencies: + decode-uri-component "^0.2.2" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +queue@6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/queue/-/queue-6.0.2.tgz#b91525283e2315c7553d2efa18d83e76432fed65" + integrity sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA== + dependencies: + inherits "~2.0.3" + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51" + integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA== + dependencies: + bytes "~3.1.2" + http-errors "~2.0.1" + iconv-lite "~0.7.0" + unpipe "~1.0.0" + +react-devtools-core@^6.1.5: + version "6.1.5" + resolved "https://registry.yarnpkg.com/react-devtools-core/-/react-devtools-core-6.1.5.tgz#c5eca79209dab853a03b2158c034c5166975feee" + integrity sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA== + dependencies: + shell-quote "^1.6.1" + ws "^7" + +react-freeze@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad" + integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA== + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" + integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== + +react-is@^19.1.0, react-is@^19.1.1: + version "19.2.4" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.4.tgz#a080758243c572ccd4a63386537654298c99d135" + integrity sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA== + +react-native-biometrics@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/react-native-biometrics/-/react-native-biometrics-3.0.1.tgz#23c5a0bdbae1fcb1e08b22936223fe0fc4af846e" + integrity sha512-Ru80gXRa9KG04sl5AB9HyjLjVbduhqZVjA+AiOSGqr+fNqCDmCu9y5WEksnjbnniNLmq1yGcw+qcLXmR1ddLDQ== + +react-native-device-info@^14.0.2: + version "14.1.1" + resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-14.1.1.tgz#f50c03902f87e4a99b1c51ed85163b795f02b3ea" + integrity sha512-lXFpe6DJmzbQXNLWxlMHP2xuTU5gwrKAvI8dCAZuERhW9eOXSubOQIesk9lIBnsi9pI19GMrcpJEvs4ARPRYmw== + +react-native-document-picker@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/react-native-document-picker/-/react-native-document-picker-9.3.1.tgz#f2c33237a906fd0893130e0605c8f18a3aef1605" + integrity sha512-Vcofv9wfB0j67zawFjfq9WQPMMzXxOZL9kBmvWDpjVuEcVK73ndRmlXHlkeFl5ZHVsv4Zb6oZYhqm9u5omJOPA== + dependencies: + invariant "^2.2.4" + +react-native-encrypted-storage@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/react-native-encrypted-storage/-/react-native-encrypted-storage-4.0.3.tgz#2a4d65459870511e8f4ccd22f02433dab7fa5e91" + integrity sha512-0pJA4Aj2S1PIJEbU7pN/Qvf7JIJx3hFywx+i+bLHtgK0/6Zryf1V2xVsWcrD50dfiu3jY1eN2gesQ5osGxE7jA== + +react-native-fs@^2.18.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== + dependencies: + base-64 "^0.1.0" + utf8 "^3.0.0" + +react-native-gesture-handler@^2.28.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.30.0.tgz#990c621fbeeefde853ececdcab7cbe1b621dbb8b" + integrity sha512-5YsnKHGa0X9C8lb5oCnKm0fLUPM6CRduvUUw2Bav4RIj/C3HcFh4RIUnF8wgG6JQWCL1//gRx4v+LVWgcIQdGA== + dependencies: + "@egjs/hammerjs" "^2.0.17" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + +react-native-get-random-values@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-2.0.0.tgz#21b6e40601d297a342378792a8cb9c8f853bff66" + integrity sha512-wx7/aPqsUIiWsG35D+MsUJd8ij96e3JKddklSdrdZUrheTx89gPtz3Q2yl9knBArj5u26Cl23T88ai+Q0vypdQ== + dependencies: + fast-base64-decode "^1.0.0" + +react-native-haptic-feedback@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-2.3.3.tgz#88b6876e91399a69bd1b551fe1681b2f3dc1214e" + integrity sha512-svS4D5PxfNv8o68m9ahWfwje5NqukM3qLS48+WTdhbDkNUkOhP9rDfDSRHzlhk4zq+ISjyw95EhLeh8NkKX5vQ== + +react-native-is-edge-to-edge@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" + integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== + +react-native-linear-gradient@^2.8.3: + version "2.8.3" + resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.8.3.tgz#9a116649f86d74747304ee13db325e20b21e564f" + integrity sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA== + +react-native-progress@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/react-native-progress/-/react-native-progress-5.0.1.tgz#4e15258b5661c49bad74554352326ca540bb7c58" + integrity sha512-TYfJ4auAe5vubDma2yfFvt7ktSI+UCfysqJnkdHEcLXqAitRFOozgF/cLgN5VNi/iLdaf3ga1ETi2RF4jVZ/+g== + dependencies: + prop-types "^15.7.2" + +react-native-qrcode-svg@^6.3.12: + version "6.3.21" + resolved "https://registry.yarnpkg.com/react-native-qrcode-svg/-/react-native-qrcode-svg-6.3.21.tgz#af873cf8e5b9fc68315a2c267ff6563d55c56abb" + integrity sha512-6vcj4rcdpWedvphDR+NSJcudJykNuLgNGFwm2p4xYjR8RdyTzlrELKI5LkO4ANS9cQUbqsfkpippPv64Q2tUtA== + dependencies: + prop-types "^15.8.0" + qrcode "^1.5.4" + text-encoding "^0.7.0" + +react-native-reanimated@^4.1.6: + version "4.2.2" + resolved "https://registry.yarnpkg.com/react-native-reanimated/-/react-native-reanimated-4.2.2.tgz#c7a25bac7c493387e03f53ab22ee2644f32cc528" + integrity sha512-o3kKvdD8cVlg12Z4u3jv0MFAt53QV4k7gD9OLwQqU8eZLyd8QvaOjVZIghMZhC2pjP93uUU44PlO5JgF8S4Vxw== + dependencies: + react-native-is-edge-to-edge "1.2.1" + semver "7.7.3" + +react-native-safe-area-context@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz#035699d5ec17fefb98cc1fa44a9ec852c7d530d0" + integrity sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ== + +react-native-screens@^4.17.1: + version "4.24.0" + resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-4.24.0.tgz#dbe8f610b5d2e31f71425d2ea3caaff1b9a7e2de" + integrity sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA== + dependencies: + react-freeze "^1.0.0" + warn-once "^0.1.0" + +react-native-share@^12.0.3: + version "12.2.5" + resolved "https://registry.yarnpkg.com/react-native-share/-/react-native-share-12.2.5.tgz#a793f8d7c337b2e8ee123630676b69c531c5addd" + integrity sha512-2uwd/PdlUyvpsSBfL7OMiL4sD0Ja51wu5m62JSIXjrtlF+uSTUOGsMrE49ncIRYziqAfYUeI195WwhpvqXB0ww== + +react-native-svg@^15.12.1: + version "15.15.3" + resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-15.15.3.tgz#48baf15ad9610be816b37c03ffbb1f72c056a2b0" + integrity sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA== + dependencies: + css-select "^5.1.0" + css-tree "^1.1.3" + warn-once "0.1.1" + +react-native-toast-message@^2.3.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/react-native-toast-message/-/react-native-toast-message-2.3.3.tgz#e301508d386a9902ff6b4559ecc6674f8cfdf97a" + integrity sha512-4IIUHwUPvKHu4gjD0Vj2aGQzqPATiblL1ey8tOqsxOWRPGGu52iIbL8M/mCz4uyqecvPdIcMY38AfwRuUADfQQ== + +react-native-vision-camera@^4.6.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/react-native-vision-camera/-/react-native-vision-camera-4.7.3.tgz#ed03cedabcaec54774f5aa40e69afa30069924d4" + integrity sha512-g1/neOyjSqn1kaAa2FxI/qp5KzNvPcF0bnQw6NntfbxH6tm0+8WFZszlgb5OV+iYlB6lFUztCbDtyz5IpL47OA== + +react-native-worklets@^0.7.0: + version "0.7.4" + resolved "https://registry.yarnpkg.com/react-native-worklets/-/react-native-worklets-0.7.4.tgz#6cc1eed31417ced2b007d82bfbd506ac27797de5" + integrity sha512-NYOdM1MwBb3n+AtMqy1tFy3Mn8DliQtd8sbzAVRf9Gc+uvQ0zRfxN7dS8ZzoyX7t6cyQL5THuGhlnX+iFlQTag== + dependencies: + "@babel/plugin-transform-arrow-functions" "7.27.1" + "@babel/plugin-transform-class-properties" "7.27.1" + "@babel/plugin-transform-classes" "7.28.4" + "@babel/plugin-transform-nullish-coalescing-operator" "7.27.1" + "@babel/plugin-transform-optional-chaining" "7.27.1" + "@babel/plugin-transform-shorthand-properties" "7.27.1" + "@babel/plugin-transform-template-literals" "7.27.1" + "@babel/plugin-transform-unicode-regex" "7.27.1" + "@babel/preset-typescript" "7.27.1" + convert-source-map "2.0.0" + semver "7.7.3" + +react-native-zeroconf@^0.13.8: + version "0.13.8" + resolved "https://registry.yarnpkg.com/react-native-zeroconf/-/react-native-zeroconf-0.13.8.tgz#62fe5279fdf81250a0da647454f2c3b8ee8cacae" + integrity sha512-frGS1xNbNCA7BfETSubNYODu7s7mlU55vgArEzZW9EuSQ8SYqlNQC5zpKHDZEAXy4fFqe+CgnMCxKAjQqYM6XA== + dependencies: + events "^3.0.0" + +react-native@^0.82.0: + version "0.82.1" + resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.82.1.tgz#8f850bf2d5f04d49246c2d604836218daca19af7" + integrity sha512-tFAqcU7Z4g49xf/KnyCEzI4nRTu1Opcx05Ov2helr8ZTg1z7AJR/3sr2rZ+AAVlAs2IXk+B0WOxXGmdD3+4czA== + dependencies: + "@jest/create-cache-key-function" "^29.7.0" + "@react-native/assets-registry" "0.82.1" + "@react-native/codegen" "0.82.1" + "@react-native/community-cli-plugin" "0.82.1" + "@react-native/gradle-plugin" "0.82.1" + "@react-native/js-polyfills" "0.82.1" + "@react-native/normalize-colors" "0.82.1" + "@react-native/virtualized-lists" "0.82.1" + abort-controller "^3.0.0" + anser "^1.4.9" + ansi-regex "^5.0.0" + babel-jest "^29.7.0" + babel-plugin-syntax-hermes-parser "0.32.0" + base64-js "^1.5.1" + commander "^12.0.0" + flow-enums-runtime "^0.0.6" + glob "^7.1.1" + hermes-compiler "0.0.0" + invariant "^2.2.4" + jest-environment-node "^29.7.0" + memoize-one "^5.0.0" + metro-runtime "^0.83.1" + metro-source-map "^0.83.1" + nullthrows "^1.1.1" + pretty-format "^29.7.0" + promise "^8.3.0" + react-devtools-core "^6.1.5" + react-refresh "^0.14.0" + regenerator-runtime "^0.13.2" + scheduler "0.26.0" + semver "^7.1.3" + stacktrace-parser "^0.1.10" + whatwg-fetch "^3.0.0" + ws "^6.2.3" + yargs "^17.6.2" + +react-refresh@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react-test-renderer@19.1.1: + version "19.1.1" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-19.1.1.tgz#c1e57b7a9c7291e3f52c489022071ac39f55155a" + integrity sha512-aGRXI+zcBTtg0diHofc7+Vy97nomBs9WHHFY1Csl3iV0x6xucjNYZZAkiVKGiNYUv23ecOex5jE67t8ZzqYObA== + dependencies: + react-is "^19.1.1" + scheduler "^0.26.0" + +react@19.1.1: + version "19.1.1" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.1.tgz#06d9149ec5e083a67f9a1e39ce97b06a03b644af" + integrity sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ== + +readable-stream@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz#aa113812ba899b630658c7623466be71e1f86f66" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.2: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regexp.prototype.flags@^1.5.3, regexp.prototype.flags@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.4.0.tgz#3580ce0c4faedef599eccb146612436b62a176e5" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.2" + regjsgen "^0.8.0" + regjsparser "^0.13.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.2.1" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.13.0.tgz#01f8351335cf7898d43686bc74d2dd71c847ecc0" + integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== + dependencies: + jsesc "~3.1.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" + integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== + +resolve@^1.20.0, resolve@^1.22.11: + version "1.22.11" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== + dependencies: + is-core-module "^2.16.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.5: + version "2.0.0-next.6" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.6.tgz#b3961812be69ace7b3bc35d5bf259434681294af" + integrity sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA== + dependencies: + es-errors "^1.3.0" + is-core-module "^2.16.1" + node-exports-info "^1.6.0" + object-keys "^1.1.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.3.tgz#9be54e4ba5e3559c8eee06a25cd7648bbccdf5a8" + integrity sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA== + dependencies: + hash-base "^3.1.2" + inherits "^2.0.4" + +rn-barcode-zxing-scan@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/rn-barcode-zxing-scan/-/rn-barcode-zxing-scan-1.0.4.tgz#030ddf8133eaccc1afe2bedf4c72469e0ab1c8c8" + integrity sha512-UoeNx5+3ifDlP5Jk8DvOU28cmR8du9ydP1344OC5ltrxwikPO/I1pBXAZe9UCg66kFXexmwwHJEKcNxmU79M7g== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +scheduler@0.26.0, scheduler@^0.26.0: + version "0.26.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337" + integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA== + +semver@7.7.3: + version "7.7.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946" + integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q== + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.1.3, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.7.3: + version "7.7.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" + integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== + +send@~0.19.1: + version "0.19.2" + resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29" + integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~2.0.0" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "~0.5.2" + http-errors "~2.0.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.4.1" + range-parser "~1.2.1" + statuses "~2.0.2" + +serialize-error@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" + integrity sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw== + +serve-static@^1.13.1, serve-static@^1.16.2: + version "1.16.3" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9" + integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA== + dependencies: + encodeurl "~2.0.0" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "~0.19.1" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +setprototypeof@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sf-symbols-typescript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz#926d6e0715e3d8784cadf7658431e36581254208" + integrity sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw== + +sha.js@^2.4.0, sha.js@^2.4.11: + version "2.4.12" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.12.tgz#eb8b568bf383dfd1867a32c3f2b74eb52bdbf23f" + integrity sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w== + dependencies: + inherits "^2.0.4" + safe-buffer "^5.2.1" + to-buffer "^1.2.0" + +sha256-uint8array@^0.10.3: + version "0.10.7" + resolved "https://registry.yarnpkg.com/sha256-uint8array/-/sha256-uint8array-0.10.7.tgz#c751fc914f4227b26d996980562065fa4eadcf99" + integrity sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.6.1, shell-quote@^1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b" + integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +simple-swizzle@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" + integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +stackframe@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.3.4.tgz#b881a004c8c149a5e8efef37d51b16e412943310" + integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== + +stacktrace-parser@^0.1.10: + version "0.1.11" + resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz#c7c08f9b29ef566b9a6f7b255d7db572f66fabc4" + integrity sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg== + dependencies: + type-fest "^0.7.1" + +statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +statuses@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" + integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== + +stop-iteration-iterator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz#f481ff70a548f6124d0312c3aa14cbfa7aa542ad" + integrity sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== + dependencies: + es-errors "^1.3.0" + internal-slot "^1.1.0" + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + +strict-url-sanitise@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/strict-url-sanitise/-/strict-url-sanitise-0.0.1.tgz#10cfac63c9dfdd856d98ab9f76433dad5ce99e0c" + integrity sha512-nuFtF539K8jZg3FjaWH/L8eocCR6gegz5RDOsaWxfdbF5Jqr2VXWxZayjTwUzsWJDC91k2EbnJXp6FuWW+Z4hg== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-natural-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" + integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.matchall@^4.0.12: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" + set-function-name "^2.0.2" + side-channel "^1.1.0" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strnum@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.2.0.tgz#8b582b637e4621f62ff714493e0ce30846f903a6" + integrity sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +terser@^5.15.0: + version "5.46.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.46.0.tgz#1b81e560d584bbdd74a8ede87b4d9477b0ff9695" + integrity sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.15.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-encoding@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.7.0.tgz#f895e836e45990624086601798ea98e8f36ee643" + integrity sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +throat@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" + integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== + +tinyglobby@^0.2.15: + version "0.2.15" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2" + integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.3" + +tmp@^0.2.4: + version "0.2.5" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.5.tgz#b06bcd23f0f3c8357b426891726d16015abfd8f8" + integrity sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-buffer@^1.2.0, to-buffer@^1.2.1, to-buffer@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.2.2.tgz#ffe59ef7522ada0a2d1cb5dfe03bb8abc3cdc133" + integrity sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw== + dependencies: + isarray "^2.0.5" + safe-buffer "^5.2.1" + typed-array-buffer "^1.0.3" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +ts-api-utils@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8" + integrity sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA== + +tslib@^2.3.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" + integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== + +type-is@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97" + integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw== + dependencies: + content-type "^1.0.5" + media-typer "^1.1.0" + mime-types "^3.0.0" + +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + +typescript@^5.8.3: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + +undici-types@~7.18.0: + version "7.18.2" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" + integrity sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz#65a7adfad8574c219890e219285ce4c64ed67eaa" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz#301d4f8a43d2b75c97adfad87c9dd5350c9475d1" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d" + integrity sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-latest-callback@^0.2.4: + version "0.2.6" + resolved "https://registry.yarnpkg.com/use-latest-callback/-/use-latest-callback-0.2.6.tgz#e5ea752808c86219acc179ace0ae3c1203255e77" + integrity sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg== + +use-sync-external-store@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util@^0.12.5: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + is-typed-array "^1.1.3" + which-typed-array "^1.1.2" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-to-istanbul@^9.0.1: + version "9.3.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" + integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vlq@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468" + integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w== + +walker@^1.0.7, walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +warn-once@0.1.1, warn-once@^0.1.0, warn-once@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" + integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +whatwg-fetch@^3.0.0: + version "3.6.20" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz#580ce6d791facec91d37c72890995a0b48d31c70" + integrity sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg== + +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +which-typed-array@^1.1.16, which-typed-array@^1.1.19, which-typed-array@^1.1.2: + version "1.1.20" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.20.tgz#3fdb7adfafe0ea69157b1509f3a1cd892bd1d122" + integrity sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +ws@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" + integrity sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA== + dependencies: + async-limiter "~1.0.0" + +ws@^7, ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^2.2.1, yaml@^2.2.2, yaml@^2.6.1: + version "2.8.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5" + integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^15.1.0, yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" + +yargs@^17.3.1, yargs@^17.6.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==