Skip to content

Commit

Permalink
Merge branch 'v2.0' into windows
Browse files Browse the repository at this point in the history
  • Loading branch information
MatthewLM committed Jan 18, 2024
2 parents 1eadf99 + 9fb18e0 commit 2a9fb65
Show file tree
Hide file tree
Showing 22 changed files with 339 additions and 113 deletions.
15 changes: 15 additions & 0 deletions coinlib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 2.0.0-rc.7

- Add `CoinUnit` class to convert between amount strings and satoshis.
- Change `NetworkParams` to `Network`.

## 2.0.0-rc.6

- `P2WSHAddress.fromScript` is now `P2WSHAddress.fromRedeemScript` and
`P2WSHAddress.fromScript` is now `P2WSHAddress.fromWitnessScript` to remove
ambiguity.
- `P2WSH.fromRedeemScript` is now `P2WSH.fromWitnessScript` in accordance with
usual naming for P2WSH.
- Added `MultisigProgram.sorted` to allow multisig programs with ordered public
keys.

## 2.0.0-rc.5

Reduce output size of JavaScript by encoding WASM as base64
Expand Down
8 changes: 4 additions & 4 deletions coinlib/example/coinlib_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ void main() async {
// Generate a P2PKH address with the mainnet prefix
final address = P2PKHAddress.fromPublicKey(
key1.publicKey,
version: NetworkParams.mainnet.p2pkhPrefix,
version: Network.mainnet.p2pkhPrefix,
);
print("Address: $address");

Expand All @@ -38,14 +38,14 @@ void main() async {
final msgSig = MessageSignature.sign(
key: key1.privateKey,
message: msg,
prefix: NetworkParams.mainnet.messagePrefix,
prefix: Network.mainnet.messagePrefix,
);

if (
msgSig.verifyAddress(
address: address,
message: msg,
prefix: NetworkParams.mainnet.messagePrefix,
prefix: Network.mainnet.messagePrefix,
)
) {
print("Msg signature is valid: $msgSig");
Expand Down Expand Up @@ -92,7 +92,7 @@ void main() async {

// Print P2TR address
final trAddr = P2TRAddress.fromTaproot(
taproot, hrp: NetworkParams.mainnet.bech32Hrp,
taproot, hrp: Network.mainnet.bech32Hrp,
);
print("Taproot address: $trAddr");

Expand Down
12 changes: 6 additions & 6 deletions coinlib/lib/src/address.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:coinlib/src/common/bytes.dart';
import 'package:coinlib/src/common/hex.dart';
import 'package:coinlib/src/crypto/ec_public_key.dart';
import 'package:coinlib/src/crypto/hash.dart';
import 'package:coinlib/src/network_params.dart';
import 'package:coinlib/src/network.dart';
import 'package:coinlib/src/encode/base58.dart';
import 'package:coinlib/src/encode/bech32.dart';
import 'package:coinlib/src/scripts/program.dart';
Expand All @@ -27,7 +27,7 @@ abstract class Address {
/// the type of address. Throws [InvalidAddress], [InvalidAddressNetwork],
/// [InvalidBech32Checksum] or [InvalidBase58Checksum] if there is an error
/// with the address. The address must match the [network] provided.
factory Address.fromString(String encoded, NetworkParams network) {
factory Address.fromString(String encoded, Network network) {
// Try base58
try {
return Base58Address.fromString(encoded, network);
Expand Down Expand Up @@ -68,7 +68,7 @@ abstract class Base58Address implements Address {
}
}

factory Base58Address.fromString(String encoded, NetworkParams network) {
factory Base58Address.fromString(String encoded, Network network) {

final data = base58Decode(encoded);
if (data.length != 21) throw InvalidAddress();
Expand Down Expand Up @@ -121,7 +121,7 @@ class P2SHAddress extends Base58Address {
: super._(copyCheckBytes(hash, 20), version);

/// Constructs a P2SH address for a redeemScript
P2SHAddress.fromScript(Script script, { required int version })
P2SHAddress.fromRedeemScript(Script script, { required int version })
: super._(hash160(script.compiled), version);

@override
Expand Down Expand Up @@ -172,7 +172,7 @@ abstract class Bech32Address implements Address {

}

factory Bech32Address.fromString(String encoded, NetworkParams network) {
factory Bech32Address.fromString(String encoded, Network network) {

final bech32 = Bech32.decode(encoded);

Expand Down Expand Up @@ -263,7 +263,7 @@ class P2WSHAddress extends Bech32Address {
: super._(0, copyCheckBytes(hash, 32), hrp);

/// Constructs a P2WSH address for a witnessScript
P2WSHAddress.fromScript(Script script, { required String hrp })
P2WSHAddress.fromWitnessScript(Script script, { required String hrp })
: super._(0, sha256Hash(script.compiled), hrp);

@override
Expand Down
73 changes: 73 additions & 0 deletions coinlib/lib/src/coin_unit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/// Thrown when a number does not match the expected format for a given
/// [CoinUnit].
class BadAmountString implements Exception {}

/// Objects of this class represent a coin denomination with a given number of
/// [decimals]. Use [coin] for whole coins with 6 decimal places and [sats] for
/// the smallest unit with no decimal places.
class CoinUnit {

static final _numberRegex = RegExp(r"^\d+(\.\d+)?$");
static final _trailZeroRegex = RegExp(r"\.?0*$");

/// The number of decimal places for this unit
final int decimals;
/// The number of satoshis per unit
final BigInt satsPerUnit;

/// Creates a unit with a given number of [decimals].
CoinUnit(this.decimals) : satsPerUnit = BigInt.from(10).pow(decimals);

/// Obtains the number of satoshis from a string representation of this unit.
///
/// Numbers must only contain digits and optionally one decimal point (".") in
/// the event that there are any decimals. Ensure that there is at least one
/// digit before and after the decimal point. There may only be decimals upto
/// [decimals] in number. Zeros are striped from the left and stripped from
/// the right after the decimal point.
///
/// May throw [BadAmountString] if the number is not formatted correctly.
BigInt toSats(String amount) {

// Check format
if (!_numberRegex.hasMatch(amount)) throw BadAmountString();

// Split decimal
final split = amount.split(".");
final includesPoint = split.length == 2;

// Decimal places must not exceed expected decimals
if (includesPoint && split[1].length > decimals) throw BadAmountString();

// Parse both sides into BigInt
final left = BigInt.parse(split[0]);
final right = includesPoint
? BigInt.parse(split[1].padRight(decimals, "0"))
: BigInt.zero;

return left*satsPerUnit + right;

}

/// Obtains the string representation of the satoshis ([sats]) converted into
/// this unit.
String fromSats(BigInt sats) {

final padded = sats.toString().padLeft(decimals+1, "0");
final insertIdx = padded.length-decimals;
final left = padded.substring(0, insertIdx);
final right = padded.substring(insertIdx);
final withPoint = "$left.$right";

// Remove any trailing zeros and the decimal point if it comes before those
// zeros
return withPoint.replaceFirst(_trailZeroRegex, "");

}

/// Represents a whole coin with 6 decimal places
static final coin = CoinUnit(6);
/// Represents a satoshi
static final sats = CoinUnit(0);

}
3 changes: 2 additions & 1 deletion coinlib/lib/src/coinlib_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ export 'package:coinlib/src/tx/sighash/taproot_signature_hasher.dart';
export 'package:coinlib/src/tx/sighash/witness_signature_hasher.dart';

export 'package:coinlib/src/address.dart';
export 'package:coinlib/src/network_params.dart';
export 'package:coinlib/src/coin_unit.dart';
export 'package:coinlib/src/network.dart';
export 'package:coinlib/src/taproot.dart';

Future<void> loadCoinlib() => secp256k1.load();
12 changes: 11 additions & 1 deletion coinlib/lib/src/common/bytes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ Uint8List copyCheckBytes(
Uint8List bytes, int length, { String name = "Bytes", }
) => Uint8List.fromList(checkBytes(bytes, length, name: name));

/// Determines if two objects are equal lists
/// Determines if two objects are equal Uint8List data
bool bytesEqual(Object? a, Object? b)
=> (a is Uint8List) && (b is Uint8List) && ListEquality().equals(a, b);

/// Compares two Uint8List bytes from the left-most to right-most byte. If all
/// bytes are the same apart from one list being longer, the shortest list comes
/// first.
int compareBytes(Uint8List a, Uint8List b) {
for (int i = 0; i < a.length && i < b.length; i++) {
if (a[i] != b[i]) return a[i] - b[i];
}
return a.length - b.length;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@

class NetworkParams {
class Network {

final int wifPrefix, p2pkhPrefix, p2shPrefix, privHDPrefix, pubHDPrefix;
final String bech32Hrp, messagePrefix;
final BigInt minFee, minOutput, feePerKb;

NetworkParams({
Network({
required this.wifPrefix,
required this.p2pkhPrefix,
required this.p2shPrefix,
Expand All @@ -18,7 +17,7 @@ class NetworkParams {
required this.feePerKb,
});

static final mainnet = NetworkParams(
static final mainnet = Network(
wifPrefix: 183,
p2pkhPrefix: 55,
p2shPrefix: 117,
Expand All @@ -31,7 +30,7 @@ class NetworkParams {
feePerKb: BigInt.from(10000),
);

static final testnet = NetworkParams(
static final testnet = Network(
wifPrefix: 239,
p2pkhPrefix: 111,
p2shPrefix: 196,
Expand Down
63 changes: 39 additions & 24 deletions coinlib/lib/src/scripts/programs/multisig.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import 'dart:typed_data';
import 'package:coinlib/src/common/bytes.dart';
import 'package:coinlib/src/crypto/ec_public_key.dart';
import 'package:coinlib/src/scripts/operations.dart';
import 'package:coinlib/src/scripts/program.dart';
import 'package:coinlib/src/scripts/script.dart';
import 'package:collection/collection.dart';

class MultisigProgram implements Program {

Expand All @@ -14,6 +16,43 @@ class MultisigProgram implements Program {
late final int threshold;
late final List<ECPublicKey> pubkeys;

/// Creates a multisig script program for a given [threshold] (t-of-n) and a
/// list of public keys. The public keys are inserted into the script in the
/// same order that they are given.
MultisigProgram(this.threshold, Iterable<ECPublicKey> pubkeys)
: pubkeys = List.unmodifiable(pubkeys),
script = Script([
ScriptOp.fromNumber(threshold),
...pubkeys.map((pk) => ScriptPushData(pk.data)),
ScriptOp.fromNumber(pubkeys.length),
checkmultisig,
]) {

if (pubkeys.isEmpty || pubkeys.length > maxPubkeys) {
throw ArgumentError.value(
pubkeys, "pubkeys", "must have length between 1 and $maxPubkeys",
);
}

if (threshold < 1 || threshold > pubkeys.length) {
throw ArgumentError.value(
threshold, "threshold",
"must have length between 1 and the number of public keys",
);
}

}

/// Creates a multisig script program for a given [threshold] (t-of-n) and a
/// list of public keys that are sorted according to the big-endian encoded
/// bytes. Public keys will be inserted into the script from smallest to
/// largest encoded data.
MultisigProgram.sorted(int threshold, Iterable<ECPublicKey> pubkeys)
: this(
threshold,
pubkeys.sorted((a, b) => compareBytes(a.data, b.data)),
);

MultisigProgram.fromScript(this.script) {

// Must have threshold, 1-20 public keys, pubkey number and CHECKMULTISIG
Expand Down Expand Up @@ -62,28 +101,4 @@ class MultisigProgram implements Program {

MultisigProgram.fromAsm(String asm) : this.fromScript(Script.fromAsm(asm));

MultisigProgram(this.threshold, Iterable<ECPublicKey> pubkeys)
: pubkeys = List.unmodifiable(pubkeys),
script = Script([
ScriptOp.fromNumber(threshold),
...pubkeys.map((pk) => ScriptPushData(pk.data)),
ScriptOp.fromNumber(pubkeys.length),
checkmultisig,
]) {

if (pubkeys.isEmpty || pubkeys.length > maxPubkeys) {
throw ArgumentError.value(
pubkeys, "pubkeys", "must have length between 1 and $maxPubkeys",
);
}

if (threshold < 1 || threshold > pubkeys.length) {
throw ArgumentError.value(
threshold, "threshold",
"must have length between 1 and the number of public keys",
);
}

}

}
14 changes: 7 additions & 7 deletions coinlib/lib/src/scripts/programs/p2wsh.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import 'package:coinlib/src/scripts/program.dart';
import 'package:coinlib/src/scripts/programs/p2witness.dart';
import 'package:coinlib/src/scripts/script.dart';

/// Pay-to-Witness-Script-Hash program taking a 32-byte script hash for a redeem
/// script whereby the redeem script and other push data is to be provided as
/// witness data.
/// Pay-to-Witness-Script-Hash program taking a 32-byte script hash for a
/// witness script whereby the witness script and other push data is to be
/// provided as witness data.
class P2WSH extends P2Witness {

/// Construct using an output script, not to be confused with the redeem
/// script. For that use [fromRedeemScript].
/// Construct using an output script, not to be confused with the witness
/// script. For that use [fromWitnessScript].
P2WSH.fromScript(super.script) : super.fromScript() {
if (data.length != 32 || version != 0) throw NoProgramMatch();
}
Expand All @@ -24,8 +24,8 @@ class P2WSH extends P2Witness {
P2WSH.fromHash(Uint8List scriptHash)
: super.fromData(0, checkBytes(scriptHash, 32, name: "Script hash"));

P2WSH.fromRedeemScript(Script redeemScript)
: this.fromHash(sha256Hash(redeemScript.compiled));
P2WSH.fromWitnessScript(Script witnessScript)
: this.fromHash(sha256Hash(witnessScript.compiled));

Uint8List get scriptHash => data;

Expand Down
2 changes: 1 addition & 1 deletion coinlib/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: coinlib
description:
A straight-forward, modular library for Peercoin and other Satoshi-based UTXO
blockchains
version: 2.0.0-rc.6
version: 2.0.0-rc.7
repository: https://github.com/peercoin/coinlib

environment:
Expand Down

0 comments on commit 2a9fb65

Please sign in to comment.