Skip to content

Commit 8fa2077

Browse files
committed
feat: improved Angor transaction decoder
1 parent 94f3eaa commit 8fa2077

10 files changed

+705
-160
lines changed

.vscode/settings.json

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
"editor.tabSize": 2,
33
"typescript.preferences.importModuleSpecifier": "relative",
44
"typescript.tsdk": "./backend/node_modules/typescript/lib",
5-
"rust-analyzer.procMacro.ignored": { "napi-derive": ["napi"] }
6-
}
5+
"rust-analyzer.procMacro.ignored": {
6+
"napi-derive": ["napi"]
7+
},
8+
"cSpell.words": ["angor"]
9+
}

backend/.vscode/settings.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"editor.tabSize": 2,
3-
"typescript.tsdk": "../backend/node_modules/typescript/lib"
4-
}
3+
"typescript.tsdk": "../backend/node_modules/typescript/lib",
4+
"cSpell.words": ["angor"]
5+
}

backend/src/angor/AngorTransactionDecoder.ts

+195-44
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,125 @@ import * as tinySecp256k1 from 'tiny-secp256k1';
44
import BIP32Factory from 'bip32';
55
import crypto from 'crypto';
66
import { bech32 } from 'bech32';
7+
import AngorProjectRepository from '../repositories/AngorProjectRepository';
8+
import AngorInvestmentRepository from '../repositories/AngorInvestmentRepository';
79

810
/**
911
* Represents a Bitcoin network.
1012
* Supports Bitcoin and Bitcoin Testnet.
1113
*/
12-
export enum Networks {
14+
export enum AngorSupportedNetworks {
1315
Testnet = 'testnet',
1416
Bitcoin = 'bitcoin',
1517
}
1618

19+
export enum AngorTransactionStatus {
20+
NotIdentified = 'notIdentified',
21+
Pending = 'pending',
22+
Confirmed = 'confirmed',
23+
}
24+
1725
/**
1826
* Represents a transaction related to the project at Angor platform (https://angor.io).
1927
*/
2028
export class AngorTransactionDecoder {
2129
private transaction: bitcoinJS.Transaction;
22-
private founderKeyHex: string;
23-
private founderKeyHash: string;
2430
private network: bitcoinJS.Network;
2531
private angorKeys = {
26-
[Networks.Testnet]:
32+
[AngorSupportedNetworks.Testnet]:
2733
'tpubD8JfN1evVWPoJmLgVg6Usq2HEW9tLqm6CyECAADnH5tyQosrL6NuhpL9X1cQCbSmndVrgLSGGdbRqLfUbE6cRqUbrHtDJgSyQEY2Uu7WwTL',
28-
[Networks.Bitcoin]:
34+
[AngorSupportedNetworks.Bitcoin]:
2935
'xpub661MyMwAqRbcGNxKe9aFkPisf3h32gHLJm8f9XAqx8FB1Nk6KngCY8hkhGqxFr2Gyb6yfUaQVbodxLoC1f3K5HU9LM1CXE59gkEXSGCCZ1B',
3036
};
3137
private angorKey: string;
3238

33-
/**
34-
* Angor project identifier.
35-
*/
36-
public projectId: string;
37-
/**
38-
* Angor project Nostr public key.
39-
*/
40-
public nostrPubKey: string;
41-
4239
/**
4340
* Constructs Angor transaction for the project creation.
4441
* @param transactionHex - hex of the raw transaction.
4542
* @param network - bitcoin network.
4643
*/
47-
constructor(transactionHex: string, network: Networks) {
44+
constructor(transactionHex: string, network: AngorSupportedNetworks) {
4845
this.transaction = bitcoinJS.Transaction.fromHex(transactionHex);
4946

50-
this.validateTransaction();
51-
5247
this.network = bitcoinJS.networks[network];
5348
this.angorKey = this.angorKeys[network];
49+
}
5450

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+
);
62118
}
63119

64120
/**
65121
* Validates transaction object.
66-
* @param transaction - an object representing bitcoin transaction.
67122
*/
68-
private validateTransaction(transaction = this.transaction): void {
123+
private validateProjectCreationTransaction(): void {
124+
const { transaction } = this;
125+
69126
// Throw an error if transaction object is not present.
70127
if (!transaction) {
71128
throw new Error(`Transaction object wasn't created.`);
@@ -90,12 +147,50 @@ export class AngorTransactionDecoder {
90147
}
91148
}
92149

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+
93186
/**
94187
* Decompiles (splits into chunks) OP_RETURN script.
95188
* @param transaction - an object representing bitcoin transaction.
96189
* @returns - an array of strings representing script chunks.
97190
*/
98-
private decompileOpReturnScript(transaction = this.transaction): string[] {
191+
private decompileOpReturnScript(): string[] {
192+
const { transaction } = this;
193+
99194
const script: Buffer = transaction.outs[1].script;
100195

101196
// Decompiled is an array of Buffers.
@@ -140,8 +235,7 @@ export class AngorTransactionDecoder {
140235
* Sets the founder key of the Angor project in Hex encoding.
141236
* @returns - string representing founder key in Hex encoding
142237
*/
143-
private getFounderKeyHex(): string {
144-
const chunks = this.decompileOpReturnScript();
238+
private getFounderKeyHex(chunks: string[]): string {
145239
const founderKeyBuffer = Buffer.from(chunks[0], 'hex');
146240
const founderECpair = ECPairFactory(tinySecp256k1).fromPublicKey(
147241
founderKeyBuffer,
@@ -159,11 +253,7 @@ export class AngorTransactionDecoder {
159253
* @param key - founder key in Hex encoding.
160254
* @returns - string representing founder key hash.
161255
*/
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 {
167257
// SHA-256 hash of the founder key.
168258
const firstHash = bitcoinJS.crypto.sha256(Buffer.from(key, 'hex'));
169259
// SHA-256 hash of the founder key hash.
@@ -181,11 +271,7 @@ export class AngorTransactionDecoder {
181271
* @param hash - founder key hash in Hex encoding.
182272
* @returns - founder key hash casted to an integer.
183273
*/
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 {
189275
const hashBuffer = Buffer.from(hash, 'hex');
190276
// Read an unsigned, big-endian 32-bit integer from the hash of the founder key
191277
// using 28 as an offset. The offset is used to match the result of
@@ -199,8 +285,7 @@ export class AngorTransactionDecoder {
199285
* Provides project id derivation.
200286
* @returns an integer that is derived from integer representation of founder key hash.
201287
*/
202-
private getProjectIdDerivation(): number {
203-
const founderKeyHashInt = this.hashToInt();
288+
private getProjectIdDerivation(founderKeyHashInt: number): number {
204289
// 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.
205290
const retention = Math.floor(founderKeyHashInt / 2);
206291

@@ -217,9 +302,7 @@ export class AngorTransactionDecoder {
217302
* Sets Angor project id.
218303
* @returns - string representing Angor project id.
219304
*/
220-
private getProjectId(): string {
221-
const projectIdDerivation = this.getProjectIdDerivation();
222-
305+
private getProjectId(projectIdDerivation: number): string {
223306
// BIP32 (Bitcoin Improvement Proposal 32) extended public key created
224307
// based on the angor key and the network.
225308
const extendedPublicKey = BIP32Factory(tinySecp256k1).fromBase58(
@@ -263,4 +346,72 @@ export class AngorTransactionDecoder {
263346

264347
return chunks[1];
265348
}
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+
}
266417
}

0 commit comments

Comments
 (0)