@fundtokens/builders 0.1.0-rc5
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 +384 -0
- package/LICENSE +1 -0
- package/PublicFundTransactionBuilder.js +336 -0
- package/art/asset.json +52 -0
- package/art/authhead_vault.json +47 -0
- package/art/fee.json +97 -0
- package/art/fee_minter.json +82 -0
- package/art/fund.json +96 -0
- package/art/manager.json +202 -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 +109 -0
- package/constants.js +1 -0
- package/index.js +17 -0
- package/package.json +13 -0
- package/utils.js +201 -0
|
@@ -0,0 +1,384 @@
|
|
|
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
|
+
} from './utils.js';
|
|
20
|
+
|
|
21
|
+
import managerJson from './art/manager.json' with { type: 'json' };
|
|
22
|
+
import fundJson from './art/fund.json' with { type: 'json' };
|
|
23
|
+
import assetJson from './art/asset.json' with { type: 'json' };
|
|
24
|
+
import feeJson from './art/fee.json' with { type: 'json' };
|
|
25
|
+
import simpleVaultJson from './art/simple_vault.json' with { type: 'json' };
|
|
26
|
+
|
|
27
|
+
const sortDecreasingTokenAmount = (a, b) => b.token?.amount - a.token?.amount;
|
|
28
|
+
|
|
29
|
+
export default class FundTokenTransactionBuilder extends TransactionBuilder {
|
|
30
|
+
#system = {
|
|
31
|
+
inflow: '', // 32 byte, token id
|
|
32
|
+
outflow: '', // 32 byte, token id
|
|
33
|
+
authorization: '', // 32 byte, token id
|
|
34
|
+
fee: {
|
|
35
|
+
nft: '', // 32 byte, token id
|
|
36
|
+
value: -1n, // bigint
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
#swapped = {
|
|
40
|
+
inflow: '',
|
|
41
|
+
outflow: '',
|
|
42
|
+
fee: {
|
|
43
|
+
nft: '',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
#fund = {
|
|
47
|
+
category: '',
|
|
48
|
+
amount: -1n,
|
|
49
|
+
satoshis: -1n,
|
|
50
|
+
assets: null,
|
|
51
|
+
};
|
|
52
|
+
#meta = {
|
|
53
|
+
isBitcoinFund: false,
|
|
54
|
+
};
|
|
55
|
+
#contracts = {
|
|
56
|
+
managerContract: null,
|
|
57
|
+
fundContract: null,
|
|
58
|
+
satoshiAssetContract: null,
|
|
59
|
+
assetContracts: null,
|
|
60
|
+
feeContract: null,
|
|
61
|
+
feeVaultContract: null
|
|
62
|
+
};
|
|
63
|
+
#logger = null;
|
|
64
|
+
|
|
65
|
+
constructor({
|
|
66
|
+
provider,
|
|
67
|
+
system,
|
|
68
|
+
logger,
|
|
69
|
+
fund,
|
|
70
|
+
}) {
|
|
71
|
+
if (!system) {
|
|
72
|
+
throw new Error('No system configuration provided, unable to continue');
|
|
73
|
+
}
|
|
74
|
+
super({ provider });
|
|
75
|
+
this.#system = system;
|
|
76
|
+
this.#swapped = {
|
|
77
|
+
inflow: swapEndianness(system.inflow),
|
|
78
|
+
outflow: swapEndianness(system.outflow),
|
|
79
|
+
authorization: swapEndianness(system.authorization),
|
|
80
|
+
fee: {
|
|
81
|
+
nft: swapEndianness(system.fee.nft),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
this.#fund = fund;
|
|
85
|
+
this.#meta = {
|
|
86
|
+
isBitcoinFund: this.#fund.satoshis > 0,
|
|
87
|
+
};
|
|
88
|
+
this.#logger = logger ?? console;
|
|
89
|
+
this.#buildContracts();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// build and get the contracts for this fund
|
|
93
|
+
#buildContracts() {
|
|
94
|
+
const {
|
|
95
|
+
category,
|
|
96
|
+
assets,
|
|
97
|
+
} = this.#fund;
|
|
98
|
+
const fundHash = hashFund(this.#fund);
|
|
99
|
+
|
|
100
|
+
const assetContracts = [];
|
|
101
|
+
|
|
102
|
+
let satoshiAssetContract = undefined;
|
|
103
|
+
if(this.#fund.satoshis > 0) {
|
|
104
|
+
satoshiAssetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, BitcoinCategory], { provider: this.provider });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
assets.forEach(a => {
|
|
108
|
+
const fundAssetCategory = swapEndianness(a.category);
|
|
109
|
+
|
|
110
|
+
// 32 32 32
|
|
111
|
+
const assetContract = new Contract(assetJson, [this.#swapped.outflow, fundHash, fundAssetCategory], { provider: this.provider });
|
|
112
|
+
|
|
113
|
+
assetContracts.push(assetContract);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 32 32 32 32 + 4 128 132 * 2 264
|
|
117
|
+
const fundContract = new Contract(fundJson, [this.#swapped.inflow, this.#swapped.outflow, swapEndianness(category), fundHash], { provider: this.provider });
|
|
118
|
+
|
|
119
|
+
const feeVaultContract = new Contract(simpleVaultJson, [this.#swapped.authorization], { provider: this.provider });
|
|
120
|
+
const feeVaultLockingBytecode = binToHex(cashAddressToLockingBytecode(feeVaultContract.tokenAddress).bytecode);
|
|
121
|
+
const feeContract = new Contract(feeJson, [this.#swapped.authorization, feeVaultLockingBytecode, this.#swapped.fee.nft, BigInt(this.#system.fee.value)], { provider: this.provider });
|
|
122
|
+
|
|
123
|
+
const managerContract = new Contract(managerJson, [
|
|
124
|
+
binToHex(hash256(hexToBin(feeContract.bytecode))),
|
|
125
|
+
this.#swapped.inflow,
|
|
126
|
+
this.#swapped.outflow,
|
|
127
|
+
swapEndianness(category),
|
|
128
|
+
fundHash,
|
|
129
|
+
hexToBin(fundJson.debug.bytecode),
|
|
130
|
+
hexToBin(assetJson.debug.bytecode),
|
|
131
|
+
], { provider: this.provider });
|
|
132
|
+
|
|
133
|
+
this.#contracts = { managerContract, fundContract, assetContracts, feeContract, satoshiAssetContract, feeVaultContract };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getContracts() {
|
|
137
|
+
return this.#contracts;
|
|
138
|
+
}
|
|
139
|
+
|
|
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
|
|
147
|
+
async addInflow({
|
|
148
|
+
amount,
|
|
149
|
+
payBy,
|
|
150
|
+
}) {
|
|
151
|
+
this.#logger.log('transaction builder...adding minting transaction');
|
|
152
|
+
|
|
153
|
+
const { managerContract, fundContract, assetContracts, feeContract, feeVaultContract } = this.#contracts;
|
|
154
|
+
|
|
155
|
+
const inflowUtxos = (await managerContract.getUtxos()).filter(u => u.token?.category === this.#system.inflow);
|
|
156
|
+
const fundUtxos = (await fundContract.getUtxos()).filter(u => u.token?.category === this.#fund.category);
|
|
157
|
+
const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee, authorization: this.#system.authorization });
|
|
158
|
+
|
|
159
|
+
if (!inflowUtxos?.length || !fundUtxos?.length || !bestFee) {
|
|
160
|
+
this.#logger.error('Missing required UTXO', !inflowUtxos?.length, !fundUtxos?.length, !bestFee);
|
|
161
|
+
throw new Error('Missing required UTXO');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const inflowUtxo = inflowUtxos[getRandomInt(inflowUtxos.length)];
|
|
165
|
+
const fundUtxo = fundUtxos[getRandomInt(fundUtxos.length)];
|
|
166
|
+
const feeUtxo = bestFee.utxo;
|
|
167
|
+
|
|
168
|
+
const inflowAmount = this.#fund.amount * amount;
|
|
169
|
+
const fundChangeAmount = fundUtxo.token.amount - inflowAmount;
|
|
170
|
+
|
|
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,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const bitcoinOutputs = [];
|
|
182
|
+
this.#meta.isBitcoinFund && bitcoinOutputs.push({ to: this.#contracts.satoshiAssetContract.tokenAddress, amount: this.#fund.satoshis * amount });
|
|
183
|
+
|
|
184
|
+
this.addInputs([
|
|
185
|
+
{
|
|
186
|
+
...inflowUtxo,
|
|
187
|
+
unlocker: managerContract.unlock.inflow(getFundBin(this.#fund)),
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
...fundUtxo,
|
|
191
|
+
unlocker: fundContract.unlock.mint(),
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
...feeUtxo,
|
|
195
|
+
unlocker: feeContract.unlock.pay(),
|
|
196
|
+
}
|
|
197
|
+
])
|
|
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,
|
|
211
|
+
token: {
|
|
212
|
+
category: this.#fund.assets[i].category,
|
|
213
|
+
amount: this.#fund.assets[i].amount * amount,
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}),
|
|
217
|
+
]);
|
|
218
|
+
this.#logger.log('finished adding mint transaction i/o');
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
|
|
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
|
|
230
|
+
async addOutflow({
|
|
231
|
+
amount,
|
|
232
|
+
payBy,
|
|
233
|
+
bufferHex,
|
|
234
|
+
}) {
|
|
235
|
+
this.#logger.log('transaction builder...adding redemption transaction');
|
|
236
|
+
|
|
237
|
+
const { managerContract, fundContract, assetContracts, feeContract, satoshiAssetContract, feeVaultContract } = this.#contracts;
|
|
238
|
+
|
|
239
|
+
//
|
|
240
|
+
const outflowUtxos = (await managerContract.getUtxos()).filter(u => u.token?.category === this.#system.outflow);
|
|
241
|
+
if (!outflowUtxos.length) {
|
|
242
|
+
throw new Error(`Missing required outflow ${this.#system.outflow} UTXO.`);
|
|
243
|
+
}
|
|
244
|
+
const outflowUtxo = outflowUtxos[0];
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
//
|
|
248
|
+
const fundUtxos = await fundContract.getUtxos();
|
|
249
|
+
|
|
250
|
+
if (!fundUtxos.length) {
|
|
251
|
+
throw new Error(`Missing required fund ${this.#fund.category} UTXO. Send dust UTXO to contract and redeem again.`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const existingFundUtxo = fundUtxos.filter(u => u.token?.category === this.#fund.category).sort(sortDecreasingTokenAmount);
|
|
255
|
+
const fundUtxo = existingFundUtxo.length ? existingFundUtxo[getRandomInt(existingFundUtxo.length)] : fundUtxos[getRandomInt(fundUtxos.length)];
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
//
|
|
259
|
+
const outflowAmount = this.#fund.amount * amount;
|
|
260
|
+
const updatedFundAmount = (fundUtxo.token?.amount ?? 0n) + outflowAmount;
|
|
261
|
+
|
|
262
|
+
const bestFee = await getBestFee({ feeVaultContract, feeContract, payBy, fee: this.#system.fee, authorization: this.#system.authorization });
|
|
263
|
+
const feeUtxo = bestFee.utxo;
|
|
264
|
+
|
|
265
|
+
const satoshiAssetInputs = [];
|
|
266
|
+
const satoshiAssetOutputs = [];
|
|
267
|
+
const satoshiAssetChangeAmounts = [];
|
|
268
|
+
|
|
269
|
+
const calcSatoshiAsset = async () => {
|
|
270
|
+
if (this.#meta.isBitcoinFund) {
|
|
271
|
+
const satoshiAssetUtxos = (await satoshiAssetContract.getUtxos()).filter(u => !u.token);
|
|
272
|
+
if(!satoshiAssetUtxos) {
|
|
273
|
+
throw new Error('Missing required satoshi asset UTXO');
|
|
274
|
+
}
|
|
275
|
+
let satoshiAmountAdded = 0n;
|
|
276
|
+
for (let index = 0; index < satoshiAssetUtxos.length; ++index) {
|
|
277
|
+
satoshiAssetInputs.push({
|
|
278
|
+
...satoshiAssetUtxos[index],
|
|
279
|
+
unlocker: this.#contracts.satoshiAssetContract.unlock.release()
|
|
280
|
+
});
|
|
281
|
+
satoshiAmountAdded += satoshiAssetUtxos[index].satoshis;
|
|
282
|
+
if (satoshiAmountAdded >= amount * this.#fund.satoshis) {
|
|
283
|
+
satoshiAssetChangeAmounts.push(satoshiAmountAdded - (amount * this.#fund.satoshis));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (let i = 0; i < satoshiAssetChangeAmounts.length; ++i) {
|
|
289
|
+
if (!satoshiAssetChangeAmounts[i]) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
this.#logger.log('adding satoshi output change');
|
|
293
|
+
satoshiAssetOutputs.push({
|
|
294
|
+
to: satoshiAssetContract.tokenAddress,
|
|
295
|
+
amount: satoshiAssetChangeAmounts[i],
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
await calcSatoshiAsset();
|
|
302
|
+
|
|
303
|
+
const assetInputs = [];
|
|
304
|
+
const assetOutputs = [];
|
|
305
|
+
const assetChangeAmounts = [];
|
|
306
|
+
|
|
307
|
+
const calcTokenAssets = async () => {
|
|
308
|
+
for (let i = 0; i < assetContracts.length; ++i) {
|
|
309
|
+
const assetUtxos = (await assetContracts[i].getUtxos()).filter(u => u.token?.category === this.#fund.assets[i].category).sort(sortDecreasingTokenAmount);
|
|
310
|
+
if (!assetUtxos.length) {
|
|
311
|
+
throw new Error(`Missing required asset '${this.#fund.assets[i].category}' UTXO`);
|
|
312
|
+
}
|
|
313
|
+
let tokenAmountAdded = 0n;
|
|
314
|
+
for (let j = 0; j < assetUtxos.length; ++j) {
|
|
315
|
+
assetInputs.push({
|
|
316
|
+
...assetUtxos[j],
|
|
317
|
+
unlocker: assetContracts[i].unlock.release()
|
|
318
|
+
});
|
|
319
|
+
tokenAmountAdded += assetUtxos[j].token.amount;
|
|
320
|
+
if (tokenAmountAdded >= amount * this.#fund.assets[i].amount) {
|
|
321
|
+
assetChangeAmounts.push(tokenAmountAdded - (amount * this.#fund.assets[i].amount));
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < assetChangeAmounts.length; ++i) {
|
|
328
|
+
if (!assetChangeAmounts[i]) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
assetOutputs.push(withDust({
|
|
332
|
+
to: assetContracts[i].tokenAddress,
|
|
333
|
+
token: {
|
|
334
|
+
category: this.#fund.assets[i].category,
|
|
335
|
+
amount: assetChangeAmounts[i],
|
|
336
|
+
},
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
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
|
+
|
|
346
|
+
this.addInputs([
|
|
347
|
+
{
|
|
348
|
+
...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()
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
...feeUtxo,
|
|
357
|
+
unlocker: feeContract.unlock.pay()
|
|
358
|
+
},
|
|
359
|
+
...satoshiAssetInputs,
|
|
360
|
+
...assetInputs
|
|
361
|
+
])
|
|
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
|
+
]);
|
|
380
|
+
|
|
381
|
+
this.#logger.log('finished adding redemption transaction i/o');
|
|
382
|
+
return this;
|
|
383
|
+
}
|
|
384
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copyright (c) 2026 FoldingCash LLC, doing business as Fun(d)Tokens. All rights reserved.
|