mainnet-js 1.1.1 → 1.1.3

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.
@@ -12,6 +12,8 @@ import {
12
12
  CompilationContextBCH,
13
13
  Output,
14
14
  hexToBin,
15
+ verifyTransactionTokens,
16
+ stringify,
15
17
  } from "@bitauth/libauth";
16
18
  import { NFTCapability, TokenI, UtxoI } from "../interface.js";
17
19
  import { allocateFee } from "./allocateFee.js";
@@ -29,16 +31,27 @@ import { sumUtxoValue } from "../util/sumUtxoValue.js";
29
31
  import { FeePaidByEnum } from "../wallet/enum.js";
30
32
 
31
33
  // Build a transaction for a p2pkh transaction for a non HD wallet
32
- export async function buildP2pkhNonHdTransaction(
33
- inputs: UtxoI[],
34
- outputs: Array<SendRequest | TokenSendRequest | OpReturnData>,
35
- signingKey: Uint8Array,
36
- fee: number = 0,
34
+ export async function buildP2pkhNonHdTransaction({
35
+ inputs,
36
+ outputs,
37
+ signingKey,
38
+ sourceAddress,
39
+ fee = 0,
37
40
  discardChange = false,
38
- slpOutputs: Output[] = [],
39
- feePaidBy: FeePaidByEnum = FeePaidByEnum.change,
40
- changeAddress: string = ""
41
- ) {
41
+ slpOutputs = [],
42
+ feePaidBy = FeePaidByEnum.change,
43
+ changeAddress = "",
44
+ }: {
45
+ inputs: UtxoI[];
46
+ outputs: Array<SendRequest | TokenSendRequest | OpReturnData>;
47
+ signingKey: Uint8Array;
48
+ sourceAddress: string;
49
+ fee?: number;
50
+ discardChange?: boolean;
51
+ slpOutputs?: Output[];
52
+ feePaidBy?: FeePaidByEnum;
53
+ changeAddress?: string;
54
+ }) {
42
55
  if (!signingKey) {
43
56
  throw new Error("Missing signing key when building transaction");
44
57
  }
@@ -59,7 +72,7 @@ export async function buildP2pkhNonHdTransaction(
59
72
 
60
73
  outputs = allocateFee(outputs, fee, feePaidBy, changeAmount);
61
74
 
62
- let lockedOutputs = await prepareOutputs(outputs, inputs);
75
+ const lockedOutputs = await prepareOutputs(outputs);
63
76
 
64
77
  if (discardChange !== true) {
65
78
  if (changeAmount > DUST_UTXO_THRESHOLD) {
@@ -85,31 +98,56 @@ export async function buildP2pkhNonHdTransaction(
85
98
  }
86
99
  }
87
100
 
88
- let signedInputs = prepareInputs(inputs, compiler, signingKey);
101
+ const { preparedInputs, sourceOutputs } = prepareInputs({
102
+ inputs,
103
+ compiler,
104
+ signingKey,
105
+ sourceAddress,
106
+ });
89
107
  const result = generateTransaction({
90
- inputs: signedInputs,
108
+ inputs: preparedInputs,
91
109
  locktime: 0,
92
110
  outputs: [...slpOutputs, ...lockedOutputs],
93
111
  version: 2,
94
112
  });
95
- return result;
113
+
114
+ if (!result.success) {
115
+ throw Error("Error building transaction with fee");
116
+ }
117
+
118
+ const tokenValidationResult = verifyTransactionTokens(
119
+ result.transaction,
120
+ sourceOutputs
121
+ );
122
+ if (tokenValidationResult !== true && fee > 0) {
123
+ throw tokenValidationResult;
124
+ }
125
+
126
+ return { transaction: result.transaction, sourceOutputs: sourceOutputs };
96
127
  }
97
128
 
98
- export function prepareInputs(
99
- inputs: UtxoI[],
129
+ export function prepareInputs({
130
+ inputs,
131
+ compiler,
132
+ signingKey,
133
+ sourceAddress,
134
+ }: {
135
+ inputs: UtxoI[];
100
136
  compiler: Compiler<
101
137
  CompilationContextBCH,
102
138
  AnyCompilerConfiguration<CompilationContextBCH>,
103
139
  AuthenticationProgramStateCommon
104
- >,
105
- signingKey: Uint8Array
106
- ) {
107
- let signedInputs: any[] = [];
140
+ >;
141
+ signingKey: Uint8Array;
142
+ sourceAddress: string;
143
+ }) {
144
+ const preparedInputs: any[] = [];
145
+ const sourceOutputs: any[] = [];
108
146
  for (const i of inputs) {
109
147
  const utxoTxnValue = i.satoshis;
110
148
  const utxoIndex = i.vout;
111
149
  // slice will create a clone of the array
112
- let utxoOutpointTransactionHash = new Uint8Array(
150
+ const utxoOutpointTransactionHash = new Uint8Array(
113
151
  i.txid.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16))
114
152
  );
115
153
  // reverse the cloned copy
@@ -122,30 +160,57 @@ export function prepareInputs(
122
160
  amount: BigInt(i.token.amount),
123
161
  category: hexToBin(i.token.tokenId),
124
162
  nft:
125
- i.token.capability || i.token.commitment
163
+ i.token.capability !== undefined || i.token.commitment !== undefined
126
164
  ? {
127
165
  capability: i.token.capability,
128
- commitment: i.token.commitment && hexToBin(i.token.commitment!),
166
+ commitment:
167
+ i.token.commitment !== undefined &&
168
+ hexToBin(i.token.commitment!),
129
169
  }
130
170
  : undefined,
131
171
  };
132
- let newInput = {
172
+ const newInput = signingKey?.length
173
+ ? {
174
+ outpointIndex: utxoIndex,
175
+ outpointTransactionHash: utxoOutpointTransactionHash,
176
+ sequenceNumber: 0,
177
+ unlockingBytecode: {
178
+ compiler,
179
+ data: {
180
+ keys: { privateKeys: { key: signingKey } },
181
+ },
182
+ valueSatoshis: BigInt(utxoTxnValue),
183
+ script: "unlock",
184
+ token: libAuthToken,
185
+ },
186
+ }
187
+ : {
188
+ outpointIndex: utxoIndex,
189
+ outpointTransactionHash: utxoOutpointTransactionHash,
190
+ sequenceNumber: 0,
191
+ unlockingBytecode: Uint8Array.from([]),
192
+ };
193
+
194
+ preparedInputs.push(newInput);
195
+
196
+ const lockingBytecode = cashAddressToLockingBytecode(sourceAddress);
197
+ if (typeof lockingBytecode === "string") {
198
+ throw lockingBytecode;
199
+ }
200
+
201
+ sourceOutputs.push({
133
202
  outpointIndex: utxoIndex,
134
203
  outpointTransactionHash: utxoOutpointTransactionHash,
135
204
  sequenceNumber: 0,
136
- unlockingBytecode: {
137
- compiler,
138
- data: {
139
- keys: { privateKeys: { key: signingKey } },
140
- },
141
- valueSatoshis: BigInt(utxoTxnValue),
142
- script: "unlock",
143
- token: libAuthToken,
144
- },
145
- };
146
- signedInputs.push(newInput);
205
+ unlockingBytecode: Uint8Array.from([]),
206
+
207
+ // additional info for sourceOutputs
208
+ lockingBytecode: lockingBytecode.bytecode,
209
+ valueSatoshis: BigInt(utxoTxnValue),
210
+ token: libAuthToken,
211
+ });
147
212
  }
148
- return signedInputs;
213
+ return { preparedInputs, sourceOutputs };
149
214
  }
150
215
 
151
216
  /**
@@ -156,13 +221,12 @@ export function prepareInputs(
156
221
  * @returns A promise to a list of unspent outputs
157
222
  */
158
223
  export async function prepareOutputs(
159
- outputs: Array<SendRequest | TokenSendRequest | OpReturnData>,
160
- inputs: UtxoI[]
224
+ outputs: Array<SendRequest | TokenSendRequest | OpReturnData>
161
225
  ) {
162
- let lockedOutputs: Output[] = [];
226
+ const lockedOutputs: Output[] = [];
163
227
  for (const output of outputs) {
164
228
  if (output instanceof TokenSendRequest) {
165
- lockedOutputs.push(prepareTokenOutputs(output, inputs));
229
+ lockedOutputs.push(prepareTokenOutputs(output));
166
230
  continue;
167
231
  }
168
232
 
@@ -207,49 +271,9 @@ export function prepareOpReturnOutput(request: OpReturnData): Output {
207
271
  *
208
272
  * @returns A libauth Output
209
273
  */
210
- export function prepareTokenOutputs(
211
- request: TokenSendRequest,
212
- inputs: UtxoI[]
213
- ): Output {
274
+ export function prepareTokenOutputs(request: TokenSendRequest): Output {
214
275
  const token: TokenI = request;
215
- const isGenesis = !request.tokenId || (request as any)._isGenesis;
216
- let satValue = 0;
217
- if (isGenesis) {
218
- const genesisInputs = inputs.filter((val) => val.vout === 0);
219
- if (genesisInputs.length === 0) {
220
- throw new Error(
221
- "No suitable inputs with vout=0 available for new token genesis"
222
- );
223
- }
224
- token.tokenId = genesisInputs[0].txid;
225
- satValue = request.value || 1000;
226
- (request as any)._isGenesis = true;
227
- } else {
228
- const tokenInputs = inputs.filter(
229
- (val) =>
230
- val.token?.tokenId &&
231
- val.token?.tokenId === request.tokenId &&
232
- (val.token?.capability === "none"
233
- ? val.token.capability === request.capability &&
234
- val.token?.commitment === request.commitment
235
- : true)
236
- );
237
- if (!tokenInputs.length) {
238
- throw new Error(
239
- `No suitable token utxos available to send token with id "${request.tokenId}", capability "${request.capability}", commitment "${request.commitment}"`
240
- );
241
- }
242
- if (!token.capability && tokenInputs[0].token?.capability) {
243
- token.capability = tokenInputs[0].token!.capability;
244
- }
245
- if (!token.commitment && tokenInputs[0].token?.commitment) {
246
- token.commitment = tokenInputs[0].token!.commitment;
247
- }
248
-
249
- satValue = request.value || tokenInputs[0].satoshis;
250
- }
251
-
252
- let outputLockingBytecode = cashAddressToLockingBytecode(request.cashaddr);
276
+ const outputLockingBytecode = cashAddressToLockingBytecode(request.cashaddr);
253
277
  if (typeof outputLockingBytecode === "string")
254
278
  throw new Error(outputLockingBytecode);
255
279
 
@@ -257,17 +281,18 @@ export function prepareTokenOutputs(
257
281
  amount: BigInt(token.amount),
258
282
  category: hexToBin(token.tokenId),
259
283
  nft:
260
- token.capability || token.commitment
284
+ token.capability !== undefined || token.commitment !== undefined
261
285
  ? {
262
286
  capability: token.capability,
263
- commitment: token.commitment && hexToBin(token.commitment!),
287
+ commitment:
288
+ token.commitment !== undefined && hexToBin(token.commitment!),
264
289
  }
265
290
  : undefined,
266
291
  };
267
292
 
268
293
  return {
269
294
  lockingBytecode: outputLockingBytecode.bytecode,
270
- valueSatoshis: BigInt(satValue),
295
+ valueSatoshis: BigInt(request.value || 1000),
271
296
  token: libAuthToken,
272
297
  } as Output;
273
298
  }
@@ -287,61 +312,72 @@ export async function getSuitableUtxos(
287
312
  bestHeight: number,
288
313
  feePaidBy: FeePaidByEnum,
289
314
  requests: SendRequestType[],
290
- ensureUtxos: UtxoI[] = []
315
+ ensureUtxos: UtxoI[] = [],
316
+ tokenOperation: "send" | "genesis" | "mint" | "burn" = "send"
291
317
  ): Promise<UtxoI[]> {
292
- let suitableUtxos: UtxoI[] = [];
318
+ const suitableUtxos: UtxoI[] = [...ensureUtxos];
293
319
  let amountAvailable = BigInt(0);
294
- const tokenAmountsRequired: any[] = [];
295
320
  const tokenRequests = requests.filter(
296
321
  (val) => val instanceof TokenSendRequest
297
322
  ) as TokenSendRequest[];
298
323
 
299
- // if we do a new token genesis, we shall filter all token utxos out
300
- const isTokenGenesis = tokenRequests.some(
301
- (val) => !val.tokenId || (val as any)._isGenesis
302
- );
303
- const bchOnlyTransfer = tokenRequests.length === 0;
304
- let filteredInputs =
305
- isTokenGenesis || bchOnlyTransfer
306
- ? inputs.slice(0).filter((val) => !val.token)
307
- : inputs.slice();
308
- const tokenIds = tokenRequests
309
- .map((val) => val.tokenId)
310
- .filter((value, index, array) => array.indexOf(value) === index);
311
- for (let tokenId of tokenIds) {
312
- const requiredAmount = tokenRequests
313
- .map((val) => val.amount)
314
- .reduce((prev, cur) => prev + cur, 0);
315
- tokenAmountsRequired.push({ tokenId, requiredAmount });
316
- }
324
+ const availableInputs = inputs.slice();
317
325
 
318
- // find suitable token inputs first
319
- for (const { tokenId, requiredAmount } of tokenAmountsRequired) {
320
- let tokenAmountAvailable = 0;
321
- for (const input of inputs) {
322
- if (input.token?.tokenId === tokenId) {
323
- suitableUtxos.push(input);
324
- const inputIndex = filteredInputs.indexOf(input);
325
- filteredInputs = filteredInputs.filter(
326
- (_, index) => inputIndex !== index
327
- );
328
- tokenAmountAvailable += input.token!.amount;
329
- amountAvailable += BigInt(input.satoshis);
330
- if (tokenAmountAvailable >= requiredAmount) {
331
- break;
326
+ // find matching utxos for token transfers
327
+ if (tokenOperation === "send") {
328
+ for (const request of tokenRequests) {
329
+ const tokenInputs = availableInputs.filter(
330
+ (val) => val.token?.tokenId === request.tokenId
331
+ );
332
+ const sameCommitmentTokens = [...suitableUtxos, ...tokenInputs].filter(
333
+ (val) =>
334
+ val.token?.capability === request.capability &&
335
+ val.token?.commitment === request.commitment
336
+ );
337
+ if (sameCommitmentTokens.length) {
338
+ const input = sameCommitmentTokens[0];
339
+ const index = availableInputs.indexOf(input);
340
+ if (index !== -1) {
341
+ suitableUtxos.push(input);
342
+ availableInputs.splice(index, 1);
343
+ amountAvailable += BigInt(input.satoshis);
332
344
  }
345
+
346
+ continue;
333
347
  }
348
+
349
+ if (
350
+ request.capability === NFTCapability.minting ||
351
+ request.capability === NFTCapability.mutable
352
+ ) {
353
+ const changeCommitmentTokens = [
354
+ ...suitableUtxos,
355
+ ...tokenInputs,
356
+ ].filter((val) => val.token?.capability === request.capability);
357
+ if (changeCommitmentTokens.length) {
358
+ const input = changeCommitmentTokens[0];
359
+ const index = availableInputs.indexOf(input);
360
+ if (index !== -1) {
361
+ suitableUtxos.push(input);
362
+ availableInputs.splice(index, 1);
363
+ amountAvailable += BigInt(input.satoshis);
364
+ }
365
+ continue;
366
+ }
367
+ }
368
+ throw Error(
369
+ `No suitable token utxos available to send token with id "${request.tokenId}", capability "${request.capability}", commitment "${request.commitment}"`
370
+ );
334
371
  }
335
372
  }
336
-
337
373
  // find plain bch outputs
338
- for (const u of filteredInputs) {
374
+ for (const u of availableInputs) {
339
375
  if (u.token) {
340
376
  continue;
341
377
  }
342
378
 
343
379
  if (u.coinbase && u.height && bestHeight) {
344
- let age = bestHeight - u.height;
380
+ const age = bestHeight - u.height;
345
381
  if (age > 100) {
346
382
  suitableUtxos.push(u);
347
383
  amountAvailable += BigInt(u.satoshis);
@@ -357,8 +393,11 @@ export async function getSuitableUtxos(
357
393
  }
358
394
 
359
395
  const addEnsured = (suitableUtxos) => {
360
- return [...suitableUtxos, ...ensureUtxos].filter(
361
- (val, index, array) => array.indexOf(val) === index
396
+ return [...ensureUtxos, ...suitableUtxos].filter(
397
+ (val, index, array) =>
398
+ array.findIndex(
399
+ (other) => other.txid === val.txid && other.vout === val.vout
400
+ ) === index
362
401
  );
363
402
  };
364
403
 
@@ -371,7 +410,7 @@ export async function getSuitableUtxos(
371
410
  if (typeof amountRequired === "undefined") {
372
411
  return addEnsured(suitableUtxos);
373
412
  } else if (amountAvailable < amountRequired) {
374
- let e = Error(
413
+ const e = Error(
375
414
  `Amount required was not met, ${amountRequired} satoshis needed, ${amountAvailable} satoshis available`
376
415
  );
377
416
  e["data"] = {
@@ -388,6 +427,7 @@ export async function getFeeAmount({
388
427
  utxos,
389
428
  sendRequests,
390
429
  privateKey,
430
+ sourceAddress,
391
431
  relayFeePerByteInSatoshi,
392
432
  slpOutputs,
393
433
  feePaidBy,
@@ -396,6 +436,7 @@ export async function getFeeAmount({
396
436
  utxos: UtxoI[];
397
437
  sendRequests: Array<SendRequest | TokenSendRequest | OpReturnData>;
398
438
  privateKey: Uint8Array;
439
+ sourceAddress: string;
399
440
  relayFeePerByteInSatoshi: number;
400
441
  slpOutputs: Output[];
401
442
  feePaidBy: FeePaidByEnum;
@@ -404,15 +445,18 @@ export async function getFeeAmount({
404
445
  // build transaction
405
446
  if (utxos) {
406
447
  // Build the transaction to get the approximate size
407
- let draftTransaction = await buildEncodedTransaction(
408
- utxos,
409
- sendRequests,
410
- privateKey,
411
- 0, //DUST_UTXO_THRESHOLD
412
- discardChange ?? false,
413
- slpOutputs,
414
- feePaidBy
415
- );
448
+ const { encodedTransaction: draftTransaction } =
449
+ await buildEncodedTransaction({
450
+ inputs: utxos,
451
+ outputs: sendRequests,
452
+ signingKey: privateKey,
453
+ sourceAddress,
454
+ fee: 0, //DUST_UTXO_THRESHOLD
455
+ discardChange: discardChange ?? false,
456
+ slpOutputs,
457
+ feePaidBy,
458
+ changeAddress: "",
459
+ });
416
460
 
417
461
  return draftTransaction.length * relayFeePerByteInSatoshi + 1;
418
462
  } else {
@@ -423,30 +467,38 @@ export async function getFeeAmount({
423
467
  }
424
468
 
425
469
  // Build encoded transaction
426
- export async function buildEncodedTransaction(
427
- fundingUtxos: UtxoI[],
428
- sendRequests: Array<SendRequest | TokenSendRequest | OpReturnData>,
429
- privateKey: Uint8Array,
430
- fee: number = 0,
470
+ export async function buildEncodedTransaction({
471
+ inputs,
472
+ outputs,
473
+ signingKey,
474
+ sourceAddress,
475
+ fee = 0,
431
476
  discardChange = false,
432
- slpOutputs: Output[] = [],
433
- feePaidBy: FeePaidByEnum = FeePaidByEnum.change,
434
- changeAddress: string = ""
435
- ) {
436
- let txn = await buildP2pkhNonHdTransaction(
437
- fundingUtxos,
438
- sendRequests,
439
- privateKey,
477
+ slpOutputs = [],
478
+ feePaidBy = FeePaidByEnum.change,
479
+ changeAddress = "",
480
+ }: {
481
+ inputs: UtxoI[];
482
+ outputs: Array<SendRequest | TokenSendRequest | OpReturnData>;
483
+ signingKey: Uint8Array;
484
+ sourceAddress: string;
485
+ fee?: number;
486
+ discardChange?: boolean;
487
+ slpOutputs?: Output[];
488
+ feePaidBy?: FeePaidByEnum;
489
+ changeAddress?: string;
490
+ }) {
491
+ const { transaction, sourceOutputs } = await buildP2pkhNonHdTransaction({
492
+ inputs,
493
+ outputs,
494
+ signingKey,
495
+ sourceAddress,
440
496
  fee,
441
497
  discardChange,
442
498
  slpOutputs,
443
499
  feePaidBy,
444
- changeAddress
445
- );
446
- // submit transaction
447
- if (txn.success) {
448
- return encodeTransaction(txn.transaction);
449
- } else {
450
- throw Error("Error building transaction with fee");
451
- }
500
+ changeAddress,
501
+ });
502
+
503
+ return { encodedTransaction: encodeTransaction(transaction), sourceOutputs };
452
504
  }