@alexbosworth/blockchain 1.2.1 → 1.4.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 +8 -0
- package/README.md +119 -0
- package/hashes/id_for_block.js +2 -2
- package/hashes/no_locktime_id_for_transaction.js +6 -99
- package/index.js +4 -0
- package/package.json +2 -2
- package/script/index.js +3 -0
- package/script/script_elements_as_script.js +42 -0
- package/test/hashes/test_no_locktime_id_for_transaction.js +1 -1
- package/test/script/test_script_elements_as_script.js +30 -0
- package/test/transactions/test_components_of_transaction.js +215 -0
- package/transactions/components_of_transaction.js +56 -0
- package/transactions/index.js +4 -0
- package/transactions/parse_transaction.js +176 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
- `scriptElementsAsScript`: Convert an array of script elements to a script
|
|
6
|
+
|
|
7
|
+
## 1.3.0
|
|
8
|
+
|
|
9
|
+
- `componentsOfTransaction`: Parse transaction hex into its component elements
|
|
10
|
+
|
|
3
11
|
## 1.2.1
|
|
4
12
|
|
|
5
13
|
- `noLocktimeIdForTransaction`: Fix id derivation to correct value
|
package/README.md
CHANGED
|
@@ -1,3 +1,122 @@
|
|
|
1
1
|
# Blockchain
|
|
2
2
|
|
|
3
3
|
Utility methods for working with Blockchain data
|
|
4
|
+
|
|
5
|
+
## Methods
|
|
6
|
+
|
|
7
|
+
### compactIntAsNumber
|
|
8
|
+
|
|
9
|
+
Convert a compact integer to a regular number
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
encoded: <Compact Integer Encoded Number Buffer Object>
|
|
13
|
+
[start]: <Buffer Offset Start Index Number>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@returns
|
|
17
|
+
{
|
|
18
|
+
bytes: <Byte Count Number>
|
|
19
|
+
number: <Integer Number>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
|
|
24
|
+
```node
|
|
25
|
+
const {compactIntAsNumber} = require('@alexbosworth/blockchain');
|
|
26
|
+
|
|
27
|
+
// Decode the plain number from the encoded compact int bytes
|
|
28
|
+
const {number} = compactIntAsNumber({encoded: Buffer.from('fdfd00', 'hex')});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### componentsOfTransaction
|
|
32
|
+
|
|
33
|
+
Get the components of a hex-encoded transaction
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
transaction: <Hex Encoded Transaction String>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@throws
|
|
40
|
+
<Error>
|
|
41
|
+
|
|
42
|
+
@returns
|
|
43
|
+
{
|
|
44
|
+
inputs: [{
|
|
45
|
+
id: <Spending Transaction Id Hex String>
|
|
46
|
+
script: <ScriptSig Script Hex String>
|
|
47
|
+
sequence: <Sequence Number>
|
|
48
|
+
vout: <Spending Transaction Output Index Number>
|
|
49
|
+
[witness]: [<Script Stack Element Hex String>]
|
|
50
|
+
}]
|
|
51
|
+
locktime: <Timelock nLockTime Number>
|
|
52
|
+
outputs: [{
|
|
53
|
+
script: <ScriptPub Script Hex String>
|
|
54
|
+
tokens: <Tokens Count Number>
|
|
55
|
+
}]
|
|
56
|
+
version: <Version Number>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
### idForBlock
|
|
60
|
+
|
|
61
|
+
Get an id for a block: the double sha256 hash of the block header
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
block: <Hex Encoded Block Data String>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@throws
|
|
68
|
+
<Error>
|
|
69
|
+
|
|
70
|
+
@returns
|
|
71
|
+
{
|
|
72
|
+
id: <Block Id Hex Encoded String>
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
### noLocktimeIdForTransaction
|
|
76
|
+
|
|
77
|
+
Get an id for a transaction with witness data and mlocktime not included
|
|
78
|
+
|
|
79
|
+
{
|
|
80
|
+
buffer: <Data Buffer Object>
|
|
81
|
+
[start]: <Starting Offset Index Number>
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@throws
|
|
85
|
+
<Error>
|
|
86
|
+
|
|
87
|
+
@returns
|
|
88
|
+
{
|
|
89
|
+
id: <No nLockTime Transaction Id Hex String>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
### numberAsCompactInt
|
|
93
|
+
|
|
94
|
+
Convert a number to compact size integer serialization
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
number: <Amount to Convert to Compact Integer Serialization Number>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@throws
|
|
101
|
+
<Error>
|
|
102
|
+
|
|
103
|
+
@returns
|
|
104
|
+
{
|
|
105
|
+
encoded: <Serialized Compact Integer Buffer Object>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
### scriptElementsAsScript
|
|
109
|
+
|
|
110
|
+
Map array of script buffer elements to a fully formed script
|
|
111
|
+
|
|
112
|
+
{
|
|
113
|
+
elements: [<Data Buffer>, <Script OP_CODE Number>]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@throws
|
|
117
|
+
<Error>
|
|
118
|
+
|
|
119
|
+
@returns
|
|
120
|
+
{
|
|
121
|
+
script: <Script Hex String>
|
|
122
|
+
}
|
package/hashes/id_for_block.js
CHANGED
|
@@ -7,7 +7,7 @@ const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
|
7
7
|
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
|
|
8
8
|
const sha256 = preimage => createHash('sha256').update(preimage).digest();
|
|
9
9
|
|
|
10
|
-
/** Get an id for a block
|
|
10
|
+
/** Get an id for a block: the double sha256 hash of the block header
|
|
11
11
|
|
|
12
12
|
{
|
|
13
13
|
block: <Hex Encoded Block Data String>
|
|
@@ -18,7 +18,7 @@ const sha256 = preimage => createHash('sha256').update(preimage).digest();
|
|
|
18
18
|
|
|
19
19
|
@returns
|
|
20
20
|
{
|
|
21
|
-
id: <Block Hex String>
|
|
21
|
+
id: <Block Id Hex Encoded String>
|
|
22
22
|
}
|
|
23
23
|
*/
|
|
24
24
|
module.exports = ({block}) => {
|
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
const {createHash} = require('crypto');
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {parseTransaction} = require('./../transactions');
|
|
4
4
|
|
|
5
5
|
const bufferAsHex = buffer => buffer.toString('hex');
|
|
6
|
-
const byteCountHash = 32;
|
|
7
|
-
const byteCountInt8 = 1;
|
|
8
|
-
const byteCountInt32 = 4;
|
|
9
|
-
const byteCountInt64 = 8;
|
|
10
|
-
const byteCountMarkerFlag = 2;
|
|
11
|
-
const byteCountNoMarkerFlag = 0;
|
|
12
|
-
const decodeCompactInt = (b, o) => compactIntAsNumber({encoded: b, start: o});
|
|
13
|
-
const defaultStartIndex = 0;
|
|
14
|
-
const defaultWitnessCount = 0;
|
|
15
|
-
const {isBuffer} = Buffer;
|
|
16
|
-
const segWitV0Marker = 0;
|
|
17
|
-
const segWitV0Flag = 1;
|
|
18
6
|
const sha256 = preimage => createHash('sha256').update(preimage).digest();
|
|
19
|
-
const times = n => [...Array(n).keys()];
|
|
20
7
|
|
|
21
8
|
/** Get an id for a transaction with witness data and mlocktime not included
|
|
22
9
|
|
|
@@ -34,92 +21,12 @@ const times = n => [...Array(n).keys()];
|
|
|
34
21
|
}
|
|
35
22
|
*/
|
|
36
23
|
module.exports = ({buffer, start}) => {
|
|
37
|
-
if (!isBuffer(buffer)) {
|
|
38
|
-
throw new Error('ExpectedDataBufferToGetNoLocktimeIdForTransaction');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let offset = start || defaultStartIndex;
|
|
42
|
-
|
|
43
|
-
// Transaction version is a signed 4 byte integer
|
|
44
|
-
const version = buffer.readInt32LE(offset, offset + byteCountInt32);
|
|
45
|
-
|
|
46
|
-
offset += byteCountInt32;
|
|
47
|
-
|
|
48
|
-
// For SegWit transactions, the next 2 bytes would be a marker and flag
|
|
49
|
-
const markerFlagSplit = offset + byteCountInt8;
|
|
50
|
-
|
|
51
|
-
const marker = buffer.readUInt8(offset, markerFlagSplit);
|
|
52
|
-
|
|
53
|
-
const flag = buffer.readUInt8(markerFlagSplit, offset + byteCountMarkerFlag);
|
|
54
|
-
|
|
55
|
-
// The presence of the marker and flag indicates SegWit tx encoding
|
|
56
|
-
const isSegWit = !marker && flag === segWitV0Flag;
|
|
57
|
-
|
|
58
|
-
// When tx isn't SegWit though, the bytes are not marker and flag
|
|
59
|
-
offset += isSegWit ? byteCountMarkerFlag : byteCountNoMarkerFlag;
|
|
60
|
-
|
|
61
|
-
const inputsCount = decodeCompactInt(buffer, offset);
|
|
62
|
-
|
|
63
|
-
offset += inputsCount.bytes;
|
|
64
|
-
|
|
65
|
-
// For SegWit the witness stacks will be at the end of the transaction
|
|
66
|
-
const witnessCount = isSegWit ? inputsCount.number : defaultWitnessCount;
|
|
67
|
-
|
|
68
|
-
// Read in the inputs
|
|
69
|
-
const inputs = times(inputsCount.number).map(i => {
|
|
70
|
-
// The hash is the internal byte order hash of the tx being spent
|
|
71
|
-
const hash = buffer.subarray(offset, offset + byteCountHash);
|
|
72
|
-
|
|
73
|
-
offset += byteCountHash;
|
|
74
|
-
|
|
75
|
-
// The vout is the 4 byte output index of the tx being spent in this input
|
|
76
|
-
const vout = buffer.readUInt32LE(offset, offset + byteCountInt32);
|
|
77
|
-
|
|
78
|
-
offset += byteCountInt32;
|
|
79
|
-
|
|
80
|
-
// Before SegWit, scripts were encoded in this input space
|
|
81
|
-
const scriptLength = decodeCompactInt(buffer, offset);
|
|
82
|
-
|
|
83
|
-
offset += scriptLength.bytes;
|
|
84
|
-
|
|
85
|
-
// Scripts are variable byte length
|
|
86
|
-
const script = buffer.subarray(offset, offset + scriptLength.number);
|
|
87
|
-
|
|
88
|
-
offset += scriptLength.number;
|
|
89
|
-
|
|
90
|
-
// Sequence is a 4 byte unsigned number for an input, used for CSV mainly
|
|
91
|
-
const sequence = buffer.readUInt32LE(offset, offset + byteCountInt32);
|
|
92
|
-
|
|
93
|
-
offset += byteCountInt32;
|
|
94
|
-
|
|
95
|
-
return {hash, script, sequence, vout};
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const outputsCount = decodeCompactInt(buffer, offset);
|
|
99
|
-
|
|
100
|
-
offset += outputsCount.bytes;
|
|
101
|
-
|
|
102
|
-
// Read in the outputs of the transaction
|
|
103
|
-
const outputs = times(outputsCount.number).map(i => {
|
|
104
|
-
// The value being spent
|
|
105
|
-
const tokens = buffer.readBigUInt64LE(offset, offset + byteCountInt64);
|
|
106
|
-
|
|
107
|
-
offset += byteCountInt64;
|
|
108
|
-
|
|
109
|
-
// The script being spent to has a variable length
|
|
110
|
-
const scriptLength = decodeCompactInt(buffer, offset);
|
|
111
|
-
|
|
112
|
-
offset += scriptLength.bytes;
|
|
113
|
-
|
|
114
|
-
const script = buffer.subarray(offset, offset + scriptLength.number);
|
|
115
|
-
|
|
116
|
-
offset += scriptLength.number;
|
|
117
|
-
|
|
118
|
-
return {script, tokens};
|
|
119
|
-
});
|
|
120
|
-
|
|
121
24
|
// The remainder of the transaction bytes are witnesses and nlocktime
|
|
122
|
-
const bytes =
|
|
25
|
+
const {bytes} = parseTransaction({
|
|
26
|
+
buffer,
|
|
27
|
+
start,
|
|
28
|
+
is_terminating_after_outputs: true,
|
|
29
|
+
});
|
|
123
30
|
|
|
124
31
|
return {id: bufferAsHex(sha256(bytes))};
|
|
125
32
|
};
|
package/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
const {compactIntAsNumber} = require('./numbers');
|
|
2
|
+
const {componentsOfTransaction} = require('./transactions');
|
|
2
3
|
const {idForBlock} = require('./hashes');
|
|
3
4
|
const {noLocktimeIdForTransaction} = require('./hashes');
|
|
4
5
|
const {numberAsCompactInt} = require('./numbers');
|
|
6
|
+
const {scriptElementsAsScript} = require('./script');
|
|
5
7
|
|
|
6
8
|
module.exports = {
|
|
7
9
|
compactIntAsNumber,
|
|
10
|
+
componentsOfTransaction,
|
|
8
11
|
idForBlock,
|
|
9
12
|
noLocktimeIdForTransaction,
|
|
10
13
|
numberAsCompactInt,
|
|
14
|
+
scriptElementsAsScript,
|
|
11
15
|
};
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/alexbosworth/blockchain.git"
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 200 test/hashes/*.js test/numbers/*.js"
|
|
23
|
+
"test": "tap -j 2 --branches=1 --functions=1 --lines=1 --statements=1 -t 200 test/hashes/*.js test/numbers/*.js test/script/*.js test/transactions/*.js"
|
|
24
24
|
},
|
|
25
|
-
"version": "1.
|
|
25
|
+
"version": "1.4.0"
|
|
26
26
|
}
|
package/script/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const {numberAsCompactInt} = require('./../numbers');
|
|
2
|
+
|
|
3
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
4
|
+
const {concat} = Buffer;
|
|
5
|
+
const encode = number => numberAsCompactInt({number}).encoded;
|
|
6
|
+
const flatten = arr => arr.reduce((sum, n) => Buffer.concat([sum, n]));
|
|
7
|
+
const {from} = Buffer;
|
|
8
|
+
const {isArray} = Array;
|
|
9
|
+
const {isBuffer} = Buffer;
|
|
10
|
+
|
|
11
|
+
/** Map array of script buffer elements to a fully formed script
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
elements: [<Data Buffer>, <Script OP_CODE Number>]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@throws
|
|
18
|
+
<Error>
|
|
19
|
+
|
|
20
|
+
@returns
|
|
21
|
+
{
|
|
22
|
+
script: <Script Hex String>
|
|
23
|
+
}
|
|
24
|
+
*/
|
|
25
|
+
module.exports = ({elements}) => {
|
|
26
|
+
if (!isArray(elements)) {
|
|
27
|
+
throw new Error('ExpectedArrayOfScriptElementsToEncodeScript');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Convert numbers to buffers and hex data to pushdata
|
|
31
|
+
const fullScript = elements.map(element => {
|
|
32
|
+
// Exit early when element is a data push
|
|
33
|
+
if (isBuffer(element)) {
|
|
34
|
+
return concat([encode(element.length), element]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Non data elements are direct bytes
|
|
38
|
+
return from([element]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {script: bufferAsHex(flatten(fullScript))};
|
|
42
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const {test} = require('@alexbosworth/tap');
|
|
2
|
+
|
|
3
|
+
const {scriptElementsAsScript} = require('./../../');
|
|
4
|
+
|
|
5
|
+
const tests = [
|
|
6
|
+
{
|
|
7
|
+
args: {},
|
|
8
|
+
description: 'An array of eleemnts is required',
|
|
9
|
+
error: 'ExpectedArrayOfScriptElementsToEncodeScript',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
args: {elements: [118, 169, Buffer.alloc(20), 136, 172]},
|
|
13
|
+
description: 'Elements are mapped to an output script',
|
|
14
|
+
expected: {script: '76a914000000000000000000000000000000000000000088ac'},
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
tests.forEach(({args, description, error, expected}) => {
|
|
19
|
+
return test(description, ({end, strictSame, throws}) => {
|
|
20
|
+
if (!!error) {
|
|
21
|
+
throws(() => scriptElementsAsScript(args), new Error(error), 'Got err');
|
|
22
|
+
} else {
|
|
23
|
+
const res = scriptElementsAsScript(args);
|
|
24
|
+
|
|
25
|
+
strictSame(res, expected, 'Got expected result');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return end();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const {test} = require('@alexbosworth/tap');
|
|
2
|
+
|
|
3
|
+
const {componentsOfTransaction} = require('./../../');
|
|
4
|
+
|
|
5
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
6
|
+
|
|
7
|
+
const tests = [
|
|
8
|
+
{
|
|
9
|
+
args: {transaction: ''},
|
|
10
|
+
description: 'A transaction hex is required',
|
|
11
|
+
error: 'ExpectedHexEncodedTransactionToGetComponentsOf',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
args: {
|
|
15
|
+
transaction: '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000',
|
|
16
|
+
},
|
|
17
|
+
description: 'A coinbase transaction is parsed',
|
|
18
|
+
expected: {
|
|
19
|
+
inputs: [{
|
|
20
|
+
id: '0000000000000000000000000000000000000000000000000000000000000000',
|
|
21
|
+
script: '04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73',
|
|
22
|
+
sequence: 4294967295,
|
|
23
|
+
vout: 4294967295,
|
|
24
|
+
witness: undefined,
|
|
25
|
+
}],
|
|
26
|
+
locktime: 0,
|
|
27
|
+
outputs: [{
|
|
28
|
+
script: '4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac',
|
|
29
|
+
tokens: 5000000000,
|
|
30
|
+
}],
|
|
31
|
+
version: 1,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
args: {
|
|
36
|
+
transaction: '01000000019ac03d5ae6a875d970128ef9086cef276a1919684a6988023cc7254691d97e6d010000006b4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fdffffffff080100000000000000010101000000000000000202010100000000000000014c0100000000000000034c02010100000000000000014d0100000000000000044dffff010100000000000000014e0100000000000000064effffffff0100000000',
|
|
37
|
+
},
|
|
38
|
+
description: 'invalid script',
|
|
39
|
+
expected: {
|
|
40
|
+
inputs: [{
|
|
41
|
+
id: '6d7ed9914625c73c0288694a6819196a27ef6c08f98e1270d975a8e65a3dc09a',
|
|
42
|
+
script: '4830450221009d41dc793ba24e65f571473d40b299b6459087cea1509f0d381740b1ac863cb6022039c425906fcaf51b2b84d8092569fb3213de43abaff2180e2a799d4fcb4dd0aa012102d5ede09a8ae667d0f855ef90325e27f6ce35bbe60a1e6e87af7f5b3c652140fd',
|
|
43
|
+
sequence: 4294967295,
|
|
44
|
+
vout: 1,
|
|
45
|
+
witness: undefined,
|
|
46
|
+
}],
|
|
47
|
+
locktime: 0,
|
|
48
|
+
outputs: [
|
|
49
|
+
{
|
|
50
|
+
script: '01',
|
|
51
|
+
tokens: 1,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
script: '0201',
|
|
55
|
+
tokens: 1,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
script: '4c',
|
|
59
|
+
tokens: 1,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
script: '4c0201',
|
|
63
|
+
tokens: 1,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
script: '4d',
|
|
67
|
+
tokens: 1,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
script: '4dffff01',
|
|
71
|
+
tokens: 1,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
script: '4e',
|
|
75
|
+
tokens: 1,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
script: '4effffffff01',
|
|
79
|
+
tokens: 1,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
version: 1,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
args: {
|
|
87
|
+
transaction: '02000000000103ae8caf3aad8861142597a29d6377f524a55f4542d5294c8292ce123a956d3e520000000000ffffffff9cbb1c5531a677b90c489d75a198a14f02d49e554ab9cdaa17bd956b127724570200000000ffffffff9cbb1c5531a677b90c489d75a198a14f02d49e554ab9cdaa17bd956b127724570100000000ffffffff0640420f0000000000160014eaa9b76637b1ad340b6efadf773a73b53637d5b6de91f62901000000225120f4c82416bcb08422c195af995e1d248d1378d8b48dafa9f45bc213b83101d49240420f00000000001600148729d17b2aa507ab19051a028384bc6e0ce25e455e368900000000002251200249ccc5af06fa5642f12d42d2a34bfbb08688d54a9b99d07b98619b35df03b440420f0000000000160014d2d59a8a59f997cbc8888411010faf1658e0e3465e368900000000002251207febd720c78518b52aa1a2443823cc8f55e373910f616e112d5d7bd622fe1ab2024830450221008ac71eff4d7e298941be012fc14f0ac9bf62ae6ffeac13522bb27b5b4108d3aa0220192a69ad6fdb86b1e09c7fdcaaafeb58d25060e44199c734dc0d7d385b5d800d0121029943eaccd3987fa495a6b4f47f2fafeb0521e4e12f39498d9465a564ef75329602483045022100f9cde9adb00c0a6c62dae8604ca750039201288c0dafff952461da3caf05e3ae0220679c01f2518413951de3b62531b1cf36bb92562e3bd4197f0fa6e6e3e231272e0121027326b48c9f2729597e328ab6d05f5af75866e1ffa203fadf78387a3b202ff80d0247304402202550beec478845af2df929abf85708f9fcceaae31377f2e01d803e2acf7b426f022036c312b1e38ca333fe70aa37d3093387ac7486f08438eb8eed323699594468cb012102275a197f7ccfece19cf0532b068b6e38ceceda146e791875ecbdc55500bb7efe00000000',
|
|
88
|
+
},
|
|
89
|
+
description: 'Transaction with witnesses and P2TR and P2WPKH outputs',
|
|
90
|
+
expected: {
|
|
91
|
+
inputs: [
|
|
92
|
+
{
|
|
93
|
+
id: '523e6d953a12ce92824c29d542455fa524f577639da29725146188ad3aaf8cae',
|
|
94
|
+
script: '',
|
|
95
|
+
sequence: 4294967295,
|
|
96
|
+
vout: 0,
|
|
97
|
+
witness: [
|
|
98
|
+
'30450221008ac71eff4d7e298941be012fc14f0ac9bf62ae6ffeac13522bb27b5b4108d3aa0220192a69ad6fdb86b1e09c7fdcaaafeb58d25060e44199c734dc0d7d385b5d800d01',
|
|
99
|
+
'029943eaccd3987fa495a6b4f47f2fafeb0521e4e12f39498d9465a564ef753296',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: '572477126b95bd17aacdb94a559ed4024fa198a1759d480cb977a631551cbb9c',
|
|
104
|
+
script: '',
|
|
105
|
+
sequence: 4294967295,
|
|
106
|
+
vout: 2,
|
|
107
|
+
witness: [
|
|
108
|
+
'3045022100f9cde9adb00c0a6c62dae8604ca750039201288c0dafff952461da3caf05e3ae0220679c01f2518413951de3b62531b1cf36bb92562e3bd4197f0fa6e6e3e231272e01',
|
|
109
|
+
'027326b48c9f2729597e328ab6d05f5af75866e1ffa203fadf78387a3b202ff80d',
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: '572477126b95bd17aacdb94a559ed4024fa198a1759d480cb977a631551cbb9c',
|
|
114
|
+
script: '',
|
|
115
|
+
sequence: 4294967295,
|
|
116
|
+
vout: 1,
|
|
117
|
+
witness: [
|
|
118
|
+
'304402202550beec478845af2df929abf85708f9fcceaae31377f2e01d803e2acf7b426f022036c312b1e38ca333fe70aa37d3093387ac7486f08438eb8eed323699594468cb01',
|
|
119
|
+
'02275a197f7ccfece19cf0532b068b6e38ceceda146e791875ecbdc55500bb7efe'
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
locktime: 0,
|
|
124
|
+
outputs: [
|
|
125
|
+
{
|
|
126
|
+
script: '0014eaa9b76637b1ad340b6efadf773a73b53637d5b6',
|
|
127
|
+
tokens: 1000000,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
script: '5120f4c82416bcb08422c195af995e1d248d1378d8b48dafa9f45bc213b83101d492',
|
|
131
|
+
tokens: 4998992350,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
script: '00148729d17b2aa507ab19051a028384bc6e0ce25e45',
|
|
135
|
+
tokens: 1000000,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
script: '51200249ccc5af06fa5642f12d42d2a34bfbb08688d54a9b99d07b98619b35df03b4',
|
|
139
|
+
tokens: 8992350,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
script: '0014d2d59a8a59f997cbc8888411010faf1658e0e346',
|
|
143
|
+
tokens: 1000000,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
script: '51207febd720c78518b52aa1a2443823cc8f55e373910f616e112d5d7bd622fe1ab2',
|
|
147
|
+
tokens: 8992350,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
version: 2,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
args: {
|
|
155
|
+
transaction: '0100000001f709fa82596e4f908ee331cb5e0ed46ab331d7dcfaf697fe95891e73dac4ebcb000000008c20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100ffffffff01c075790000000000232103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac00000000',
|
|
156
|
+
},
|
|
157
|
+
description: 'A non-segwit transaction is read',
|
|
158
|
+
expected: {
|
|
159
|
+
inputs: [{
|
|
160
|
+
id: 'cbebc4da731e8995fe97f6fadcd731b36ad40e5ecb31e38e904f6e5982fa09f7',
|
|
161
|
+
script: '20ca42095840735e89283fec298e62ac2ddea9b5f34a8cbb7097ad965b87568100201b1b01dc829177da4a14551d2fc96a9db00c6501edfa12f22cd9cefd335c227f483045022100a9df60536df5733dd0de6bc921fab0b3eee6426501b43a228afa2c90072eb5ca02201c78b74266fac7d1db5deff080d8a403743203f109fbcabf6d5a760bf87386d20100',
|
|
162
|
+
sequence: 4294967295,
|
|
163
|
+
vout: 0,
|
|
164
|
+
witness: undefined,
|
|
165
|
+
}],
|
|
166
|
+
locktime: 0,
|
|
167
|
+
outputs: [{
|
|
168
|
+
script: '2103611f9a45c18f28f06f19076ad571c344c82ce8fcfe34464cf8085217a2d294a6ac',
|
|
169
|
+
tokens: 7960000,
|
|
170
|
+
}],
|
|
171
|
+
version: 1,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
args: {
|
|
176
|
+
transaction: '0100000001c997a5e56e104102fa209c6a852dd90660a20b2d9c352423edce25857fcd3704000000004847304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901ffffffff0200ca9a3b00000000434104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac00286bee0000000043410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac00000000',
|
|
177
|
+
},
|
|
178
|
+
description: 'A P2PK transaction is read',
|
|
179
|
+
expected: {
|
|
180
|
+
inputs: [{
|
|
181
|
+
id: '0437cd7f8525ceed2324359c2d0ba26006d92d856a9c20fa0241106ee5a597c9',
|
|
182
|
+
script: '47304402204e45e16932b8af514961a1d3a1a25fdf3f4f7732e9d624c6c61548ab5fb8cd410220181522ec8eca07de4860a4acdd12909d831cc56cbbac4622082221a8768d1d0901',
|
|
183
|
+
sequence: 4294967295,
|
|
184
|
+
vout: 0,
|
|
185
|
+
witness: undefined,
|
|
186
|
+
}],
|
|
187
|
+
locktime: 0,
|
|
188
|
+
outputs: [
|
|
189
|
+
{
|
|
190
|
+
script: '4104ae1a62fe09c5f51b13905f07f06b99a2f7159b2225f374cd378d71302fa28414e7aab37397f554a7df5f142c21c1b7303b8a0626f1baded5c72a704f7e6cd84cac',
|
|
191
|
+
tokens: 1000000000,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
script: '410411db93e1dcdb8a016b49840f8c53bc1eb68a382e97b1482ecad7b148a6909a5cb2e0eaddfb84ccf9744464f82e160bfa9b8b64f9d4c03f999b8643f656b412a3ac',
|
|
195
|
+
tokens: 4000000000,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
version: 1,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
tests.forEach(({args, description, error, expected}) => {
|
|
204
|
+
return test(description, ({end, strictSame, throws}) => {
|
|
205
|
+
if (!!error) {
|
|
206
|
+
throws(() => componentsOfTransaction(args), new Error(error), 'Got err');
|
|
207
|
+
} else {
|
|
208
|
+
const res = componentsOfTransaction(args);
|
|
209
|
+
|
|
210
|
+
strictSame(res, expected, 'Got expected result');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return end();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const parseTransaction = require('./parse_transaction');
|
|
2
|
+
|
|
3
|
+
const bufferAsHex = buffer => buffer.toString('hex');
|
|
4
|
+
const hexAsBuffer = hex => Buffer.from(hex, 'hex');
|
|
5
|
+
const isHex = n => !!n && !(n.length % 2) && /^[0-9A-F]*$/i.test(n);
|
|
6
|
+
const txHashAsTxId = hash => hash.reverse();
|
|
7
|
+
|
|
8
|
+
/** Get the components of a hex-encoded transaction
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
transaction: <Hex Encoded Transaction String>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@throws
|
|
15
|
+
<Error>
|
|
16
|
+
|
|
17
|
+
@returns
|
|
18
|
+
{
|
|
19
|
+
inputs: [{
|
|
20
|
+
id: <Spending Transaction Id Hex String>
|
|
21
|
+
script: <ScriptSig Script Hex String>
|
|
22
|
+
sequence: <Sequence Number>
|
|
23
|
+
vout: <Spending Transaction Output Index Number>
|
|
24
|
+
[witness]: [<Script Stack Element Hex String>]
|
|
25
|
+
}]
|
|
26
|
+
locktime: <Timelock nLockTime Number>
|
|
27
|
+
outputs: [{
|
|
28
|
+
script: <ScriptPub Script Hex String>
|
|
29
|
+
tokens: <Tokens Count Number>
|
|
30
|
+
}]
|
|
31
|
+
version: <Version Number>
|
|
32
|
+
}
|
|
33
|
+
*/
|
|
34
|
+
module.exports = ({transaction}) => {
|
|
35
|
+
if (!isHex(transaction)) {
|
|
36
|
+
throw new Error('ExpectedHexEncodedTransactionToGetComponentsOf');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const details = parseTransaction({buffer: hexAsBuffer(transaction)});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
inputs: details.inputs.map(input => ({
|
|
43
|
+
id: bufferAsHex(txHashAsTxId(input.hash)),
|
|
44
|
+
script: bufferAsHex(input.script),
|
|
45
|
+
sequence: input.sequence,
|
|
46
|
+
vout: input.vout,
|
|
47
|
+
witness: !!input.witness ? input.witness.map(bufferAsHex) : undefined,
|
|
48
|
+
})),
|
|
49
|
+
locktime: details.locktime,
|
|
50
|
+
outputs: details.outputs.map(output => ({
|
|
51
|
+
script: bufferAsHex(output.script),
|
|
52
|
+
tokens: output.tokens,
|
|
53
|
+
})),
|
|
54
|
+
version: details.version,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const {compactIntAsNumber} = require('./../numbers');
|
|
2
|
+
|
|
3
|
+
const byteCountHash = 32;
|
|
4
|
+
const byteCountInt8 = 1;
|
|
5
|
+
const byteCountInt32 = 4;
|
|
6
|
+
const byteCountInt64 = 8;
|
|
7
|
+
const byteCountMarkerFlag = 2;
|
|
8
|
+
const byteCountNoMarkerFlag = 0;
|
|
9
|
+
const decodeCompactInt = (b, o) => compactIntAsNumber({encoded: b, start: o});
|
|
10
|
+
const defaultLocktime = 0;
|
|
11
|
+
const defaultStartIndex = 0;
|
|
12
|
+
const defaultWitnessCount = 0;
|
|
13
|
+
const {isBuffer} = Buffer;
|
|
14
|
+
const segWitV0Marker = 0;
|
|
15
|
+
const segWitV0Flag = 1;
|
|
16
|
+
const times = n => [...Array(n).keys()];
|
|
17
|
+
|
|
18
|
+
/** Parse a raw transaction out of a buffer at a specific offset start
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
buffer: <Data Buffer Object>
|
|
22
|
+
is_terminating_after_outputs: <Ignore Data After Outputs Bool>
|
|
23
|
+
[start]: <Starting Offset Index Number>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@returns
|
|
27
|
+
{
|
|
28
|
+
bytes: <Raw Transaction Bytes Buffer Object>
|
|
29
|
+
inputs: [{
|
|
30
|
+
hash: <Spending Internal Byte Order Transaction Id Buffer Object>
|
|
31
|
+
script: <Script Buffer Object>
|
|
32
|
+
sequence: <Sequence Number>
|
|
33
|
+
vout: <Spending Transaction Output Index Number>
|
|
34
|
+
[witness]: [<Script Stack Element Buffer Object>]
|
|
35
|
+
}]
|
|
36
|
+
locktime: <Timelock nLockTime Number>
|
|
37
|
+
outputs: [{
|
|
38
|
+
script: <Output Script Buffer Object>
|
|
39
|
+
tokens: <Tokens Count Number>
|
|
40
|
+
}]
|
|
41
|
+
version: <Version Number>
|
|
42
|
+
}
|
|
43
|
+
*/
|
|
44
|
+
module.exports = args => {
|
|
45
|
+
if (!isBuffer(args.buffer)) {
|
|
46
|
+
throw new Error('ExpectedDataBufferToReadTransactionDataOutOf');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const start = args.start || defaultStartIndex;
|
|
50
|
+
|
|
51
|
+
let offset = start;
|
|
52
|
+
|
|
53
|
+
// Transaction version is a signed 4 byte integer
|
|
54
|
+
const version = args.buffer.readInt32LE(offset, offset + byteCountInt32);
|
|
55
|
+
|
|
56
|
+
offset += byteCountInt32;
|
|
57
|
+
|
|
58
|
+
// For SegWit transactions, the next 2 bytes would be a marker and flag
|
|
59
|
+
const flagSplit = offset + byteCountInt8;
|
|
60
|
+
|
|
61
|
+
const marker = args.buffer.readUInt8(offset, flagSplit);
|
|
62
|
+
|
|
63
|
+
const flag = args.buffer.readUInt8(flagSplit, offset + byteCountMarkerFlag);
|
|
64
|
+
|
|
65
|
+
// The presence of the marker and flag indicates SegWit tx encoding
|
|
66
|
+
const isSegWit = !marker && flag === segWitV0Flag;
|
|
67
|
+
|
|
68
|
+
// When tx isn't SegWit though, the bytes are not marker and flag
|
|
69
|
+
offset += isSegWit ? byteCountMarkerFlag : byteCountNoMarkerFlag;
|
|
70
|
+
|
|
71
|
+
const inputsCount = decodeCompactInt(args.buffer, offset);
|
|
72
|
+
|
|
73
|
+
offset += inputsCount.bytes;
|
|
74
|
+
|
|
75
|
+
// For SegWit the witness stacks will be at the end of the transaction
|
|
76
|
+
const witnessCount = isSegWit ? inputsCount.number : defaultWitnessCount;
|
|
77
|
+
|
|
78
|
+
// Read in the inputs
|
|
79
|
+
const inputs = times(inputsCount.number).map(i => {
|
|
80
|
+
// The hash is the internal byte order hash of the tx being spent
|
|
81
|
+
const hash = args.buffer.subarray(offset, offset + byteCountHash);
|
|
82
|
+
|
|
83
|
+
offset += byteCountHash;
|
|
84
|
+
|
|
85
|
+
// The vout is the 4 byte output index of the tx being spent in this input
|
|
86
|
+
const vout = args.buffer.readUInt32LE(offset, offset + byteCountInt32);
|
|
87
|
+
|
|
88
|
+
offset += byteCountInt32;
|
|
89
|
+
|
|
90
|
+
// Before SegWit, scripts were encoded in this input space
|
|
91
|
+
const scriptLength = decodeCompactInt(args.buffer, offset);
|
|
92
|
+
|
|
93
|
+
offset += scriptLength.bytes;
|
|
94
|
+
|
|
95
|
+
// Scripts are variable byte length
|
|
96
|
+
const script = args.buffer.subarray(offset, offset + scriptLength.number);
|
|
97
|
+
|
|
98
|
+
offset += scriptLength.number;
|
|
99
|
+
|
|
100
|
+
// Sequence is a 4 byte unsigned number for an input, used for CSV mainly
|
|
101
|
+
const sequence = args.buffer.readUInt32LE(offset, offset + byteCountInt32);
|
|
102
|
+
|
|
103
|
+
offset += byteCountInt32;
|
|
104
|
+
|
|
105
|
+
return {hash, script, sequence, vout};
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const outputsCount = decodeCompactInt(args.buffer, offset);
|
|
109
|
+
|
|
110
|
+
offset += outputsCount.bytes;
|
|
111
|
+
|
|
112
|
+
// Read in the outputs of the transaction
|
|
113
|
+
const outputs = times(outputsCount.number).map(i => {
|
|
114
|
+
// The value being spent
|
|
115
|
+
const value = args.buffer.readBigUInt64LE(offset, offset + byteCountInt64);
|
|
116
|
+
|
|
117
|
+
offset += byteCountInt64;
|
|
118
|
+
|
|
119
|
+
// The script being spent to has a variable length
|
|
120
|
+
const scriptLength = decodeCompactInt(args.buffer, offset);
|
|
121
|
+
|
|
122
|
+
offset += scriptLength.bytes;
|
|
123
|
+
|
|
124
|
+
const script = args.buffer.subarray(offset, offset + scriptLength.number);
|
|
125
|
+
|
|
126
|
+
offset += scriptLength.number;
|
|
127
|
+
|
|
128
|
+
return {script, tokens: Number(value)};
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Exit early when ignoring data after the outputs
|
|
132
|
+
if (!!args.is_terminating_after_outputs) {
|
|
133
|
+
return {
|
|
134
|
+
inputs,
|
|
135
|
+
outputs,
|
|
136
|
+
version,
|
|
137
|
+
bytes: args.buffer.subarray(start, offset),
|
|
138
|
+
locktime: defaultLocktime,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// SegWit places the input witnesses after the outputs
|
|
143
|
+
times(witnessCount).forEach(i => {
|
|
144
|
+
// Witnesses are a stack of witness elements
|
|
145
|
+
const stackElementsCount = decodeCompactInt(args.buffer, offset);
|
|
146
|
+
|
|
147
|
+
offset += stackElementsCount.bytes;
|
|
148
|
+
|
|
149
|
+
// Read in the witness stack elements
|
|
150
|
+
const elements = [...Array(stackElementsCount.number).keys()].map(i => {
|
|
151
|
+
// Stack elements are variable length
|
|
152
|
+
const elementLength = decodeCompactInt(args.buffer, offset);
|
|
153
|
+
|
|
154
|
+
offset += elementLength.bytes;
|
|
155
|
+
|
|
156
|
+
const item = args.buffer.subarray(offset, offset + elementLength.number);
|
|
157
|
+
|
|
158
|
+
offset += elementLength.number;
|
|
159
|
+
|
|
160
|
+
return item;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Insert the witness stack into the related input
|
|
164
|
+
return inputs[i].witness = elements;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The final element is the 4 byte transaction nLockTime
|
|
168
|
+
const locktime = args.buffer.readUInt32LE(offset, offset + byteCountInt32);
|
|
169
|
+
|
|
170
|
+
offset += byteCountInt32;
|
|
171
|
+
|
|
172
|
+
// Take the bytes of this transaction
|
|
173
|
+
const bytes = args.buffer.subarray(start, offset);
|
|
174
|
+
|
|
175
|
+
return {bytes, inputs, locktime, outputs, version};
|
|
176
|
+
};
|