@fundtokens/builders 0.1.0-rc10
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 +457 -0
- package/LICENSE +1 -0
- package/PublicFundTransactionBuilder.js +346 -0
- package/README.md +85 -0
- package/art/asset.json +60 -0
- package/art/authhead_vault.json +51 -0
- package/art/fee.json +101 -0
- package/art/fee_minter.json +82 -0
- package/art/fund.json +76 -0
- package/art/manager.json +206 -0
- package/art/mint_inflow.json +84 -0
- package/art/mint_outflow.json +84 -0
- package/art/public.json +149 -0
- package/art/public_vault.json +122 -0
- package/art/simple_minter.json +73 -0
- package/art/simple_vault.json +37 -0
- package/art/startup.json +113 -0
- package/constants.js +1 -0
- package/index.js +18 -0
- package/package.json +21 -0
- package/utils.js +254 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Contract,
|
|
3
|
+
TransactionBuilder,
|
|
4
|
+
} from 'cashscript';
|
|
5
|
+
import {
|
|
6
|
+
swapEndianness,
|
|
7
|
+
hash256,
|
|
8
|
+
hexToBin,
|
|
9
|
+
binToHex,
|
|
10
|
+
cashAddressToLockingBytecode,
|
|
11
|
+
} from '@bitauth/libauth';
|
|
12
|
+
import { BitcoinCategory } from './constants.js';
|
|
13
|
+
import {
|
|
14
|
+
getFundBin,
|
|
15
|
+
hashFund,
|
|
16
|
+
getBestFee,
|
|
17
|
+
getRandomInt,
|
|
18
|
+
withDust,
|
|
19
|
+
categoryAscending,
|
|
20
|
+
} from './utils.js';
|
|
21
|
+
|
|
22
|
+
import managerJson from './art/manager.json' with { type: 'json' };
|
|
23
|
+
import fundJson from './art/fund.json' with { type: 'json' };
|
|
24
|
+
import assetJson from './art/asset.json' with { type: 'json' };
|
|
25
|
+
import feeJson from './art/fee.json' with { type: 'json' };
|
|
26
|
+
import simpleVaultJson from './art/simple_vault.json' with { type: 'json' };
|
|
27
|
+
|
|
28
|
+
const sortDecreasingTokenAmount = (a, b) => {
|
|
29
|
+
const aAmount = a.token?.amount ?? 0n;
|
|
30
|
+
const bAmount = b.token?.amount ?? 0n;
|
|
31
|
+
|
|
32
|
+
if(aAmount === bAmount) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if(aAmount > bAmount) {
|
|
37
|
+
return -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 1;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default class FundTokenTransactionBuilder extends TransactionBuilder {
|
|
44
|
+
#system = {
|
|
45
|
+
inflow: '', // 32 byte, token id
|
|
46
|
+
outflow: '', // 32 byte, token id
|
|
47
|
+
authorization: '', // 32 byte, token id
|
|
48
|
+
fee: {
|
|
49
|
+
nft: '', // 32 byte, token id
|
|
50
|
+
value: -1n, // bigint
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
#swapped = {
|
|
54
|
+
inflow: '',
|
|
55
|
+
outflow: '',
|
|
56
|
+
fee: {
|
|
57
|
+
nft: '',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
#fund = {
|
|
61
|
+
category: '',
|
|
62
|
+
amount: -1n,
|
|
63
|
+
satoshis: -1n,
|
|
64
|
+
assets: null,
|
|
65
|
+
};
|
|
66
|
+
#meta = {
|
|
67
|
+
isBitcoinFund: false,
|
|
68
|
+
};
|
|
69
|
+
#contracts = {
|
|
70
|
+
managerContract: null,
|
|
71
|
+
fundContract: null,
|
|
72
|
+
satoshiAssetContract: null,
|
|
73
|
+
assetContracts: null,
|
|
74
|
+
feeContract: null,
|
|
75
|
+
feeVaultContract: null
|
|
76
|
+
};
|
|
77
|
+
#logger = null;
|
|
78
|
+
|
|
79
|
+
constructor({
|
|
80
|
+
provider,
|
|
81
|
+
system,
|
|
82
|
+
logger,
|
|
83
|
+
fund,
|
|
84
|
+
}) {
|
|
85
|
+
if (!system) {
|
|
86
|
+
throw new Error('No system configuration provided, unable to continue');
|
|
87
|
+
}
|
|
88
|
+
super({ provider });
|
|
89
|
+
this.#system = {
|
|
90
|
+
...system,
|
|
91
|
+
};
|
|
92
|
+
this.#swapped = {
|
|
93
|
+
inflow: swapEndianness(system.inflow),
|
|
94
|
+
outflow: swapEndianness(system.outflow),
|
|
95
|
+
authorization: swapEndianness(system.authorization),
|
|
96
|
+
fee: {
|
|
97
|
+
nft: swapEndianness(system.fee.nft),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
this.#fund = {
|
|
101
|
+
...fund,
|
|
102
|
+
assets: [...fund.assets.map(a => ({ ...a })).sort(categoryAscending)] ?? [],
|
|
103
|
+
};
|
|
104
|
+
this.#meta = {
|
|
105
|
+
isBitcoinFund: this.#fund.satoshis > 0,
|
|
106
|
+
};
|
|
107
|
+
this.#logger = logger ?? console;
|
|
108
|
+
this.#buildContracts();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// build and get the contracts for this fund
|
|
112
|
+
#buildContracts() {
|
|
113
|
+
const {
|
|
114
|
+
category,
|
|
115
|
+
assets,
|
|
116
|
+
} = this.#fund;
|
|
117
|
+
const fundHash = hashFund(this.#fund);
|
|
118
|
+
|
|
119
|
+
const assetContracts = [];
|
|
120
|
+
|
|
121
|
+
let satoshiAssetContract = undefined;
|
|
122
|
+
if (this.#fund.satoshis > 0) {
|
|
123
|
+
satoshiAssetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, BitcoinCategory], { provider: this.provider });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
assets.forEach(a => {
|
|
127
|
+
const fundAssetCategory = swapEndianness(a.category);
|
|
128
|
+
|
|
129
|
+
// 32 32 32
|
|
130
|
+
const assetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, fundAssetCategory], { provider: this.provider });
|
|
131
|
+
|
|
132
|
+
assetContracts.push(assetContract);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 32 32 32 32 + 4 128 132 * 2 264
|
|
136
|
+
const fundContract = new Contract(fundJson, [this.#swapped.inflow, this.#swapped.outflow, swapEndianness(category), fundHash], { provider: this.provider });
|
|
137
|
+
|
|
138
|
+
const feeVaultContract = new Contract(simpleVaultJson, [this.#swapped.authorization], { provider: this.provider });
|
|
139
|
+
const feeVaultLockingBytecode = binToHex(cashAddressToLockingBytecode(feeVaultContract.tokenAddress).bytecode);
|
|
140
|
+
const feeContract = new Contract(feeJson, [this.#swapped.authorization, feeVaultLockingBytecode, this.#swapped.fee.nft, BigInt(this.#system.fee.value)], { provider: this.provider });
|
|
141
|
+
|
|
142
|
+
const managerContract = new Contract(managerJson, [
|
|
143
|
+
binToHex(hash256(hexToBin(feeContract.bytecode))),
|
|
144
|
+
this.#swapped.inflow,
|
|
145
|
+
this.#swapped.outflow,
|
|
146
|
+
swapEndianness(category),
|
|
147
|
+
fundHash,
|
|
148
|
+
hexToBin(fundJson.debug.bytecode),
|
|
149
|
+
hexToBin(assetJson.debug.bytecode),
|
|
150
|
+
], { provider: this.provider });
|
|
151
|
+
|
|
152
|
+
this.#contracts = { managerContract, fundContract, assetContracts, feeContract, satoshiAssetContract, feeVaultContract };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
getContracts() {
|
|
156
|
+
return this.#contracts;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* addInflow(amount, payBy): Builds a fund token minting transaction
|
|
161
|
+
*
|
|
162
|
+
* Randomly selects:
|
|
163
|
+
* - One inflow thread (for thread distribution)
|
|
164
|
+
* - Multiple fund UTXOs to cover the minting amount (reduces collision)
|
|
165
|
+
* - Best fee option (Bitcoin or token-based)
|
|
166
|
+
*
|
|
167
|
+
* Constructs transaction with:
|
|
168
|
+
* - Inflow manager input + output (threaded signal)
|
|
169
|
+
* - Fund UTXO inputs collected randomly + outputs (one per input)
|
|
170
|
+
* - Fee validation and routing
|
|
171
|
+
* - Asset custody outputs (prepared for user deposit)
|
|
172
|
+
*
|
|
173
|
+
* The consuming app is responsible for:
|
|
174
|
+
* - Adding user inputs (assets to deposit)
|
|
175
|
+
* - Adding user outputs (fund tokens minted, Bitcoin change, token change)
|
|
176
|
+
*/
|
|
177
|
+
async addInflow({
|
|
178
|
+
amount,
|
|
179
|
+
payBy,
|
|
180
|
+
}) {
|
|
181
|
+
this.#logger.log('transaction builder...adding minting transaction', amount, payBy);
|
|
182
|
+
|
|
183
|
+
const { managerContract, fundContract, assetContracts, feeContract, feeVaultContract } = this.#contracts;
|
|
184
|
+
|
|
185
|
+
const inflowUtxos = (await managerContract.getUtxos()).filter(u => u.token?.category === this.#system.inflow);
|
|
186
|
+
const fundUtxos = (await fundContract.getUtxos()).filter(u => u.token?.category === this.#fund.category);
|
|
187
|
+
const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee, authorization: this.#system.authorization });
|
|
188
|
+
|
|
189
|
+
if (!inflowUtxos?.length || !fundUtxos?.length || !bestFee) {
|
|
190
|
+
this.#logger.error('Missing required UTXO', !inflowUtxos?.length, !fundUtxos?.length, !bestFee);
|
|
191
|
+
throw new Error('Missing required UTXO');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
|
|
195
|
+
const feeUtxo = bestFee.utxo;
|
|
196
|
+
|
|
197
|
+
const inflowAmount = this.#fund.amount * amount;
|
|
198
|
+
|
|
199
|
+
// Randomly select fund UTXOs to cover the inflow amount
|
|
200
|
+
// Shuffle to reduce collision chances when multiple transactions are building
|
|
201
|
+
const shuffledFundUtxos = [...fundUtxos].sort(() => Math.random() - 0.5);
|
|
202
|
+
let totalFundAmount = 0n;
|
|
203
|
+
const selectedFundUtxos = [];
|
|
204
|
+
|
|
205
|
+
for (const utxo of shuffledFundUtxos) {
|
|
206
|
+
selectedFundUtxos.push(utxo);
|
|
207
|
+
totalFundAmount += utxo.token.amount;
|
|
208
|
+
|
|
209
|
+
if (totalFundAmount >= inflowAmount) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (totalFundAmount < inflowAmount) {
|
|
215
|
+
throw new Error(`Insufficient fund tokens: need ${inflowAmount}, have ${totalFundAmount}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const fundChangeAmount = totalFundAmount - inflowAmount;
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
// Create one input per selected UTXO
|
|
222
|
+
const fundInputs = selectedFundUtxos.map(utxo => ({
|
|
223
|
+
...utxo,
|
|
224
|
+
unlocker: fundContract.unlock.mint(),
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
// Create one output per input (maintain input/output balance)
|
|
228
|
+
// First output contains any change, others are dust returns
|
|
229
|
+
const fundOutputs = selectedFundUtxos.map((utxo, index) => {
|
|
230
|
+
if (index === 0 && fundChangeAmount > 0) {
|
|
231
|
+
// Last output: return change to contract
|
|
232
|
+
return withDust({
|
|
233
|
+
to: fundContract.tokenAddress,
|
|
234
|
+
token: {
|
|
235
|
+
category: this.#fund.category,
|
|
236
|
+
amount: fundChangeAmount,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
// Other outputs: return as dust
|
|
241
|
+
return withDust({
|
|
242
|
+
to: fundContract.tokenAddress,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const bitcoinOutputs = [];
|
|
248
|
+
this.#meta.isBitcoinFund && bitcoinOutputs.push({ to: this.#contracts.satoshiAssetContract.tokenAddress, amount: this.#fund.satoshis * amount });
|
|
249
|
+
|
|
250
|
+
this.addInputs([
|
|
251
|
+
{
|
|
252
|
+
...inflowUtxo,
|
|
253
|
+
unlocker: managerContract.unlock.inflow(getFundBin(this.#fund)),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
...feeUtxo,
|
|
257
|
+
unlocker: feeContract.unlock.pay(),
|
|
258
|
+
},
|
|
259
|
+
...fundInputs,
|
|
260
|
+
])
|
|
261
|
+
.addOutputs([
|
|
262
|
+
withDust({
|
|
263
|
+
to: managerContract.tokenAddress,
|
|
264
|
+
token: {
|
|
265
|
+
...inflowUtxo.token,
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
...bestFee.outputs,
|
|
269
|
+
...fundOutputs,
|
|
270
|
+
...bitcoinOutputs,
|
|
271
|
+
...assetContracts.map((assetContract, i) => {
|
|
272
|
+
return withDust({
|
|
273
|
+
to: assetContract.tokenAddress,
|
|
274
|
+
token: {
|
|
275
|
+
category: this.#fund.assets[i].category,
|
|
276
|
+
amount: this.#fund.assets[i].amount * amount,
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}),
|
|
280
|
+
]);
|
|
281
|
+
this.#logger.log('finished adding mint transaction i/o');
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* addOutflow(amount, payBy): Builds a fund token redemption transaction
|
|
287
|
+
*
|
|
288
|
+
* Randomly selects:
|
|
289
|
+
* - One outflow thread (for thread distribution)
|
|
290
|
+
* - A fund UTXO to collect redeemed tokens into
|
|
291
|
+
* - Best fee option (Bitcoin or token-based)
|
|
292
|
+
* - Asset UTXOs from each asset contract (largest first)
|
|
293
|
+
* - Satoshi UTXOs if fund includes Bitcoin (largest first)
|
|
294
|
+
*
|
|
295
|
+
* Constructs transaction with:
|
|
296
|
+
* - Outflow manager input + output (threaded signal)
|
|
297
|
+
* - Fund UTXO input + output (collects redeemed tokens)
|
|
298
|
+
* - Fee validation and routing
|
|
299
|
+
* - Asset release inputs + outputs (with change handling)
|
|
300
|
+
* - Satoshi release inputs + outputs (with change handling)
|
|
301
|
+
*
|
|
302
|
+
* The consuming app is responsible for:
|
|
303
|
+
* - Adding user inputs (fund tokens to redeem)
|
|
304
|
+
* - Adding user outputs (underlying assets received, change)
|
|
305
|
+
*/
|
|
306
|
+
async addOutflow({
|
|
307
|
+
amount,
|
|
308
|
+
payBy,
|
|
309
|
+
}) {
|
|
310
|
+
this.#logger.log('transaction builder...adding redemption transaction');
|
|
311
|
+
|
|
312
|
+
const { managerContract, fundContract, assetContracts, feeContract, satoshiAssetContract, feeVaultContract } = this.#contracts;
|
|
313
|
+
|
|
314
|
+
//
|
|
315
|
+
const outflowUtxos = (await managerContract.getUtxos()).filter(u => u.token?.category === this.#system.outflow);
|
|
316
|
+
if (!outflowUtxos.length) {
|
|
317
|
+
throw new Error(`Missing required outflow ${this.#system.outflow} UTXO.`);
|
|
318
|
+
}
|
|
319
|
+
const outflowUtxo = outflowUtxos[getRandomInt(outflowUtxos.length)];
|
|
320
|
+
|
|
321
|
+
const fundUtxos = await fundContract.getUtxos();
|
|
322
|
+
|
|
323
|
+
if (!fundUtxos.length) {
|
|
324
|
+
throw new Error(`Missing required fund ${this.#fund.category} UTXO. Send dust UTXO to contract and redeem again.`)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const existingFundUtxo = fundUtxos.filter(u => u.token?.category === this.#fund.category);
|
|
328
|
+
const fundUtxo = existingFundUtxo.length ? existingFundUtxo[getRandomInt(existingFundUtxo.length)] : fundUtxos[getRandomInt(fundUtxos.length)];
|
|
329
|
+
|
|
330
|
+
//
|
|
331
|
+
const outflowAmount = this.#fund.amount * amount;
|
|
332
|
+
const updatedFundAmount = (fundUtxo.token?.amount ?? 0n) + outflowAmount;
|
|
333
|
+
|
|
334
|
+
const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee });
|
|
335
|
+
const feeUtxo = bestFee.utxo;
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
const fundInputs = [{
|
|
339
|
+
...fundUtxo,
|
|
340
|
+
unlocker: fundContract.unlock.redeem()
|
|
341
|
+
}];
|
|
342
|
+
const fundOutputs = [withDust({
|
|
343
|
+
to: fundContract.tokenAddress,
|
|
344
|
+
token: {
|
|
345
|
+
category: this.#fund.category,
|
|
346
|
+
amount: updatedFundAmount,
|
|
347
|
+
},
|
|
348
|
+
})];
|
|
349
|
+
|
|
350
|
+
const satoshiAssetInputs = [];
|
|
351
|
+
const satoshiAssetOutputs = [];
|
|
352
|
+
const satoshiAssetChangeAmounts = [];
|
|
353
|
+
|
|
354
|
+
const calcSatoshiAsset = async () => {
|
|
355
|
+
if (this.#meta.isBitcoinFund) {
|
|
356
|
+
const satoshiAssetUtxos = (await satoshiAssetContract.getUtxos()).filter(u => !u.token);
|
|
357
|
+
if (!satoshiAssetUtxos) {
|
|
358
|
+
throw new Error('Missing required satoshi asset UTXO');
|
|
359
|
+
}
|
|
360
|
+
let satoshiAmountAdded = 0n;
|
|
361
|
+
for (let index = 0; index < satoshiAssetUtxos.length; ++index) {
|
|
362
|
+
satoshiAssetInputs.push({
|
|
363
|
+
...satoshiAssetUtxos[index],
|
|
364
|
+
unlocker: this.#contracts.satoshiAssetContract.unlock.release()
|
|
365
|
+
});
|
|
366
|
+
satoshiAmountAdded += satoshiAssetUtxos[index].satoshis;
|
|
367
|
+
if (satoshiAmountAdded >= amount * this.#fund.satoshis) {
|
|
368
|
+
satoshiAssetChangeAmounts.push(satoshiAmountAdded - (amount * this.#fund.satoshis));
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (let i = 0; i < satoshiAssetChangeAmounts.length; ++i) {
|
|
374
|
+
if (!satoshiAssetChangeAmounts[i]) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
this.#logger.log('adding satoshi output change');
|
|
378
|
+
satoshiAssetOutputs.push({
|
|
379
|
+
to: satoshiAssetContract.tokenAddress,
|
|
380
|
+
amount: satoshiAssetChangeAmounts[i],
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
await calcSatoshiAsset();
|
|
387
|
+
|
|
388
|
+
const assetInputs = [];
|
|
389
|
+
const assetOutputs = [];
|
|
390
|
+
const assetChangeAmounts = [];
|
|
391
|
+
|
|
392
|
+
const calcTokenAssets = async () => {
|
|
393
|
+
for (let i = 0; i < assetContracts.length; ++i) {
|
|
394
|
+
const assetUtxos = (await assetContracts[i].getUtxos()).filter(u => u.token?.category === this.#fund.assets[i].category).sort(sortDecreasingTokenAmount);
|
|
395
|
+
if (!assetUtxos.length) {
|
|
396
|
+
throw new Error(`Missing required asset '${this.#fund.assets[i].category}' UTXO`);
|
|
397
|
+
}
|
|
398
|
+
let tokenAmountAdded = 0n;
|
|
399
|
+
for (let j = 0; j < assetUtxos.length; ++j) {
|
|
400
|
+
assetInputs.push({
|
|
401
|
+
...assetUtxos[j],
|
|
402
|
+
unlocker: assetContracts[i].unlock.release()
|
|
403
|
+
});
|
|
404
|
+
tokenAmountAdded += assetUtxos[j].token.amount;
|
|
405
|
+
if (tokenAmountAdded >= amount * this.#fund.assets[i].amount) {
|
|
406
|
+
assetChangeAmounts.push(tokenAmountAdded - (amount * this.#fund.assets[i].amount));
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < assetChangeAmounts.length; ++i) {
|
|
413
|
+
if (!assetChangeAmounts[i]) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
assetOutputs.push(withDust({
|
|
417
|
+
to: assetContracts[i].tokenAddress,
|
|
418
|
+
token: {
|
|
419
|
+
category: this.#fund.assets[i].category,
|
|
420
|
+
amount: assetChangeAmounts[i],
|
|
421
|
+
},
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await calcTokenAssets();
|
|
427
|
+
|
|
428
|
+
this.addInputs([
|
|
429
|
+
{
|
|
430
|
+
...outflowUtxo,
|
|
431
|
+
unlocker: managerContract.unlock.outflow(getFundBin(this.#fund))
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
...feeUtxo,
|
|
435
|
+
unlocker: feeContract.unlock.pay()
|
|
436
|
+
},
|
|
437
|
+
...fundInputs,
|
|
438
|
+
...satoshiAssetInputs,
|
|
439
|
+
...assetInputs,
|
|
440
|
+
])
|
|
441
|
+
.addOutputs([
|
|
442
|
+
withDust({
|
|
443
|
+
to: managerContract.tokenAddress,
|
|
444
|
+
token: {
|
|
445
|
+
...outflowUtxo.token,
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
448
|
+
...bestFee.outputs,
|
|
449
|
+
...fundOutputs,
|
|
450
|
+
...satoshiAssetOutputs,
|
|
451
|
+
...assetOutputs
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
this.#logger.log('finished adding redemption transaction i/o');
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright (c) 2026 FoldingCash LLC, doing business as Fun(d)Tokens. All rights reserved.
|