--- a/tests/.eslintrc.json +++ b/tests/.eslintrc.json @@ -55,7 +55,13 @@ "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "varsIgnorePattern": "_.+", + "argsIgnorePattern": "_.+" + } + ], "no-async-promise-executor": "warn", "@typescript-eslint/no-empty-interface": "off", "prefer-const": [ --- /dev/null +++ b/tests/src/calibrate.ts @@ -0,0 +1,182 @@ +import {ApiPromise} from '@polkadot/api'; +import {IKeyringPair} from '@polkadot/types/types'; +import Web3 from 'web3'; +import {collectionIdToAddress, createEthAccount, createEthAccountWithBalance, GAS_ARGS, recordEthFee, usingWeb3} from './eth/util/helpers'; +import usingApi, {executeTransaction} from './substrate/substrate-api'; +import {createCollectionExpectSuccess, createItemExpectSuccess, transferExpectSuccess, UNIQUE, waitNewBlocks} from './util/helpers'; +import nonFungibleAbi from './eth/nonFungibleAbi.json'; + +function linearRegression(points: { x: bigint, y: bigint }[]) { + let sumxy = 0n; + let sumx = 0n; + let sumy = 0n; + let sumx2 = 0n; + const n = points.length; + for (let i = 0; i < n; i++) { + const p = points[i]; + sumxy += p.x * p.y; + sumx += p.x; + sumy += p.y; + sumx2 += p.x * p.x; + } + + const nb = BigInt(n); + + const a = (nb * sumxy - sumx * sumy) / (nb * sumx2 - sumx * sumx); + const b = (sumy - a * sumx) / nb; + + return {a, b}; +} + +// JS has no builtin function to calculate sqrt of bigint +// https://stackoverflow.com/a/53684036/6190169 +function sqrt(value: bigint) { + if (value < 0n) { + throw 'square root of negative numbers is not supported'; + } + + if (value < 2n) { + return value; + } + + function newtonIteration(n: bigint, x0: bigint): bigint { + const x1 = ((n / x0) + x0) >> 1n; + if (x0 === x1 || x0 === (x1 - 1n)) { + return x0; + } + return newtonIteration(n, x1); + } + + return newtonIteration(value, 1n); +} + +function _error(points: { x: bigint, y: bigint }[], hypothesis: (a: bigint) => bigint) { + return sqrt(points.map(p => { + const v = hypothesis(p.x); + const vv = p.y; + + return (v - vv) ** 2n; + }).reduce((a, b) => a + b, 0n) / BigInt(points.length)); +} + +async function calibrateWeightToFee(api: ApiPromise, privateKey: (account: string) => IKeyringPair) { + const alice = privateKey('//Alice'); + const bob = privateKey('//Bob'); + const dataPoints = []; + + { + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); + const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); + await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); + const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + + console.log(`Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE)} UNQ`); + } + + const defaultCoeff = (api.consts.configuration.defaultWeightToFeeCoefficient as any).toBigInt(); + for (let i = -5; i < 5; i++) { + await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(defaultCoeff + defaultCoeff / 1000n * BigInt(i)))); + + const coefficient = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt(); + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); + + const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); + await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); + const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + + const transferPrice = aliceBalanceBefore - aliceBalanceAfter; + + dataPoints.push({x: transferPrice, y: coefficient}); + } + const {a, b} = linearRegression(dataPoints); + + // console.log(`Error: ${error(dataPoints, x => a*x+b)}`); + + const perfectValue = a * UNIQUE / 10n + b; + await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toString()))); + + { + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT'); + const aliceBalanceBefore = (await api.query.system.account(alice.address)).data.free.toBigInt(); + await transferExpectSuccess(collectionId, tokenId, alice, bob, 1, 'NFT'); + const aliceBalanceAfter = (await api.query.system.account(alice.address)).data.free.toBigInt(); + + console.log(`Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(UNIQUE)} UNQ`); + } +} + +async function calibrateMinGasPrice(api: ApiPromise, web3: Web3, privateKey: (account: string) => IKeyringPair) { + const alice = privateKey('//Alice'); + const caller = await createEthAccountWithBalance(api, web3, privateKey); + const receiver = createEthAccount(web3); + const dataPoints = []; + + { + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + + const address = collectionIdToAddress(collectionId); + const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS}); + + const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + + console.log(`Original price: ${Number(cost) / Number(UNIQUE)} UNQ`); + } + + const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt(); + for (let i = -8; i < 8; i++) { + const gasPrice = defaultCoeff + defaultCoeff / 100000n * BigInt(i); + const gasPriceStr = '0x' + gasPrice.toString(16); + await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice))); + + const coefficient = (await api.query.configuration.minGasPriceOverride() as any).toBigInt(); + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + + const address = collectionIdToAddress(collectionId); + const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, gasPrice: gasPriceStr, ...GAS_ARGS}); + + const transferPrice = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + + dataPoints.push({x: transferPrice, y: coefficient}); + } + + const {a, b} = linearRegression(dataPoints); + + // console.log(`Error: ${error(dataPoints, x => a*x+b)}`); + + // * 0.15 = * 10000 / 66666 + const perfectValue = a * UNIQUE * 1000000n / 6666666n + b; + await executeTransaction(api, alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toString()))); + + { + const collectionId = await createCollectionExpectSuccess(); + const tokenId = await createItemExpectSuccess(alice, collectionId, 'NFT', {Ethereum: caller}); + + const address = collectionIdToAddress(collectionId); + const contract = new web3.eth.Contract(nonFungibleAbi as any, address, {from: caller, ...GAS_ARGS}); + + const cost = await recordEthFee(api, caller, () => contract.methods.transfer(receiver, tokenId).send(caller)); + + console.log(`Calibrated price: ${Number(cost) / Number(UNIQUE)} UNQ`); + } +} + +(async () => { + await usingApi(async (api, privateKey) => { + // Second run slightly reduces error sometimes, as price line is not actually straight, this is a curve + + await calibrateWeightToFee(api, privateKey); + await calibrateWeightToFee(api, privateKey); + + await usingWeb3(async web3 => { + await calibrateMinGasPrice(api, web3, privateKey); + await calibrateMinGasPrice(api, web3, privateKey); + }); + + await api.disconnect(); + }); +})(); --- /dev/null +++ b/tests/src/calibrateApply.ts @@ -0,0 +1,38 @@ +import {readFile, writeFile} from 'fs/promises'; +import path from 'path'; +import usingApi from './substrate/substrate-api'; + +const formatNumber = (num: string): string => num.split('').reverse().join('').replace(/([0-9]{3})/g, '$1_').split('').reverse().join('').replace(/^_/, ''); + +(async () => { + let weightToFeeCoefficientOverride: string; + let minGasPriceOverride: string; + await usingApi(async (api, _privateKey) => { + weightToFeeCoefficientOverride = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt().toString(); + minGasPriceOverride = (await api.query.configuration.minGasPriceOverride() as any).toBigInt().toString(); + }); + const constantsFile = path.resolve(__dirname, '../../primitives/common/src/constants.rs'); + let constants = (await readFile(constantsFile)).toString(); + + let weight2feeFound = false; + constants = constants.replace(/(\/\*\*\/)[0-9_]+(\/\*<\/weight2fee>\*\/)/, (_f, p, s) => { + weight2feeFound = true; + return p+formatNumber(weightToFeeCoefficientOverride)+s; + }); + if (!weight2feeFound) { + throw new Error('failed to find weight2fee marker in source code'); + } + + let minGasPriceFound = false; + constants = constants.replace(/(\/\*\*\/)[0-9_]+(\/\*<\/mingasprice>\*\/)/, (_f, p, s) => { + minGasPriceFound = true; + return p+formatNumber(minGasPriceOverride)+s; + }); + if (!minGasPriceFound) { + throw new Error('failed to find mingasprice marker in source code'); + } + + await writeFile(constantsFile, constants); +})().catch(e => { + console.log(e.stack); +});