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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/reusable-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ jobs:
./app_config.sh
popd

- name: Build PIVX Sapling Lib
run: |
set -x -e
pushd scripts/android
./build_pivx.sh
popd

- name: Install Flutter dependencies
run: |
flutter pub get
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ lib/dogecoin/dogecoin.dart
lib/base/base.dart
lib/arbitrum/arbitrum.dart
lib/evm/evm.dart
lib/pivx/pivx.dart
lib/zcash/zcash.dart

ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Expand Down
22 changes: 22 additions & 0 deletions assets/images/pivx_chain_qr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/images/pivx_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions assets/pivx_electrum_server_list.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-
uri: electrum02.chainster.org:50002
is_default: true
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum01.chainster.org:50002
useSSL: true
isEnabledForAutoSwitching: true
-
uri: electrum02.chainster.org:50001
useSSL: false
isEnabledForAutoSwitching: true
-
uri: electrum01.chainster.org:50001
useSSL: false
isEnabledForAutoSwitching: true
52 changes: 47 additions & 5 deletions cw_bitcoin/lib/electrum.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,17 @@ class ElectrumClient {
(Uint8List event) {
try {
final msg = utf8.decode(event.toList());
final messagesList = msg.split("\n");
for (var message in messagesList) {
if (message.isEmpty) {
continue;
// Accumulate across reads; large responses span multiple TCP packets.
unterminatedString += msg;

while (unterminatedString.contains('\n')) {
final newlineIndex = unterminatedString.indexOf('\n');
final completeLine = unterminatedString.substring(0, newlineIndex);
unterminatedString = unterminatedString.substring(newlineIndex + 1);

if (completeLine.isNotEmpty) {
_parseResponse(completeLine);
}
_parseResponse(message);
}
} catch (e) {
printV("socket.listen: $e");
Expand Down Expand Up @@ -637,6 +642,15 @@ class ElectrumClient {
id: 'blockchain.headers.subscribe', method: 'blockchain.headers.subscribe');
}

// PIVX Sapling 0-conf mempool push feed: initial snapshot then the same
// envelope on every change.
BehaviorSubject<Object>? saplingMempoolSubscribe() {
_id += 1;
return subscribe<Object>(
id: 'blockchain.sapling.mempool.subscribe',
method: 'blockchain.sapling.mempool.subscribe');
}

BehaviorSubject<Object>? scripthashUpdate(String scripthash) {
_id += 1;
return subscribe<Object>(
Expand Down Expand Up @@ -718,12 +732,35 @@ class ElectrumClient {
}

void _resetInternalStateCompletely() {
// unblock awaiting callers before clearing _tasks. an explicit close or
// reconnect that clears without onDone firing first would orphan an in-flight
// completer (e.g. node switching mid shield sync).
failPendingRequests();
_id = 0;
_tasks.clear();
_errors.clear();
unterminatedString = '';
}

// fail in-flight request completers on disconnect so callers awaiting call()
// unblock instead of hanging. call() has no timeout, so a dropped/half-open
// socket leaves the completer dangling forever (this wedged pivx shield sync
// until restart). subscriptions are left alone so they resume on reconnect.
@visibleForTesting
void failPendingRequests() {
final pending = _tasks.entries
.where((task) => !task.value.isSubscription && task.value.completer != null)
.toList();
for (final task in pending) {
final completer = task.value.completer!;
if (!completer.isCompleted) {
completer.completeError(
RequestFailedTimeoutException('connection_closed', 0));
}
_tasks.remove(task.key);
}
}

void _registryTask(int id, Completer<dynamic> completer) =>
_tasks[id.toString()] = SocketTask(completer: completer, isSubscription: false);

Expand Down Expand Up @@ -769,6 +806,10 @@ class ElectrumClient {
final params = request['params'] as List<dynamic>;
_tasks[_tasks.keys.first]?.subject?.add(params.last);
break;
case 'blockchain.sapling.mempool.subscribe':
final params = request['params'] as List<dynamic>;
_tasks['blockchain.sapling.mempool.subscribe']?.subject?.add(params.last);
break;
default:
break;
}
Expand All @@ -782,6 +823,7 @@ class ElectrumClient {
socket?.destroy();
} catch (_) {}
socket = null;
failPendingRequests();
}
}

Expand Down
30 changes: 27 additions & 3 deletions cw_bitcoin/lib/electrum_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ abstract class ElectrumWalletBase
return bitcoinCashHDWallet(seedBytes);
case CryptoCurrency.doge:
return dogecoinHDWallet(seedBytes);
case CryptoCurrency.pivx:
return pivxHDWallet(seedBytes);
default:
throw Exception("Unsupported currency");
}
Expand All @@ -260,6 +262,9 @@ abstract class ElectrumWalletBase
static Bip32Slip10Secp256k1 dogecoinHDWallet(Uint8List seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/3'/0'") as Bip32Slip10Secp256k1;

static Bip32Slip10Secp256k1 pivxHDWallet(Uint8List seedBytes) =>
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/44'/119'/0'") as Bip32Slip10Secp256k1;

static int estimatedTransactionSize(int inputsCount, int outputsCounts) =>
inputsCount * 68 + outputsCounts * 34 + 10;

Expand Down Expand Up @@ -945,6 +950,12 @@ abstract class ElectrumWalletBase
return utx.bitcoinAddressRecord.type == SegwitAddresType.mweb;
case UnspentCoinType.nonMweb:
return utx.bitcoinAddressRecord.type != SegwitAddresType.mweb;
case UnspentCoinType.sapling:
// PIVX shielded notes, not tracked as UTXOs
return false;
case UnspentCoinType.transparent:
// PIVX transparent, all regular UTXOs
return true;
case UnspentCoinType.any:
case UnspentCoinType.lightning:
return true;
Expand Down Expand Up @@ -1909,9 +1920,7 @@ abstract class ElectrumWalletBase
}
}

final results = shouldUseBatchFetching
? await _fetchUnspentsBatch(targetAddresses)
: await _fetchUnspentsRegular(targetAddresses);
final results = await fetchUnspentsForAddresses(targetAddresses);

final failedCount = results.where((result) => result == null).length;

Expand Down Expand Up @@ -1944,6 +1953,18 @@ abstract class ElectrumWalletBase
await _refreshUnspentCoinsInfo();
}

/// Fetch each address's unspents as a per-address list (null entry = fetch
/// failed for that address). Overridable so a coin with a custom scripthash or
/// confirmation source can batch its own way; the default picks batch vs
/// regular fetching.
Future<List<List<BitcoinUnspent>?>> fetchUnspentsForAddresses(
List<BitcoinAddressRecord> addresses,
) async {
return shouldUseBatchFetching
? await _fetchUnspentsBatch(addresses)
: await _fetchUnspentsRegular(addresses);
}

Future<List<List<BitcoinUnspent>?>> _fetchUnspentsRegular(
List<BitcoinAddressRecord> addresses,
) async {
Expand Down Expand Up @@ -2606,6 +2627,9 @@ abstract class ElectrumWalletBase
await Future.wait(DOGECOIN_ADDRESS_TYPES.map((type) => shouldUseBatchFetching
? fetchTransactionsForAddressTypeBatch(historiesWithDetails, type)
: fetchTransactionsForAddressType(historiesWithDetails, type)));
} else if (type == WalletType.pivx) {
await Future.wait(PIVX_ADDRESS_TYPES
.map((type) => fetchTransactionsForAddressType(historiesWithDetails, type)));
}

transactionHistory.transactions.values.forEach((tx) async {
Expand Down
56 changes: 52 additions & 4 deletions cw_bitcoin/lib/electrum_wallet_addresses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ const List<BitcoinAddressType> DOGECOIN_ADDRESS_TYPES = [
P2pkhAddressType.p2pkh,
];

const List<BitcoinAddressType> PIVX_ADDRESS_TYPES = [
P2pkhAddressType.p2pkh,
];

abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
ElectrumWalletAddressesBase(
WalletInfo walletInfo, {
Expand Down Expand Up @@ -365,6 +369,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}
} else if (walletInfo.type == WalletType.dogecoin) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.pivx) {
await _generateInitialAddresses(type: P2pkhAddressType.p2pkh);
} else if (walletInfo.type == WalletType.bitcoin) {
await _generateInitialAddresses(isLegacyDerivation: true);
await _generateInitialAddresses();
Expand All @@ -385,9 +391,12 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
}

updateAddressesByMatch();
// reconcile branch/derivation before building the receive/change lists
// (they partition on isHidden), so a corrected isHidden lands in the right
// list.
await _validateAddresses();
updateReceiveAddresses();
updateChangeAddresses();
_validateAddresses();
await updateAddressesInBox();

if (currentReceiveAddressIndex >= receiveAddresses.length) {
Expand Down Expand Up @@ -624,6 +633,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
case WalletType.dogecoin:
addP2PKHAddressTypes();
break;
case WalletType.pivx:
addP2PKHAddressTypes();
break;
default:
break;
}
Expand Down Expand Up @@ -869,13 +881,20 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
updateAddressesByMatch();
}

void _validateAddresses() {
_addresses.forEach((element) async {
Future<void> _validateAddresses() async {
await Future.wait(_addresses.map((element) async {
if (element.type == SegwitAddresType.mweb) {
// this would add a ton of startup lag for mweb addresses since we have 1000 of them
return;
}

// pivx re-derives against branch and derivation combos; other coins keep
// the base flip so their behavior is unchanged.
if (walletInfo.type == WalletType.pivx) {
await _reconcileAddressMetadata(element);
return;
}

final mainHd = _hdFor(
isHidden: false, type: element.type, isLegacyDerivation: element.isLegacyDerivation);
final sideHd = _hdFor(
Expand All @@ -889,7 +908,36 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store {
await getAddressAsync(index: element.index, hd: sideHd, addressType: element.type)) {
element.isHidden = false;
}
});
}));
}

/// Set a record's branch (isHidden) and derivation (isLegacyDerivation) to a
/// combo that re-derives its stored address. The base flip changed isHidden on
/// any mismatch without checking the target re-derives, leaving a still-wrong
/// record whose key can't sign its UTXO. Try the stored combo first, then
/// alternatives, apply only one that verifies. index is final, so a record
/// matching none is left for the signer to reject.
Future<void> _reconcileAddressMetadata(BitcoinAddressRecord element) async {
for (final isLegacyDerivation in <bool>[
element.isLegacyDerivation,
!element.isLegacyDerivation,
]) {
for (final isHidden in <bool>[element.isHidden, !element.isHidden]) {
final hd = _hdFor(
isHidden: isHidden,
type: element.type,
isLegacyDerivation: isLegacyDerivation);
final derived = await getAddressAsync(
index: element.index, hd: hd, addressType: element.type);
if (element.address == derived) {
if (element.isHidden != isHidden) element.isHidden = isHidden;
if (element.isLegacyDerivation != isLegacyDerivation) {
element.isLegacyDerivation = isLegacyDerivation;
}
return;
}
}
}
}

@override
Expand Down
4 changes: 2 additions & 2 deletions cw_bitcoin/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ dependency_overrides:
protobuf: ^3.1.0
bitcoin_base:
git:
url: https://github.com/cake-tech/bitcoin_base
ref: 4e41f96f4838139895c65c3f49109d05af5d46aa
url: https://github.com/Liquid369/bitcoin_base
ref: 490c1b644090685a49b2952b2b1205f0b4c50e84
pointycastle: 3.7.4
ffi: 2.1.0
intl: any
Expand Down
Loading