mainnet-js 1.1.1 → 1.1.2

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