WIP: Add p2sh address for bch

This commit is contained in:
Likho 2022-09-23 14:31:14 +02:00
parent 559da849ce
commit 909436afdc
2 changed files with 344 additions and 146 deletions

View file

@ -51,7 +51,7 @@ const String GENESIS_HASH_MAINNET =
const String GENESIS_HASH_TESTNET =
"000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
enum DerivePathType { bip44 }
enum DerivePathType { bip44, bip49 }
bip32.BIP32 getBip32Node(int chain, int index, String mnemonic,
NetworkType network, DerivePathType derivePathType) {
@ -90,6 +90,8 @@ bip32.BIP32 getBip32NodeFromRoot(
switch (derivePathType) {
case DerivePathType.bip44:
return root.derivePath("m/44'/$coinType'/0'/$chain/$index");
case DerivePathType.bip49:
return root.derivePath("m/49'/$coinType'/0'/$chain/$index");
default:
throw Exception("DerivePathType must not be null.");
}
@ -203,9 +205,13 @@ class BitcoinCashWallet extends CoinServiceAPI {
Future<String> get currentReceivingAddress =>
_currentReceivingAddressP2PKH ??=
_getCurrentAddressForChain(0, DerivePathType.bip44);
Future<String>? _currentReceivingAddressP2PKH;
Future<String> get currentReceivingAddressP2SH =>
_currentReceivingAddressP2SH ??=
_getCurrentAddressForChain(0, DerivePathType.bip49);
Future<String>? _currentReceivingAddressP2SH;
@override
Future<void> exit() async {
_hasCalledExit = true;
@ -269,6 +275,11 @@ class BitcoinCashWallet extends CoinServiceAPI {
// P2PKH
return DerivePathType.bip44;
}
if (decodeBase58[0] == _network.scriptHash) {
// P2SH
return DerivePathType.bip49;
}
throw ArgumentError('Invalid version or Network mismatch');
} else {
try {
@ -356,163 +367,72 @@ class BitcoinCashWallet extends CoinServiceAPI {
Map<String, Map<String, String>> p2pkhReceiveDerivations = {};
Map<String, Map<String, String>> p2pkhChangeDerivations = {};
Map<String, Map<String, String>> p2shReceiveDerivations = {};
Map<String, Map<String, String>> p2shChangeDerivations = {};
final root = await compute(getBip32RootWrapper, Tuple2(mnemonic, _network));
List<String> p2pkhReceiveAddressArray = [];
List<String> p2shReceiveAddressArray = [];
int p2pkhReceiveIndex = -1;
int p2shReceiveIndex = -1;
List<String> p2pkhChangeAddressArray = [];
List<String> p2shChangeAddressArray = [];
int p2pkhChangeIndex = -1;
int p2shChangeIndex = -1;
// The gap limit will be capped at [maxUnusedAddressGap]
int receivingGapCounter = 0;
int changeGapCounter = 0;
// int receivingGapCounter = 0;
// int changeGapCounter = 0;
// actual size is 12 due to p2pkh so 12x1
// actual size is 24 due to p2pkh and p2sh so 12x2
const txCountBatchSize = 12;
try {
// receiving addresses
Logging.instance
.log("checking receiving addresses...", level: LogLevel.Info);
for (int index = 0;
index < maxNumberOfIndexesToCheck &&
receivingGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t receivingGapCounter: $receivingGapCounter",
level: LogLevel.Info);
final receivingP2pkhID = "k_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
// bip44 / P2PKH
final node44 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
0,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhReceiveAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
receivingNodes.addAll({
"${receivingP2pkhID}_$j": {
"node": node44,
"address": p2pkhReceiveAddress,
}
});
txCountCallArgs.addAll({
"${receivingP2pkhID}_$j": p2pkhReceiveAddress,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int p2pkhTxCount = counts["${receivingP2pkhID}_$k"]!;
if (p2pkhTxCount > 0) {
final node = receivingNodes["${receivingP2pkhID}_$k"];
// add address to array
p2pkhReceiveAddressArray.add(node["address"] as String);
// set current index
p2pkhReceiveIndex = index + k;
// reset counter
receivingGapCounter = 0;
// add info to derivations
p2pkhReceiveDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found
if (p2pkhTxCount == 0) {
receivingGapCounter++;
}
}
}
final resultReceive44 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 0);
final resultReceive49 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 0);
Logging.instance
.log("checking change addresses...", level: LogLevel.Info);
// change addresses
for (int index = 0;
index < maxNumberOfIndexesToCheck &&
changeGapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
Logging.instance.log(
"index: $index, \t changeGapCounter: $changeGapCounter",
level: LogLevel.Info);
final changeP2pkhID = "k_$index";
Map<String, String> args = {};
final Map<String, dynamic> changeNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
// bip44 / P2PKH
final node44 = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
1,
index + j,
root,
DerivePathType.bip44,
),
);
final p2pkhChangeAddress = P2PKH(
data: PaymentData(pubkey: node44.publicKey),
network: _network)
.data
.address!;
changeNodes.addAll({
"${changeP2pkhID}_$j": {
"node": node44,
"address": p2pkhChangeAddress,
}
});
args.addAll({
"${changeP2pkhID}_$j": p2pkhChangeAddress,
});
}
final resultChange44 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip44, 1);
// get address tx counts
final counts = await _getBatchTxCount(addresses: args);
final resultChange49 = _checkGaps(maxNumberOfIndexesToCheck,
maxUnusedAddressGap, txCountBatchSize, root, DerivePathType.bip49, 1);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int p2pkhTxCount = counts["${changeP2pkhID}_$k"]!;
if (p2pkhTxCount > 0) {
final node = changeNodes["${changeP2pkhID}_$k"];
// add address to array
p2pkhChangeAddressArray.add(node["address"] as String);
// set current index
p2pkhChangeIndex = index + k;
// reset counter
changeGapCounter = 0;
// add info to derivations
p2pkhChangeDerivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
await Future.wait(
[resultReceive44, resultReceive49, resultChange44, resultChange49]);
// increase counter when no tx history found
if (p2pkhTxCount == 0) {
changeGapCounter++;
}
}
}
p2pkhReceiveAddressArray =
(await resultReceive44)['addressArray'] as List<String>;
p2pkhReceiveIndex = (await resultReceive44)['index'] as int;
p2pkhReceiveDerivations = (await resultReceive44)['derivations']
as Map<String, Map<String, String>>;
p2shReceiveAddressArray =
(await resultReceive49)['addressArray'] as List<String>;
p2shReceiveIndex = (await resultReceive49)['index'] as int;
p2shReceiveDerivations = (await resultReceive49)['derivations']
as Map<String, Map<String, String>>;
p2pkhChangeAddressArray =
(await resultChange44)['addressArray'] as List<String>;
p2pkhChangeIndex = (await resultChange44)['index'] as int;
p2pkhChangeDerivations = (await resultChange44)['derivations']
as Map<String, Map<String, String>>;
p2shChangeAddressArray =
(await resultChange49)['addressArray'] as List<String>;
p2shChangeIndex = (await resultChange49)['index'] as int;
p2shChangeDerivations = (await resultChange49)['derivations']
as Map<String, Map<String, String>>;
// save the derivations (if any)
if (p2pkhReceiveDerivations.isNotEmpty) {
@ -521,12 +441,25 @@ class BitcoinCashWallet extends CoinServiceAPI {
derivePathType: DerivePathType.bip44,
derivationsToAdd: p2pkhReceiveDerivations);
}
if (p2shReceiveDerivations.isNotEmpty) {
await addDerivations(
chain: 0,
derivePathType: DerivePathType.bip49,
derivationsToAdd: p2shReceiveDerivations);
}
if (p2pkhChangeDerivations.isNotEmpty) {
await addDerivations(
chain: 1,
derivePathType: DerivePathType.bip44,
derivationsToAdd: p2pkhChangeDerivations);
}
if (p2shChangeDerivations.isNotEmpty) {
await addDerivations(
chain: 1,
derivePathType: DerivePathType.bip49,
derivationsToAdd: p2shChangeDerivations);
}
// If restoring a wallet that never received any funds, then set receivingArray manually
// If we didn't do this, it'd store an empty array
@ -536,6 +469,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
p2pkhReceiveAddressArray.add(address);
p2pkhReceiveIndex = 0;
}
if (p2shReceiveIndex == -1) {
final address =
await _generateAddressForChain(0, 0, DerivePathType.bip49);
p2shReceiveAddressArray.add(address);
p2shReceiveIndex = 0;
}
// If restoring a wallet that never sent any funds with change, then set changeArray
// manually. If we didn't do this, it'd store an empty array.
@ -545,6 +484,12 @@ class BitcoinCashWallet extends CoinServiceAPI {
p2pkhChangeAddressArray.add(address);
p2pkhChangeIndex = 0;
}
if (p2shChangeIndex == -1) {
final address =
await _generateAddressForChain(1, 0, DerivePathType.bip49);
p2shChangeAddressArray.add(address);
p2shChangeIndex = 0;
}
await DB.instance.put<dynamic>(
boxName: walletId,
@ -554,12 +499,29 @@ class BitcoinCashWallet extends CoinServiceAPI {
boxName: walletId,
key: 'changeAddressesP2PKH',
value: p2pkhChangeAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingAddressesP2SH',
value: p2shReceiveAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'changeAddressesP2SH',
value: p2shChangeAddressArray);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingIndexP2PKH',
value: p2pkhReceiveIndex);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'receivingIndexP2SH',
value: p2shReceiveIndex);
await DB.instance.put<dynamic>(
boxName: walletId, key: 'changeIndexP2PKH', value: p2pkhChangeIndex);
await DB.instance.put<dynamic>(
boxName: walletId, key: 'changeIndexP2SH', value: p2shChangeIndex);
await DB.instance
.put<dynamic>(boxName: walletId, key: "id", value: _walletId);
await DB.instance
@ -576,6 +538,131 @@ class BitcoinCashWallet extends CoinServiceAPI {
}
}
Future<Map<String, dynamic>> _checkGaps(
int maxNumberOfIndexesToCheck,
int maxUnusedAddressGap,
int txCountBatchSize,
bip32.BIP32 root,
DerivePathType type,
int account) async {
List<String> addressArray = [];
int returningIndex = -1;
Map<String, Map<String, String>> derivations = {};
int gapCounter = 0;
for (int index = 0;
index < maxNumberOfIndexesToCheck && gapCounter < maxUnusedAddressGap;
index += txCountBatchSize) {
List<String> iterationsAddressArray = [];
Logging.instance.log(
"index: $index, \t GapCounter $account ${type.name}: $gapCounter",
level: LogLevel.Info);
final _id = "k_$index";
Map<String, String> txCountCallArgs = {};
final Map<String, dynamic> receivingNodes = {};
for (int j = 0; j < txCountBatchSize; j++) {
final node = await compute(
getBip32NodeFromRootWrapper,
Tuple4(
account,
index + j,
root,
type,
),
);
String? address;
switch (type) {
case DerivePathType.bip44:
address = P2PKH(
data: PaymentData(pubkey: node.publicKey),
network: _network)
.data
.address!;
break;
case DerivePathType.bip49:
address = P2SH(
data: PaymentData(
redeem: P2WPKH(
data: PaymentData(pubkey: node.publicKey),
network: _network)
.data),
network: _network)
.data
.address!;
break;
default:
throw Exception("No Path type $type exists");
}
receivingNodes.addAll({
"${_id}_$j": {
"node": node,
"address": address,
}
});
txCountCallArgs.addAll({
"${_id}_$j": address,
});
}
// get address tx counts
final counts = await _getBatchTxCount(addresses: txCountCallArgs);
// check and add appropriate addresses
for (int k = 0; k < txCountBatchSize; k++) {
int count = counts["${_id}_$k"]!;
if (count > 0) {
final node = receivingNodes["${_id}_$k"];
// add address to array
addressArray.add(node["address"] as String);
iterationsAddressArray.add(node["address"] as String);
// set current index
returningIndex = index + k;
// reset counter
gapCounter = 0;
// add info to derivations
derivations[node["address"] as String] = {
"pubKey": Format.uint8listToString(
(node["node"] as bip32.BIP32).publicKey),
"wif": (node["node"] as bip32.BIP32).toWIF(),
};
}
// increase counter when no tx history found
if (count == 0) {
gapCounter++;
}
}
// cache all the transactions while waiting for the current function to finish.
unawaited(getTransactionCacheEarly(iterationsAddressArray));
}
return {
"addressArray": addressArray,
"index": returningIndex,
"derivations": derivations
};
}
Future<void> getTransactionCacheEarly(List<String> allAddresses) async {
try {
final List<Map<String, dynamic>> allTxHashes =
await _fetchHistory(allAddresses);
for (final txHash in allTxHashes) {
try {
unawaited(cachedElectrumXClient.getTransaction(
txHash: txHash["tx_hash"] as String,
verbose: true,
coin: coin,
));
} catch (e) {
continue;
}
}
} catch (e) {
//
}
}
Future<bool> refreshIfThereIsNewData() async {
if (longMutex) return false;
if (_hasCalledExit) return false;
@ -1263,6 +1350,10 @@ class BitcoinCashWallet extends CoinServiceAPI {
.put<dynamic>(boxName: walletId, key: "receivingIndexP2PKH", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndexP2PKH", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "receivingIndexP2SH", value: 0);
await DB.instance
.put<dynamic>(boxName: walletId, key: "changeIndexP2SH", value: 0);
await DB.instance.put<dynamic>(
boxName: walletId,
key: 'blocked_tx_hashes',
@ -1275,31 +1366,34 @@ class BitcoinCashWallet extends CoinServiceAPI {
value: <String, String>{});
// Generate and add addresses to relevant arrays
// final initialReceivingAddress =
// await _generateAddressForChain(0, 0, DerivePathType.bip44);
// final initialChangeAddress =
// await _generateAddressForChain(1, 0, DerivePathType.bip44);
final initialReceivingAddressP2PKH =
await _generateAddressForChain(0, 0, DerivePathType.bip44);
final initialChangeAddressP2PKH =
await _generateAddressForChain(1, 0, DerivePathType.bip44);
// await _addToAddressesArrayForChain(
// initialReceivingAddress, 0, DerivePathType.bip44);
// await _addToAddressesArrayForChain(
// initialChangeAddress, 1, DerivePathType.bip44);
final initialReceivingAddressP2SH =
await _generateAddressForChain(0, 0, DerivePathType.bip49);
final initialChangeAddressP2SH =
await _generateAddressForChain(1, 0, DerivePathType.bip49);
await _addToAddressesArrayForChain(
initialReceivingAddressP2PKH, 0, DerivePathType.bip44);
await _addToAddressesArrayForChain(
initialChangeAddressP2PKH, 1, DerivePathType.bip44);
await _addToAddressesArrayForChain(
initialReceivingAddressP2SH, 0, DerivePathType.bip49);
await _addToAddressesArrayForChain(
initialChangeAddressP2SH, 1, DerivePathType.bip49);
// this._currentReceivingAddress = Future(() => initialReceivingAddress);
_currentReceivingAddressP2PKH = Future(() => initialReceivingAddressP2PKH);
_currentReceivingAddressP2SH = Future(() => initialReceivingAddressP2SH);
Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info);
}
/// Generates a new internal or external chain address for the wallet using a BIP44 derivation path.
/// Generates a new internal or external chain address for the wallet using a BIP44 or BIP49 derivation path.
/// [chain] - Use 0 for receiving (external), 1 for change (internal). Should not be any other value!
/// [index] - This can be any integer >= 0
Future<String> _generateAddressForChain(
@ -1319,12 +1413,19 @@ class BitcoinCashWallet extends CoinServiceAPI {
),
);
final data = PaymentData(pubkey: node.publicKey);
final p2shData = PaymentData(
redeem:
P2WPKH(data: PaymentData(pubkey: node.publicKey), network: _network)
.data);
String address;
switch (derivePathType) {
case DerivePathType.bip44:
address = P2PKH(data: data, network: _network).data.address!;
break;
case DerivePathType.bip49:
address = P2SH(data: p2shData, network: _network).data.address!;
break;
// default:
// // should never hit this due to all enum cases handled
// return null;
@ -1352,6 +1453,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
indexKey += "P2PKH";
break;
case DerivePathType.bip49:
indexKey += "P2SH";
break;
}
final newIndex =
@ -1375,6 +1479,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
chainArray += "P2PKH";
break;
case DerivePathType.bip49:
chainArray += "P2SH";
break;
}
final addressArray =
@ -1409,6 +1516,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
arrayKey += "P2PKH";
break;
case DerivePathType.bip49:
arrayKey += "P2SH";
break;
}
final internalChainArray =
DB.instance.get<dynamic>(boxName: walletId, key: arrayKey);
@ -1423,6 +1533,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
key = "${walletId}_${chainId}DerivationsP2PKH";
break;
case DerivePathType.bip49:
key = "${walletId}_${chainId}DerivationsP2SH";
break;
}
return key;
}
@ -1740,6 +1853,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
indexKey += "P2PKH";
break;
case DerivePathType.bip49:
indexKey += "P2SH";
break;
}
final newReceivingIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
@ -1758,6 +1874,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
_currentReceivingAddressP2PKH = Future(() => newReceivingAddress);
break;
case DerivePathType.bip49:
_currentReceivingAddressP2SH = Future(() => newReceivingAddress);
break;
}
}
} on SocketException catch (se, s) {
@ -1793,6 +1912,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
indexKey += "P2PKH";
break;
case DerivePathType.bip49:
indexKey += "P2SH";
break;
}
final newChangeIndex =
DB.instance.get<dynamic>(boxName: walletId, key: indexKey) as int;
@ -2608,6 +2730,7 @@ class BitcoinCashWallet extends CoinServiceAPI {
// addresses to check
List<String> addressesP2PKH = [];
List<String> addressesP2SH = [];
try {
// Populating the addresses to check
@ -2630,6 +2753,9 @@ class BitcoinCashWallet extends CoinServiceAPI {
case DerivePathType.bip44:
addressesP2PKH.add(address);
break;
case DerivePathType.bip49:
addressesP2SH.add(address);
break;
}
}
}
@ -2693,6 +2819,77 @@ class BitcoinCashWallet extends CoinServiceAPI {
}
}
// p2sh / bip49
final p2shLength = addressesP2SH.length;
if (p2shLength > 0) {
final receiveDerivations = await _fetchDerivations(
chain: 0,
derivePathType: DerivePathType.bip49,
);
final changeDerivations = await _fetchDerivations(
chain: 1,
derivePathType: DerivePathType.bip49,
);
for (int i = 0; i < p2shLength; i++) {
// receives
final receiveDerivation = receiveDerivations[addressesP2SH[i]];
// if a match exists it will not be null
if (receiveDerivation != null) {
final p2wpkh = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(
receiveDerivation["pubKey"] as String)),
network: _network)
.data;
final redeemScript = p2wpkh.output;
final data =
P2SH(data: PaymentData(redeem: p2wpkh), network: _network).data;
for (String tx in addressTxid[addressesP2SH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
receiveDerivation["wif"] as String,
network: _network,
),
"redeemScript": redeemScript,
};
}
} else {
// if its not a receive, check change
final changeDerivation = changeDerivations[addressesP2SH[i]];
// if a match exists it will not be null
if (changeDerivation != null) {
final p2wpkh = P2WPKH(
data: PaymentData(
pubkey: Format.stringToUint8List(
changeDerivation["pubKey"] as String)),
network: _network)
.data;
final redeemScript = p2wpkh.output;
final data =
P2SH(data: PaymentData(redeem: p2wpkh), network: _network)
.data;
for (String tx in addressTxid[addressesP2SH[i]]!) {
results[tx] = {
"output": data.output,
"keyPair": ECPair.fromWIF(
changeDerivation["wif"] as String,
network: _network,
),
"redeemScript": redeemScript,
};
}
}
}
}
}
return results;
} catch (e, s) {
Logging.instance

View file

@ -43,8 +43,9 @@ void main() {
});
test("bitcoincash DerivePathType enum", () {
expect(DerivePathType.values.length, 1);
expect(DerivePathType.values.toString(), "[DerivePathType.bip44]");
expect(DerivePathType.values.length, 2);
expect(DerivePathType.values.toString(),
"[DerivePathType.bip44, DerivePathType.bip49]");
});
group("bip32 node/root", () {