Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
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
15 changes: 15 additions & 0 deletions cw_bitcoin/lib/bitcoin_wallet_creation_credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,21 @@ class BitcoinWalletFromKeysCredentials extends WalletCredentials {
final String xpub;
}

class LitecoinWalletFromKeysCredentials extends WalletCredentials {
LitecoinWalletFromKeysCredentials({
required String name,
required String password,
required this.xpub,
required this.scanSecret,
required this.spendPubkey,
WalletInfo? walletInfo,
}) : super(name: name, password: password, walletInfo: walletInfo);

final String xpub;
final String scanSecret;
final String spendPubkey;
}

class BitcoinRestoreWalletFromHardware extends WalletCredentials {
BitcoinRestoreWalletFromHardware({
required String name,
Expand Down
256 changes: 202 additions & 54 deletions cw_bitcoin/lib/litecoin_wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:convert/convert.dart' as convert;
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:cw_bitcoin/address_from_output.dart';
import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart';
import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/mweb_utxo.dart';
Expand Down Expand Up @@ -51,6 +52,9 @@ import 'package:bitcoin_base/src/crypto/keypair/sign_utils.dart';
import 'package:pointycastle/ecc/api.dart';
import 'package:pointycastle/ecc/curves/secp256k1.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:ur/cbor_lite.dart';
import 'package:ur/ur.dart';
import 'package:ur/ur_decoder.dart';

part 'litecoin_wallet.g.dart';

Expand All @@ -65,6 +69,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
Uint8List? seedBytes,
String? mnemonic,
String? xpub,
this.scanSecretOverride,
this.spendPubkeyOverride,
String? passphrase,
String? addressPageType,
List<BitcoinAddressRecord>? initialAddresses,
Expand Down Expand Up @@ -93,6 +99,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
mwebHd =
Bip32Slip10Secp256k1.fromSeed(seedBytes).derivePath("m/1000'") as Bip32Slip10Secp256k1;
mwebEnabled = alwaysScan ?? false;
} else if (scanSecretOverride != null && spendPubkeyOverride != null) {
mwebHd = null;
mwebEnabled = alwaysScan ?? false;
} else {
mwebHd = null;
mwebEnabled = false;
Expand All @@ -108,6 +117,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
network: network,
mwebHd: mwebHd,
mwebEnabled: mwebEnabled,
scanSecretOverride: scanSecretOverride,
spendPubkeyOverride: spendPubkeyOverride,
isHardwareWallet: walletInfo.isHardwareWallet,
);
autorun((_) {
Expand Down Expand Up @@ -159,7 +170,11 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
@override
bool get hasRescan => true;

List<int> get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
final String? scanSecretOverride;
final String? spendPubkeyOverride;
List<int> get scanSecret => scanSecretOverride != null
? hex.decode(scanSecretOverride!)
: mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw;
List<int> get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw;

static Future<LitecoinWallet> create(
Expand Down Expand Up @@ -206,6 +221,10 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
);
}

@override
WalletKeysData get walletKeysData =>
WalletKeysData(mnemonic: seed, xPub: xpub, passphrase: passphrase, scanSecret: scanSecretOverride, spendPubkey: spendPubkeyOverride);

static Future<LitecoinWallet> open({
required String name,
required WalletInfo walletInfo,
Expand Down Expand Up @@ -271,6 +290,8 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return LitecoinWallet(
mnemonic: keysData.mnemonic,
xpub: keysData.xPub,
scanSecretOverride: keysData.scanSecret,
spendPubkeyOverride: keysData.spendPubkey,
password: password,
walletInfo: walletInfo,
unspentCoinsInfo: unspentCoinsInfo,
Expand Down Expand Up @@ -1030,7 +1051,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
final resp = await CwMweb.create(CreateRequest(
rawTx: txb.buildTransaction((a, b, c, d) => '').toBytes(),
scanSecret: scanSecret,
spendSecret: spendSecret,
spendSecret: List.filled(32, 0),
feeRatePerKb: Int64(feeRate * 1000),
dryRun: true));
final tx = BtcTransaction.fromRaw(hex.encode(resp.rawTx));
Expand All @@ -1057,6 +1078,47 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
return fee.toInt() + feeIncrease;
}

Future<Uint8List> buildPsbt(PendingBitcoinTransaction transaction, bool isMweb) async {
final List<TxInput> inputs = [];
final List<TxOut> txouts = [];
for (final utxo in transaction.utxos) {
if (utxo.utxo.scriptType != SegwitAddresType.mweb) {
inputs.add(utxo.utxo.toInput());
txouts.add(TxOut(value: Int64(utxo.utxo.value.toInt()),
pkScript: utxo.ownerDetails.address.toScriptPubKey().toBytes()));
}
}
var resp = await CwMweb.psbtCreate(PsbtCreateRequest(
rawTx: inputs.isEmpty ? null : BtcTransaction(
inputs: inputs,
outputs: isMweb ? [] : transaction.outputs,
).toBytes(),
witnessUtxo: txouts,
));
for (final utxo in transaction.utxos) {
if (utxo.utxo.scriptType == SegwitAddresType.mweb) {
resp = await CwMweb.psbtAddInput(PsbtAddInputRequest(
psbtB64: resp.psbtB64,
scanSecret: scanSecret,
outputId: utxo.utxo.txHash,
addressIndex: utxo.utxo.vout,
));
}
}
if (isMweb) for (final output in transaction.outputs) {
var address = addressFromOutputScript(output.scriptPubKey, LitecoinNetwork.mainnet);
if (output.scriptPubKey.getAddressType() == SegwitAddresType.mweb) {
address = SegwitBech32Encoder.encode("ltcmweb", 0, output.scriptPubKey.toBytes());
}
resp = await CwMweb.psbtAddRecipient(PsbtAddRecipientRequest(
psbtB64: resp.psbtB64,
recipient: PsbtRecipient(address: address, value: Int64(output.amount.toInt())),
feeRatePerKb: Int64.parseInt(transaction.feeRate) * 1000,
));
}
return base64.decode(resp.psbtB64);
}

@override
Future<PendingTransaction> createTransaction(Object credentials) async {
try {
Expand All @@ -1067,18 +1129,13 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
tx.changeAddressOverride =
(await (walletAddresses as LitecoinWalletAddresses).getChangeAddress(coinTypeToSpendFrom: UnspentCoinType.nonMweb))
.address;
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = await buildPsbt(tx, false);
}
return tx;
}
await waitForMwebAddresses();

final resp = await CwMweb.create(CreateRequest(
rawTx: hex.decode(tx.hex),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
));
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));

// check if the transaction doesn't contain any mweb inputs or outputs:
final transactionCredentials = credentials as BitcoinTransactionCredentials;

Expand Down Expand Up @@ -1110,6 +1167,26 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
for (final utxo in tx.utxos) {
if (utxo.utxo.scriptType == SegwitAddresType.mweb) {
hasMwebInput = true;

} else {
// check if any of the inputs of this transaction are hog-ex:
// this list is only non-mweb inputs:
bool isHogEx = true;

final coin = unspentCoins
.firstWhere((coin) => coin.hash == utxo.utxo.txHash && coin.vout == utxo.utxo.vout);

// TODO: detect actual hog-ex inputs

if (!isHogEx) {
continue;
}

int confirmations = coin.confirmations ?? 0;
if (confirmations < 6) {
throw Exception(
"A transaction input has less than 6 confirmations, please try again later.");
}
}
}

Expand All @@ -1123,29 +1200,24 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.address;
if (isRegular) {
tx.isMweb = false;
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = await buildPsbt(tx, false);
}
return tx;
}

// check if any of the inputs of this transaction are hog-ex:
// this list is only non-mweb inputs:
tx2.inputs.forEach((txInput) {
bool isHogEx = true;

final utxo = unspentCoins
.firstWhere((utxo) => utxo.hash == txInput.txId && utxo.vout == txInput.txIndex);

// TODO: detect actual hog-ex inputs

if (!isHogEx) {
return;
}
if (tx.shouldCommitUR()) {
tx.unsignedPsbt = await buildPsbt(tx, true);
return tx;
}

int confirmations = utxo.confirmations ?? 0;
if (confirmations < 6) {
throw Exception(
"A transaction input has less than 6 confirmations, please try again later.");
}
});
final resp = await CwMweb.create(CreateRequest(
rawTx: hex.decode(tx.hex),
scanSecret: scanSecret,
spendSecret: spendSecret,
feeRatePerKb: Int64.parseInt(tx.feeRate) * 1000,
));
final tx2 = BtcTransaction.fromRaw(hex.encode(resp.rawTx));

tx.hexOverride = tx2
.copyWith(
Expand All @@ -1168,31 +1240,107 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store {
.toHex();
tx.outputAddresses = resp.outputId;

return tx
..addListener((transaction) async {
final addresses = <String>{};
transaction.inputAddresses?.forEach((id) async {
final utxo = mwebUtxosBox.get(id);
// await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
if (utxo == null) return;
// mark utxo as spent so we add it to the unconfirmed balance (as negative):
utxo.spent = true;
await mwebUtxosBox.put(id, utxo);
final addressRecord = walletAddresses.allAddresses
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
if (!addresses.contains(utxo.address)) {
addresses.add(utxo.address);
}
addressRecord.balance -= utxo.value.toInt();
});
transaction.inputAddresses?.addAll(addresses);
printV("isPegIn: $isPegIn, isPegOut: $isPegOut");
transaction.additionalInfo["isPegIn"] = isPegIn;
transaction.additionalInfo["isPegOut"] = isPegOut;
transactionHistory.addOne(transaction);
await updateUnspent();
await updateBalance();
});
addTransactionListener(tx, [], isPegIn, isPegOut);
return tx;
} catch (e, s) {
printV(e);
printV(s);
if (e.toString().contains("commit failed")) {
printV(e);
throw Exception("Transaction commit failed (no peers responded), please try again.");
}
rethrow;
}
}

void addTransactionListener(PendingBitcoinTransaction tx,
List<String> inputAddresses, bool isPegIn, bool isPegOut) {
tx.addListener((transaction) async {
final addresses = <String>{};
transaction.inputAddresses?.addAll(inputAddresses);
transaction.inputAddresses?.forEach((id) async {
final utxo = mwebUtxosBox.get(id);
// await mwebUtxosBox.delete(id); // gets deleted in checkMwebUtxosSpent
if (utxo == null) return;
// mark utxo as spent so we add it to the unconfirmed balance (as negative):
utxo.spent = true;
await mwebUtxosBox.put(id, utxo);
final addressRecord = walletAddresses.allAddresses
.firstWhere((addressRecord) => addressRecord.address == utxo.address);
if (!addresses.contains(utxo.address)) {
addresses.add(utxo.address);
}
addressRecord.balance -= utxo.value.toInt();
});
transaction.inputAddresses?.addAll(addresses);
printV("isPegIn: $isPegIn, isPegOut: $isPegOut");
transaction.additionalInfo["isPegIn"] = isPegIn;
transaction.additionalInfo["isPegOut"] = isPegOut;
transactionHistory.addOne(transaction);
await updateUnspent();
await updateBalance();
});
}

Future<void> commitPsbtUR(List<String> urCodes) async {
if (urCodes.isEmpty) throw Exception("No QR code got scanned");
bool isUr = urCodes.any((str) {
return str.startsWith("ur:psbt/");
});
if (!isUr) return;

final ur = URDecoder();
for (final inp in urCodes) {
ur.receivePart(inp);
}
final result = ur.result as UR;
final psbtB64 = base64Encode(CBORDecoder(result.cbor).decodeBytes().$1);

final resp = await CwMweb.psbtGetRecipients(PsbtGetRecipientsRequest(psbtB64: psbtB64));

bool hasMwebInput = false;
bool hasMwebOutput = false;
bool hasRegularOutput = false;

for (final recipient in resp.recipient) {
if (recipient.address.contains("mweb")) {
hasMwebOutput = true;
} else {
hasRegularOutput = true;
}
}

for (final address in resp.inputAddress) {
try {
LitecoinAddress(address);
} catch (_) {
hasMwebInput = true;
}
}

bool isPegIn = !hasMwebInput && hasMwebOutput;
bool isPegOut = hasMwebInput && hasRegularOutput;
bool isRegular = !hasMwebInput && !hasMwebOutput;

final resp2 = await CwMweb.psbtExtract(PsbtExtractRequest(psbtB64: psbtB64));

final tx = PendingBitcoinTransaction(
BtcTransaction.fromRaw(hex.encode(resp2.rawTx)),
type,
electrumClient: electrumClient,
amount: 0,
fee: resp.fee.toInt(),
feeRate: "",
network: network,
hasChange: resp.recipient.length > 1,
isMweb: !isRegular,
isViewOnly: false,
);
tx.outputAddresses = resp2.outputId;
addTransactionListener(tx, resp.inputAddress, isPegIn, isPegOut);

try {
await tx.commit();
} catch (e, s) {
printV(e);
printV(s);
Expand Down
Loading
Loading