mainnet-js 1.1.0 → 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,48 +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) => val.token?.tokenId === request.tokenId
230
- );
231
- if (!tokenInputs.length) {
232
- throw new Error(`No token utxos available to send ${request.tokenId}`);
233
- }
234
- if (!token.capability && tokenInputs[0].token?.capability) {
235
- token.capability = tokenInputs[0].token!.capability;
236
- }
237
- if (!token.commitment && tokenInputs[0].token?.commitment) {
238
- token.commitment = tokenInputs[0].token!.commitment;
239
- }
240
-
241
- if (
242
- tokenInputs[0].token?.capability === NFTCapability.none &&
243
- tokenInputs[0].token?.commitment !== token.commitment
244
- ) {
245
- throw new Error("Can not change the commitment of an immutable token");
246
- }
247
-
248
- satValue = request.value || tokenInputs[0].satoshis;
249
- }
250
-
251
- let outputLockingBytecode = cashAddressToLockingBytecode(request.cashaddr);
269
+ const outputLockingBytecode = cashAddressToLockingBytecode(request.cashaddr);
252
270
  if (typeof outputLockingBytecode === "string")
253
271
  throw new Error(outputLockingBytecode);
254
272
 
@@ -256,17 +274,18 @@ export function prepareTokenOutputs(
256
274
  amount: BigInt(token.amount),
257
275
  category: hexToBin(token.tokenId),
258
276
  nft:
259
- token.capability || token.commitment
277
+ token.capability !== undefined || token.commitment !== undefined
260
278
  ? {
261
279
  capability: token.capability,
262
- commitment: token.commitment && hexToBin(token.commitment!),
280
+ commitment:
281
+ token.commitment !== undefined && hexToBin(token.commitment!),
263
282
  }
264
283
  : undefined,
265
284
  };
266
285
 
267
286
  return {
268
287
  lockingBytecode: outputLockingBytecode.bytecode,
269
- valueSatoshis: BigInt(satValue),
288
+ valueSatoshis: BigInt(request.value || 1000),
270
289
  token: libAuthToken,
271
290
  } as Output;
272
291
  }
@@ -286,61 +305,72 @@ export async function getSuitableUtxos(
286
305
  bestHeight: number,
287
306
  feePaidBy: FeePaidByEnum,
288
307
  requests: SendRequestType[],
289
- ensureUtxos: UtxoI[] = []
308
+ ensureUtxos: UtxoI[] = [],
309
+ tokenOperation: "send" | "genesis" | "mint" | "burn" = "send"
290
310
  ): Promise<UtxoI[]> {
291
- let suitableUtxos: UtxoI[] = [];
311
+ const suitableUtxos: UtxoI[] = [...ensureUtxos];
292
312
  let amountAvailable = BigInt(0);
293
- const tokenAmountsRequired: any[] = [];
294
313
  const tokenRequests = requests.filter(
295
314
  (val) => val instanceof TokenSendRequest
296
315
  ) as TokenSendRequest[];
297
316
 
298
- // if we do a new token genesis, we shall filter all token utxos out
299
- const isTokenGenesis = tokenRequests.some(
300
- (val) => !val.tokenId || (val as any)._isGenesis
301
- );
302
- const bchOnlyTransfer = tokenRequests.length === 0;
303
- let filteredInputs =
304
- isTokenGenesis || bchOnlyTransfer
305
- ? inputs.slice(0).filter((val) => !val.token)
306
- : inputs.slice();
307
- const tokenIds = tokenRequests
308
- .map((val) => val.tokenId)
309
- .filter((value, index, array) => array.indexOf(value) === index);
310
- for (let tokenId of tokenIds) {
311
- const requiredAmount = tokenRequests
312
- .map((val) => val.amount)
313
- .reduce((prev, cur) => prev + cur, 0);
314
- tokenAmountsRequired.push({ tokenId, requiredAmount });
315
- }
317
+ const availableInputs = inputs.slice();
318
+
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);
337
+ }
316
338
 
317
- // find suitable token inputs first
318
- for (const { tokenId, requiredAmount } of tokenAmountsRequired) {
319
- let tokenAmountAvailable = 0;
320
- for (const input of inputs) {
321
- if (input.token?.tokenId === tokenId) {
322
- suitableUtxos.push(input);
323
- const inputIndex = filteredInputs.indexOf(input);
324
- filteredInputs = filteredInputs.filter(
325
- (_, index) => inputIndex !== index
326
- );
327
- tokenAmountAvailable += input.token!.amount;
328
- amountAvailable += BigInt(input.satoshis);
329
- if (tokenAmountAvailable >= requiredAmount) {
330
- break;
339
+ continue;
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;
331
359
  }
332
360
  }
361
+ throw Error(
362
+ `No suitable token utxos available to send token with id "${request.tokenId}", capability "${request.capability}", commitment "${request.commitment}"`
363
+ );
333
364
  }
334
365
  }
335
-
336
366
  // find plain bch outputs
337
- for (const u of filteredInputs) {
367
+ for (const u of availableInputs) {
338
368
  if (u.token) {
339
369
  continue;
340
370
  }
341
371
 
342
372
  if (u.coinbase && u.height && bestHeight) {
343
- let age = bestHeight - u.height;
373
+ const age = bestHeight - u.height;
344
374
  if (age > 100) {
345
375
  suitableUtxos.push(u);
346
376
  amountAvailable += BigInt(u.satoshis);
@@ -356,8 +386,11 @@ export async function getSuitableUtxos(
356
386
  }
357
387
 
358
388
  const addEnsured = (suitableUtxos) => {
359
- return [...suitableUtxos, ...ensureUtxos].filter(
360
- (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
361
394
  );
362
395
  };
363
396
 
@@ -370,7 +403,7 @@ export async function getSuitableUtxos(
370
403
  if (typeof amountRequired === "undefined") {
371
404
  return addEnsured(suitableUtxos);
372
405
  } else if (amountAvailable < amountRequired) {
373
- let e = Error(
406
+ const e = Error(
374
407
  `Amount required was not met, ${amountRequired} satoshis needed, ${amountAvailable} satoshis available`
375
408
  );
376
409
  e["data"] = {
@@ -387,6 +420,7 @@ export async function getFeeAmount({
387
420
  utxos,
388
421
  sendRequests,
389
422
  privateKey,
423
+ sourceAddress,
390
424
  relayFeePerByteInSatoshi,
391
425
  slpOutputs,
392
426
  feePaidBy,
@@ -395,6 +429,7 @@ export async function getFeeAmount({
395
429
  utxos: UtxoI[];
396
430
  sendRequests: Array<SendRequest | TokenSendRequest | OpReturnData>;
397
431
  privateKey: Uint8Array;
432
+ sourceAddress: string;
398
433
  relayFeePerByteInSatoshi: number;
399
434
  slpOutputs: Output[];
400
435
  feePaidBy: FeePaidByEnum;
@@ -403,15 +438,18 @@ export async function getFeeAmount({
403
438
  // build transaction
404
439
  if (utxos) {
405
440
  // Build the transaction to get the approximate size
406
- let draftTransaction = await buildEncodedTransaction(
407
- utxos,
408
- sendRequests,
409
- privateKey,
410
- 0, //DUST_UTXO_THRESHOLD
411
- discardChange ?? false,
412
- slpOutputs,
413
- feePaidBy
414
- );
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
+ });
415
453
 
416
454
  return draftTransaction.length * relayFeePerByteInSatoshi + 1;
417
455
  } else {
@@ -422,30 +460,38 @@ export async function getFeeAmount({
422
460
  }
423
461
 
424
462
  // Build encoded transaction
425
- export async function buildEncodedTransaction(
426
- fundingUtxos: UtxoI[],
427
- sendRequests: Array<SendRequest | TokenSendRequest | OpReturnData>,
428
- privateKey: Uint8Array,
429
- fee: number = 0,
463
+ export async function buildEncodedTransaction({
464
+ inputs,
465
+ outputs,
466
+ signingKey,
467
+ sourceAddress,
468
+ fee = 0,
430
469
  discardChange = false,
431
- slpOutputs: Output[] = [],
432
- feePaidBy: FeePaidByEnum = FeePaidByEnum.change,
433
- changeAddress: string = ""
434
- ) {
435
- let txn = await buildP2pkhNonHdTransaction(
436
- fundingUtxos,
437
- sendRequests,
438
- 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,
439
489
  fee,
440
490
  discardChange,
441
491
  slpOutputs,
442
492
  feePaidBy,
443
- changeAddress
444
- );
445
- // submit transaction
446
- if (txn.success) {
447
- return encodeTransaction(txn.transaction);
448
- } else {
449
- throw Error("Error building transaction with fee");
450
- }
493
+ changeAddress,
494
+ });
495
+
496
+ return { encodedTransaction: encodeTransaction(transaction), sourceOutputs };
451
497
  }