@@ -4,68 +4,125 @@ import * as tinySecp256k1 from 'tiny-secp256k1';
4
4
import BIP32Factory from 'bip32' ;
5
5
import crypto from 'crypto' ;
6
6
import { bech32 } from 'bech32' ;
7
+ import AngorProjectRepository from '../repositories/AngorProjectRepository' ;
8
+ import AngorInvestmentRepository from '../repositories/AngorInvestmentRepository' ;
7
9
8
10
/**
9
11
* Represents a Bitcoin network.
10
12
* Supports Bitcoin and Bitcoin Testnet.
11
13
*/
12
- export enum Networks {
14
+ export enum AngorSupportedNetworks {
13
15
Testnet = 'testnet' ,
14
16
Bitcoin = 'bitcoin' ,
15
17
}
16
18
19
+ export enum AngorTransactionStatus {
20
+ NotIdentified = 'notIdentified' ,
21
+ Pending = 'pending' ,
22
+ Confirmed = 'confirmed' ,
23
+ }
24
+
17
25
/**
18
26
* Represents a transaction related to the project at Angor platform (https://angor.io).
19
27
*/
20
28
export class AngorTransactionDecoder {
21
29
private transaction : bitcoinJS . Transaction ;
22
- private founderKeyHex : string ;
23
- private founderKeyHash : string ;
24
30
private network : bitcoinJS . Network ;
25
31
private angorKeys = {
26
- [ Networks . Testnet ] :
32
+ [ AngorSupportedNetworks . Testnet ] :
27
33
'tpubD8JfN1evVWPoJmLgVg6Usq2HEW9tLqm6CyECAADnH5tyQosrL6NuhpL9X1cQCbSmndVrgLSGGdbRqLfUbE6cRqUbrHtDJgSyQEY2Uu7WwTL' ,
28
- [ Networks . Bitcoin ] :
34
+ [ AngorSupportedNetworks . Bitcoin ] :
29
35
'xpub661MyMwAqRbcGNxKe9aFkPisf3h32gHLJm8f9XAqx8FB1Nk6KngCY8hkhGqxFr2Gyb6yfUaQVbodxLoC1f3K5HU9LM1CXE59gkEXSGCCZ1B' ,
30
36
} ;
31
37
private angorKey : string ;
32
38
33
- /**
34
- * Angor project identifier.
35
- */
36
- public projectId : string ;
37
- /**
38
- * Angor project Nostr public key.
39
- */
40
- public nostrPubKey : string ;
41
-
42
39
/**
43
40
* Constructs Angor transaction for the project creation.
44
41
* @param transactionHex - hex of the raw transaction.
45
42
* @param network - bitcoin network.
46
43
*/
47
- constructor ( transactionHex : string , network : Networks ) {
44
+ constructor ( transactionHex : string , network : AngorSupportedNetworks ) {
48
45
this . transaction = bitcoinJS . Transaction . fromHex ( transactionHex ) ;
49
46
50
- this . validateTransaction ( ) ;
51
-
52
47
this . network = bitcoinJS . networks [ network ] ;
53
48
this . angorKey = this . angorKeys [ network ] ;
49
+ }
54
50
55
- this . decompileOpReturnScript ( ) ;
56
- this . founderKeyHex = this . getFounderKeyHex ( ) ;
57
- this . founderKeyHash = this . getKeyHash ( ) ;
58
- this . hashToInt ( ) ;
59
- this . getProjectIdDerivation ( ) ;
60
- this . projectId = this . getProjectId ( ) ;
61
- this . nostrPubKey = this . getNostrPubKey ( ) ;
51
+ /**
52
+ * Decode and store transaction as Angor project creation transaction.
53
+ * If transaction is not an Angor project creation transaction, an error will be thrown.
54
+ * @param transactionStatus - status of the transaction.
55
+ */
56
+ public async decodeAndStoreProjectCreationTransaction (
57
+ transactionStatus : AngorTransactionStatus
58
+ ) : Promise < void > {
59
+ this . validateProjectCreationTransaction ( ) ;
60
+
61
+ const chunks = this . decompileOpReturnScript ( ) ;
62
+ const founderKeyHex = this . getFounderKeyHex ( chunks ) ;
63
+ const founderKeyHash = this . getKeyHash ( founderKeyHex ) ;
64
+ const founderKeyHashInt = this . hashToInt ( founderKeyHash ) ;
65
+ const projectIdDerivation = this . getProjectIdDerivation ( founderKeyHashInt ) ;
66
+ const projectId = this . getProjectId ( projectIdDerivation ) ;
67
+ const nostrPubKey = this . getNostrPubKey ( ) ;
68
+ const addressOnFeeOutput = this . getAddressOnFeeOutput ( ) ;
69
+
70
+ // Store Angor project in the DB.
71
+ await this . storeProjectInfo (
72
+ projectId ,
73
+ nostrPubKey ,
74
+ addressOnFeeOutput ,
75
+ transactionStatus
76
+ ) ;
77
+
78
+ // If transaction is confirmed (in the block), update statuses
79
+ // of the investment transactions related to this project.
80
+ if ( transactionStatus === AngorTransactionStatus . Confirmed ) {
81
+ await this . updateInvestmentsStatus (
82
+ addressOnFeeOutput ,
83
+ AngorTransactionStatus . Confirmed
84
+ ) ;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Decode and store transaction as Angor investment transaction.
90
+ * If transaction is not an Angor investment transaction, an error will be thrown.
91
+ * @param transactionStatus - status of the transaction.
92
+ */
93
+ public async decodeAndStoreInvestmentTransaction (
94
+ transactionStatus : AngorTransactionStatus
95
+ ) : Promise < void > {
96
+ this . validateInvestmentTransaction ( ) ;
97
+
98
+ const addressOnFeeOutput = this . getAddressOnFeeOutput ( ) ;
99
+
100
+ // Get Angor project with the same address on fee output.
101
+ const project = await this . getProject ( addressOnFeeOutput ) ;
102
+
103
+ // Return of there is no Angor project with the same address on fee output.
104
+ if ( ! project ) {
105
+ return ;
106
+ }
107
+
108
+ const txid = this . transaction . getId ( ) ;
109
+ const amount = this . transaction . outs [ 0 ] . value ;
110
+
111
+ // Store Angor investment in the DB.
112
+ await this . storeInvestmentInfo (
113
+ txid ,
114
+ amount ,
115
+ addressOnFeeOutput ,
116
+ transactionStatus
117
+ ) ;
62
118
}
63
119
64
120
/**
65
121
* Validates transaction object.
66
- * @param transaction - an object representing bitcoin transaction.
67
122
*/
68
- private validateTransaction ( transaction = this . transaction ) : void {
123
+ private validateProjectCreationTransaction ( ) : void {
124
+ const { transaction } = this ;
125
+
69
126
// Throw an error if transaction object is not present.
70
127
if ( ! transaction ) {
71
128
throw new Error ( `Transaction object wasn't created.` ) ;
@@ -90,12 +147,50 @@ export class AngorTransactionDecoder {
90
147
}
91
148
}
92
149
150
+ /**
151
+ * Validates transaction object.
152
+ */
153
+ private validateInvestmentTransaction ( ) : void {
154
+ const { transaction } = this ;
155
+
156
+ // Throw an error if transaction object is not present.
157
+ if ( ! transaction ) {
158
+ throw new Error ( `Transaction object wasn't created.` ) ;
159
+ }
160
+
161
+ // Throw an error if transaction outputs are not present.
162
+ if ( ! transaction . outs ) {
163
+ throw new Error ( `Transaction object doesn't have outputs.` ) ;
164
+ }
165
+ // Throw an error if the amount of transaction outputs is not equal to 3.
166
+ else if ( transaction . outs . length < 1 ) {
167
+ throw new Error ( `Transaction object has invalid amount of outputs.` ) ;
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Fetches Angor project by address on fee output from the DB.
173
+ * @param address - address on fee output.
174
+ * @returns - promise that resolves into an array of Angor projects.
175
+ */
176
+ private async getProject ( address ) : Promise < any > {
177
+ const project = await AngorProjectRepository . $getProject ( address ) ;
178
+
179
+ if ( project . length ) {
180
+ return project [ 0 ] ;
181
+ }
182
+
183
+ return undefined ;
184
+ }
185
+
93
186
/**
94
187
* Decompiles (splits into chunks) OP_RETURN script.
95
188
* @param transaction - an object representing bitcoin transaction.
96
189
* @returns - an array of strings representing script chunks.
97
190
*/
98
- private decompileOpReturnScript ( transaction = this . transaction ) : string [ ] {
191
+ private decompileOpReturnScript ( ) : string [ ] {
192
+ const { transaction } = this ;
193
+
99
194
const script : Buffer = transaction . outs [ 1 ] . script ;
100
195
101
196
// Decompiled is an array of Buffers.
@@ -140,8 +235,7 @@ export class AngorTransactionDecoder {
140
235
* Sets the founder key of the Angor project in Hex encoding.
141
236
* @returns - string representing founder key in Hex encoding
142
237
*/
143
- private getFounderKeyHex ( ) : string {
144
- const chunks = this . decompileOpReturnScript ( ) ;
238
+ private getFounderKeyHex ( chunks : string [ ] ) : string {
145
239
const founderKeyBuffer = Buffer . from ( chunks [ 0 ] , 'hex' ) ;
146
240
const founderECpair = ECPairFactory ( tinySecp256k1 ) . fromPublicKey (
147
241
founderKeyBuffer ,
@@ -159,11 +253,7 @@ export class AngorTransactionDecoder {
159
253
* @param key - founder key in Hex encoding.
160
254
* @returns - string representing founder key hash.
161
255
*/
162
- private getKeyHash ( key = this . founderKeyHex ) : string {
163
- if ( ! key ) {
164
- throw new Error ( `Key is not provided nor present.` ) ;
165
- }
166
-
256
+ private getKeyHash ( key : string ) : string {
167
257
// SHA-256 hash of the founder key.
168
258
const firstHash = bitcoinJS . crypto . sha256 ( Buffer . from ( key , 'hex' ) ) ;
169
259
// SHA-256 hash of the founder key hash.
@@ -181,11 +271,7 @@ export class AngorTransactionDecoder {
181
271
* @param hash - founder key hash in Hex encoding.
182
272
* @returns - founder key hash casted to an integer.
183
273
*/
184
- private hashToInt ( hash = this . founderKeyHash ) : number {
185
- if ( ! hash ) {
186
- throw new Error ( `Hash is not provided nor present.` ) ;
187
- }
188
-
274
+ private hashToInt ( hash : string ) : number {
189
275
const hashBuffer = Buffer . from ( hash , 'hex' ) ;
190
276
// Read an unsigned, big-endian 32-bit integer from the hash of the founder key
191
277
// using 28 as an offset. The offset is used to match the result of
@@ -199,8 +285,7 @@ export class AngorTransactionDecoder {
199
285
* Provides project id derivation.
200
286
* @returns an integer that is derived from integer representation of founder key hash.
201
287
*/
202
- private getProjectIdDerivation ( ) : number {
203
- const founderKeyHashInt = this . hashToInt ( ) ;
288
+ private getProjectIdDerivation ( founderKeyHashInt : number ) : number {
204
289
// The max size of bip32 derivation range is 2,147,483,648 (2^31) the max number of uint is 4,294,967,295 so we must to divide by 2 and round it to the floor.
205
290
const retention = Math . floor ( founderKeyHashInt / 2 ) ;
206
291
@@ -217,9 +302,7 @@ export class AngorTransactionDecoder {
217
302
* Sets Angor project id.
218
303
* @returns - string representing Angor project id.
219
304
*/
220
- private getProjectId ( ) : string {
221
- const projectIdDerivation = this . getProjectIdDerivation ( ) ;
222
-
305
+ private getProjectId ( projectIdDerivation : number ) : string {
223
306
// BIP32 (Bitcoin Improvement Proposal 32) extended public key created
224
307
// based on the angor key and the network.
225
308
const extendedPublicKey = BIP32Factory ( tinySecp256k1 ) . fromBase58 (
@@ -263,4 +346,72 @@ export class AngorTransactionDecoder {
263
346
264
347
return chunks [ 1 ] ;
265
348
}
349
+
350
+ /**
351
+ * Provides address on fee output of project creation transaction.
352
+ * @returns - string that represents address on fee output.
353
+ */
354
+ private getAddressOnFeeOutput ( ) : string {
355
+ const script : Buffer = this . transaction . outs [ 0 ] . script ;
356
+ const address = bitcoinJS . address . fromOutputScript ( script , this . network ) ;
357
+
358
+ return address ;
359
+ }
360
+
361
+ /**
362
+ * Stores Angor project into the DB.
363
+ * @param projectId - project ID.
364
+ * @param nostrPubKey - Nostr public key of the project.
365
+ * @param addressOnFeeOutput - address on fee output.
366
+ * @param transactionStatus - status of the transaction.
367
+ */
368
+ private async storeProjectInfo (
369
+ projectId : string ,
370
+ nostrPubKey : string ,
371
+ addressOnFeeOutput : string ,
372
+ transactionStatus : AngorTransactionStatus
373
+ ) : Promise < void > {
374
+ await AngorProjectRepository . $setProject (
375
+ projectId ,
376
+ nostrPubKey ,
377
+ addressOnFeeOutput ,
378
+ transactionStatus
379
+ ) ;
380
+ }
381
+
382
+ /**
383
+ * Stores Angor investment into the DB.
384
+ * @param txid - transaction ID.
385
+ * @param amount - transaction amount in sats.
386
+ * @param addressOnFeeOutput - address on fee output.
387
+ * @param transactionStatus - status of the transaction.
388
+ */
389
+ private async storeInvestmentInfo (
390
+ txid : string ,
391
+ amount : number ,
392
+ addressOnFeeOutput : string ,
393
+ transactionStatus : AngorTransactionStatus
394
+ ) : Promise < void > {
395
+ await AngorInvestmentRepository . $setInvestment (
396
+ txid ,
397
+ amount ,
398
+ addressOnFeeOutput ,
399
+ transactionStatus
400
+ ) ;
401
+ }
402
+
403
+ /**
404
+ * Updates statuses of the transactions filtered by address on fee output.
405
+ * @param addressOnFeeOutput - address on fee output.
406
+ * @param transactionStatus - transaction status.
407
+ */
408
+ private async updateInvestmentsStatus (
409
+ addressOnFeeOutput : string ,
410
+ transactionStatus : AngorTransactionStatus
411
+ ) : Promise < void > {
412
+ await AngorInvestmentRepository . $updateInvestmentsStatus (
413
+ addressOnFeeOutput ,
414
+ transactionStatus
415
+ ) ;
416
+ }
266
417
}
0 commit comments