@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
package/utils.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import {
|
|
2
|
+
swapEndianness,
|
|
3
|
+
hash256,
|
|
4
|
+
bigIntToBinUint64LEClamped,
|
|
5
|
+
hexToBin,
|
|
6
|
+
binToHex,
|
|
7
|
+
cashAddressToLockingBytecode,
|
|
8
|
+
binToBigIntUint64LE,
|
|
9
|
+
lockingBytecodeToCashAddress,
|
|
10
|
+
getDustThreshold,
|
|
11
|
+
assertSuccess,
|
|
12
|
+
} from '@bitauth/libauth';
|
|
13
|
+
import { BitcoinCategory } from './constants.js';
|
|
14
|
+
import { getNetworkPrefix } from 'cashscript/dist/utils.js';
|
|
15
|
+
|
|
16
|
+
export const withDust = output => {
|
|
17
|
+
const o = {
|
|
18
|
+
lockingBytecode: cashAddressToLockingBytecode(output.to).bytecode,
|
|
19
|
+
valueSatoshis: 0n,
|
|
20
|
+
};
|
|
21
|
+
if(output.token) {
|
|
22
|
+
o.token = {
|
|
23
|
+
...output.token,
|
|
24
|
+
category: hexToBin(output.token?.category),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...output,
|
|
29
|
+
amount: getDustThreshold(o),
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const categoryAscending = (a, b) => {
|
|
34
|
+
return a.category.localeCompare(b.category);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// base - 48bytes
|
|
38
|
+
// per asset - 40bytes
|
|
39
|
+
//
|
|
40
|
+
// two = 128bytes
|
|
41
|
+
// three = 168bytes
|
|
42
|
+
// four = 208bytes
|
|
43
|
+
export function getFundHex(fund) {
|
|
44
|
+
const fundClone = {
|
|
45
|
+
...fund,
|
|
46
|
+
assets: [...fund.assets.map(a => ({ ...a }))],
|
|
47
|
+
};
|
|
48
|
+
const {
|
|
49
|
+
category,
|
|
50
|
+
amount,
|
|
51
|
+
satoshis,
|
|
52
|
+
assets,
|
|
53
|
+
} = fundClone;
|
|
54
|
+
const hex = [];
|
|
55
|
+
hex.push(swapEndianness(category)); // 32 bytes
|
|
56
|
+
hex.push(binToHex(bigIntToBinUint64LEClamped(amount))); // 8 bytes
|
|
57
|
+
hex.push(binToHex(bigIntToBinUint64LEClamped(satoshis))); // 8 bytes
|
|
58
|
+
assets.sort(categoryAscending).map(asset => {
|
|
59
|
+
hex.push(swapEndianness(asset.category)); // 32 bytes
|
|
60
|
+
hex.push(binToHex(bigIntToBinUint64LEClamped(asset.amount))); // 8 bytes
|
|
61
|
+
});
|
|
62
|
+
return hex.join('');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getFundBin(fund) {
|
|
66
|
+
return hexToBin(getFundHex(fund));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function decodeFund(hex) {
|
|
70
|
+
if(typeof hex !== 'string' && typeof hex !== 'number') {
|
|
71
|
+
throw new Error('provide the fund hex as a string or number');
|
|
72
|
+
}
|
|
73
|
+
hex = typeof hex === 'number' ? hex.toString(16) : hex;
|
|
74
|
+
|
|
75
|
+
const fund = {
|
|
76
|
+
category: swapEndianness(hex.slice(0, 64)),
|
|
77
|
+
amount: binToBigIntUint64LE(hexToBin(hex.slice(64, 80))),
|
|
78
|
+
satoshis: binToBigIntUint64LE(hexToBin(hex.slice(80, 96))),
|
|
79
|
+
assets: [],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let assetsHex = hex.slice(96);
|
|
83
|
+
|
|
84
|
+
while(assetsHex.length > 0) {
|
|
85
|
+
fund.assets.push({
|
|
86
|
+
category: swapEndianness(assetsHex.slice(0, 64)),
|
|
87
|
+
amount: binToBigIntUint64LE(hexToBin(assetsHex.slice(64, 80))),
|
|
88
|
+
});
|
|
89
|
+
assetsHex = assetsHex.slice(80);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return fund;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const hashFund = fund => binToHex(hash256(getFundBin(fund)));
|
|
96
|
+
|
|
97
|
+
export function decodeFee({ prefix, network, hex }) {
|
|
98
|
+
const category = swapEndianness(hex.slice(0, 64));
|
|
99
|
+
const amount = binToBigIntUint64LE(hexToBin(hex.slice(64, 80)));
|
|
100
|
+
if(hex.length > 80) {
|
|
101
|
+
const lockingBytecode = hex.slice(80);
|
|
102
|
+
const { address } = assertSuccess(
|
|
103
|
+
lockingBytecodeToCashAddress({
|
|
104
|
+
prefix: prefix || getNetworkPrefix(network),
|
|
105
|
+
bytecode: hexToBin(lockingBytecode),
|
|
106
|
+
tokenSupport: true,
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
return { category, amount, destination: address };
|
|
110
|
+
}
|
|
111
|
+
return { category, amount };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function encodeFee({ category, amount, destination }) {
|
|
115
|
+
if(!amount) {
|
|
116
|
+
throw new Error('Unable to encode fee, amount is required');
|
|
117
|
+
}
|
|
118
|
+
let encoded = swapEndianness(category ?? BitcoinCategory) + binToHex(bigIntToBinUint64LEClamped(amount));
|
|
119
|
+
if(destination) {
|
|
120
|
+
encoded += binToHex(cashAddressToLockingBytecode(destination).bytecode);
|
|
121
|
+
}
|
|
122
|
+
return encoded;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// return [{ category: '', amount: 0n }]
|
|
126
|
+
export async function getAvailableFees({ feeContract, fee }) {
|
|
127
|
+
if(!feeContract) {
|
|
128
|
+
throw new Error('Expected a fee contract');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
nft: feeCategory,
|
|
133
|
+
value: defaultValue,
|
|
134
|
+
} = fee;
|
|
135
|
+
|
|
136
|
+
const utxos = await feeContract.getUtxos();
|
|
137
|
+
|
|
138
|
+
return utxos
|
|
139
|
+
.filter(u => !u.token || u.token.category === feeCategory)
|
|
140
|
+
.reduce((prev, curr) => {
|
|
141
|
+
if(!curr.token) {
|
|
142
|
+
prev[BitcoinCategory] = {
|
|
143
|
+
category: BitcoinCategory,
|
|
144
|
+
amount: prev[BitcoinCategory]?.amount < defaultValue ? prev[BitcoinCategory].amount : defaultValue,
|
|
145
|
+
};
|
|
146
|
+
} else {
|
|
147
|
+
const {
|
|
148
|
+
category,
|
|
149
|
+
amount,
|
|
150
|
+
} = decodeFee({ hex: curr.token.nft.commitment });
|
|
151
|
+
prev[category] = {
|
|
152
|
+
category,
|
|
153
|
+
amount: prev[category]?.amount < amount ? prev[category].amount : amount,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return prev;
|
|
157
|
+
}, {});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function getBestFee({ feeContract, feeVaultContract, fee, payBy }) {
|
|
161
|
+
if(!feeContract || !feeVaultContract) {
|
|
162
|
+
throw new Error('Expected fee contract and fee vault contracts');
|
|
163
|
+
}
|
|
164
|
+
if(feeContract.provider.network !== feeVaultContract.provider.network) {
|
|
165
|
+
throw new Error('Expected the contracts to be using the same network');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const network = feeContract.provider.network;
|
|
169
|
+
const defaultDestination = feeVaultContract.tokenAddress;
|
|
170
|
+
|
|
171
|
+
const {
|
|
172
|
+
nft,
|
|
173
|
+
value: defaultValue,
|
|
174
|
+
} = fee;
|
|
175
|
+
const feeUtxos = (await feeContract.getUtxos())
|
|
176
|
+
.filter(u => {
|
|
177
|
+
if(!u.token) {
|
|
178
|
+
return true;
|
|
179
|
+
} else {
|
|
180
|
+
return u.token.category === nft;
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
.map(u => {
|
|
184
|
+
if(!u.token) {
|
|
185
|
+
return {
|
|
186
|
+
isBitcoin: true,
|
|
187
|
+
amount: defaultValue,
|
|
188
|
+
destination: defaultDestination,
|
|
189
|
+
utxo: u,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const encodedFee = decodeFee({ network, hex: u.token.nft.commitment });
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
isBitcoin: encodedFee.category === BitcoinCategory,
|
|
197
|
+
category: encodedFee.category,
|
|
198
|
+
amount: encodedFee.amount,
|
|
199
|
+
destination: encodedFee.destination ?? defaultDestination,
|
|
200
|
+
utxo: u,
|
|
201
|
+
};
|
|
202
|
+
})
|
|
203
|
+
.filter(b => {
|
|
204
|
+
const payByBitcoin = !payBy || payBy === '' || payBy === BitcoinCategory;
|
|
205
|
+
if(payByBitcoin) {
|
|
206
|
+
return b.isBitcoin;
|
|
207
|
+
} else {
|
|
208
|
+
return b.category === payBy;
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
.sort((a, b) => {
|
|
212
|
+
return a.amount > b.amount;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if(!feeUtxos || !feeUtxos.length) {
|
|
216
|
+
throw new Error('No acceptable fee UTXOs found');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const bestFee = feeUtxos[0];
|
|
220
|
+
|
|
221
|
+
const result = {
|
|
222
|
+
isBitcoin: bestFee.isBitcoin,
|
|
223
|
+
category: bestFee.category,
|
|
224
|
+
amount: bestFee.amount,
|
|
225
|
+
destination: bestFee.destination,
|
|
226
|
+
utxo: bestFee.utxo,
|
|
227
|
+
outputs: [
|
|
228
|
+
withDust({
|
|
229
|
+
...bestFee.utxo,
|
|
230
|
+
to: feeContract.tokenAddress,
|
|
231
|
+
}),
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if(result.isBitcoin) {
|
|
236
|
+
result.outputs.push({
|
|
237
|
+
to: result.destination,
|
|
238
|
+
amount: result.amount,
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
result.outputs.push(withDust({
|
|
242
|
+
to: result.destination,
|
|
243
|
+
token: {
|
|
244
|
+
category: result.category,
|
|
245
|
+
amount: result.amount,
|
|
246
|
+
}
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
//
|
|
254
|
+
export const getRandomInt = max => Math.floor(Math.random() * max);
|