@fundtokens/builders 0.1.0-rc5 → 0.1.0-rc6
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.
- package/FundTokenTransactionBuilder.js +152 -92
- package/PublicFundTransactionBuilder.js +68 -67
- package/README.md +85 -0
- package/art/asset.json +19 -11
- package/art/authhead_vault.json +13 -9
- package/art/fee.json +29 -29
- package/art/fee_minter.json +22 -22
- package/art/fund.json +17 -37
- package/art/manager.json +67 -71
- package/art/mint_inflow.json +5 -5
- package/art/mint_outflow.json +1 -1
- package/art/public.json +25 -25
- package/art/public_vault.json +34 -34
- package/art/simple_minter.json +13 -13
- package/art/simple_vault.json +7 -7
- package/art/startup.json +15 -15
- package/package.json +2 -2
- package/utils.js +48 -7
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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[
|
|
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)
|
|
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
|
|
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)
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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;
|
|
@@ -186,33 +186,34 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
|
|
|
186
186
|
publicFundVaultContract,
|
|
187
187
|
} = this.#contracts;
|
|
188
188
|
|
|
189
|
-
if(this.inputs.length === 0) {
|
|
189
|
+
if (this.inputs.length === 0) {
|
|
190
190
|
throw new Error('User genesis input is expected to be added prior to calling this function');
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
if(this.outputs.length > 0) {
|
|
193
|
+
if (this.outputs.length > 0) {
|
|
194
194
|
throw new Error('No outputs should be added to the transaction');
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
const genesisUtxo = this.inputs[0];
|
|
198
198
|
|
|
199
|
-
if(genesisUtxo.vout !== 0 || genesisUtxo.token) {
|
|
199
|
+
if (genesisUtxo.vout !== 0 || genesisUtxo.token) {
|
|
200
200
|
throw new Error('First input must be a genesis input (vout is 0) with no tokens');
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
|
|
204
204
|
const bestFee = await getBestFee({ feeVaultContract, feeContract: createFundFeeContract, payBy, fee: this.#system.fees.create });
|
|
205
205
|
|
|
206
|
-
const
|
|
206
|
+
const startupUtxos = await startupContract.getUtxos();
|
|
207
207
|
const mintInflowUtxos = await mintInflowContract.getUtxos();
|
|
208
208
|
const mintOutflowUtxos = await mintOutflowContract.getUtxos();
|
|
209
209
|
const publicUtxos = await publicFundContract.getUtxos();
|
|
210
|
+
|
|
210
211
|
const inflowUtxos = mintInflowUtxos.filter(u => u.token?.category === this.#system.inflow);
|
|
211
212
|
const outflowUtxos = mintOutflowUtxos.filter(u => u.token?.category === this.#system.outflow);
|
|
212
213
|
const publicFundUtxos = publicUtxos.filter(u => u.token?.category === this.#system.publicFund)
|
|
213
214
|
|
|
214
215
|
|
|
215
|
-
const
|
|
216
|
+
const startupUtxo = startupUtxos[getRandomInt(startupUtxos.length)];
|
|
216
217
|
const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
|
|
217
218
|
const outflowUtxo = outflowUtxos[getRandomInt(outflowUtxos.length)];
|
|
218
219
|
const publicFundUtxo = publicFundUtxos[getRandomInt(publicFundUtxos.length)];
|
|
@@ -223,13 +224,13 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
|
|
|
223
224
|
|
|
224
225
|
this.addInputs([
|
|
225
226
|
{
|
|
226
|
-
...
|
|
227
|
+
...startupUtxo,
|
|
227
228
|
unlocker: startupContract.unlock.start(getFundBin(fund)),
|
|
228
|
-
},
|
|
229
|
+
},
|
|
229
230
|
{
|
|
230
231
|
...inflowUtxo,
|
|
231
232
|
unlocker: mintInflowContract.unlock.mint(),
|
|
232
|
-
},
|
|
233
|
+
},
|
|
233
234
|
{
|
|
234
235
|
...outflowUtxo,
|
|
235
236
|
unlocker: mintOutflowContract.unlock.mint(),
|
|
@@ -243,77 +244,77 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
|
|
|
243
244
|
unlocker: publicFundContract.unlock.broadcast(getFundBin(fund))
|
|
244
245
|
}
|
|
245
246
|
])
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
247
|
+
.addOutputs([
|
|
248
|
+
withDust({
|
|
249
|
+
to: authHeadVaultContract.tokenAddress,
|
|
250
|
+
}),
|
|
251
|
+
{
|
|
252
|
+
to: startupContract.tokenAddress,
|
|
253
|
+
amount: startupUtxo.satoshis,
|
|
254
|
+
token: startupUtxo.token,
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
to: mintInflowContract.tokenAddress,
|
|
258
|
+
amount: inflowUtxo.satoshis,
|
|
259
|
+
token: inflowUtxo.token,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
to: mintOutflowContract.tokenAddress,
|
|
263
|
+
amount: outflowUtxo.satoshis,
|
|
264
|
+
token: outflowUtxo.token,
|
|
265
|
+
},
|
|
266
|
+
...bestFee.outputs,
|
|
267
|
+
withDust({
|
|
268
|
+
to: managerContract.tokenAddress,
|
|
269
|
+
token: {
|
|
270
|
+
...inflowUtxo.token,
|
|
271
|
+
nft: {
|
|
272
|
+
capability: 'none',
|
|
273
|
+
commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
|
|
274
|
+
}
|
|
273
275
|
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
276
|
+
}),
|
|
277
|
+
withDust({
|
|
278
|
+
to: managerContract.tokenAddress,
|
|
279
|
+
token: {
|
|
280
|
+
...outflowUtxo.token,
|
|
281
|
+
nft: {
|
|
282
|
+
capability: 'none',
|
|
283
|
+
commitment: swapEndianness(genesisUtxo.txid) + binToHex(hash256(getFundBin(fund))),
|
|
284
|
+
}
|
|
283
285
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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: '',
|
|
286
|
+
}),
|
|
287
|
+
withDust({
|
|
288
|
+
to: fundContract.tokenAddress,
|
|
289
|
+
token: {
|
|
290
|
+
category: genesisUtxo.txid,
|
|
291
|
+
amount: fundTokenAmount,
|
|
301
292
|
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
293
|
+
}),
|
|
294
|
+
withDust({
|
|
295
|
+
to: publicFundContract.tokenAddress,
|
|
296
|
+
token: {
|
|
297
|
+
category: this.#system.publicFund,
|
|
298
|
+
amount: 0n,
|
|
299
|
+
nft: {
|
|
300
|
+
capability: 'minting',
|
|
301
|
+
commitment: '',
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}),
|
|
305
|
+
]);
|
|
305
306
|
|
|
306
307
|
|
|
307
308
|
const maxSize = 128 * 2;
|
|
308
309
|
|
|
309
310
|
const fundHex = getFundHex(fund);
|
|
310
311
|
const fundHexParts = [];
|
|
311
|
-
|
|
312
|
+
|
|
312
313
|
let curr = 0;
|
|
313
314
|
let next = maxSize;
|
|
314
315
|
|
|
315
316
|
|
|
316
|
-
while(curr < fundHex.length) {
|
|
317
|
+
while (curr < fundHex.length) {
|
|
317
318
|
fundHexParts.push(fundHex.slice(curr, next));
|
|
318
319
|
curr = next;
|
|
319
320
|
next += maxSize
|
|
@@ -321,7 +322,7 @@ export default class PublicFundTransactionBuilder extends TransactionBuilder {
|
|
|
321
322
|
|
|
322
323
|
fundHexParts.forEach(part => {
|
|
323
324
|
this.addOutput(withDust({
|
|
324
|
-
to:
|
|
325
|
+
to: publicFundVaultContract.tokenAddress,
|
|
325
326
|
token: {
|
|
326
327
|
category: this.#system.publicFund,
|
|
327
328
|
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.
|