@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/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);