Skip to content

Commit d54525d

Browse files
committed
implemented bip21
1 parent 7f05f37 commit d54525d

File tree

4 files changed

+177
-54
lines changed

4 files changed

+177
-54
lines changed

lib/src/address.dart

+94-44
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,45 @@ class Address {
1616

1717
static const _CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
1818
static const _CHARSET_INVERSE_INDEX = {
19-
'q': 0, 'p': 1, 'z': 2, 'r': 3, 'y': 4, '9': 5, 'x': 6, '8': 7,
20-
'g': 8, 'f': 9, '2': 10, 't': 11, 'v': 12, 'd': 13, 'w': 14, '0': 15,
21-
's': 16, '3': 17, 'j': 18, 'n': 19, '5': 20, '4': 21, 'k': 22, 'h': 23,
22-
'c': 24, 'e': 25, '6': 26, 'm': 27, 'u': 28, 'a': 29, '7': 30, 'l': 31,
19+
'q': 0,
20+
'p': 1,
21+
'z': 2,
22+
'r': 3,
23+
'y': 4,
24+
'9': 5,
25+
'x': 6,
26+
'8': 7,
27+
'g': 8,
28+
'f': 9,
29+
'2': 10,
30+
't': 11,
31+
'v': 12,
32+
'd': 13,
33+
'w': 14,
34+
'0': 15,
35+
's': 16,
36+
'3': 17,
37+
'j': 18,
38+
'n': 19,
39+
'5': 20,
40+
'4': 21,
41+
'k': 22,
42+
'h': 23,
43+
'c': 24,
44+
'e': 25,
45+
'6': 26,
46+
'm': 27,
47+
'u': 28,
48+
'a': 29,
49+
'7': 30,
50+
'l': 31,
2351
};
2452

2553
/// Returns information about the given Bitcoin Cash address.
2654
///
2755
/// See https://developer.bitcoin.com/bitbox/docs/util for details about returned format
2856
static Future<Map<String, dynamic>> validateAddress(String address) async =>
29-
await RestApi.sendGetRequest("util/validateAddress", address);
57+
await RestApi.sendGetRequest("util/validateAddress", address);
3058

3159
/// Returns details of the provided address or addresses
3260
///
@@ -40,7 +68,7 @@ class Address {
4068
/// See https://developer.bitcoin.com/bitbox/docs/address#details for details about returned format. However
4169
/// note, that processing from array to map is done on the library side
4270
static Future<dynamic> details(addresses, [returnAsMap = false]) async =>
43-
await _sendRequest("details", addresses, returnAsMap);
71+
await _sendRequest("details", addresses, returnAsMap);
4472

4573
/// Returns list of unconfirmed transactions
4674
///
@@ -53,7 +81,8 @@ class Address {
5381
///
5482
/// See https://developer.bitcoin.com/bitbox/docs/address#unconfirmed for details about the returned format. However
5583
/// note, that processing from array to map is done on the library side
56-
static Future<dynamic> getUnconfirmed(addresses, [returnAsMap = false]) async {
84+
static Future<dynamic> getUnconfirmed(addresses,
85+
[returnAsMap = false]) async {
5786
final result = await _sendRequest("unconfirmed", addresses);
5887

5988
if (result is Map) {
@@ -64,9 +93,11 @@ class Address {
6493

6594
result.forEach((addressUtxoMap) {
6695
if (returnAsMap) {
67-
returnMap[addressUtxoMap["cashAddr"]] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
96+
returnMap[addressUtxoMap["cashAddr"]] =
97+
Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
6898
} else {
69-
addressUtxoMap["utxos"] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
99+
addressUtxoMap["utxos"] =
100+
Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
70101
returnList.add(addressUtxoMap);
71102
}
72103
});
@@ -95,9 +126,11 @@ class Address {
95126

96127
result.forEach((addressUtxoMap) {
97128
if (returnAsMap) {
98-
returnMap[addressUtxoMap["cashAddress"]] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
129+
returnMap[addressUtxoMap["cashAddress"]] =
130+
Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
99131
} else {
100-
addressUtxoMap["utxos"] = Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
132+
addressUtxoMap["utxos"] =
133+
Utxo.convertMapListToUtxos(addressUtxoMap["utxos"]);
101134
returnList.add(addressUtxoMap);
102135
}
103136
});
@@ -109,15 +142,16 @@ class Address {
109142
}
110143

111144
/// Converts legacy address to cash address
112-
static String toCashAddress(String legacyAddress, [bool includePrefix = true]) {
145+
static String toCashAddress(String legacyAddress,
146+
[bool includePrefix = true]) {
113147
final decoded = Address._decodeLegacyAddress(legacyAddress);
114148
String prefix = "";
115149
if (includePrefix) {
116150
switch (decoded["version"]) {
117-
case Network.bchPublic :
151+
case Network.bchPublic:
118152
prefix = "bitcoincash";
119153
break;
120-
case Network.bchTestnetPublic :
154+
case Network.bchTestnetPublic:
121155
prefix = "bchtest";
122156
break;
123157
default:
@@ -172,22 +206,25 @@ class Address {
172206
static _encode(String prefix, String type, Uint8List hash) {
173207
final prefixData = _prefixToUint5List(prefix) + Uint8List(1);
174208
final versionByte = _getHashSizeBits(hash);
175-
final payloadData = _convertBits(Uint8List.fromList([versionByte] + hash), 8, 5);
209+
final payloadData =
210+
_convertBits(Uint8List.fromList([versionByte] + hash), 8, 5);
176211
final checksumData = prefixData + payloadData + Uint8List(8);
177212
final payload = payloadData + _checksumToUint5Array(_polymod(checksumData));
178213
return "$prefix:" + _base32Encode(payload);
179214
}
180215

181216
/// Helper method for sending generic requests to Bitbox API. Accepts [String] or [List] of Strings and optionally
182217
/// converts the List returned by Bitbox into [Map], which uses cashAddress as a key
183-
static Future<dynamic> _sendRequest(String path, dynamic addresses, [bool returnAsMap = false]) async {
218+
static Future<dynamic> _sendRequest(String path, dynamic addresses,
219+
[bool returnAsMap = false]) async {
184220
assert(addresses is String || addresses is List<String>);
185221

186222
if (addresses is String) {
187223
return await RestApi.sendGetRequest("address/$path", addresses) as Map;
188224
} else if (addresses is List<String>) {
189-
return await RestApi.sendPostRequest("address/$path", "addresses", addresses,
190-
returnKey: returnAsMap ? "cashAddress" : null);
225+
return await RestApi.sendPostRequest(
226+
"address/$path", "addresses", addresses,
227+
returnKey: returnAsMap ? "cashAddress" : null);
191228
} else {
192229
throw TypeError();
193230
}
@@ -274,7 +311,7 @@ class Address {
274311
return <String, dynamic>{
275312
"version": buffer.first,
276313
"hash": buffer.sublist(1),
277-
"format" : formatLegacy,
314+
"format": formatLegacy,
278315
};
279316
}
280317

@@ -316,7 +353,8 @@ class Address {
316353
continue;
317354
}
318355

319-
final payloadData = _fromUint5Array(payload.sublist(0, payload.length - 8));
356+
final payloadData =
357+
_fromUint5Array(payload.sublist(0, payload.length - 8));
320358
final hash = payloadData.sublist(1);
321359

322360
if (_getHashSize(payloadData[0]) != hash.length * 8) {
@@ -327,9 +365,9 @@ class Address {
327365
// If the loop got all the way here, it means validations went through and the address was decoded.
328366
// Return the decoded data
329367
return <String, dynamic>{
330-
"prefix" : prefixes[i],
331-
"hash" : hash,
332-
"format" : formatCashAddr
368+
"prefix": prefixes[i],
369+
"hash": hash,
370+
"format": formatCashAddr
333371
};
334372
}
335373

@@ -357,8 +395,11 @@ class Address {
357395

358396
/// Converts a list of integers made up of 'from' bits into an array of integers made up of 'to' bits.
359397
/// The output array is zero-padded if necessary, unless strict mode is true.
360-
static Uint8List _convertBits(List data, int from, int to, [bool strictMode = false]) {
361-
final length = strictMode ? (data.length * from / to).floor() : (data.length * from / to).ceil();
398+
static Uint8List _convertBits(List data, int from, int to,
399+
[bool strictMode = false]) {
400+
final length = strictMode
401+
? (data.length * from / to).floor()
402+
: (data.length * from / to).ceil();
362403
int mask = (1 << to) - 1;
363404
var result = Uint8List(length);
364405
int index = 0;
@@ -382,7 +423,8 @@ class Address {
382423
}
383424
} else {
384425
if (bits < from && ((accumulator << (to - bits)) & mask).toInt() != 0) {
385-
throw FormatException("Input cannot be converted to $to bits without padding, but strict mode was used.");
426+
throw FormatException(
427+
"Input cannot be converted to $to bits without padding, but strict mode was used.");
386428
}
387429
}
388430
return result;
@@ -391,7 +433,13 @@ class Address {
391433
/// Computes a checksum from the given input data as specified for the CashAddr format:
392434
// https://github.com/Bitcoin-UAHF/spec/blob/master/cashaddr.md.
393435
static int _polymod(List data) {
394-
const GENERATOR = [0x98f2bc8e61, 0x79b76d99e2, 0xf33e5fb3c4, 0xae2eabe2a8, 0x1e4f43e470];
436+
const GENERATOR = [
437+
0x98f2bc8e61,
438+
0x79b76d99e2,
439+
0xf33e5fb3c4,
440+
0xae2eabe2a8,
441+
0x1e4f43e470
442+
];
395443

396444
int checksum = 1;
397445

@@ -414,7 +462,8 @@ class Address {
414462
final data = Uint8List(string.length);
415463
for (int i = 0; i < string.length; i++) {
416464
final value = string[i];
417-
if (!_CHARSET_INVERSE_INDEX.containsKey(value)) throw FormatException("Invalid character '$value'");
465+
if (!_CHARSET_INVERSE_INDEX.containsKey(value))
466+
throw FormatException("Invalid character '$value'");
418467
data[i] = _CHARSET_INVERSE_INDEX[string[i]];
419468
}
420469

@@ -452,16 +501,17 @@ class Utxo {
452501
final int height;
453502
final int confirmations;
454503

455-
Utxo(this.txid, this.vout, this.amount, this.satoshis, this.height, this.confirmations);
504+
Utxo(this.txid, this.vout, this.amount, this.satoshis, this.height,
505+
this.confirmations);
456506

457507
/// Create [Utxo] instance from utxo [Map]
458-
Utxo.fromMap(Map<String, dynamic> utxoMap) :
459-
this.txid = utxoMap['txid'],
460-
this.vout = utxoMap['vout'],
461-
this.amount = utxoMap['amount'],
462-
this.satoshis = utxoMap['satoshis'],
463-
this.height = utxoMap.containsKey('height') ? utxoMap['height'] : null,
464-
this.confirmations = utxoMap['confirmations'];
508+
Utxo.fromMap(Map<String, dynamic> utxoMap)
509+
: this.txid = utxoMap['txid'],
510+
this.vout = utxoMap['vout'],
511+
this.amount = utxoMap['amount'],
512+
this.satoshis = utxoMap['satoshis'],
513+
this.height = utxoMap.containsKey('height') ? utxoMap['height'] : null,
514+
this.confirmations = utxoMap['confirmations'];
465515

466516
/// Converts List of utxo maps into a list of [Utxo] objects
467517
static List<Utxo> convertMapListToUtxos(List utxoMapList) {
@@ -475,11 +525,11 @@ class Utxo {
475525

476526
@override
477527
String toString() => jsonEncode({
478-
"txid": txid,
479-
"vout": vout,
480-
"amount": amount,
481-
"satoshis": satoshis,
482-
"height": height,
483-
"confirmations" : confirmations
484-
});
485-
}
528+
"txid": txid,
529+
"vout": vout,
530+
"amount": amount,
531+
"satoshis": satoshis,
532+
"height": height,
533+
"confirmations": confirmations
534+
});
535+
}

lib/src/bitcoincash.dart

+12
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import 'utils/bip21.dart';
2+
13
/// Bitcoin Cash specific utilities
24
class BitcoinCash {
35
/// Converts Bitcoin Cash units to satoshi units
@@ -14,4 +16,14 @@ class BitcoinCash {
1416
static int getByteCount(int inputs, int outputs) {
1517
return ((inputs * 148 * 4 + 34 * 4 * outputs + 10 * 4) / 4).ceil();
1618
}
19+
20+
// Converts a [String] bch address and its [Map] options into [String] bip-21 uri
21+
static String encodeBIP21(String address, Map<String, dynamic> options) {
22+
return Bip21.encode(address, options);
23+
}
24+
25+
// Converts [String] bip-21 uri into a [Map] of bch address and its options
26+
static Map<String, dynamic> decodeBIP21(String uri) {
27+
return Bip21.decode(uri);
28+
}
1729
}

lib/src/utils/bip21.dart

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
class Bip21 {
2+
static Map<String, dynamic> decode(String uri) {
3+
if (uri.indexOf('bitcoincash') != 0 || uri['bitcoincash'.length] != ":")
4+
throw ("Invalid BIP21 URI");
5+
6+
int split = uri.indexOf("?");
7+
Map<String, String> uriOptions = Uri.parse(uri).queryParameters;
8+
9+
Map<String, dynamic> options = Map.from({
10+
"message": uriOptions["message"],
11+
"label": uriOptions["label"],
12+
});
13+
14+
String address = uri.substring(0, split == -1 ? null : split);
15+
16+
if (uriOptions["amount"] != null) {
17+
if (uriOptions["amount"].indexOf(",") != -1)
18+
throw ("Invalid amount: commas are invalid");
19+
20+
double amount = double.tryParse(uriOptions["amount"]);
21+
if (amount == null || amount.isNaN)
22+
throw ("Invalid amount: not a number");
23+
if (!amount.isFinite) throw ("Invalid amount: not finite");
24+
if (amount < 0) throw ("Invalid amount: not positive");
25+
options["amount"] = amount;
26+
}
27+
28+
return {
29+
'address': address,
30+
'options': options,
31+
};
32+
}
33+
34+
static String encode(String address, Map<String, dynamic> options) {
35+
var isCashAddress = address.startsWith('bitcoincash:');
36+
if (!isCashAddress) {
37+
address = 'bitcoincash:$address';
38+
}
39+
40+
String query = "";
41+
if (options != null && options.isNotEmpty) {
42+
if (options['amount'] != null) {
43+
if (!options['amount'].isFinite) throw ("Invalid amount: not finite");
44+
if (options['amount'] < 0) throw ("Invalid amount: not positive");
45+
}
46+
47+
Map<String, dynamic> uriOptions = options;
48+
uriOptions.removeWhere((key, value) => value == null);
49+
uriOptions.forEach((key, value) {
50+
uriOptions[key] = value.toString();
51+
});
52+
53+
if (uriOptions.isEmpty) uriOptions = null;
54+
query = Uri(queryParameters: uriOptions).toString();
55+
// Dart isn't following RFC-3986...
56+
query = query.replaceAll(RegExp(r"\+"), "%20");
57+
}
58+
59+
return "$address$query";
60+
}
61+
}

0 commit comments

Comments
 (0)