-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add CoinSelection for specified inputs
- Loading branch information
Showing
6 changed files
with
458 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,47 @@ | ||
|
||
class NetworkParams { | ||
|
||
final int wifPrefix; | ||
final int p2pkhPrefix; | ||
final int p2shPrefix; | ||
final int privHDPrefix; | ||
final int pubHDPrefix; | ||
final String bech32Hrp; | ||
final String messagePrefix; | ||
final int wifPrefix, p2pkhPrefix, p2shPrefix, privHDPrefix, pubHDPrefix; | ||
final String bech32Hrp, messagePrefix; | ||
final BigInt minFee, minOutput, feePerKb; | ||
|
||
const NetworkParams({ | ||
NetworkParams({ | ||
required this.wifPrefix, | ||
required this.p2pkhPrefix, | ||
required this.p2shPrefix, | ||
required this.privHDPrefix, | ||
required this.pubHDPrefix, | ||
required this.bech32Hrp, | ||
required this.messagePrefix, | ||
required this.minFee, | ||
required this.minOutput, | ||
required this.feePerKb, | ||
}); | ||
|
||
static const mainnet = NetworkParams( | ||
static final mainnet = NetworkParams( | ||
wifPrefix: 183, | ||
p2pkhPrefix: 55, | ||
p2shPrefix: 117, | ||
privHDPrefix: 0x0488ade4, | ||
pubHDPrefix: 0x0488b21e, | ||
bech32Hrp: "pc", | ||
messagePrefix: "Peercoin Signed Message:\n", | ||
minFee: BigInt.from(1000), | ||
minOutput: BigInt.from(10000), | ||
feePerKb: BigInt.from(10000), | ||
); | ||
|
||
static const testnet = NetworkParams( | ||
static final testnet = NetworkParams( | ||
wifPrefix: 239, | ||
p2pkhPrefix: 111, | ||
p2shPrefix: 196, | ||
privHDPrefix: 0x043587CF, | ||
pubHDPrefix: 0x04358394, | ||
bech32Hrp: "tpc", | ||
messagePrefix: "Peercoin Signed Message:\n", | ||
minFee: BigInt.from(1000), | ||
minOutput: BigInt.from(10000), | ||
feePerKb: BigInt.from(10000), | ||
); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import 'package:coinlib/src/common/serial.dart'; | ||
import 'package:coinlib/src/crypto/random.dart'; | ||
import 'package:coinlib/src/scripts/program.dart'; | ||
import 'inputs/input.dart'; | ||
import 'output.dart'; | ||
import 'transaction.dart'; | ||
|
||
class InsufficientFunds implements Exception {} | ||
|
||
/// A candidate input to spend a UTXO with the UTXO value | ||
class InputCandidate { | ||
/// Input that can spend the UTXO | ||
final Input input; | ||
/// Value of UTXO to be spent | ||
final BigInt value; | ||
InputCandidate({ required this.input, required this.value }); | ||
} | ||
|
||
/// Represents a selection of inputs to fund a transaction. If the inputs | ||
/// provide sufficient value to cover the ouputs and fee for a transaction that | ||
/// isn't too large, [ready] shall be true and it is possible to obtain a | ||
/// signable [transaction]. | ||
class CoinSelection { | ||
|
||
final int version; | ||
final List<InputCandidate> selected; | ||
final List<Output> recipients; | ||
final Program changeProgram; | ||
final BigInt feePerKb; | ||
final BigInt minFee; | ||
final BigInt minChange; | ||
final int locktime; | ||
|
||
/// The total value of selected inputs | ||
late final BigInt inputValue; | ||
/// The total value of all recipient outputs | ||
late final BigInt recipientValue; | ||
/// The fee to be paid by the transaction | ||
late final BigInt fee; | ||
/// The value of the change output. This is 0 for a changeless transaction or | ||
/// negative if there aren't enough funds. | ||
late final BigInt changeValue; | ||
/// The maximum size of the transaction after being fully signed | ||
late final int signedSize; | ||
|
||
int _sizeGivenChange(int fixedSize, bool includeChange) | ||
=> fixedSize | ||
+ recipients.fold(0, (acc, output) => acc + output.size) | ||
+ (includeChange ? Output.fromProgram(BigInt.zero, changeProgram).size : 0) | ||
+ MeasureWriter.varIntSizeOfInt( | ||
recipients.length + (includeChange ? 1 : 0), | ||
) as int; | ||
|
||
BigInt _feeForSize(int size) { | ||
final feeForSize = feePerKb * BigInt.from(size) ~/ BigInt.from(1000); | ||
return feeForSize.compareTo(minFee) > 0 ? feeForSize : minFee; | ||
} | ||
|
||
/// Selects all the inputs from [selected] to send to the [recipients] outputs | ||
/// and provide change to the [changeProgram]. The [feePerKb] specifies the | ||
/// required fee in sats per KB with a minimum fee specified with | ||
/// [minFee]. The [minChange] is the minimum allowed change. | ||
CoinSelection({ | ||
this.version = Transaction.currentVersion, | ||
required Iterable<InputCandidate> selected, | ||
required Iterable<Output> recipients, | ||
required this.changeProgram, | ||
required this.feePerKb, | ||
required this.minFee, | ||
required this.minChange, | ||
this.locktime = 0, | ||
}) : selected = List.unmodifiable(selected), | ||
recipients = List.unmodifiable(recipients) { | ||
|
||
if (selected.any((candidate) => candidate.input.signedSize == null)) { | ||
throw ArgumentError("Cannot select inputs without known max signed size"); | ||
} | ||
|
||
// Get input and recipient values | ||
inputValue = selected | ||
.fold(BigInt.zero, (acc, candidate) => acc + candidate.value); | ||
recipientValue = recipients | ||
.fold(BigInt.zero, (acc, output) => acc + output.value); | ||
|
||
// Get unchanging size | ||
final int fixedSize | ||
// Version and locktime | ||
= 8 | ||
// Fully signed inputs | ||
+ MeasureWriter.varIntSizeOfInt(selected.length) | ||
+ selected.fold(0, (acc, candidate) => acc + candidate.input.signedSize!); | ||
|
||
// Determine size and fee with change | ||
final sizeWithChange = _sizeGivenChange(fixedSize, true); | ||
final feeWithChange = _feeForSize(sizeWithChange); | ||
final includedChangeValue = inputValue - recipientValue - feeWithChange; | ||
|
||
// If change is under the required minimum, remove the change output | ||
if (includedChangeValue.compareTo(minChange) < 0) { | ||
|
||
final changelessSize = _sizeGivenChange(fixedSize, false); | ||
final feeForSize = _feeForSize(changelessSize); | ||
final excess = inputValue - recipientValue - feeForSize; | ||
|
||
if (!excess.isNegative) { | ||
// Exceeded without change. Fee is the input value minus the recipient | ||
// value | ||
signedSize = changelessSize; | ||
fee = inputValue - recipientValue; | ||
changeValue = BigInt.zero; | ||
return; | ||
} | ||
// Else haven't met requirement | ||
|
||
} | ||
|
||
// Either haven't met requirement, or have met requirement with change so | ||
// provide details of change-containing transaction | ||
signedSize = sizeWithChange; | ||
fee = feeWithChange; | ||
changeValue = includedChangeValue; | ||
|
||
} | ||
|
||
/// Obtains the transaction with selected inputs and outputs including any | ||
/// change at a random location, ready to be signed. Throws | ||
/// [InsufficientFunds] if there is not enough input value to meet the output | ||
/// value and fee, or [TransactionTooLarge] if the resulting signed | ||
/// transaction would be too large. | ||
Transaction get transaction { | ||
if (!enoughFunds) throw InsufficientFunds(); | ||
if (tooLarge) throw TransactionTooLarge(); | ||
return Transaction( | ||
version: version, | ||
inputs: selected.map((candidate) => candidate.input), | ||
outputs: changeless ? recipients : insertRandom( | ||
recipients, | ||
Output.fromProgram(changeValue, changeProgram), | ||
), | ||
locktime: locktime, | ||
); | ||
} | ||
|
||
/// True when the input value covers the outputs and fee | ||
bool get enoughFunds => !changeValue.isNegative; | ||
/// True when the change output is omitted | ||
bool get changeless => changeValue.compareTo(BigInt.zero) == 0; | ||
/// True if the resulting fully signed transaction will be too large | ||
bool get tooLarge => signedSize > Transaction.maxSize; | ||
/// True if a signable solution have been found | ||
bool get ready => enoughFunds && !tooLarge; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.