dxs-stas-sdk 1.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.
Files changed (49) hide show
  1. package/jest.config.js +6 -0
  2. package/package.json +38 -0
  3. package/rollup.config.ts +26 -0
  4. package/src/base.ts +4 -0
  5. package/src/bitcoin/address.ts +42 -0
  6. package/src/bitcoin/destination.ts +7 -0
  7. package/src/bitcoin/index.ts +15 -0
  8. package/src/bitcoin/mnemonic.ts +40 -0
  9. package/src/bitcoin/network.ts +11 -0
  10. package/src/bitcoin/op-codes.ts +143 -0
  11. package/src/bitcoin/out-point.ts +61 -0
  12. package/src/bitcoin/payment.ts +8 -0
  13. package/src/bitcoin/private-key.ts +57 -0
  14. package/src/bitcoin/script-type.ts +9 -0
  15. package/src/bitcoin/sig-hash-type.ts +7 -0
  16. package/src/bitcoin/token-scheme.ts +28 -0
  17. package/src/bitcoin/transaction-input.ts +33 -0
  18. package/src/bitcoin/transaction-output.ts +105 -0
  19. package/src/bitcoin/transaction.ts +31 -0
  20. package/src/bitcoin/wallet.ts +59 -0
  21. package/src/buffer/buffer-reader.ts +88 -0
  22. package/src/buffer/buffer-utils.ts +116 -0
  23. package/src/buffer/buffer-writer.ts +74 -0
  24. package/src/buffer/index.ts +3 -0
  25. package/src/hashes.ts +13 -0
  26. package/src/index.ts +8 -0
  27. package/src/script/build/null-data-builder.ts +21 -0
  28. package/src/script/build/p2pkh-builder.ts +34 -0
  29. package/src/script/build/p2stas-builder.ts +38 -0
  30. package/src/script/build/script-builder.ts +128 -0
  31. package/src/script/index.ts +10 -0
  32. package/src/script/read/script-reader.ts +79 -0
  33. package/src/script/script-samples.ts +27 -0
  34. package/src/script/script-token.ts +66 -0
  35. package/src/script/script-utils.ts +12 -0
  36. package/src/stas-bundle-factory.ts +367 -0
  37. package/src/transaction/build/input-builder.ts +270 -0
  38. package/src/transaction/build/output-builder.ts +25 -0
  39. package/src/transaction/build/transaction-builder.ts +163 -0
  40. package/src/transaction/index.ts +4 -0
  41. package/src/transaction/read/transaction-reader.ts +54 -0
  42. package/src/transaction-factory.ts +221 -0
  43. package/tests/script-build.test.ts +41 -0
  44. package/tests/script-read.test.ts +34 -0
  45. package/tests/stas-transactios.ts +41 -0
  46. package/tests/transaction-build.test.ts +394 -0
  47. package/tests/transaction-reader.test.ts +50 -0
  48. package/tsconfig.json +110 -0
  49. package/tslint.json +3 -0
@@ -0,0 +1,27 @@
1
+ import { OpCode } from "../bitcoin/op-codes";
2
+ import { ScriptReader } from "./read/script-reader";
3
+ import { ScriptToken } from "./script-token";
4
+
5
+ export const nullDataTokens = [ScriptToken.forSample(OpCode.OP_0)];
6
+
7
+ export const p2phkTokens = [
8
+ new ScriptToken(OpCode.OP_DUP),
9
+ new ScriptToken(OpCode.OP_HASH160),
10
+ ScriptToken.forSample(20, 20, true),
11
+ new ScriptToken(OpCode.OP_EQUALVERIFY),
12
+ new ScriptToken(OpCode.OP_CHECKSIG),
13
+ ];
14
+
15
+ export const p2stasSampleHex =
16
+ "76a914001122334455667788990011223344556677889988ac6976aa607f5f7f7c5e7f7c5d7f7c5c7f7c5b7f7c5a7f7c597f7c587f7c577f7c567f7c557f7c547f7c537f7c527f7c517f7c7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7c5f7f7c5e7f7c5d7f7c5c7f7c5b7f7c5a7f7c597f7c587f7c577f7c567f7c557f7c547f7c537f7c527f7c517f7c7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e011f7f7d7e01007e8111414136d08c5ed2bf3ba048afe6dcaebafe01005f80837e01007e7652967b537a7601ff877c0100879b7d648b6752799368537a7d9776547aa06394677768263044022079be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179802207c607f5f7f7c5e7f7c5d7f7c5c7f7c5b7f7c5a7f7c597f7c587f7c577f7c567f7c557f7c547f7c537f7c527f7c517f7c7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7c5f7f7c5e7f7c5d7f7c5c7f7c5b7f7c5a7f7c597f7c587f7c577f7c567f7c557f7c547f7c537f7c527f7c517f7c7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e7e01417e7c6421038ff83d8cf12121491609c4939dc11c4aa35503508fe432dc5a5c1905608b92186721023635954789a02e39fb7e54440b6f528d53efd65635ddad7f3c4085f97fdbdc4868ad547f7701207f01207f7701247f517f7801007e02fd00a063546752687f7801007e817f727e7b537f7701147f76020c057f7701147f757b876b7b557a766471567a577a786354807e7e676d68aa880067765158a569765187645294567a5379587a7e7e78637c8c7c53797e577a7e6878637c8c7c53797e577a7e6878637c8c7c53797e577a7e6878637c8c7c53797e577a7e6878637c8c7c53797e577a7e6867567a6876aa587a7d54807e577a597a5a7a786354807e6f7e7eaa727c7e676d6e7eaa7c687b7eaa587a7d877663516752687c72879b69537a6491687c7b547f77517f7853a0916901247f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e816854937f77788c6301247f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e816854937f777852946301247f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e816854937f77686877517f7c52797d8b9f7c53a09b91697c76638c7c587f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e81687f777c6876638c7c587f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e81687f777c6863587f77517f7c01007e817602fc00a06302fd00a063546752687f7c01007e81687f7768587f517f7801007e817602fc00a06302fd00a063546752687f7801007e81727e7b7b687f75537f7c0376a9148801147f775379645579887567726881687863547a677b68587f7c815379635379528763547a6b547a6b7b6b67567a6b567a6b6b7c68677b93687c547f7701207f75748c7a7669765880041976a9147858790376a9147e7e748c7a7d7e5879727e0288ac727e547a00587a64745da0637c748c7a76697d937b7b58807e59790376a9147e748c7a7e59797e7e68676c766976748c7a9d58807e6c0376a9147e748c7a7e6c7e7e68745da0637c748c7a76697d937b7b58807e59790376a9147e748c7a7e59797e7e68745da0637c748c7a76697d937b7b58807e59790376a9147e748c7a7e59797e7e687c577a9d7d7e5979635a795880041976a9145b797e0288ac7e7e6700687d7e597a766302006a7c7e827602fc00a06301fd7c7e536751687f757c7e0058807c7e687d7eaa6b7e7e7e7e7eaa78877c6c877c6c9a9b726d7777";
17
+
18
+ let stasTokens: ScriptToken[] | null = null;
19
+
20
+ export const getP2stasTokens = () => {
21
+ if (stasTokens === null) {
22
+ stasTokens = ScriptReader.read(Buffer.from(p2stasSampleHex, "hex"));
23
+ stasTokens[2].IsReceiverId = true;
24
+ }
25
+
26
+ return stasTokens;
27
+ };
@@ -0,0 +1,66 @@
1
+ import { OpCode } from "../bitcoin/op-codes";
2
+ import { isOpCode } from "./script-utils";
3
+
4
+ export class ScriptToken {
5
+ OpCodeNum: number;
6
+ OpCode?: OpCode;
7
+ Data?: Buffer;
8
+ DataLength: number = 0;
9
+ IsReceiverId: boolean = false;
10
+
11
+ constructor(opCodeNum: number, opCode?: OpCode) {
12
+ this.OpCode = opCode;
13
+ this.OpCodeNum = opCodeNum;
14
+ }
15
+
16
+ static fromBuffer(buffer: Buffer) {
17
+ const opCodeNum =
18
+ buffer.length === 0
19
+ ? -1
20
+ : buffer.length < 76
21
+ ? buffer.length
22
+ : buffer.length <= 255
23
+ ? OpCode.OP_PUSHDATA1
24
+ : buffer.length <= 65535
25
+ ? OpCode.OP_PUSHDATA2
26
+ : buffer.length <= 4294967295
27
+ ? OpCode.OP_PUSHDATA4
28
+ : -1;
29
+
30
+ if (opCodeNum === -1) throw new Error(`No data provided: ${buffer.length}`);
31
+
32
+ const token = new ScriptToken(opCodeNum);
33
+
34
+ token.Data = buffer;
35
+ token.DataLength = buffer.length;
36
+
37
+ return token;
38
+ }
39
+
40
+ static fromScriptToken(from: ScriptToken) {
41
+ const token = from.Data
42
+ ? ScriptToken.fromBuffer(from.Data)
43
+ : new ScriptToken(from.OpCodeNum, from.OpCode);
44
+
45
+ token.IsReceiverId = from.IsReceiverId;
46
+
47
+ return token;
48
+ }
49
+
50
+ static forSample(
51
+ opCodeNum: number,
52
+ dataLength: number = 0,
53
+ isReceiverId: boolean = false
54
+ ) {
55
+ const token = new ScriptToken(opCodeNum);
56
+
57
+ const { valid, opCode } = isOpCode(opCodeNum);
58
+
59
+ if (valid === false) token.OpCode = opCode;
60
+
61
+ token.DataLength = dataLength;
62
+ token.IsReceiverId = isReceiverId;
63
+
64
+ return token;
65
+ }
66
+ }
@@ -0,0 +1,12 @@
1
+ import { OpCode } from "../bitcoin/op-codes";
2
+
3
+ export const isOpCode = (opCodeNum: number) => {
4
+ if (
5
+ opCodeNum === OpCode.OP_0 ||
6
+ (opCodeNum >= OpCode.OP_PUSHDATA1 && opCodeNum <= OpCode.OP_INVALIDOPCODE)
7
+ ) {
8
+ return { valid: true, opCode: opCodeNum };
9
+ }
10
+
11
+ return { valid: false, opCode: OpCode.OP_INVALIDOPCODE };
12
+ };
@@ -0,0 +1,367 @@
1
+ import {
2
+ Address,
3
+ OutPoint,
4
+ ScriptType,
5
+ TDestination,
6
+ TokenScheme,
7
+ TPayment,
8
+ Transaction,
9
+ Wallet,
10
+ } from "./bitcoin";
11
+ import { TransactionBuilder, TransactionReader } from "./transaction";
12
+ import {
13
+ BuildMergeTx,
14
+ BuildSplitTx,
15
+ BuildTransferTx,
16
+ FeeRate,
17
+ } from "./transaction-factory";
18
+
19
+ export const AvgFeeForMerge = 500;
20
+
21
+ export type TGetUtxoFunction = (satoshis?: number) => Promise<OutPoint[]>;
22
+ export type TGetTransactionsFunction = (
23
+ ids: string[]
24
+ ) => Promise<Record<string, Transaction>>;
25
+ export type TStasPayoutBundle = {
26
+ transactions?: string[];
27
+ feeSatoshis: number;
28
+ message?: string;
29
+ };
30
+
31
+ export class StasBundleFactory {
32
+ constructor(
33
+ private readonly tokenScheme: TokenScheme,
34
+ private readonly stasWallet: Wallet,
35
+ private readonly feeWallet: Wallet,
36
+ private readonly getFeeUtxoSet: TGetUtxoFunction,
37
+ private readonly getStasUtxoSet: TGetUtxoFunction,
38
+ private readonly getTransactions: TGetTransactionsFunction
39
+ ) {}
40
+
41
+ public createBundle = async (
42
+ amount: number,
43
+ to: Address,
44
+ note?: Buffer[]
45
+ ): Promise<TStasPayoutBundle> => {
46
+ const satoshisToSend = Math.round(
47
+ amount * this.tokenScheme.SatoshisPerToken
48
+ );
49
+ const stasUtxoSet = (await this.getStasUtxoSet(satoshisToSend)).sort(
50
+ (a, b) => a.Satoshis - b.Satoshis
51
+ );
52
+ const availableSatoshis = stasUtxoSet.reduce((a, x) => a + x.Satoshis, 0);
53
+
54
+ if (availableSatoshis < satoshisToSend)
55
+ return {
56
+ message: "Insufficient STAS tokens balance",
57
+ feeSatoshis: 0,
58
+ };
59
+
60
+ const stasUtxos = this.getStasUtxo(stasUtxoSet, satoshisToSend);
61
+
62
+ let { feeSatoshis: estimatedFee } = await this._createBundle(
63
+ [],
64
+ stasUtxos,
65
+ satoshisToSend,
66
+ new OutPoint(
67
+ "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
68
+ 0,
69
+ Buffer.from(
70
+ "76a91462e907b15cbf27d5425399ebf6f0fb50ebb88f1888ac",
71
+ "hex"
72
+ ),
73
+ 5000000000,
74
+ this.feeWallet.Address,
75
+ ScriptType.p2pkh
76
+ ),
77
+ to,
78
+ note
79
+ );
80
+
81
+ estimatedFee =
82
+ estimatedFee + stasUtxos.length * 9 + 1; /* Fee for fee transactio */
83
+ const feeUtxoSet = await this.getFeeUtxoSet();
84
+ const feeSatoshis = feeUtxoSet.reduce((a, x) => a + x.Satoshis, 0);
85
+
86
+ if (estimatedFee > feeSatoshis) {
87
+ return {
88
+ message: "Insufficient balance to pay fee",
89
+ feeSatoshis: 0,
90
+ };
91
+ }
92
+
93
+ const transactions: string[] = [];
94
+ const { feeTransaction, feeUtxo } = this.buildFeeTransaction(
95
+ feeUtxoSet,
96
+ estimatedFee
97
+ );
98
+
99
+ if (feeTransaction) transactions.push(feeTransaction);
100
+
101
+ return await this._createBundle(
102
+ transactions,
103
+ stasUtxos,
104
+ satoshisToSend,
105
+ feeUtxo,
106
+ to,
107
+ note
108
+ );
109
+ };
110
+
111
+ private _createBundle = async (
112
+ transactions: string[],
113
+ stasUtxos: OutPoint[],
114
+ satoshisToSend: number,
115
+ feeUtxo: OutPoint,
116
+ to: Address,
117
+ note?: Buffer[]
118
+ ) => {
119
+ const { mergeTransactions, mergeFeeUtxo, stasUtxo } =
120
+ await this.mergeStasTransactions(stasUtxos, satoshisToSend, feeUtxo);
121
+
122
+ if (mergeTransactions) {
123
+ for (const mergeTx of mergeTransactions) {
124
+ transactions.push(mergeTx);
125
+ }
126
+ }
127
+
128
+ if (stasUtxo.Satoshis === satoshisToSend) {
129
+ transactions.push(
130
+ this.buildTransferTransaction(stasUtxo, mergeFeeUtxo, to, note)
131
+ );
132
+ } else {
133
+ transactions.push(
134
+ this.buildSplitTransaction(
135
+ stasUtxo,
136
+ satoshisToSend,
137
+ to,
138
+ mergeFeeUtxo,
139
+ note
140
+ )
141
+ );
142
+ }
143
+
144
+ const transferTx = TransactionReader.readHex(
145
+ transactions[transactions.length - 1]
146
+ );
147
+ const feeUtxoIdx = note
148
+ ? transferTx.Outputs.length - 2
149
+ : transferTx.Outputs.length - 1;
150
+ const paidFee = feeUtxo.Satoshis - transferTx.Outputs[feeUtxoIdx].Satoshis;
151
+
152
+ return { transactions, feeSatoshis: paidFee };
153
+ };
154
+
155
+ private getStasUtxo = (utxos: OutPoint[], satoshis: number): OutPoint[] => {
156
+ const exactOrGreater = utxos.find((x) => x.Satoshis >= satoshis);
157
+
158
+ if (exactOrGreater && exactOrGreater.Satoshis === satoshis) {
159
+ return [exactOrGreater];
160
+ }
161
+
162
+ const result: OutPoint[] = [];
163
+ let accumulated = 0;
164
+ for (const utxo of utxos) {
165
+ result.push(utxo);
166
+ accumulated += utxo.Satoshis;
167
+
168
+ if (accumulated >= satoshis) return result;
169
+ }
170
+
171
+ return [exactOrGreater!];
172
+ };
173
+
174
+ private buildFeeTransaction = (
175
+ utxos: OutPoint[],
176
+ satoshis: number
177
+ ): {
178
+ feeTransaction?: string;
179
+ feeUtxo: OutPoint;
180
+ } => {
181
+ if (utxos.length === 1)
182
+ return {
183
+ feeUtxo: utxos[0],
184
+ };
185
+
186
+ const txBuilder = TransactionBuilder.init().addP2PkhOutput(
187
+ 0,
188
+ this.feeWallet.Address
189
+ );
190
+ let accumulated = 0;
191
+
192
+ for (const utxo of utxos) {
193
+ txBuilder.addInput(utxo, this.feeWallet);
194
+
195
+ const fee = txBuilder.getFee(FeeRate);
196
+
197
+ accumulated += utxo.Satoshis;
198
+
199
+ if (accumulated - fee >= satoshis) break;
200
+ }
201
+
202
+ txBuilder.Outputs = [];
203
+
204
+ const result = txBuilder
205
+ .addChangeOutputWithFee(this.feeWallet.Address, accumulated, FeeRate)
206
+ .sign()
207
+ .toHex();
208
+
209
+ return { feeTransaction: result, feeUtxo: OutPoint.fromHex(result, 0) };
210
+ };
211
+
212
+ private mergeStasTransactions = async (
213
+ stasUtxos: OutPoint[],
214
+ satoshis: number,
215
+ mergeFeeUtxo: OutPoint
216
+ ): Promise<{
217
+ mergeTransactions?: string[];
218
+ stasUtxo: OutPoint;
219
+ mergeFeeUtxo: OutPoint;
220
+ }> => {
221
+ if (stasUtxos.length === 1) return { mergeFeeUtxo, stasUtxo: stasUtxos[0] };
222
+
223
+ const mergeTransactions: string[] = [];
224
+ const utxos = stasUtxos.map(({ TxId, Vout }) => ({ TxId, Vout }));
225
+ const txIds = Array.from(new Set(stasUtxos.map(({ TxId }) => TxId)));
226
+ const sourceTransactions = await this.getTransactions(txIds);
227
+ const mergeLevels: OutPoint[][] = [[]];
228
+
229
+ for (const { TxId, Vout } of utxos) {
230
+ const tx = sourceTransactions[TxId];
231
+
232
+ if (!tx) throw new Error(`Transaction ${TxId} not found`);
233
+
234
+ mergeLevels[0].push(OutPoint.fromTransaction(tx, Vout));
235
+ }
236
+
237
+ let feePayment: TPayment = {
238
+ OutPoint: mergeFeeUtxo,
239
+ Owner: this.feeWallet,
240
+ };
241
+ let currentLevel = mergeLevels[0];
242
+ let levelsBeforeTransfer = 0;
243
+ let stasUtxo = stasUtxos[0];
244
+
245
+ while (currentLevel.length !== 1) {
246
+ const newLevel: OutPoint[] = [];
247
+ mergeLevels.push(newLevel);
248
+
249
+ if (levelsBeforeTransfer === 3) {
250
+ levelsBeforeTransfer = 0;
251
+
252
+ for (let outPoint of currentLevel) {
253
+ const stasPayment: TPayment = {
254
+ OutPoint: outPoint,
255
+ Owner: this.stasWallet,
256
+ };
257
+ const txRaw = BuildTransferTx({
258
+ tokenScheme: this.tokenScheme,
259
+ stasPayment,
260
+ feePayment,
261
+ to: this.stasWallet.Address,
262
+ });
263
+ const tx = TransactionReader.readHex(txRaw);
264
+
265
+ newLevel.push(OutPoint.fromTransaction(tx, 0));
266
+ mergeTransactions.push(txRaw);
267
+
268
+ stasUtxo = OutPoint.fromTransaction(tx, 0);
269
+ feePayment.OutPoint = OutPoint.fromTransaction(tx, 1);
270
+ }
271
+ } else {
272
+ levelsBeforeTransfer++;
273
+
274
+ const mergeCounts = Math.floor(currentLevel.length / 2);
275
+ const remainder = currentLevel.length % 2;
276
+
277
+ if (remainder !== 0)
278
+ newLevel.push(currentLevel[currentLevel.length - 1]);
279
+
280
+ let currentIdx = 0;
281
+
282
+ for (var i = 0; i < mergeCounts; i++) {
283
+ const outPoint1 = currentLevel[currentIdx++];
284
+ const outPoint2 = currentLevel[currentIdx++];
285
+ const lastMerge = mergeCounts === 1 && remainder === 0;
286
+ const inputSatoshis = outPoint1.Satoshis + outPoint2.Satoshis;
287
+
288
+ let destination: TDestination = {
289
+ Address: this.stasWallet.Address,
290
+ Satoshis: inputSatoshis,
291
+ };
292
+ let splitDestination: TDestination | undefined = undefined;
293
+
294
+ if (lastMerge && inputSatoshis !== satoshis) {
295
+ destination = {
296
+ Address: this.stasWallet.Address,
297
+ Satoshis: satoshis,
298
+ };
299
+ splitDestination = {
300
+ Address: this.stasWallet.Address,
301
+ Satoshis: inputSatoshis - satoshis,
302
+ };
303
+ }
304
+
305
+ const txRaw = BuildMergeTx({
306
+ tokenScheme: this.tokenScheme,
307
+ outPoint1,
308
+ outPoint2,
309
+ owner: this.stasWallet,
310
+ feePayment,
311
+ destination,
312
+ splitDestination,
313
+ });
314
+ const tx = TransactionReader.readHex(txRaw);
315
+
316
+ newLevel.push(OutPoint.fromTransaction(tx, 0));
317
+ mergeTransactions.push(txRaw);
318
+
319
+ stasUtxo = OutPoint.fromTransaction(tx, 0);
320
+ feePayment.OutPoint = OutPoint.fromTransaction(
321
+ tx,
322
+ tx.Outputs.length - 1
323
+ );
324
+ }
325
+ }
326
+
327
+ currentLevel = newLevel;
328
+ }
329
+
330
+ return { mergeTransactions, mergeFeeUtxo: feePayment.OutPoint, stasUtxo };
331
+ };
332
+
333
+ private buildTransferTransaction = (
334
+ stasUtxo: OutPoint,
335
+ feeUtxo: OutPoint,
336
+ to: Address,
337
+ note?: Buffer[]
338
+ ): string =>
339
+ BuildTransferTx({
340
+ tokenScheme: this.tokenScheme,
341
+ stasPayment: { OutPoint: stasUtxo, Owner: this.stasWallet },
342
+ feePayment: { OutPoint: feeUtxo, Owner: this.feeWallet },
343
+ to,
344
+ note,
345
+ });
346
+
347
+ private buildSplitTransaction = (
348
+ stasUtxo: OutPoint,
349
+ satoshis: number,
350
+ to: Address,
351
+ feeUtxo: OutPoint,
352
+ note?: Buffer[]
353
+ ): string =>
354
+ BuildSplitTx({
355
+ tokenScheme: this.tokenScheme,
356
+ stasPayment: { OutPoint: stasUtxo, Owner: this.stasWallet },
357
+ feePayment: { OutPoint: feeUtxo, Owner: this.feeWallet },
358
+ destinations: [
359
+ { Satoshis: satoshis, Address: to },
360
+ {
361
+ Satoshis: stasUtxo.Satoshis - satoshis,
362
+ Address: this.stasWallet.Address,
363
+ },
364
+ ],
365
+ note,
366
+ });
367
+ }