1import {IKeyringPair} from '@polkadot/types/types';2import {usingEthPlaygrounds, EthUniqueHelper} from './eth/util';34class Fract {5 static ZERO = new Fract(0n);6 constructor(public readonly a: bigint, public readonly b: bigint = 1n) {7 if (b === 0n) throw new Error('division by zero');8 if (b < 0n) throw new Error('missing normalization');9 }1011 mul(other: Fract) {12 return new Fract(this.a * other.a, this.b * other.b);13 }1415 div(other: Fract) {16 return this.mul(other.inv());17 }1819 plus(other: Fract) {20 return new Fract(this.a * other.b + other.a * this.b, this.b * other.b);21 }2223 minus(other: Fract) {24 return this.plus(other.neg());25 }2627 neg() {28 return new Fract(-this.a, this.b);29 }30 inv() {31 return new Fract(this.b, this.a);32 }3334 toBigInt() {35 return this.a / this.b;36 }3738 lt(other: Fract) {39 return this.a * other.b < other.a * this.b;40 }41 eq(other: Fract) {42 return this.a * other.b === other.a * this.b;43 }4445 sqrt() {46 if (this.a < 0n) {47 throw 'square root of negative numbers is not supported';48 }4950 if (this.lt(new Fract(2n))) {51 return this;52 }5354 function newtonIteration(n: Fract, x0: Fract): Fract {55 const x1 = rpn(n, x0, '/', x0, '+', new Fract(2n), '/');56 if (x0.eq(x1) || x0.eq(x1.minus(new Fract(1n)))) {57 return x0;58 }59 return newtonIteration(n, x1);60 }6162 return newtonIteration(this, new Fract(1n));63 }64}6566type Op = Fract | '+' | '-' | '*' | '/' | 'dup' | Op[];67function rpn(...ops: (Op)[]) {68 const stack: Fract[] = [];69 for (const op of ops) {70 if (op instanceof Fract) {71 stack.push(op);72 } else if (op === '+') {73 if (stack.length < 2)74 throw new Error('stack underflow');75 const a = stack.pop()!;76 const b = stack.pop()!;77 stack.push(a.plus(b));78 } else if (op === '*') {79 if (stack.length < 2)80 throw new Error('stack underflow');81 const a = stack.pop()!;82 const b = stack.pop()!;83 stack.push(a.mul(b));84 } else if (op === '-') {85 if (stack.length < 2)86 throw new Error('stack underflow');87 const a = stack.pop()!;88 const b = stack.pop()!;89 stack.push(a.minus(b));90 } else if (op === '/') {91 if (stack.length < 2)92 throw new Error('stack underflow');93 const a = stack.pop()!;94 const b = stack.pop()!;95 stack.push(a.div(b));96 } else if (op === 'dup') {97 if (stack.length < 1)98 throw new Error('stack underflow');99 const a = stack.pop()!;100 stack.push(a);101 stack.push(a);102 } else if (Array.isArray(op)) {103 stack.push(rpn(...op));104 } else {105 throw new Error(`unknown operand: ${op}`);106 }107 }108 if (stack.length != 1)109 throw new Error('one element should be left on stack');110 return stack[0]!;111}112113function linearRegression(points: { x: Fract, y: Fract }[]) {114 let sumxy = Fract.ZERO;115 let sumx = Fract.ZERO;116 let sumy = Fract.ZERO;117 let sumx2 = Fract.ZERO;118 const n = points.length;119 for (let i = 0; i < n; i++) {120 const p = points[i];121 sumxy = rpn(p.x, p.y, '*', sumxy, '+');122 sumx = sumx.plus(p.x);123 sumy = sumy.plus(p.y);124 sumx2 = rpn(p.x, p.x, '*', sumx2, '+');125 }126127 const nb = new Fract(BigInt(n));128129 130 131 132 133134 const a = rpn(135 [nb, sumxy, '*', sumx, sumy, '*', '-'],136 [nb, sumx, '*', sumx, sumx, '*', '-'],137 '/',138 );139 const b = rpn(140 [sumy, a, sumx, '*', '-'],141 nb,142 '/',143 );144145 return {a, b};146}147148const hypothesisLinear = (a: Fract, b: Fract) => (x: Fract) => rpn(x, a, '*', b, '+');149150151152function error(points: { x: Fract, y: Fract }[], hypothesis: (a: Fract) => Fract) {153 return points.map(p => {154 const v = hypothesis(p.x);155 const vv = p.y;156157 return rpn(v, vv, '-', 'dup', '*');158 }).reduce((a, b) => a.plus(b), Fract.ZERO).sqrt().div(new Fract(BigInt(points.length)));159}160161async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {162 const alice = await privateKey('//Alice');163 const bob = await privateKey('//Bob');164 const dataPoints = [];165166 {167 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});168 const token = await collection.mintToken(alice, {Substrate: alice.address});169 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);170 await token.transfer(alice, {Substrate: bob.address});171 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);172173 console.log(`\t[NFT Transfer] Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);174 }175176 const api = helper.getApi();177 const base = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt();178 for (let i = -5; i < 5; i++) {179 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));180181 const coefficient = new Fract((await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt());182 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});183 const token = await collection.mintToken(alice, {Substrate: alice.address});184185 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);186 await token.transfer(alice, {Substrate: bob.address});187 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);188189 const transferPrice = new Fract(aliceBalanceBefore - aliceBalanceAfter);190191 dataPoints.push({x: transferPrice, y: coefficient});192 }193 const {a, b} = linearRegression(dataPoints);194195 const hyp = hypothesisLinear(a, b);196 console.log(`Error: ${error(dataPoints, hyp)}`);197198 199 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(1n, 10n), '*'));200 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toBigInt())));201202 {203 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});204 const token = await collection.mintToken(alice, {Substrate: alice.address});205 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);206 await token.transfer(alice, {Substrate: bob.address});207 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);208209 console.log(`\t[NFT Transfer] Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);210 }211}212213async function calibrateMinGasPrice(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {214 const alice = await privateKey('//Alice');215 const caller = await helper.eth.createAccountWithBalance(alice);216 const receiver = helper.eth.createAccount();217 const dataPoints = [];218219 {220 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});221 const token = await collection.mintToken(alice, {Ethereum: caller});222223 const address = helper.ethAddress.fromCollectionId(collection.collectionId);224 const contract = helper.ethNativeContract.collection(address, 'nft', caller);225226 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));227228 console.log(`\t[ETH NFT transfer] Original price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);229 }230231 const api = helper.getApi();232 233 const base = (await api.query.configuration.minGasPriceOverride() as any).toBigInt();234 for (let i = -8; i < 8; i++) {235 const gasPrice = base + base / 100000n * BigInt(i);236 const gasPriceStr = '0x' + gasPrice.toString(16);237 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));238239 const coefficient = new Fract((await api.query.configuration.minGasPriceOverride() as any).toBigInt());240 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});241 const token = await collection.mintToken(alice, {Ethereum: caller});242243 const address = helper.ethAddress.fromCollectionId(collection.collectionId);244 const contract = helper.ethNativeContract.collection(address, 'nft', caller);245246 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})));247248 dataPoints.push({x: transferPrice, y: coefficient});249 }250251 const {a, b} = linearRegression(dataPoints);252253 const hyp = hypothesisLinear(a, b);254 console.log(`Error: ${error(dataPoints, hyp)}`);255256 257 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(15n, 100n), '*'));258 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toBigInt())));259260 {261 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});262 const token = await collection.mintToken(alice, {Ethereum: caller});263264 const address = helper.ethAddress.fromCollectionId(collection.collectionId);265 const contract = helper.ethNativeContract.collection(address, 'nft', caller);266267 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));268269 console.log(`\t[ETH NFT transfer] Calibrated price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);270 }271}272273(async () => {274 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {275 276277 const iterations = 3;278279 console.log('[Calibrate WeightToFee]');280 for (let i = 0; i < iterations; i++) {281 await calibrateWeightToFee(helper, privateKey);282 }283284 console.log();285286 console.log('[Calibrate MinGasPrice]');287 for (let i = 0; i < iterations; i++) {288 await calibrateMinGasPrice(helper, privateKey);289 }290 });291})();