@alexbosworth/blockchain 1.7.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,10 +1,22 @@
1
1
  # Versions
2
2
 
3
- ## 1.7.0
3
+ ## 3.0.0
4
+
5
+ - `decodeBase58Address`: Add method to convert b58 address to hash and version
6
+
7
+ ### Breaking Changes
8
+
9
+ - End support for Node.js v18, require v20+
10
+
11
+ ## 2.0.0
4
12
 
5
13
  - `idForTransactionComponents`: Add method to get transaction id for components
6
14
  - `queryTransactions`: Add method to find transaction outputs matching a query
7
15
 
16
+ ### Breaking Changes
17
+
18
+ - End support for Node.js v16, require v18+
19
+
8
20
  ## 1.6.0
9
21
 
10
22
  - `previousBlockId`: Add method to get the previous block id from a block
package/README.md CHANGED
@@ -56,6 +56,23 @@ Get the components of a hex-encoded transaction
56
56
  version: <Version Number>
57
57
  }
58
58
 
59
+ ### decodeBase58Address
60
+
61
+ Derive output hash and version data from a base58 address string
62
+
63
+ {
64
+ address: <Base58 Encoded Address String>
65
+ }
66
+
67
+ @throws
68
+ <Error>
69
+
70
+ @returns
71
+ {
72
+ hash: <Output Hash Buffer Object>
73
+ version: <Script Version Byte Number>
74
+ }
75
+
59
76
  ### idForBlock
60
77
 
61
78
  Get an id for a block: the double sha256 hash of the block header
@@ -0,0 +1,129 @@
1
+ const {createHash} = require('crypto');
2
+
3
+ const base58Alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
4
+ const byteMask = 0xff;
5
+ const {ceil} = Math;
6
+ const countChecksumBytes = 4;
7
+ const countHashBytes = 20;
8
+ const countVersionBytes = 1;
9
+ const leadingZeroCharCode = 0x31;
10
+ const notFoundIndex = -1;
11
+ const sha256 = preimage => createHash('sha256').update(preimage).digest();
12
+
13
+ /** Derive output hash and version data from a base58 address string
14
+
15
+ {
16
+ address: <Base58 Encoded Address String>
17
+ }
18
+
19
+ @throws
20
+ <Error>
21
+
22
+ @returns
23
+ {
24
+ hash: <Output Hash Buffer Object>
25
+ version: <Script Version Byte Number>
26
+ }
27
+ */
28
+ module.exports = ({address}) => {
29
+ if (typeof address !== 'string') {
30
+ throw new Error('ExpectedBase58AddressStringToDecode');
31
+ }
32
+
33
+ // Ignore address string whitespace
34
+ const str = address.trim();
35
+
36
+ // Exit early with error on empty address, since it's not an address
37
+ if (!str.length) {
38
+ throw new Error('ExpectedNonEmptyBase58AddressToDecode');
39
+ }
40
+
41
+ let bytesLength = 0;
42
+ let countLeadingZeros = 0;
43
+ let index = 0;
44
+
45
+ // Iterate to determine the number of leading zeros to preserve
46
+ while (index < str.length && str.charCodeAt(index) === leadingZeroCharCode) {
47
+ countLeadingZeros++;
48
+ index++;
49
+ }
50
+
51
+ // Total bytes needed are log(58) / log(256) (256 values in a byte) = 0.732..
52
+ const workingSize = ceil((str.length - index) * 733 / 1000);
53
+
54
+ // Working buffer for the base256 (byte) representation, right-aligned
55
+ const working = new Uint8Array(workingSize);
56
+
57
+ // Iterate through the address characters and grab the decoded bytes
58
+ while (index < str.length) {
59
+ const value = base58Alphabet.indexOf(str[index]);
60
+
61
+ if (value === notFoundIndex) {
62
+ throw new Error('ExpectedAllBase58CharactersInBase58Address');
63
+ }
64
+
65
+ let carry = value;
66
+ let i = 0;
67
+
68
+ for (
69
+ let position = workingSize - 1;
70
+ (carry !== 0 || i < bytesLength) && position >= 0;
71
+ --position, ++i
72
+ ) {
73
+ carry += base58Alphabet.length * working[position];
74
+
75
+ // Take the least-significant byte
76
+ working[position] = carry & byteMask;
77
+
78
+ // Carry forward the remaining bytes
79
+ carry = (carry / 256) | 0;
80
+ }
81
+
82
+ // Track the number of bytes being used
83
+ bytesLength = i;
84
+
85
+ // Move to the next character in the address
86
+ index++;
87
+ }
88
+
89
+ // Meaningful bytes are right-aligned in working
90
+ const bytesStart = workingSize - bytesLength;
91
+ const meaningfulCount = workingSize - bytesStart;
92
+
93
+ // Final buffer: leading zeros plus meaningful bytes
94
+ const decoded = Buffer.alloc(countLeadingZeros + meaningfulCount);
95
+
96
+ // Make a view into the meaningful bytes
97
+ const dataBytes = working.subarray(bytesStart, bytesStart + meaningfulCount);
98
+
99
+ // Copy the meaningful bytes into the final decoded data
100
+ decoded.set(dataBytes, countLeadingZeros);
101
+
102
+ // Exit early with error when we don't have both a payload and checksum
103
+ if (decoded.length <= countChecksumBytes) {
104
+ throw new Error('ExpectedPayloadAndChecksumInBase58Address');
105
+ }
106
+
107
+ // The last bytes of a base58 address are a double sha256 checksum
108
+ const checksum = decoded.subarray(decoded.length - countChecksumBytes);
109
+
110
+ // Aside from the checksum there is a version byte and then a hash
111
+ const payload = decoded.subarray(0, decoded.length - countChecksumBytes);
112
+
113
+ // Make sure that we have a base58 payload that has the version and hash
114
+ if (payload.length !== countVersionBytes + countHashBytes) {
115
+ throw new Error('ExpectedVersionAnd20ByteHashInBase58AddressPayload');
116
+ }
117
+
118
+ // Calculate the checksum for the given payload
119
+ const payloadCheck = sha256(sha256(payload)).subarray(0, countChecksumBytes);
120
+
121
+ // Exit early with error when the double SHA256 of payload doesn't match
122
+ if (!checksum.equals(payloadCheck)) {
123
+ throw new Error('ExpectedValidPayloadChecksumInBase58Address');
124
+ }
125
+
126
+ const [version] = payload;
127
+
128
+ return {version, hash: payload.subarray(countVersionBytes)};
129
+ };
@@ -0,0 +1,3 @@
1
+ const decodeBase58Address = require('./decode_base58_address');
2
+
3
+ module.exports = {decodeBase58Address};
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const {compactIntAsNumber} = require('./numbers');
2
2
  const {componentsOfTransaction} = require('./transactions');
3
+ const {decodeBase58Address} = require('./addresses');
3
4
  const {idForBlock} = require('./hashes');
4
5
  const {idForTransactionComponents} = require('./hashes');
5
6
  const {noLocktimeIdForTransaction} = require('./transactions');
@@ -12,6 +13,7 @@ const {scriptElementsAsScript} = require('./script');
12
13
  module.exports = {
13
14
  compactIntAsNumber,
14
15
  componentsOfTransaction,
16
+ decodeBase58Address,
15
17
  idForBlock,
16
18
  idForTransactionComponents,
17
19
  noLocktimeIdForTransaction,
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "description": "Blockchain data utility methods",
10
10
  "engines": {
11
- "node": ">=16"
11
+ "node": ">=20"
12
12
  },
13
13
  "license": "MIT",
14
14
  "name": "@alexbosworth/blockchain",
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/alexbosworth/blockchain.git"
18
18
  },
19
19
  "scripts": {
20
- "test": "npx nyc@15.1.0 node --experimental-test-coverage --test test/hashes/*.js test/numbers/*.js test/script/*.js test/transactions/*.js"
20
+ "test": "npx nyc@17.1.0 node --experimental-test-coverage --test test/addresses/*.js test/hashes/*.js test/numbers/*.js test/script/*.js test/transactions/*.js"
21
21
  },
22
- "version": "1.7.0"
22
+ "version": "3.0.0"
23
23
  }
@@ -0,0 +1,70 @@
1
+ const {deepEqual} = require('node:assert').strict;
2
+ const test = require('node:test');
3
+ const {throws} = require('node:assert').strict;
4
+
5
+ const {decodeBase58Address} = require('./../../');
6
+
7
+ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
8
+
9
+ const tests = [
10
+ {
11
+ args: {},
12
+ description: 'An address is expected',
13
+ error: 'ExpectedBase58AddressStringToDecode',
14
+ },
15
+ {
16
+ args: {address: ' '},
17
+ description: 'A non-empty address is expected',
18
+ error: 'ExpectedNonEmptyBase58AddressToDecode',
19
+ },
20
+ {
21
+ args: {address: '16ro3Jptwo4asSevZnsRX6vfRS24TGE6uP'},
22
+ description: 'Address checksum must match',
23
+ error: 'ExpectedValidPayloadChecksumInBase58Address',
24
+ },
25
+ {
26
+ args: {address: '16ro3Jptwo4asSevZnsRX6vfRS24TGE6'},
27
+ description: 'Address checksum must be present',
28
+ error: 'ExpectedVersionAnd20ByteHashInBase58AddressPayload',
29
+ },
30
+ {
31
+ args: {address: '1'},
32
+ description: 'Address payload and checksum must be present',
33
+ error: 'ExpectedPayloadAndChecksumInBase58Address',
34
+ },
35
+ {
36
+ args: {address: '16ro3Jptwo4asSevZnsRX6vfRS24TGE6u!'},
37
+ description: 'Characters must be base58 set',
38
+ error: 'ExpectedAllBase58CharactersInBase58Address',
39
+ },
40
+ {
41
+ args: {address: '16ro3Jptwo4asSevZnsRX6vfRS24TGE6uK'},
42
+ description: 'A hash and version are returned for a p2pkh address',
43
+ expected: {
44
+ hash: hexAsBuffer('404371705fa9bd789a2fcd52d2c580b65d35549d'),
45
+ version: 0,
46
+ },
47
+ },
48
+ {
49
+ args: {address: '3P14159f73E4gFr7JterCCQh9QjiTjiZrG'},
50
+ description: 'A hash and version are returned for a p2sh address',
51
+ expected: {
52
+ hash: hexAsBuffer('e9c3dd0c07aac76179ebc76a6c78d4d67c6c160a'),
53
+ version: 5,
54
+ },
55
+ },
56
+ ];
57
+
58
+ tests.forEach(({args, description, error, expected}) => {
59
+ return test(description, (t, end) => {
60
+ if (!!error) {
61
+ throws(() => decodeBase58Address(args), new Error(error), 'Err');
62
+ } else {
63
+ const res = decodeBase58Address(args);
64
+
65
+ deepEqual(res, expected, 'Got expected result');
66
+ }
67
+
68
+ return end();
69
+ });
70
+ });