• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MinterTeam / minter-js-sdk / 5122024234

pending completion
5122024234

push

github

shrpne
getPoolInfo: convert values from pip

699 of 873 branches covered (80.07%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 2 files covered. (100.0%)

1213 of 1349 relevant lines covered (89.92%)

133.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.85
/src/api/estimate-tx-commission.js
1
import {Big, BIG_ROUND_DOWN, convertFromPip, FeePrice, TX_TYPE} from 'minterjs-util';
2
import GetCommissionPrice from './get-commission-price.js';
3
import GetPoolInfo from './get-pool-info.js';
4
import {GetCoinId, ReplaceCoinSymbol} from './replace-coin.js';
5
import {fillDefaultData} from '../tx-data/index.js';
6
import {prepareTx} from '../tx.js';
7
// import {isCoinId, validateUint} from '../utils.js';
8

9
/**
10
 * @enum {string}
11
 */
12
export const FEE_PRECISION_SETTING = {
51✔
13
    // value may be precise, imprecise, or even omitted
14
    AUTO: 'auto',
15
    // precise value is required
16
    PRECISE: 'precise',
17
    // some value is required but it can be imprecise
18
    IMPRECISE: 'imprecise',
19
    OMIT: 'omit',
20
};
21
const PRECISION = FEE_PRECISION_SETTING;
51✔
22

23
/**
24
 * @param {MinterApiInstance} apiInstance
25
 * @param {import('axios').AxiosRequestConfig} [factoryAxiosOptions]
26
 * @param {import('axios').AxiosRequestConfig} [factoryExtraAxiosOptions]
27
 * @return {EstimateTxCommissionInstance}
28
 */
29
export default function EstimateTxCommission(apiInstance, factoryAxiosOptions, factoryExtraAxiosOptions) {
30
    const getCommissionPrice = GetCommissionPrice(apiInstance, factoryExtraAxiosOptions);
22✔
31
    const getPoolInfo = GetPoolInfo(apiInstance, factoryExtraAxiosOptions);
22✔
32
    const getCoinId = GetCoinId(apiInstance, factoryExtraAxiosOptions);
22✔
33
    const replaceCoinSymbol = ReplaceCoinSymbol(apiInstance, factoryExtraAxiosOptions);
22✔
34

35
    /**
36
     * @typedef {MinterFeeEstimationDirect|MinterFeeEstimationCalculate} MinterFeeEstimation
37
     */
38
    /**
39
     * @typedef {Function} EstimateTxCommissionInstance
40
     * @param {TxParams|string} txParams
41
     * @param {object} [options]
42
     * @param {FEE_PRECISION_SETTING} [options.needGasCoinFee]
43
     * @param {FEE_PRECISION_SETTING} [options.needBaseCoinFee]
44
     * @param {FEE_PRECISION_SETTING} [options.needPriceCoinFee]
45
     * @param {boolean} [options.loose] - DEPRECATED
46
     * @param {boolean} [options.direct] - DEPRECATED
47
     * @param {import('axios').AxiosRequestConfig} [axiosOptions] - for main request (estimation)
48
     * @param {import('axios').AxiosRequestConfig} [extraAxiosOptions] - for secondary requests (commission price data, coin IDs, and pool info)
49
     * @return {Promise<MinterFeeEstimation>}
50
     */
51
    return function estimateTxCommission(txParams, {
22✔
52
        needGasCoinFee = PRECISION.AUTO,
18✔
53
        needBaseCoinFee = PRECISION.AUTO,
94✔
54
        needPriceCoinFee = PRECISION.AUTO,
94✔
55
        loose,
56
        direct,
57
    } = {}, axiosOptions = undefined, extraAxiosOptions = undefined) {
348✔
58
        axiosOptions = {
174✔
59
            ...factoryAxiosOptions,
60
            ...axiosOptions,
61
        };
62
        if (direct !== undefined) {
174✔
63
            // eslint-disable-next-line no-console
64
            console.warn('`direct` option in `estimateTxCommission` is deprecated, use `needGasCoinFee`, `needBaseCoinFee`, and `needPriceCoinFee` options instead');
2✔
65
            needGasCoinFee = direct ? PRECISION.PRECISE : PRECISION.IMPRECISE;
2!
66
            needBaseCoinFee = direct ? PRECISION.OMIT : PRECISION.IMPRECISE;
2!
67
            needPriceCoinFee = direct ? PRECISION.OMIT : PRECISION.PRECISE;
2!
68
        }
69
        if (loose !== undefined) {
174✔
70
            // eslint-disable-next-line no-console
71
            console.warn('`loose` option in `estimateTxCommission` is deprecated, use `needGasCoinFee`, `needBaseCoinFee`, and `needPriceCoinFee` options instead');
10✔
72
            needGasCoinFee = !loose ? PRECISION.PRECISE : PRECISION.IMPRECISE;
10✔
73
            needBaseCoinFee = !loose ? PRECISION.OMIT : PRECISION.IMPRECISE;
10✔
74
            needPriceCoinFee = !loose ? PRECISION.OMIT : PRECISION.PRECISE;
10✔
75
        }
76

77
        let paramsPromise;
78
        if (typeof txParams === 'object') {
174✔
79
            /*
80
            if (loose) {
81
                paramsPromise = getCoinId(txParams.gasCoin || 0, txParams.chainId, extraAxiosOptions)
82
                    .then((coinId) => {
83
                        validateUint(coinId, 'gasCoin');
84
                        return {
85
                            ...txParams,
86
                            gasCoin: coinId,
87
                        };
88
                    });
89
            } else {
90
            */
91
            // @TODO some fields of tx data can dropped, because they don't affect fee, it will reduce coin id requests and make estimation requests more cacheable
92
            paramsPromise = replaceCoinSymbol(txParams, extraAxiosOptions);
172✔
93
            // }
94
        } else {
95
            paramsPromise = Promise.resolve(txParams);
2✔
96
        }
97

98
        return paramsPromise
174✔
99
            .then((updatedTxParams) => {
100
                if (typeof updatedTxParams !== 'object') {
174✔
101
                    return estimateFeeDirect(updatedTxParams, axiosOptions);
2✔
102
                } else {
103
                    return estimateFeeCalculate(updatedTxParams, {needGasCoinFee, needBaseCoinFee, needPriceCoinFee, axiosOptions, extraAxiosOptions});
172✔
104
                }
105
            });
106
    };
107

108
    /**
109
     * @typedef {{commission: number|string}} MinterFeeEstimationDirect
110
     */
111
    /**
112
     * @param {string|TxParams} txParams
113
     * @param {import('axios').AxiosRequestConfig} [axiosOptions]
114
     * @return {Promise<MinterFeeEstimationDirect>}
115
     */
116
    function estimateFeeDirect(txParams, axiosOptions) {
117
        if (!txParams) {
86!
118
            return Promise.reject(new Error('Transaction not specified'));
×
119
        }
120
        let tx;
121
        if (typeof txParams === 'string') {
86✔
122
            tx = txParams;
2✔
123
        } else {
124
            tx = prepareTx({
84✔
125
                ...txParams,
126
                data: fillDefaultData(txParams.type, txParams.data),
127
            }, {
128
                disableValidation: true,
129
                disableDecorationParams: true,
130
            }).serializeToString();
131
        }
132

133
        return apiInstance.get(`estimate_tx_commission/${tx}`, axiosOptions)
86✔
134
            .then((response) => {
135
                return {
86✔
136
                    commission: convertFromPip(response.data.commission),
137
                };
138
            });
139
    }
140

141
    /**
142
     * @typedef {MinterFeeEstimationDirect&{baseCoinCommission: number|string, priceCoinCommission: number|string, commissionPriceData: CommissionPriceData}} MinterFeeEstimationCalculate
143
     */
144
    /**
145
     * @param {TxParams} txParams
146
     * @param {object} [options]
147
     * @param {FEE_PRECISION_SETTING} [options.needGasCoinFee]
148
     * @param {FEE_PRECISION_SETTING} [options.needBaseCoinFee]
149
     * @param {FEE_PRECISION_SETTING} [options.needPriceCoinFee]
150
     * @param {import('axios').AxiosRequestConfig} [options.axiosOptions]
151
     * @param {import('axios').AxiosRequestConfig} [options.extraAxiosOptions] - applied to secondary requests
152
     * @return {Promise<MinterFeeEstimationCalculate>}
153
     */
154
    async function estimateFeeCalculate(txParams, {needGasCoinFee, needBaseCoinFee, needPriceCoinFee, axiosOptions, extraAxiosOptions}) {
155
        if (!txParams || typeof txParams !== 'object') {
172!
156
            throw new TypeError('Invalid txParams');
×
157
        }
158

159
        if (needPriceCoinFee === PRECISION.AUTO) {
172✔
160
            needPriceCoinFee = PRECISION.OMIT;
80✔
161
        }
162
        if (needBaseCoinFee === PRECISION.AUTO) {
172✔
163
            needBaseCoinFee = PRECISION.OMIT;
80✔
164
        }
165
        if (needGasCoinFee === PRECISION.AUTO) {
172✔
166
            needGasCoinFee = PRECISION.PRECISE;
4✔
167
        }
168

169
        const commissionPriceData = needPriceCoinFee !== PRECISION.OMIT ? await getCommissionPrice(extraAxiosOptions) : undefined;
172✔
170

171
        // coins may be same only if they both defined
172
        const sameGasAndBaseCoins = isGasCoinSameAsBaseCoin(txParams.gasCoin);
172✔
173
        const sameGasAndPriceCoins = isGasCoinSameAsPriceCoin(txParams.gasCoin, commissionPriceData);
172✔
174
        const samePriceAndBaseCoins = isPriceCoinSameAsBaseCoin(commissionPriceData);
172✔
175
        const gasDependsOnBase = sameGasAndBaseCoins && needGasCoinFee === PRECISION.IMPRECISE && needBaseCoinFee === PRECISION.IMPRECISE && commissionPriceData;
172✔
176
        const baseDependsOnGas = sameGasAndBaseCoins && needGasCoinFee === PRECISION.PRECISE;
172✔
177

178
        // priceCoin
179
        const priceCoinFee = (() => {
172✔
180
            // OMIT
181
            if (!commissionPriceData) {
172✔
182
                return undefined;
84✔
183
            }
184
            // PRECISE
185
            const feePrice = new FeePrice(commissionPriceData);
88✔
186
            return feePrice.getFeeValue(txParams.type, getFeePriceOptionsFromTxParams(txParams, true));
88✔
187
        })();
188

189
        // baseCoin
190
        let baseCoinFee = await (async () => {
172✔
191
            // PRECISE
192
            if (samePriceAndBaseCoins) {
172!
193
                return priceCoinFee;
×
194
            }
195
            if (baseDependsOnGas) {
172✔
196
                // depends on gasCoinFee, will be updated later
197
                return undefined;
84✔
198
            }
199
            // OMIT
200
            if (needBaseCoinFee === PRECISION.OMIT) {
88!
201
                return undefined;
×
202
            }
203
            // IMPRECISE
204
            if (needBaseCoinFee === PRECISION.IMPRECISE && commissionPriceData) {
88!
205
                const priceCoinPool = await getPoolInfo(0, commissionPriceData.coin.id, extraAxiosOptions);
88✔
206
                return getBaseCoinAmountFromPool(priceCoinFee, priceCoinPool);
88✔
207
            }
208
            if (needBaseCoinFee === PRECISION.IMPRECISE && !commissionPriceData) {
×
209
                // @TODO maybe just force commissionPriceData
210
                throw new Error('base coin fee imprecise estimation with omitted price coin is not implemented yet');
×
211
            }
212
            // @TODO precise estimation
213
            throw new Error('base coin fee precise estimation not implemented yet');
×
214
        })();
215

216
        // gasCoin
217
        let fee;
218
        if (sameGasAndPriceCoins) {
172!
219
            // PRECISE
220
            // actual fee may be few pips less than priceCoinFee, assume it's not worth of extra http request
221
            fee = priceCoinFee;
×
222
        } else if (gasDependsOnBase) {
172✔
223
            // IMPRECISE
224
            fee = baseCoinFee;
88✔
225
        } else if (needGasCoinFee !== PRECISION.OMIT) {
84!
226
            // PRECISE
227
            const {commission} = await estimateFeeDirect(txParams, axiosOptions);
84✔
228
            fee = commission;
84✔
229
        }
230

231
        if (baseDependsOnGas) {
172✔
232
            baseCoinFee = fee;
84✔
233
        }
234

235

236
        return {
172✔
237
            commission: fee,
238
            baseCoinCommission: baseCoinFee,
239
            priceCoinCommission: priceCoinFee,
240
            commissionPriceData,
241
        };
242
    }
243
}
244

245
/**
246
 * @param {CommissionPriceData} commissionPriceData
247
 * @return {boolean}
248
 */
249
function isPriceCoinSameAsBaseCoin(commissionPriceData) {
250
    return Number.parseInt(commissionPriceData?.coin.id, 10) === 0;
172✔
251
}
252

253
/**
254
 * @param {number|string} gasCoinId
255
 * @param {CommissionPriceData} commissionPriceData
256
 * @return {boolean}
257
 */
258
function isGasCoinSameAsPriceCoin(gasCoinId, commissionPriceData) {
259
    return Number.parseInt(gasCoinId, 10) === Number.parseInt(commissionPriceData?.coin.id, 10);
172✔
260
}
261

262
/**
263
 * @param {number|string} gasCoinId
264
 * @return {boolean}
265
 */
266
function isGasCoinSameAsBaseCoin(gasCoinId) {
267
    return Number.parseInt(gasCoinId, 10) === 0;
172✔
268
}
269

270

271
/**
272
 *
273
 * @param {number|string} priceCoinAmount
274
 * @param {PoolInfo} pool
275
 * @return {string|number}
276
 */
277
function getBaseCoinAmountFromPool(priceCoinAmount, pool) {
278
    // amount of base coin in pool
279
    const reserveBase = new Big(pool.amount0);
88✔
280
    // amount of price coin in pool
281
    const reservePrice = new Big(pool.amount1);
88✔
282
    // amount of price coin in pool
283
    const priceCoinAmountPip = new Big(priceCoinAmount);
88✔
284

285
    // @see https://github.com/MinterTeam/minter-go-node/blob/6e44d5691c9df1a9c725d0f52c5921e8523c7f18/coreV2/state/swap/swap.go#L642
286
    // reserveBase - (reservePrice * reserveBase) / (priceCoinAmount * 0.997 + reservePrice)
287
    let result = reserveBase.minus(reservePrice.times(reserveBase).div(new Big(priceCoinAmount).times(0.997).plus(reservePrice)));
88✔
288

289
    // received amount from pool rounds down, spent amount to pool rounds up
290
    // round down
291
    result = result.round(18, BIG_ROUND_DOWN);
88✔
292

293
    return result;
88✔
294
}
295

296
/**
297
 * @param {TxParams} txParams
298
 * @param {boolean} [disableValidation]
299
 * @return FeePriceOptions
300
 */
301
function getFeePriceOptionsFromTxParams(txParams, disableValidation) {
302
    const txType = txParams.type;
88✔
303
    if (!txType) {
88!
304
        throw new Error('Tx `type` not specified');
×
305
    }
306

307
    const isTickerType = txType === TX_TYPE.CREATE_COIN || txType === TX_TYPE.CREATE_TOKEN;
88✔
308
    const coinSymbol = isTickerType ? txParams.data?.symbol : undefined;
88✔
309
    if (isTickerType && !coinSymbol && !disableValidation) {
88!
310
        throw new Error('`symbol` not specified for ticker creation tx');
×
311
    }
312

313
    let deltaItemCount;
314
    if (txType === TX_TYPE.BUY_SWAP_POOL || txType === TX_TYPE.SELL_SWAP_POOL || txType === TX_TYPE.SELL_ALL_SWAP_POOL) {
88✔
315
        const coinCount = txParams.data?.coins?.length;
6✔
316
        if (!coinCount && !disableValidation) {
6!
317
            throw new Error('Invalid `coins` field in swap pool tx');
×
318
        }
319
        // count of pools
320
        deltaItemCount = coinCount - 1;
6✔
321
    }
322
    if (txType === TX_TYPE.MULTISEND) {
88✔
323
        // count of recipients
324
        deltaItemCount = txParams.data?.list?.length;
2✔
325
        if (!deltaItemCount && !disableValidation) {
2!
326
            throw new Error('Invalid `list` field in multisend tx');
×
327
        }
328
    }
329

330
    return {
88✔
331
        payload: txParams.payload,
332
        coinSymbol,
333
        deltaItemCount,
334
        fallbackOnInvalidInput: disableValidation,
335
    };
336
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc