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 const a = rpn(130 [nb, sumxy, '*', sumx, sumy, '*', '-'],131 [nb, sumx2, '*', sumx, sumx, '*', '-'],132 '/',133 );134 const b = rpn(135 [sumy, a, sumx, '*', '-'],136 nb,137 '/',138 );139140 return {a, b};141}142143const hypothesisLinear = (a: Fract, b: Fract) => (x: Fract) => rpn(x, a, '*', b, '+');144145146147function error(points: { x: Fract, y: Fract }[], hypothesis: (a: Fract) => Fract) {148 return points.map(p => {149 const v = hypothesis(p.x);150 const vv = p.y;151152 return rpn(v, vv, '-', 'dup', '*');153 }).reduce((a, b) => a.plus(b), Fract.ZERO).sqrt().div(new Fract(BigInt(points.length)));154}155156async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {157 const alice = await privateKey('//Alice');158 const bob = await privateKey('//Bob');159 const dataPoints = [];160161 {162 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});163 const token = await collection.mintToken(alice, {Substrate: alice.address});164 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);165 await token.transfer(alice, {Substrate: bob.address});166 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);167168 console.log(`\t[NFT Transfer] Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);169 }170171 const api = helper.getApi();172 const base = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt();173 for (let i = -5; i < 5; i++) {174 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));175176 const coefficient = new Fract((await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt());177 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});178 const token = await collection.mintToken(alice, {Substrate: alice.address});179180 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);181 await token.transfer(alice, {Substrate: bob.address});182 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);183184 const transferPrice = new Fract(aliceBalanceBefore - aliceBalanceAfter);185186 dataPoints.push({x: transferPrice, y: coefficient});187 }188 const {a, b} = linearRegression(dataPoints);189190 const hyp = hypothesisLinear(a, b);191 console.log(`Error: ${error(dataPoints, hyp)}`);192193 194 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(1n, 10n), '*'));195 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toBigInt())));196197 {198 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});199 const token = await collection.mintToken(alice, {Substrate: alice.address});200 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);201 await token.transfer(alice, {Substrate: bob.address});202 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);203204 console.log(`\t[NFT Transfer] Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);205 }206}207208async function calibrateMinGasPrice(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {209 const alice = await privateKey('//Alice');210 const caller = await helper.eth.createAccountWithBalance(alice);211 const receiver = helper.eth.createAccount();212 const dataPoints = [];213214 {215 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});216 const token = await collection.mintToken(alice, {Ethereum: caller});217218 const address = helper.ethAddress.fromCollectionId(collection.collectionId);219 const contract = helper.ethNativeContract.collection(address, 'nft', caller);220221 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));222223 console.log(`\t[ETH NFT transfer] Original price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);224 }225226 const api = helper.getApi();227 228 const base = (await api.query.configuration.minGasPriceOverride() as any).toBigInt();229 for (let i = -8; i < 8; i++) {230 const gasPrice = base + base / 100000n * BigInt(i);231 const gasPriceStr = '0x' + gasPrice.toString(16);232 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));233234 const coefficient = new Fract((await api.query.configuration.minGasPriceOverride() as any).toBigInt());235 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});236 const token = await collection.mintToken(alice, {Ethereum: caller});237238 const address = helper.ethAddress.fromCollectionId(collection.collectionId);239 const contract = helper.ethNativeContract.collection(address, 'nft', caller);240241 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})));242243 dataPoints.push({x: transferPrice, y: coefficient});244 }245246 const {a, b} = linearRegression(dataPoints);247248 const hyp = hypothesisLinear(a, b);249 console.log(`Error: ${error(dataPoints, hyp)}`);250251 252 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(15n, 100n), '*'));253 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toBigInt())));254255 {256 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});257 const token = await collection.mintToken(alice, {Ethereum: caller});258259 const address = helper.ethAddress.fromCollectionId(collection.collectionId);260 const contract = helper.ethNativeContract.collection(address, 'nft', caller);261262 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));263264 console.log(`\t[ETH NFT transfer] Calibrated price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);265 }266}267268(async () => {269 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {270 271272 const iterations = 3;273274 console.log('[Calibrate WeightToFee]');275 for (let i = 0; i < iterations; i++) {276 await calibrateWeightToFee(helper, privateKey);277 }278279 console.log();280281 console.log('[Calibrate MinGasPrice]');282 for (let i = 0; i < iterations; i++) {283 await calibrateMinGasPrice(helper, privateKey);284 }285 });286})();