1import type {IKeyringPair} from '@polkadot/types/types';2import {usingEthPlaygrounds} from '@unique/test-utils/eth/util.js';3import {EthUniqueHelper} from '@unique/test-utils/eth/index.js';45class Fract {6 static ZERO = new Fract(0n);7 constructor(public readonly a: bigint, public readonly b: bigint = 1n) {8 if(b === 0n) throw new Error('division by zero');9 if(b < 0n) throw new Error('missing normalization');10 }1112 mul(other: Fract) {13 return new Fract(this.a * other.a, this.b * other.b).optimize();14 }1516 div(other: Fract) {17 return this.mul(other.inv());18 }1920 plus(other: Fract) {21 if(this.b === other.b) {22 return new Fract(this.a + other.a, this.b);23 }24 return new Fract(this.a * other.b + other.a * this.b, this.b * other.b).optimize();25 }2627 minus(other: Fract) {28 return this.plus(other.neg());29 }3031 neg() {32 return new Fract(-this.a, this.b);33 }34 inv() {35 if(this.a < 0) {36 return new Fract(-this.b, -this.a);37 } else {38 return new Fract(this.b, this.a);39 }40 }4142 optimize() {43 function gcd(x: bigint, y: bigint) {44 if(x < 0n)45 x = -x;46 if(y < 0n)47 y = -y;48 while(y) {49 const t = y;50 y = x % y;51 x = t;52 }53 return x;54 }55 const v = gcd(this.a, this.b);56 return new Fract(this.a / v, this.b / v);57 }5859 toBigInt() {60 return this.a / this.b;61 }62 toNumber() {63 const v = this.optimize();64 return Number(v.a) / Number(v.b);65 }66 toString() {67 const v = this.optimize();68 return `${v.a} / ${v.b}`;69 }7071 lt(other: Fract) {72 return this.a * other.b < other.a * this.b;73 }74 eq(other: Fract) {75 return this.a * other.b === other.a * this.b;76 }7778 sqrt() {79 if(this.a < 0n) {80 throw new Error('square root of negative numbers is not supported');81 }8283 if(this.lt(new Fract(2n))) {84 return this;85 }8687 function newtonIteration(n: Fract, x0: Fract): Fract {88 const x1 = rpn(n, x0, '/', x0, '+', new Fract(2n), '/');89 if(x0.eq(x1) || x0.eq(x1.minus(new Fract(1n)))) {90 return x0;91 }92 return newtonIteration(n, x1);93 }9495 return newtonIteration(this, new Fract(1n));96 }97}9899type Op = Fract | '+' | '-' | '*' | '/' | 'dup' | Op[];100function rpn(...ops: (Op)[]) {101 const stack: Fract[] = [];102 for(const op of ops) {103 if(op instanceof Fract) {104 stack.push(op);105 } else if(op === '+') {106 if(stack.length < 2)107 throw new Error('stack underflow');108 const b = stack.pop()!;109 const a = stack.pop()!;110 stack.push(a.plus(b));111 } else if(op === '*') {112 if(stack.length < 2)113 throw new Error('stack underflow');114 const b = stack.pop()!;115 const a = stack.pop()!;116 stack.push(a.mul(b));117 } else if(op === '-') {118 if(stack.length < 2)119 throw new Error('stack underflow');120 const b = stack.pop()!;121 const a = stack.pop()!;122 stack.push(a.minus(b));123 } else if(op === '/') {124 if(stack.length < 2)125 throw new Error('stack underflow');126 const b = stack.pop()!;127 const a = stack.pop()!;128 stack.push(a.div(b));129 } else if(op === 'dup') {130 if(stack.length < 1)131 throw new Error('stack underflow');132 const a = stack.pop()!;133 stack.push(a);134 stack.push(a);135 } else if(Array.isArray(op)) {136 stack.push(rpn(...op));137 } else {138 throw new Error(`unknown operand: ${op}`);139 }140 }141 if(stack.length != 1)142 throw new Error('one element should be left on stack');143 return stack[0]!;144}145146function linearRegression(points: { x: Fract, y: Fract }[]) {147 let sumxy = Fract.ZERO;148 let sumx = Fract.ZERO;149 let sumy = Fract.ZERO;150 let sumx2 = Fract.ZERO;151 const n = points.length;152 for(let i = 0; i < n; i++) {153 const p = points[i];154 sumxy = rpn(p.x, p.y, '*', sumxy, '+');155 sumx = sumx.plus(p.x);156 sumy = sumy.plus(p.y);157 sumx2 = rpn(p.x, p.x, '*', sumx2, '+');158 }159160 const nb = new Fract(BigInt(n));161162 const a = rpn(163 [nb, sumxy, '*', sumx, sumy, '*', '-'],164 [nb, sumx2, '*', sumx, sumx, '*', '-'],165 '/',166 );167 const b = rpn(168 [sumy, a, sumx, '*', '-'],169 nb,170 '/',171 );172173 return {a, b};174}175176const hypothesisLinear = (a: Fract, b: Fract) => (x: Fract) => rpn(x, a, '*', b, '+');177178179180181182183184185186187async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {188 const alice = await privateKey('//Alice');189 const bob = await privateKey('//Bob');190 const dataPoints = [];191192 {193 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});194 const token = await collection.mintToken(alice, {Substrate: alice.address});195 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);196 await token.transfer(alice, {Substrate: bob.address});197 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);198199 console.log(`\t[NFT transfer] Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);200 }201202 const api = helper.getApi();203 const base = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt();204 for(let i = -5; i < 5; i++) {205 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));206207 const coefficient = new Fract((await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt());208 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});209 const token = await collection.mintToken(alice, {Substrate: alice.address});210211 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);212 await token.transfer(alice, {Substrate: bob.address});213 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);214215 const transferPrice = new Fract(aliceBalanceBefore - aliceBalanceAfter);216217 dataPoints.push({x: transferPrice, y: coefficient});218 }219 const {a, b} = linearRegression(dataPoints);220221 const hyp = hypothesisLinear(a, b);222 223224 225 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(1n, 10n), '*'));226 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toBigInt())));227228 {229 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});230 const token = await collection.mintToken(alice, {Substrate: alice.address});231 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);232 await token.transfer(alice, {Substrate: bob.address});233 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);234235 console.log(`\t[NFT transfer] Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);236 }237}238239async function calibrateMinGasPrice(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {240 const alice = await privateKey('//Alice');241 const caller = await helper.eth.createAccountWithBalance(alice);242 const receiver = helper.eth.createAccount();243 const dataPoints = [];244245 {246 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});247 const token = await collection.mintToken(alice, {Ethereum: caller});248249 const address = helper.ethAddress.fromCollectionId(collection.collectionId);250 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);251252 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));253254 console.log(`\t[ETH NFT transfer] Original price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);255 }256257 const api = helper.getApi();258 259 const base = (await api.query.configuration.minGasPriceOverride() as any).toBigInt();260 for(let i = -8; i < 8; i++) {261 const gasPrice = base + base / 100000n * BigInt(i);262 const gasPriceStr = '0x' + gasPrice.toString(16);263 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));264265 const coefficient = new Fract((await api.query.configuration.minGasPriceOverride() as any).toBigInt());266 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});267 const token = await collection.mintToken(alice, {Ethereum: caller});268269 const address = helper.ethAddress.fromCollectionId(collection.collectionId);270 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);271272 const transferPrice = new Fract(await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gasPrice: gasPriceStr, gas: helper.eth.DEFAULT_GAS})));273274 dataPoints.push({x: transferPrice, y: coefficient});275 }276277 const {a, b} = linearRegression(dataPoints);278279 const hyp = hypothesisLinear(a, b);280 281282 283 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(15n, 100n), '*'));284 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toBigInt())));285286 {287 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});288 const token = await collection.mintToken(alice, {Ethereum: caller});289290 const address = helper.ethAddress.fromCollectionId(collection.collectionId);291 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);292293 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));294295 console.log(`\t[ETH NFT transfer] Calibrated price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);296 }297}298299300(async () => {301 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {302 303304 const iterations = 3;305306 console.log('[Calibrate WeightToFee]');307 for(let i = 0; i < iterations; i++) {308 await calibrateWeightToFee(helper, privateKey);309 }310311 console.log();312313 console.log('[Calibrate MinGasPrice]');314 for(let i = 0; i < iterations; i++) {315 await calibrateMinGasPrice(helper, privateKey);316 }317 });318})();