mirror of
https://github.com/cypherstack/stack_wallet.git
synced 2024-12-23 11:59:30 +00:00
copy extension into a mixin
This commit is contained in:
parent
9c44dc6c6b
commit
907c380812
1 changed files with 899 additions and 0 deletions
899
lib/services/mixins/paynym_support.dart
Normal file
899
lib/services/mixins/paynym_support.dart
Normal file
|
@ -0,0 +1,899 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:bip32/bip32.dart' as bip32;
|
||||
import 'package:bip47/bip47.dart';
|
||||
import 'package:bip47/src/util.dart';
|
||||
import 'package:bitcoindart/bitcoindart.dart' as btc_dart;
|
||||
import 'package:bitcoindart/src/utils/constants/op.dart' as op;
|
||||
import 'package:bitcoindart/src/utils/script.dart' as bscript;
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:pointycastle/digests/sha256.dart';
|
||||
import 'package:stackwallet/db/main_db.dart';
|
||||
import 'package:stackwallet/electrumx_rpc/electrumx.dart';
|
||||
import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart';
|
||||
import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart';
|
||||
import 'package:stackwallet/models/isar/models/isar_models.dart';
|
||||
import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart';
|
||||
import 'package:stackwallet/utilities/bip32_utils.dart';
|
||||
import 'package:stackwallet/utilities/enums/coin_enum.dart';
|
||||
import 'package:stackwallet/utilities/enums/derive_path_type_enum.dart';
|
||||
import 'package:stackwallet/utilities/format.dart';
|
||||
import 'package:stackwallet/utilities/logger.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
const kPaynymDerivePath = "m/47'/0'/0'";
|
||||
|
||||
mixin PaynymSupport {
|
||||
// passed in wallet data
|
||||
late final String walletId;
|
||||
late final String walletName;
|
||||
late final btc_dart.NetworkType network;
|
||||
late final Coin coin;
|
||||
late final MainDB db;
|
||||
late final ElectrumX electrumXClient;
|
||||
|
||||
// passed in wallet functions
|
||||
late final Future<List<String>> Function() getMnemonic;
|
||||
late final Future<int> Function() getChainHeight;
|
||||
late final Future<String> Function() getCurrentChangeAddress;
|
||||
late final int Function({
|
||||
required int vSize,
|
||||
required int feeRatePerKB,
|
||||
}) estimateTxFee;
|
||||
late final Future<Map<String, dynamic>> Function({
|
||||
required String address,
|
||||
required int satoshiAmount,
|
||||
Map<String, dynamic>? args,
|
||||
}) prepareSend;
|
||||
late final Future<int> Function({
|
||||
required String address,
|
||||
}) getTxCount;
|
||||
late final Future<Map<String, dynamic>> Function(
|
||||
List<UTXO> utxosToUse,
|
||||
) fetchBuildTxData;
|
||||
late final Future<void> Function() refresh;
|
||||
late final Future<void> Function() checkChangeAddressForTransactions;
|
||||
|
||||
// initializer
|
||||
void initPaynymSupport({
|
||||
required String walletId,
|
||||
required String walletName,
|
||||
required btc_dart.NetworkType network,
|
||||
required Coin coin,
|
||||
required MainDB db,
|
||||
required ElectrumX electrumXClient,
|
||||
required Future<List<String>> Function() getMnemonic,
|
||||
required Future<int> Function() getChainHeight,
|
||||
required Future<String> Function() getCurrentChangeAddress,
|
||||
required int Function({
|
||||
required int vSize,
|
||||
required int feeRatePerKB,
|
||||
})
|
||||
estimateTxFee,
|
||||
required Future<Map<String, dynamic>> Function({
|
||||
required String address,
|
||||
required int satoshiAmount,
|
||||
Map<String, dynamic>? args,
|
||||
})
|
||||
prepareSend,
|
||||
required Future<int> Function({
|
||||
required String address,
|
||||
})
|
||||
getTxCount,
|
||||
required Future<Map<String, dynamic>> Function(
|
||||
List<UTXO> utxosToUse,
|
||||
)
|
||||
fetchBuildTxData,
|
||||
required Future<void> Function() refresh,
|
||||
required Future<void> Function() checkChangeAddressForTransactions,
|
||||
}) {
|
||||
this.walletId = walletId;
|
||||
this.walletName = walletName;
|
||||
this.network = network;
|
||||
this.coin = coin;
|
||||
this.db = db;
|
||||
this.electrumXClient = electrumXClient;
|
||||
this.getMnemonic = getMnemonic;
|
||||
this.getChainHeight = getChainHeight;
|
||||
this.getCurrentChangeAddress = getCurrentChangeAddress;
|
||||
this.estimateTxFee = estimateTxFee;
|
||||
this.prepareSend = prepareSend;
|
||||
this.getTxCount = getTxCount;
|
||||
this.fetchBuildTxData = fetchBuildTxData;
|
||||
this.refresh = refresh;
|
||||
this.checkChangeAddressForTransactions = checkChangeAddressForTransactions;
|
||||
}
|
||||
|
||||
// generate bip32 payment code root
|
||||
Future<bip32.BIP32> getRootNode({
|
||||
required List<String> mnemonic,
|
||||
}) async {
|
||||
final root = await Bip32Utils.getBip32Root(mnemonic.join(" "), network);
|
||||
return root;
|
||||
}
|
||||
|
||||
Future<Uint8List> deriveNotificationPrivateKey({
|
||||
required List<String> mnemonic,
|
||||
}) async {
|
||||
final root = await getRootNode(mnemonic: mnemonic);
|
||||
final node = root.derivePath(kPaynymDerivePath).derive(0);
|
||||
return node.privateKey!;
|
||||
}
|
||||
|
||||
/// fetch or generate this wallet's bip47 payment code
|
||||
Future<PaymentCode> getPaymentCode(
|
||||
DerivePathType derivePathType,
|
||||
) async {
|
||||
final address = await getMyNotificationAddress(derivePathType);
|
||||
final paymentCode = PaymentCode.fromPaymentCode(
|
||||
address.otherData!,
|
||||
network,
|
||||
);
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
Future<Uint8List> signWithNotificationKey(Uint8List data) async {
|
||||
final privateKey =
|
||||
await deriveNotificationPrivateKey(mnemonic: await getMnemonic());
|
||||
final pair = btc_dart.ECPair.fromPrivateKey(privateKey, network: network);
|
||||
final signed = pair.sign(SHA256Digest().process(data));
|
||||
return signed;
|
||||
}
|
||||
|
||||
Future<String> signStringWithNotificationKey(String data) async {
|
||||
final bytes =
|
||||
await signWithNotificationKey(Uint8List.fromList(utf8.encode(data)));
|
||||
return Format.uint8listToString(bytes);
|
||||
}
|
||||
|
||||
Future<Future<Map<String, dynamic>>> preparePaymentCodeSend(
|
||||
{required PaymentCode paymentCode,
|
||||
required int satoshiAmount,
|
||||
Map<String, dynamic>? args}) async {
|
||||
if (!(await hasConnected(paymentCode.notificationAddressP2PKH()))) {
|
||||
throw PaynymSendException(
|
||||
"No notification transaction sent to $paymentCode");
|
||||
} else {
|
||||
final myPrivateKey =
|
||||
await deriveNotificationPrivateKey(mnemonic: await getMnemonic());
|
||||
final sendToAddress = await nextUnusedSendAddressFrom(
|
||||
pCode: paymentCode,
|
||||
privateKey: myPrivateKey,
|
||||
);
|
||||
|
||||
return prepareSend(
|
||||
address: sendToAddress.value, satoshiAmount: satoshiAmount);
|
||||
}
|
||||
}
|
||||
|
||||
/// get the next unused address to send to given the receiver's payment code
|
||||
/// and your own private key
|
||||
Future<Address> nextUnusedSendAddressFrom({
|
||||
required PaymentCode pCode,
|
||||
required Uint8List privateKey,
|
||||
int startIndex = 0,
|
||||
}) async {
|
||||
// https://en.bitcoin.it/wiki/BIP_0047#Path_levels
|
||||
const maxCount = 2147483647;
|
||||
|
||||
for (int i = startIndex; i < maxCount; i++) {
|
||||
final address = await db
|
||||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.subTypeEqualTo(AddressSubType.paynymSend)
|
||||
.and()
|
||||
.otherDataEqualTo(pCode.toString())
|
||||
.and()
|
||||
.derivationIndexEqualTo(i)
|
||||
.findFirst();
|
||||
|
||||
if (address != null) {
|
||||
final count = await getTxCount(address: address.value);
|
||||
// return address if unused, otherwise continue to next index
|
||||
if (count == 0) {
|
||||
return address;
|
||||
}
|
||||
} else {
|
||||
final pair = PaymentAddress.initWithPrivateKey(
|
||||
privateKey,
|
||||
pCode,
|
||||
i, // index to use
|
||||
).getSendAddressKeyPair();
|
||||
|
||||
// add address to local db
|
||||
final address = generatePaynymSendAddressFromKeyPair(
|
||||
pair: pair,
|
||||
derivationIndex: i,
|
||||
derivePathType: DerivePathType.bip44,
|
||||
toPaymentCode: pCode,
|
||||
);
|
||||
await db.putAddress(address);
|
||||
|
||||
final count = await getTxCount(address: address.value);
|
||||
// return address if unused, otherwise continue to next index
|
||||
if (count == 0) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw PaynymSendException("Exhausted unused send addresses!");
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> prepareNotificationTx({
|
||||
required int selectedTxFeeRate,
|
||||
required String targetPaymentCodeString,
|
||||
int additionalOutputs = 0,
|
||||
List<UTXO>? utxos,
|
||||
}) async {
|
||||
const amountToSend = DUST_LIMIT;
|
||||
final List<UTXO> availableOutputs =
|
||||
utxos ?? await db.getUTXOs(walletId).findAll();
|
||||
final List<UTXO> spendableOutputs = [];
|
||||
int spendableSatoshiValue = 0;
|
||||
|
||||
// Build list of spendable outputs and totaling their satoshi amount
|
||||
for (var i = 0; i < availableOutputs.length; i++) {
|
||||
if (availableOutputs[i].isBlocked == false &&
|
||||
availableOutputs[i]
|
||||
.isConfirmed(await getChainHeight(), MINIMUM_CONFIRMATIONS) ==
|
||||
true) {
|
||||
spendableOutputs.add(availableOutputs[i]);
|
||||
spendableSatoshiValue += availableOutputs[i].value;
|
||||
}
|
||||
}
|
||||
|
||||
if (spendableSatoshiValue < amountToSend) {
|
||||
// insufficient balance
|
||||
throw InsufficientBalanceException(
|
||||
"Spendable balance is less than the minimum required for a notification transaction.");
|
||||
} else if (spendableSatoshiValue == amountToSend) {
|
||||
// insufficient balance due to missing amount to cover fee
|
||||
throw InsufficientBalanceException(
|
||||
"Remaining balance does not cover the network fee.");
|
||||
}
|
||||
|
||||
// sort spendable by age (oldest first)
|
||||
spendableOutputs.sort((a, b) => b.blockTime!.compareTo(a.blockTime!));
|
||||
|
||||
int satoshisBeingUsed = 0;
|
||||
int outputsBeingUsed = 0;
|
||||
List<UTXO> utxoObjectsToUse = [];
|
||||
|
||||
for (int i = 0;
|
||||
satoshisBeingUsed < amountToSend && i < spendableOutputs.length;
|
||||
i++) {
|
||||
utxoObjectsToUse.add(spendableOutputs[i]);
|
||||
satoshisBeingUsed += spendableOutputs[i].value;
|
||||
outputsBeingUsed += 1;
|
||||
}
|
||||
|
||||
// add additional outputs if required
|
||||
for (int i = 0;
|
||||
i < additionalOutputs && outputsBeingUsed < spendableOutputs.length;
|
||||
i++) {
|
||||
utxoObjectsToUse.add(spendableOutputs[outputsBeingUsed]);
|
||||
satoshisBeingUsed += spendableOutputs[outputsBeingUsed].value;
|
||||
outputsBeingUsed += 1;
|
||||
}
|
||||
|
||||
// gather required signing data
|
||||
final utxoSigningData = await fetchBuildTxData(utxoObjectsToUse);
|
||||
|
||||
final int vSizeForNoChange = (await _createNotificationTx(
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
utxosToUse: utxoObjectsToUse,
|
||||
utxoSigningData: utxoSigningData,
|
||||
change: 0))
|
||||
.item2;
|
||||
|
||||
final int vSizeForWithChange = (await _createNotificationTx(
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
utxosToUse: utxoObjectsToUse,
|
||||
utxoSigningData: utxoSigningData,
|
||||
change: satoshisBeingUsed - amountToSend))
|
||||
.item2;
|
||||
|
||||
// Assume 2 outputs, for recipient and payment code script
|
||||
int feeForNoChange = estimateTxFee(
|
||||
vSize: vSizeForNoChange,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
);
|
||||
|
||||
// Assume 3 outputs, for recipient, payment code script, and change
|
||||
int feeForWithChange = estimateTxFee(
|
||||
vSize: vSizeForWithChange,
|
||||
feeRatePerKB: selectedTxFeeRate,
|
||||
);
|
||||
|
||||
if (feeForNoChange < vSizeForNoChange * 1000) {
|
||||
feeForNoChange = vSizeForNoChange * 1000;
|
||||
}
|
||||
if (feeForWithChange < vSizeForWithChange * 1000) {
|
||||
feeForWithChange = vSizeForWithChange * 1000;
|
||||
}
|
||||
|
||||
if (satoshisBeingUsed - amountToSend > feeForNoChange + DUST_LIMIT) {
|
||||
// try to add change output due to "left over" amount being greater than
|
||||
// the estimated fee + the dust limit
|
||||
int changeAmount = satoshisBeingUsed - amountToSend - feeForWithChange;
|
||||
|
||||
// check estimates are correct and build notification tx
|
||||
if (changeAmount >= DUST_LIMIT &&
|
||||
satoshisBeingUsed - amountToSend - changeAmount == feeForWithChange) {
|
||||
final txn = await _createNotificationTx(
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
utxosToUse: utxoObjectsToUse,
|
||||
utxoSigningData: utxoSigningData,
|
||||
change: changeAmount,
|
||||
);
|
||||
|
||||
int feeBeingPaid = satoshisBeingUsed - amountToSend - changeAmount;
|
||||
|
||||
Map<String, dynamic> transactionObject = {
|
||||
"hex": txn.item1,
|
||||
"recipientPaynym": targetPaymentCodeString,
|
||||
"amount": amountToSend,
|
||||
"fee": feeBeingPaid,
|
||||
"vSize": txn.item2,
|
||||
};
|
||||
return transactionObject;
|
||||
} else {
|
||||
// something broke during fee estimation or the change amount is smaller
|
||||
// than the dust limit. Try without change
|
||||
final txn = await _createNotificationTx(
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
utxosToUse: utxoObjectsToUse,
|
||||
utxoSigningData: utxoSigningData,
|
||||
change: 0,
|
||||
);
|
||||
|
||||
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
||||
|
||||
Map<String, dynamic> transactionObject = {
|
||||
"hex": txn.item1,
|
||||
"recipientPaynym": targetPaymentCodeString,
|
||||
"amount": amountToSend,
|
||||
"fee": feeBeingPaid,
|
||||
"vSize": txn.item2,
|
||||
};
|
||||
return transactionObject;
|
||||
}
|
||||
} else if (satoshisBeingUsed - amountToSend >= feeForNoChange) {
|
||||
// since we already checked if we need to add a change output we can just
|
||||
// build without change here
|
||||
final txn = await _createNotificationTx(
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
utxosToUse: utxoObjectsToUse,
|
||||
utxoSigningData: utxoSigningData,
|
||||
change: 0,
|
||||
);
|
||||
|
||||
int feeBeingPaid = satoshisBeingUsed - amountToSend;
|
||||
|
||||
Map<String, dynamic> transactionObject = {
|
||||
"hex": txn.item1,
|
||||
"recipientPaynym": targetPaymentCodeString,
|
||||
"amount": amountToSend,
|
||||
"fee": feeBeingPaid,
|
||||
"vSize": txn.item2,
|
||||
};
|
||||
return transactionObject;
|
||||
} else {
|
||||
// if we get here we do not have enough funds to cover the tx total so we
|
||||
// check if we have any more available outputs and try again
|
||||
if (spendableOutputs.length > outputsBeingUsed) {
|
||||
return prepareNotificationTx(
|
||||
selectedTxFeeRate: selectedTxFeeRate,
|
||||
targetPaymentCodeString: targetPaymentCodeString,
|
||||
additionalOutputs: additionalOutputs + 1,
|
||||
);
|
||||
} else {
|
||||
throw InsufficientBalanceException(
|
||||
"Remaining balance does not cover the network fee.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return tuple with string value equal to the raw tx hex and the int value
|
||||
// equal to its vSize
|
||||
Future<Tuple2<String, int>> _createNotificationTx({
|
||||
required String targetPaymentCodeString,
|
||||
required List<UTXO> utxosToUse,
|
||||
required Map<String, dynamic> utxoSigningData,
|
||||
required int change,
|
||||
}) async {
|
||||
final targetPaymentCode =
|
||||
PaymentCode.fromPaymentCode(targetPaymentCodeString, network);
|
||||
final myCode = await getPaymentCode(DerivePathType.bip44);
|
||||
|
||||
final utxo = utxosToUse.first;
|
||||
final txPoint = utxo.txid.fromHex.toList();
|
||||
final txPointIndex = utxo.vout;
|
||||
|
||||
final rev = Uint8List(txPoint.length + 4);
|
||||
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
||||
final buffer = rev.buffer.asByteData();
|
||||
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
||||
|
||||
final myKeyPair = utxoSigningData[utxo.txid]["keyPair"] as btc_dart.ECPair;
|
||||
|
||||
final S = SecretPoint(
|
||||
myKeyPair.privateKey!,
|
||||
targetPaymentCode.notificationPublicKey(),
|
||||
);
|
||||
|
||||
final blindingMask = PaymentCode.getMask(S.ecdhSecret(), rev);
|
||||
|
||||
final blindedPaymentCode = PaymentCode.blind(
|
||||
myCode.getPayload(),
|
||||
blindingMask,
|
||||
);
|
||||
|
||||
final opReturnScript = bscript.compile([
|
||||
(op.OPS["OP_RETURN"] as int),
|
||||
blindedPaymentCode,
|
||||
]);
|
||||
|
||||
// build a notification tx
|
||||
final txb = btc_dart.TransactionBuilder(network: network);
|
||||
txb.setVersion(1);
|
||||
|
||||
txb.addInput(
|
||||
utxo.txid,
|
||||
txPointIndex,
|
||||
);
|
||||
|
||||
// todo: modify address once segwit support is in our bip47
|
||||
txb.addOutput(targetPaymentCode.notificationAddressP2PKH(), DUST_LIMIT);
|
||||
txb.addOutput(opReturnScript, 0);
|
||||
|
||||
// TODO: add possible change output and mark output as dangerous
|
||||
if (change > 0) {
|
||||
// generate new change address if current change address has been used
|
||||
await checkChangeAddressForTransactions();
|
||||
final String changeAddress = await getCurrentChangeAddress();
|
||||
txb.addOutput(changeAddress, change);
|
||||
}
|
||||
|
||||
txb.sign(
|
||||
vin: 0,
|
||||
keyPair: myKeyPair,
|
||||
);
|
||||
|
||||
// sign rest of possible inputs
|
||||
for (var i = 1; i < utxosToUse.length - 1; i++) {
|
||||
final txid = utxosToUse[i].txid;
|
||||
txb.sign(
|
||||
vin: i,
|
||||
keyPair: utxoSigningData[txid]["keyPair"] as btc_dart.ECPair,
|
||||
// witnessValue: utxosToUse[i].value,
|
||||
);
|
||||
}
|
||||
|
||||
final builtTx = txb.build();
|
||||
|
||||
return Tuple2(builtTx.toHex(), builtTx.virtualSize());
|
||||
}
|
||||
|
||||
Future<String> broadcastNotificationTx(
|
||||
{required Map<String, dynamic> preparedTx}) async {
|
||||
try {
|
||||
Logging.instance.log("confirmNotificationTx txData: $preparedTx",
|
||||
level: LogLevel.Info);
|
||||
final txHash = await electrumXClient.broadcastTransaction(
|
||||
rawTx: preparedTx["hex"] as String);
|
||||
Logging.instance.log("Sent txHash: $txHash", level: LogLevel.Info);
|
||||
|
||||
// TODO: only refresh transaction data
|
||||
try {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"refresh() failed in confirmNotificationTx ($walletName::$walletId): $e",
|
||||
level: LogLevel.Error,
|
||||
);
|
||||
}
|
||||
|
||||
return txHash;
|
||||
} catch (e, s) {
|
||||
Logging.instance.log("Exception rethrown from confirmSend(): $e\n$s",
|
||||
level: LogLevel.Error);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO optimize
|
||||
Future<bool> hasConnected(String paymentCodeString) async {
|
||||
final myNotificationAddress =
|
||||
await getMyNotificationAddress(DerivePathTypeExt.primaryFor(coin));
|
||||
|
||||
final txns = await db
|
||||
.getTransactions(walletId)
|
||||
.filter()
|
||||
.subTypeEqualTo(TransactionSubType.bip47Notification)
|
||||
.findAll();
|
||||
|
||||
for (final tx in txns) {
|
||||
// quick check that may cause problems?
|
||||
if (tx.address.value?.value == myNotificationAddress.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final unBlindedPaymentCode = await unBlindedPaymentCodeFromTransaction(
|
||||
transaction: tx,
|
||||
myNotificationAddress: myNotificationAddress,
|
||||
);
|
||||
|
||||
if (paymentCodeString == unBlindedPaymentCode.toString()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise return no
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<PaymentCode?> unBlindedPaymentCodeFromTransaction({
|
||||
required Transaction transaction,
|
||||
required Address myNotificationAddress,
|
||||
}) async {
|
||||
if (transaction.address.value != null &&
|
||||
transaction.address.value!.value != myNotificationAddress.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final blindedCode =
|
||||
transaction.outputs.elementAt(1).scriptPubKeyAsm!.split(" ")[1];
|
||||
|
||||
final designatedInput = transaction.inputs.first;
|
||||
|
||||
final txPoint = designatedInput.txid.fromHex.toList();
|
||||
final txPointIndex = designatedInput.vout;
|
||||
|
||||
final rev = Uint8List(txPoint.length + 4);
|
||||
Util.copyBytes(Uint8List.fromList(txPoint), 0, rev, 0, txPoint.length);
|
||||
final buffer = rev.buffer.asByteData();
|
||||
buffer.setUint32(txPoint.length, txPointIndex, Endian.little);
|
||||
|
||||
final pubKey = designatedInput.scriptSigAsm!.split(" ")[1].fromHex;
|
||||
|
||||
final myPrivateKey =
|
||||
await deriveNotificationPrivateKey(mnemonic: await getMnemonic());
|
||||
|
||||
final S = SecretPoint(myPrivateKey, pubKey);
|
||||
|
||||
final mask = PaymentCode.getMask(S.ecdhSecret(), rev);
|
||||
|
||||
final unBlindedPayload = PaymentCode.blind(blindedCode.fromHex, mask);
|
||||
|
||||
final unBlindedPaymentCode =
|
||||
PaymentCode.initFromPayload(unBlindedPayload);
|
||||
|
||||
return unBlindedPaymentCode;
|
||||
} catch (e) {
|
||||
Logging.instance.log(
|
||||
"unBlindedPaymentCodeFromTransaction() failed: $e",
|
||||
level: LogLevel.Warning,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<PaymentCode>>
|
||||
getAllPaymentCodesFromNotificationTransactions() async {
|
||||
final myAddress =
|
||||
await getMyNotificationAddress(DerivePathTypeExt.primaryFor(coin));
|
||||
final txns = await db
|
||||
.getTransactions(walletId)
|
||||
.filter()
|
||||
.subTypeEqualTo(TransactionSubType.bip47Notification)
|
||||
.findAll();
|
||||
|
||||
List<PaymentCode> unBlindedList = [];
|
||||
|
||||
for (final tx in txns) {
|
||||
final unBlinded = await unBlindedPaymentCodeFromTransaction(
|
||||
transaction: tx,
|
||||
myNotificationAddress: myAddress,
|
||||
);
|
||||
if (unBlinded != null) {
|
||||
unBlindedList.add(unBlinded);
|
||||
}
|
||||
}
|
||||
|
||||
return unBlindedList;
|
||||
}
|
||||
|
||||
Future<void> restoreHistoryWith(
|
||||
PaymentCode other,
|
||||
int maxUnusedAddressGap,
|
||||
int maxNumberOfIndexesToCheck,
|
||||
) async {
|
||||
// https://en.bitcoin.it/wiki/BIP_0047#Path_levels
|
||||
const maxCount = 2147483647;
|
||||
assert(maxNumberOfIndexesToCheck < maxCount);
|
||||
|
||||
final myPrivateKey =
|
||||
await deriveNotificationPrivateKey(mnemonic: await getMnemonic());
|
||||
|
||||
List<Address> addresses = [];
|
||||
int receivingGapCounter = 0;
|
||||
int outgoingGapCounter = 0;
|
||||
|
||||
for (int i = 0;
|
||||
i < maxNumberOfIndexesToCheck &&
|
||||
(receivingGapCounter < maxUnusedAddressGap ||
|
||||
outgoingGapCounter < maxUnusedAddressGap);
|
||||
i++) {
|
||||
final paymentAddress = PaymentAddress.initWithPrivateKey(
|
||||
myPrivateKey,
|
||||
other,
|
||||
i, // index to use
|
||||
);
|
||||
|
||||
if (receivingGapCounter < maxUnusedAddressGap) {
|
||||
final pair = paymentAddress.getSendAddressKeyPair();
|
||||
final address = generatePaynymSendAddressFromKeyPair(
|
||||
pair: pair,
|
||||
derivationIndex: i,
|
||||
derivePathType: DerivePathType.bip44,
|
||||
toPaymentCode: other,
|
||||
);
|
||||
addresses.add(address);
|
||||
|
||||
final count = await getTxCount(address: address.value);
|
||||
|
||||
if (count > 0) {
|
||||
receivingGapCounter++;
|
||||
} else {
|
||||
receivingGapCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (outgoingGapCounter < maxUnusedAddressGap) {
|
||||
final pair = paymentAddress.getReceiveAddressKeyPair();
|
||||
final address = generatePaynymReceivingAddressFromKeyPair(
|
||||
pair: pair,
|
||||
derivationIndex: i,
|
||||
derivePathType: DerivePathType.bip44,
|
||||
fromPaymentCode: other,
|
||||
);
|
||||
addresses.add(address);
|
||||
|
||||
final count = await getTxCount(address: address.value);
|
||||
|
||||
if (count > 0) {
|
||||
outgoingGapCounter++;
|
||||
} else {
|
||||
outgoingGapCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
await db.putAddresses(addresses);
|
||||
}
|
||||
|
||||
Address generatePaynymSendAddressFromKeyPair({
|
||||
required btc_dart.ECPair pair,
|
||||
required int derivationIndex,
|
||||
required DerivePathType derivePathType,
|
||||
required PaymentCode toPaymentCode,
|
||||
}) {
|
||||
final data = btc_dart.PaymentData(pubkey: pair.publicKey);
|
||||
|
||||
String addressString;
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
addressString =
|
||||
btc_dart.P2PKH(data: data, network: network).data.address!;
|
||||
break;
|
||||
|
||||
// The following doesn't apply currently
|
||||
// case DerivePathType.bip49:
|
||||
// addressString = btc_dart
|
||||
// .P2SH(
|
||||
// data: btc_dart.PaymentData(
|
||||
// redeem: btc_dart
|
||||
// .P2WPKH(
|
||||
// data: data,
|
||||
// network: network,
|
||||
// )
|
||||
// .data),
|
||||
// network: network,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// break;
|
||||
//
|
||||
// case DerivePathType.bip84:
|
||||
// addressString = btc_dart
|
||||
// .P2WPKH(
|
||||
// network: network,
|
||||
// data: data,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// break;
|
||||
default:
|
||||
throw UnimplementedError("segwit paynyms not implemented yet");
|
||||
}
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: addressString,
|
||||
publicKey: pair.publicKey,
|
||||
derivationIndex: derivationIndex,
|
||||
type: AddressType.nonWallet,
|
||||
subType: AddressSubType.paynymSend,
|
||||
otherData: toPaymentCode.toString(),
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
Address generatePaynymReceivingAddressFromKeyPair({
|
||||
required btc_dart.ECPair pair,
|
||||
required int derivationIndex,
|
||||
required DerivePathType derivePathType,
|
||||
required PaymentCode fromPaymentCode,
|
||||
}) {
|
||||
final data = btc_dart.PaymentData(pubkey: pair.publicKey);
|
||||
|
||||
String addressString;
|
||||
AddressType addrType;
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
addressString = btc_dart
|
||||
.P2PKH(
|
||||
data: data,
|
||||
network: network,
|
||||
)
|
||||
.data
|
||||
.address!;
|
||||
addrType = AddressType.p2pkh;
|
||||
break;
|
||||
|
||||
// The following doesn't apply currently
|
||||
// case DerivePathType.bip49:
|
||||
// addressString = btc_dart
|
||||
// .P2SH(
|
||||
// data: btc_dart.PaymentData(
|
||||
// redeem: btc_dart
|
||||
// .P2WPKH(
|
||||
// data: data,
|
||||
// network: network,
|
||||
// )
|
||||
// .data),
|
||||
// network: network,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// addrType = AddressType.p2sh;
|
||||
// break;
|
||||
//
|
||||
// case DerivePathType.bip84:
|
||||
// addressString = btc_dart
|
||||
// .P2WPKH(
|
||||
// network: network,
|
||||
// data: data,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// addrType = AddressType.p2wpkh;
|
||||
// break;
|
||||
default:
|
||||
throw UnimplementedError("segwit paynyms not implemented yet");
|
||||
}
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: addressString,
|
||||
publicKey: pair.publicKey,
|
||||
derivationIndex: derivationIndex,
|
||||
type: addrType,
|
||||
subType: AddressSubType.paynymReceive,
|
||||
otherData: fromPaymentCode.toString(),
|
||||
);
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
Future<Address> getMyNotificationAddress(
|
||||
DerivePathType derivePathType,
|
||||
) async {
|
||||
// TODO: fix when segwit is here
|
||||
derivePathType = DerivePathType.bip44;
|
||||
|
||||
AddressType type;
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
type = AddressType.p2pkh;
|
||||
break;
|
||||
case DerivePathType.bip49:
|
||||
type = AddressType.p2sh;
|
||||
break;
|
||||
case DerivePathType.bip84:
|
||||
type = AddressType.p2wpkh;
|
||||
break;
|
||||
}
|
||||
|
||||
final storedAddress = await db
|
||||
.getAddresses(walletId)
|
||||
.filter()
|
||||
.subTypeEqualTo(AddressSubType.paynymNotification)
|
||||
.and()
|
||||
.typeEqualTo(type)
|
||||
.and()
|
||||
.not()
|
||||
.typeEqualTo(AddressType.nonWallet)
|
||||
.findFirst();
|
||||
|
||||
if (storedAddress != null) {
|
||||
return storedAddress;
|
||||
} else {
|
||||
final root = await getRootNode(mnemonic: await getMnemonic());
|
||||
final node = root.derivePath(kPaynymDerivePath);
|
||||
final paymentCode = PaymentCode.initFromPubKey(
|
||||
node.publicKey,
|
||||
node.chainCode,
|
||||
network,
|
||||
);
|
||||
|
||||
String addressString;
|
||||
final data =
|
||||
btc_dart.PaymentData(pubkey: paymentCode.notificationPublicKey());
|
||||
switch (derivePathType) {
|
||||
case DerivePathType.bip44:
|
||||
addressString = btc_dart
|
||||
.P2PKH(
|
||||
data: data,
|
||||
network: network,
|
||||
)
|
||||
.data
|
||||
.address!;
|
||||
break;
|
||||
// case DerivePathType.bip49:
|
||||
// addressString = btc_dart
|
||||
// .P2SH(
|
||||
// data: btc_dart.PaymentData(
|
||||
// redeem: btc_dart
|
||||
// .P2WPKH(
|
||||
// data: data,
|
||||
// network: network,
|
||||
// )
|
||||
// .data),
|
||||
// network: network,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// break;
|
||||
// case DerivePathType.bip84:
|
||||
// addressString = btc_dart
|
||||
// .P2WPKH(
|
||||
// network: network,
|
||||
// data: data,
|
||||
// )
|
||||
// .data
|
||||
// .address!;
|
||||
// break;
|
||||
default:
|
||||
throw UnimplementedError("segwit paynyms not implemented yet");
|
||||
}
|
||||
|
||||
final address = Address(
|
||||
walletId: walletId,
|
||||
value: addressString,
|
||||
publicKey: paymentCode.getPubKey(),
|
||||
derivationIndex: 0,
|
||||
type: type,
|
||||
subType: AddressSubType.paynymNotification,
|
||||
otherData: paymentCode.toString(),
|
||||
);
|
||||
|
||||
await db.putAddress(address);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue