difftreelog
test price calibration script
in: master
3 files changed
tests/.eslintrc.jsondiffbeforeafterboth1{2 "env": {3 "browser": true,4 "es2020": true5 },6 "extends": [7 "eslint:recommended",8 "plugin:@typescript-eslint/recommended"9 ],10 "parser": "@typescript-eslint/parser",11 "parserOptions": {12 "ecmaVersion": 11,13 "sourceType": "module"14 },15 "plugins": [16 "@typescript-eslint"17 ],18 "rules": {19 "indent": [20 "error",21 2,22 {23 "SwitchCase": 124 }25 ],26 "function-call-argument-newline": [27 "error",28 "consistent"29 ],30 "function-paren-newline": [31 "error",32 "multiline"33 ],34 "linebreak-style": [35 "error",36 "unix"37 ],38 "quotes": [39 "error",40 "single",41 {42 "avoidEscape": true43 }44 ],45 "semi": [46 "error",47 "always"48 ],49 "@typescript-eslint/explicit-module-boundary-types": "off",50 "comma-dangle": [51 "error",52 "always-multiline"53 ],54 "no-unused-vars": "off",55 "@typescript-eslint/no-empty-function": "off",56 "@typescript-eslint/no-non-null-assertion": "off",57 "@typescript-eslint/no-explicit-any": "off",58 "@typescript-eslint/no-unused-vars": "warn",59 "no-async-promise-executor": "warn",60 "@typescript-eslint/no-empty-interface": "off",61 "prefer-const": [62 "error",63 {64 "destructuring": "all"65 }66 ],67 "@typescript-eslint/ban-ts-comment": "off",68 "object-curly-spacing": "warn",69 "arrow-spacing": "warn",70 "array-bracket-spacing": "warn",71 "template-curly-spacing": "warn",72 "space-in-parens": "warn",73 "@typescript-eslint/naming-convention": [74 "warn",75 {76 "selector": "default",77 "format": [78 "camelCase"79 ],80 "leadingUnderscore": "allow",81 "trailingUnderscore": "allow"82 },83 {84 "selector": "variable",85 "format": [86 "camelCase",87 "UPPER_CASE"88 ],89 "leadingUnderscore": "allow",90 "trailingUnderscore": "allow"91 },92 {93 "selector": "typeLike",94 "format": [95 "PascalCase"96 ]97 },98 {99 "selector": "memberLike",100 "format": null101 }102 ]103 }104}tests/src/calibrate.tsdiffbeforeafterboth--- /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();
+ });
+})();
tests/src/calibrateApply.tsdiffbeforeafterboth--- /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(/(\/\*<weight2fee>\*\/)[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(/(\/\*<mingasprice>\*\/)[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);
+});