-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
208 lines (182 loc) · 5.76 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
import { bech32m } from 'bech32';
const network = ['mainnet', 'testnet', 'regtest'] as const;
/** Supported bitcoin networks */
export type Network = (typeof network)[number];
const PAYLOAD_LENGTH_NO_OUTPOINT = 15;
const PAYLOAD_LENGTH_WITH_OUTPOINT = 18;
const magicCodes = {
mainnet: 0x3,
testnet: 0x6,
regtest: 0x0,
};
const prefixes = {
mainnet: 'tx',
testnet: 'txtest',
regtest: 'txrt',
};
const magicCodesByPrefix: Record<string, number | undefined> = {
tx: 0x3,
txtest: 0x6,
txrt: 0x0,
};
const networkByPrefix: Record<string, Network | undefined> = {
tx: 'mainnet',
txtest: 'testnet',
txrt: 'regtest',
};
function addSeparators(encoded: string) {
const separatorIndex = encoded.indexOf('1');
const payload = encoded.substring(separatorIndex + 1);
const separatedPayload = payload.match(/.{1,4}/g)?.join('-');
return encoded.substring(0, separatorIndex) + '1:' + separatedPayload;
}
/**
* The data parts encoded by a TxRef
*/
export type DataParts = {
network: Network;
blockHeight: number;
txIndex: number;
outpoint?: number;
};
/**
* Encodes a `blockheight`, `txIndex`, and optionally `network` and/or
* `outpoint` into a TxRef string.
*/
export function encode({
blockHeight,
txIndex,
outpoint,
network = 'mainnet',
}: DataParts): string {
const hasOutpoint = !!outpoint;
const magicCode = magicCodes[network] + (hasOutpoint ? 1 : 0);
const prefix = prefixes[network];
const words: number[] = Array.from({ length: hasOutpoint ? 11 : 8 });
words[0] = magicCode;
words[1] |= (blockHeight & 0xf) << 1;
words[2] |= (blockHeight & 0x1f0) >> 4;
words[3] |= (blockHeight & 0x3e00) >> 9;
words[4] |= (blockHeight & 0x7c000) >> 14;
words[5] |= (blockHeight & 0xf80000) >> 19;
words[6] |= txIndex & 0x1f;
words[7] |= (txIndex & 0x3e0) >> 5;
words[8] |= (txIndex & 0x7c00) >> 10;
if (hasOutpoint) {
words[9] |= outpoint & 0x1f;
words[10] |= (outpoint & 0x3e0) >> 5;
words[11] |= (outpoint & 0x7c00) >> 10;
}
return addSeparators(bech32m.encode(prefix, words));
}
/**
* Strips invalid characters that are not part of bech32 alphabet from an
* encoded TxRef string.
* @param txRef A raw TxRef we are expected to decode
* @returns A TxRef string without any invalid characters
*/
function stripNonBechCharacters(txRef: string) {
const separatorIndex = txRef.indexOf('1');
const payload = txRef.substring(separatorIndex + 1);
const strippedPayload = payload.replace(/[^ac-hj-np-zAC-HJ-NP-Z02-9]/g, '');
if (payload !== strippedPayload) {
if (separatorIndex === -1) {
// there's no prefix
return strippedPayload;
} else {
return `${txRef.substring(0, separatorIndex)}1${strippedPayload}`;
}
} else {
return txRef;
}
}
/**
* Appends the HRP prefix to a TxRef string if it is not already present.
* @param strippedTxRef A TxRef stripped of any invalid characters
* @returns A TxRef with the appropriate HRP bech prefix. If the provided TxRef
* already had a prefix, it is returned unmodified.
*
*/
function appendHrpPrefixIfMissing(strippedTxRef: string) {
// some txrefs may have had the HRP stripped off leaving just the payload
// prepend the bech32 prefix and separator if needed to properly decode
if (strippedTxRef.length === PAYLOAD_LENGTH_NO_OUTPOINT) {
switch (strippedTxRef[0]) {
case 'r':
return `${prefixes.mainnet}1${strippedTxRef}`;
case 'x':
return `${prefixes.testnet}1${strippedTxRef}`;
case 'q':
return `${prefixes.regtest}1${strippedTxRef}`;
default:
throw new Error('txref magic code not recognized');
}
} else if (
strippedTxRef.length === PAYLOAD_LENGTH_WITH_OUTPOINT &&
!strippedTxRef.toLowerCase().startsWith('tx1')
) {
switch (strippedTxRef[0]) {
case 'y':
return `${prefixes.mainnet}1${strippedTxRef}`;
case '8':
return `${prefixes.testnet}1${strippedTxRef}`;
case 'p':
return `${prefixes.regtest}1${strippedTxRef}`;
default:
throw new Error('txref magic code not recognized');
}
}
return strippedTxRef;
}
/**
* Sanitizes a TxRef so it can be successfully decoded as a bech32 string.
*/
function sanitizeTxRef(txRef: string) {
const strippedTxRef = stripNonBechCharacters(txRef);
return appendHrpPrefixIfMissing(strippedTxRef);
}
/**
* Decodes a TxRef string into its data parts such as `blockHeight` and `txIndex`.
* @param txRef An encoded TxRef string with or without separators
* and with or without a human readable prefix.
*/
export function decode(txRef: string): DataParts {
const encoded = sanitizeTxRef(txRef);
const { prefix, words } = bech32m.decode(encoded);
const magicCode = words[0];
const prefixMagicCode = magicCodesByPrefix[prefix];
const network = networkByPrefix[prefix];
if (prefixMagicCode === undefined || network === undefined) {
throw new Error('txref prefix not recognized');
}
let outpoint: number | undefined;
if (words.length === 9) {
// no encoded outpoint
if (magicCode !== prefixMagicCode) {
throw new Error('invalid magic code');
}
} else if (words.length === 12) {
// we have an outpoint
if (magicCode !== prefixMagicCode + 1) {
throw new Error('invalid magic code');
}
outpoint = words[9];
outpoint |= words[10] << 5;
outpoint |= words[11] << 10;
} else {
throw new Error('invalid length of bech32 encoded words');
}
const version = words[1] & 0x1;
if (version !== 0) {
throw new Error('unknown version');
}
let blockHeight = words[1] >> 1;
blockHeight |= words[2] << 4;
blockHeight |= words[3] << 9;
blockHeight |= words[4] << 14;
blockHeight |= words[5] << 19;
let txIndex = words[6];
txIndex |= words[7] << 5;
txIndex |= words[8] << 10;
return { network, blockHeight, txIndex, outpoint };
}