@fundtokens/builders 0.1.0-rc5 → 0.1.0-rc7

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.
@@ -16,6 +16,7 @@ import {
16
16
  getBestFee,
17
17
  getRandomInt,
18
18
  withDust,
19
+ categoryAscending,
19
20
  } from './utils.js';
20
21
 
21
22
  import managerJson from './art/manager.json' with { type: 'json' };
@@ -72,7 +73,9 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
72
73
  throw new Error('No system configuration provided, unable to continue');
73
74
  }
74
75
  super({ provider });
75
- this.#system = system;
76
+ this.#system = {
77
+ ...system,
78
+ };
76
79
  this.#swapped = {
77
80
  inflow: swapEndianness(system.inflow),
78
81
  outflow: swapEndianness(system.outflow),
@@ -81,7 +84,10 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
81
84
  nft: swapEndianness(system.fee.nft),
82
85
  },
83
86
  };
84
- this.#fund = fund;
87
+ this.#fund = {
88
+ ...fund,
89
+ assets: [...fund.assets.map(a => ({ ...a })).sort(categoryAscending)] ?? [],
90
+ };
85
91
  this.#meta = {
86
92
  isBitcoinFund: this.#fund.satoshis > 0,
87
93
  };
@@ -98,18 +104,18 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
98
104
  const fundHash = hashFund(this.#fund);
99
105
 
100
106
  const assetContracts = [];
101
-
107
+
102
108
  let satoshiAssetContract = undefined;
103
- if(this.#fund.satoshis > 0) {
109
+ if (this.#fund.satoshis > 0) {
104
110
  satoshiAssetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, BitcoinCategory], { provider: this.provider });
105
111
  }
106
112
 
107
113
  assets.forEach(a => {
108
114
  const fundAssetCategory = swapEndianness(a.category);
109
-
115
+
110
116
  // 32 32 32
111
117
  const assetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, fundAssetCategory], { provider: this.provider });
112
-
118
+
113
119
  assetContracts.push(assetContract);
114
120
  });
115
121
 
@@ -137,18 +143,29 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
137
143
  return this.#contracts;
138
144
  }
139
145
 
140
- // TODO: Scaling fund UTXO selection
141
- // As new threads are added to the fund token contract, we should select for inflow tx the UTXO with the most tokens
142
- // Although this means we still target one thread, should have a range maybe and then randomly select
143
- // Maybe should just randomly select enough to fulfills the needs for the tx
144
-
145
- // This method should be called while the transaction has same transaction input and output lengths
146
- // The consuming app is responsible for adding an output for Bitcoin change, fund token minted, and token change
146
+ /**
147
+ * addInflow(amount, payBy): Builds a fund token minting transaction
148
+ *
149
+ * Randomly selects:
150
+ * - One inflow thread (for thread distribution)
151
+ * - Multiple fund UTXOs to cover the minting amount (reduces collision)
152
+ * - Best fee option (Bitcoin or token-based)
153
+ *
154
+ * Constructs transaction with:
155
+ * - Inflow manager input + output (threaded signal)
156
+ * - Fund UTXO inputs collected randomly + outputs (one per input)
157
+ * - Fee validation and routing
158
+ * - Asset custody outputs (prepared for user deposit)
159
+ *
160
+ * The consuming app is responsible for:
161
+ * - Adding user inputs (assets to deposit)
162
+ * - Adding user outputs (fund tokens minted, Bitcoin change, token change)
163
+ */
147
164
  async addInflow({
148
165
  amount,
149
166
  payBy,
150
167
  }) {
151
- this.#logger.log('transaction builder...adding minting transaction');
168
+ this.#logger.log('transaction builder...adding minting transaction', amount, payBy);
152
169
 
153
170
  const { managerContract, fundContract, assetContracts, feeContract, feeVaultContract } = this.#contracts;
154
171
 
@@ -162,20 +179,56 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
162
179
  }
163
180
 
164
181
  const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
165
- const fundUtxo = fundUtxos[getRandomInt(fundUtxos.length)];
166
182
  const feeUtxo = bestFee.utxo;
167
183
 
168
184
  const inflowAmount = this.#fund.amount * amount;
169
- const fundChangeAmount = fundUtxo.token.amount - inflowAmount;
170
185
 
171
- const fundContractOutput = fundChangeAmount > 0 ? withDust({
172
- to: fundContract.tokenAddress,
173
- token: {
174
- category: this.#fund.category,
175
- amount: fundChangeAmount,
176
- },
177
- }) : withDust({
178
- to: fundContract.tokenAddress,
186
+ // Randomly select fund UTXOs to cover the inflow amount
187
+ // Shuffle to reduce collision chances when multiple transactions are building
188
+ const shuffledFundUtxos = [...fundUtxos].sort(() => Math.random() - 0.5);
189
+ let totalFundAmount = 0n;
190
+ const selectedFundUtxos = [];
191
+
192
+ for (const utxo of shuffledFundUtxos) {
193
+ selectedFundUtxos.push(utxo);
194
+ totalFundAmount += utxo.token.amount;
195
+
196
+ if (totalFundAmount >= inflowAmount) {
197
+ break;
198
+ }
199
+ }
200
+
201
+ if (totalFundAmount < inflowAmount) {
202
+ throw new Error(`Insufficient fund tokens: need ${inflowAmount}, have ${totalFundAmount}`);
203
+ }
204
+
205
+ const fundChangeAmount = totalFundAmount - inflowAmount;
206
+
207
+
208
+ // Create one input per selected UTXO
209
+ const fundInputs = selectedFundUtxos.map(utxo => ({
210
+ ...utxo,
211
+ unlocker: fundContract.unlock.mint(),
212
+ }));
213
+
214
+ // Create one output per input (maintain input/output balance)
215
+ // First output contains any change, others are dust returns
216
+ const fundOutputs = selectedFundUtxos.map((utxo, index) => {
217
+ if (index === 0 && fundChangeAmount > 0) {
218
+ // Last output: return change to contract
219
+ return withDust({
220
+ to: fundContract.tokenAddress,
221
+ token: {
222
+ category: this.#fund.category,
223
+ amount: fundChangeAmount,
224
+ },
225
+ });
226
+ } else {
227
+ // Other outputs: return as dust
228
+ return withDust({
229
+ to: fundContract.tokenAddress,
230
+ });
231
+ }
179
232
  });
180
233
 
181
234
  const bitcoinOutputs = [];
@@ -186,51 +239,60 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
186
239
  ...inflowUtxo,
187
240
  unlocker: managerContract.unlock.inflow(getFundBin(this.#fund)),
188
241
  },
189
- {
190
- ...fundUtxo,
191
- unlocker: fundContract.unlock.mint(),
192
- },
193
242
  {
194
243
  ...feeUtxo,
195
244
  unlocker: feeContract.unlock.pay(),
196
- }
245
+ },
246
+ ...fundInputs,
197
247
  ])
198
- .addOutputs([
199
- withDust({
200
- to: managerContract.tokenAddress,
201
- token: {
202
- ...inflowUtxo.token,
203
- },
204
- }),
205
- fundContractOutput,
206
- ...bestFee.outputs,
207
- ...bitcoinOutputs,
208
- ...assetContracts.map((assetContract, i) => {
209
- return withDust({
210
- to: assetContract.tokenAddress,
248
+ .addOutputs([
249
+ withDust({
250
+ to: managerContract.tokenAddress,
211
251
  token: {
212
- category: this.#fund.assets[i].category,
213
- amount: this.#fund.assets[i].amount * amount,
214
- }
215
- });
216
- }),
217
- ]);
252
+ ...inflowUtxo.token,
253
+ },
254
+ }),
255
+ ...bestFee.outputs,
256
+ ...fundOutputs,
257
+ ...bitcoinOutputs,
258
+ ...assetContracts.map((assetContract, i) => {
259
+ return withDust({
260
+ to: assetContract.tokenAddress,
261
+ token: {
262
+ category: this.#fund.assets[i].category,
263
+ amount: this.#fund.assets[i].amount * amount,
264
+ }
265
+ });
266
+ }),
267
+ ]);
218
268
  this.#logger.log('finished adding mint transaction i/o');
219
269
  return this;
220
270
  }
221
271
 
222
- // TODO: Scaling fund UTXO selection
223
- // As new threads are added to the fund token contract, we should select for outflow tx the UTXO with the least tokens
224
- // Although this means we still target one thread, should have a range maybe and then randomly select
225
- // Maybe should just randomly select one that fulfills the needs
226
-
227
- // This method should be called while the transaction has same transaction input and output lengths
228
- // The consuming user is responsible for adding inputs for the fund token
229
- // The consuming app is responsible for adding an outputs for Bitcoin change and token change
272
+ /**
273
+ * addOutflow(amount, payBy): Builds a fund token redemption transaction
274
+ *
275
+ * Randomly selects:
276
+ * - One outflow thread (for thread distribution)
277
+ * - A fund UTXO to collect redeemed tokens into
278
+ * - Best fee option (Bitcoin or token-based)
279
+ * - Asset UTXOs from each asset contract (largest first)
280
+ * - Satoshi UTXOs if fund includes Bitcoin (largest first)
281
+ *
282
+ * Constructs transaction with:
283
+ * - Outflow manager input + output (threaded signal)
284
+ * - Fund UTXO input + output (collects redeemed tokens)
285
+ * - Fee validation and routing
286
+ * - Asset release inputs + outputs (with change handling)
287
+ * - Satoshi release inputs + outputs (with change handling)
288
+ *
289
+ * The consuming app is responsible for:
290
+ * - Adding user inputs (fund tokens to redeem)
291
+ * - Adding user outputs (underlying assets received, change)
292
+ */
230
293
  async addOutflow({
231
294
  amount,
232
295
  payBy,
233
- bufferHex,
234
296
  }) {
235
297
  this.#logger.log('transaction builder...adding redemption transaction');
236
298
 
@@ -241,27 +303,37 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
241
303
  if (!outflowUtxos.length) {
242
304
  throw new Error(`Missing required outflow ${this.#system.outflow} UTXO.`);
243
305
  }
244
- const outflowUtxo = outflowUtxos[0];
245
-
306
+ const outflowUtxo = outflowUtxos[getRandomInt(outflowUtxos.length)];
246
307
 
247
- //
248
308
  const fundUtxos = await fundContract.getUtxos();
249
309
 
250
310
  if (!fundUtxos.length) {
251
311
  throw new Error(`Missing required fund ${this.#fund.category} UTXO. Send dust UTXO to contract and redeem again.`)
252
312
  }
253
313
 
254
- const existingFundUtxo = fundUtxos.filter(u => u.token?.category === this.#fund.category).sort(sortDecreasingTokenAmount);
314
+ const existingFundUtxo = fundUtxos.filter(u => u.token?.category === this.#fund.category);
255
315
  const fundUtxo = existingFundUtxo.length ? existingFundUtxo[getRandomInt(existingFundUtxo.length)] : fundUtxos[getRandomInt(fundUtxos.length)];
256
316
 
257
-
258
317
  //
259
318
  const outflowAmount = this.#fund.amount * amount;
260
319
  const updatedFundAmount = (fundUtxo.token?.amount ?? 0n) + outflowAmount;
261
320
 
262
- const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee, authorization: this.#system.authorization });
321
+ const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee });
263
322
  const feeUtxo = bestFee.utxo;
264
323
 
324
+
325
+ const fundInputs = [{
326
+ ...fundUtxo,
327
+ unlocker: fundContract.unlock.redeem()
328
+ }];
329
+ const fundOutputs = [withDust({
330
+ to: fundContract.tokenAddress,
331
+ token: {
332
+ category: this.#fund.category,
333
+ amount: updatedFundAmount,
334
+ },
335
+ })];
336
+
265
337
  const satoshiAssetInputs = [];
266
338
  const satoshiAssetOutputs = [];
267
339
  const satoshiAssetChangeAmounts = [];
@@ -269,7 +341,7 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
269
341
  const calcSatoshiAsset = async () => {
270
342
  if (this.#meta.isBitcoinFund) {
271
343
  const satoshiAssetUtxos = (await satoshiAssetContract.getUtxos()).filter(u => !u.token);
272
- if(!satoshiAssetUtxos) {
344
+ if (!satoshiAssetUtxos) {
273
345
  throw new Error('Missing required satoshi asset UTXO');
274
346
  }
275
347
  let satoshiAmountAdded = 0n;
@@ -323,7 +395,7 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
323
395
  }
324
396
  }
325
397
  }
326
-
398
+
327
399
  for (let i = 0; i < assetChangeAmounts.length; ++i) {
328
400
  if (!assetChangeAmounts[i]) {
329
401
  continue;
@@ -339,44 +411,32 @@ export default class FundTokenTransactionBuilder extends TransactionBuilder {
339
411
  }
340
412
 
341
413
  await calcTokenAssets();
342
-
343
- // 180 is good up to 12
344
- const densityBuffer = this.#fund.assets.length <= 8 ? '' : '00'.repeat(180 * (this.#fund.assets.length - 8));
345
414
 
346
415
  this.addInputs([
347
416
  {
348
417
  ...outflowUtxo,
349
- unlocker: managerContract.unlock.outflow(getFundBin(this.#fund), bufferHex ?? densityBuffer) // TODO: support density better, need to calculate the "expensive" operations // '00'.repeat(4325)
350
- },
351
- {
352
- ...fundUtxo,
353
- unlocker: fundContract.unlock.redeem()
418
+ unlocker: managerContract.unlock.outflow(getFundBin(this.#fund))
354
419
  },
355
420
  {
356
421
  ...feeUtxo,
357
422
  unlocker: feeContract.unlock.pay()
358
423
  },
424
+ ...fundInputs,
359
425
  ...satoshiAssetInputs,
360
- ...assetInputs
426
+ ...assetInputs,
361
427
  ])
362
- .addOutputs([
363
- withDust({
364
- to: managerContract.tokenAddress,
365
- token: {
366
- ...outflowUtxo.token,
367
- },
368
- }),
369
- withDust({
370
- to: fundContract.tokenAddress,
371
- token: {
372
- category: this.#fund.category,
373
- amount: updatedFundAmount,
374
- },
375
- }),
376
- ...bestFee.outputs,
377
- ...satoshiAssetOutputs,
378
- ...assetOutputs
379
- ]);
428
+ .addOutputs([
429
+ withDust({
430
+ to: managerContract.tokenAddress,
431
+ token: {
432
+ ...outflowUtxo.token,
433
+ },
434
+ }),
435
+ ...bestFee.outputs,
436
+ ...fundOutputs,
437
+ ...satoshiAssetOutputs,
438
+ ...assetOutputs
439
+ ]);
380
440
 
381
441
  this.#logger.log('finished adding redemption transaction i/o');
382
442
  return this;
@@ -171,6 +171,13 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
171
171
  return this.#contracts;
172
172
  }
173
173
 
174
+ getAuthHeadOutput() {
175
+ const { authHeadVaultContract } = this.#contracts;
176
+ return withDust({
177
+ to: authHeadVaultContract.tokenAddress,
178
+ });
179
+ }
180
+
174
181
  async addBroadcast({
175
182
  fund,
176
183
  payBy,
@@ -186,33 +193,39 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
186
193
  publicFundVaultContract,
187
194
  } = this.#contracts;
188
195
 
189
- if(this.inputs.length === 0) {
196
+ if (this.inputs.length === 0) {
190
197
  throw new Error('User genesis input is expected to be added prior to calling this function');
191
198
  }
192
199
 
193
- if(this.outputs.length > 0) {
194
- throw new Error('No outputs should be added to the transaction');
200
+ if (this.outputs.length === 0) { // add authhead output
201
+ this.addOutput(this.getAuthHeadOutput());
202
+ } else {
203
+ // verify authhead output
204
+ const authhead = this.outputs[0];
205
+ if(authhead.to != authHeadVaultContract.tokenAddress || authhead.token) {
206
+ throw new Error('Authhead output is incorrect, expecting to send to authhead vault with no tokens');
207
+ }
195
208
  }
196
209
 
197
210
  const genesisUtxo = this.inputs[0];
198
211
 
199
- if(genesisUtxo.vout !== 0 || genesisUtxo.token) {
212
+ if (genesisUtxo.vout !== 0 || genesisUtxo.token) {
200
213
  throw new Error('First input must be a genesis input (vout is 0) with no tokens');
201
214
  }
202
215
 
203
-
204
216
  const bestFee = await getBestFee({ feeVaultContract, feeContract: createFundFeeContract, payBy, fee: this.#system.fees.create });
205
217
 
206
- const broadcastUtxos = await startupContract.getUtxos();
218
+ const startupUtxos = await startupContract.getUtxos();
207
219
  const mintInflowUtxos = await mintInflowContract.getUtxos();
208
220
  const mintOutflowUtxos = await mintOutflowContract.getUtxos();
209
221
  const publicUtxos = await publicFundContract.getUtxos();
222
+
210
223
  const inflowUtxos = mintInflowUtxos.filter(u => u.token?.category === this.#system.inflow);
211
224
  const outflowUtxos = mintOutflowUtxos.filter(u => u.token?.category === this.#system.outflow);
212
225
  const publicFundUtxos = publicUtxos.filter(u => u.token?.category === this.#system.publicFund)
213
226
 
214
227
 
215
- const broadcastUtxo = broadcastUtxos[getRandomInt(broadcastUtxos.length)];
228
+ const startupUtxo = startupUtxos[getRandomInt(startupUtxos.length)];
216
229
  const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
217
230
  const outflowUtxo = outflowUtxos[getRandomInt(outflowUtxos.length)];
218
231
  const publicFundUtxo = publicFundUtxos[getRandomInt(publicFundUtxos.length)];
@@ -223,13 +236,13 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
223
236
 
224
237
  this.addInputs([
225
238
  {
226
- ...broadcastUtxo,
239
+ ...startupUtxo,
227
240
  unlocker: startupContract.unlock.start(getFundBin(fund)),
228
- },
241
+ },
229
242
  {
230
243
  ...inflowUtxo,
231
244
  unlocker: mintInflowContract.unlock.mint(),
232
- },
245
+ },
233
246
  {
234
247
  ...outflowUtxo,
235
248
  unlocker: mintOutflowContract.unlock.mint(),
@@ -243,77 +256,74 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
243
256
  unlocker: publicFundContract.unlock.broadcast(getFundBin(fund))
244
257
  }
245
258
  ])
246
- .addOutputs([
247
- withDust({
248
- to: authHeadVaultContract.tokenAddress,
249
- }),
250
- {
251
- to: startupContract.tokenAddress,
252
- amount: broadcastUtxo.satoshis,
253
- token: broadcastUtxo.token,
254
- },
255
- {
256
- to: mintInflowContract.tokenAddress,
257
- amount: inflowUtxo.satoshis,
258
- token: inflowUtxo.token,
259
- },
260
- {
261
- to: mintOutflowContract.tokenAddress,
262
- amount: outflowUtxo.satoshis,
263
- token: outflowUtxo.token,
264
- },
265
- ...bestFee.outputs,
266
- withDust({
267
- to: managerContract.tokenAddress,
268
- token: {
269
- ...inflowUtxo.token,
270
- nft: {
271
- capability: 'none',
272
- commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
259
+ .addOutputs([
260
+ {
261
+ to: startupContract.tokenAddress,
262
+ amount: startupUtxo.satoshis,
263
+ token: startupUtxo.token,
264
+ },
265
+ {
266
+ to: mintInflowContract.tokenAddress,
267
+ amount: inflowUtxo.satoshis,
268
+ token: inflowUtxo.token,
269
+ },
270
+ {
271
+ to: mintOutflowContract.tokenAddress,
272
+ amount: outflowUtxo.satoshis,
273
+ token: outflowUtxo.token,
274
+ },
275
+ ...bestFee.outputs,
276
+ withDust({
277
+ to: managerContract.tokenAddress,
278
+ token: {
279
+ ...inflowUtxo.token,
280
+ nft: {
281
+ capability: 'none',
282
+ commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
283
+ }
273
284
  }
274
- }
275
- }),
276
- withDust({
277
- to: managerContract.tokenAddress,
278
- token: {
279
- ...outflowUtxo.token,
280
- nft: {
281
- capability: 'none',
282
- commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
285
+ }),
286
+ withDust({
287
+ to: managerContract.tokenAddress,
288
+ token: {
289
+ ...outflowUtxo.token,
290
+ nft: {
291
+ capability: 'none',
292
+ commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
293
+ }
283
294
  }
284
- }
285
- }),
286
- withDust({
287
- to: fundContract.tokenAddress,
288
- token: {
289
- category: genesisUtxo.txid,
290
- amount: fundTokenAmount,
291
- }
292
- }),
293
- withDust({
294
- to: publicFundContract.tokenAddress,
295
- token: {
296
- category: this.#system.publicFund,
297
- amount: 0n,
298
- nft: {
299
- capability: 'minting',
300
- commitment: '',
295
+ }),
296
+ withDust({
297
+ to: fundContract.tokenAddress,
298
+ token: {
299
+ category: genesisUtxo.txid,
300
+ amount: fundTokenAmount,
301
301
  }
302
- }
303
- }),
304
- ]);
302
+ }),
303
+ withDust({
304
+ to: publicFundContract.tokenAddress,
305
+ token: {
306
+ category: this.#system.publicFund,
307
+ amount: 0n,
308
+ nft: {
309
+ capability: 'minting',
310
+ commitment: '',
311
+ }
312
+ }
313
+ }),
314
+ ]);
305
315
 
306
316
 
307
317
  const maxSize = 128 * 2;
308
318
 
309
319
  const fundHex = getFundHex(fund);
310
320
  const fundHexParts = [];
311
-
321
+
312
322
  let curr = 0;
313
323
  let next = maxSize;
314
324
 
315
325
 
316
- while(curr < fundHex.length) {
326
+ while (curr < fundHex.length) {
317
327
  fundHexParts.push(fundHex.slice(curr, next));
318
328
  curr = next;
319
329
  next += maxSize
@@ -321,7 +331,7 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
321
331
 
322
332
  fundHexParts.forEach(part => {
323
333
  this.addOutput(withDust({
324
- to: publicFundVaultContract.tokenAddress,
334
+ to: publicFundVaultContract.tokenAddress,
325
335
  token: {
326
336
  category: this.#system.publicFund,
327
337
  amount: 0n,
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # FundTokens.Contracts
2
+
3
+ A JavaScript library for interacting with FundTokens smart contracts on the Bitcoin Cash network. This library provides tools for creating, minting, and redeeming fund tokens while handling the complex multi-contract operations required by the FundTokens protocol.
4
+
5
+ ## Features
6
+
7
+ - **Fund Creation**: Create new public funds with custom asset compositions
8
+ - **Token Minting**: Deposit assets to mint fund tokens
9
+ - **Token Redemption**: Withdraw assets by redeeming fund tokens
10
+ - **Multi-Contract Coordination**: Handles complex transaction flows across many smart contracts
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @fundtokens/builders
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ### Creating a Public Fund
21
+
22
+ ```javascript
23
+ import { PublicFundTransactionBuilder } from '@fundtokens/builders';
24
+
25
+ const publicBuilder = new PublicFundTransactionBuilder({ provider, system });
26
+ const fund = {
27
+ category: genesisUtxo.txid,
28
+ amount: 10n,
29
+ satoshis: 1000n,
30
+ assets: [{ category: 'asset_token_id', amount: 2n }]
31
+ };
32
+
33
+ // add user's genesis UTXO
34
+ await publicBuilder.addBroadcast({ fund });
35
+ // add additional IO
36
+ await publicBuilder.send();
37
+ ```
38
+
39
+ ### Minting Fund Tokens
40
+
41
+ ```javascript
42
+ import { FundTokenTransactionBuilder } from '@fundtokens/builders';
43
+
44
+ const fundBuilder = new FundTokenTransactionBuilder({
45
+ provider, system, fund
46
+ });
47
+
48
+ await fundBuilder.addInflow({ amount: 1n });
49
+ // Add user asset inputs and fund token outputs
50
+ await fundBuilder.send();
51
+ ```
52
+
53
+ ### Redeeming Fund Tokens
54
+
55
+ ```javascript
56
+ const fundBuilder = new FundTokenTransactionBuilder({
57
+ provider, system, fund
58
+ });
59
+
60
+ await fundBuilder.addOutflow({ amount: 1n });
61
+ // Add user inputs/outputs
62
+ await fundBuilder.send();
63
+ ```
64
+
65
+ ## Key Concepts
66
+
67
+ ### Fund Lifecycle
68
+
69
+ * Fund Creation - Broadcast fund parameters
70
+ * Fund Operations - Mint and redeem tokens
71
+
72
+ ## Security Model
73
+
74
+ * Non-Custodial: Funds held in contract UTXOs controlled by code
75
+ * Parameter Immutability: Fund details hashed and committed to tokens
76
+ * Contract Isolation: Each contract has single, verified responsibility
77
+ * Thread Authorization: Operations require matching token presence
78
+ * Atomic Validation: Multi-contract validation ensures consistency
79
+
80
+ ## Requirements
81
+ * Bitcoin Cash network access
82
+
83
+ ## License
84
+
85
+ Copyright (c) 2026 FoldingCash LLC, doing business as Fun(d)Tokens. All rights reserved.