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).optimize();13 }1415 div(other: Fract) {16 return this.mul(other.inv());17 }1819 plus(other: Fract) {20 if(this.b === other.b) {21 return new Fract(this.a + other.a, this.b);22 }23 return new Fract(this.a * other.b + other.a * this.b, this.b * other.b).optimize();24 }2526 minus(other: Fract) {27 return this.plus(other.neg());28 }2930 neg() {31 return new Fract(-this.a, this.b);32 }33 inv() {34 if(this.a < 0) {35 return new Fract(-this.b, -this.a);36 } else {37 return new Fract(this.b, this.a);38 }39 }4041 optimize() {42 function gcd(x: bigint, y: bigint) {43 if(x < 0n)44 x = -x;45 if(y < 0n)46 y = -y;47 while(y) {48 const t = y;49 y = x % y;50 x = t;51 }52 return x;53 }54 const v = gcd(this.a, this.b);55 return new Fract(this.a / v, this.b / v);56 }5758 toBigInt() {59 return this.a / this.b;60 }61 toNumber() {62 const v = this.optimize();63 return Number(v.a) / Number(v.b);64 }65 toString() {66 const v = this.optimize();67 return `${v.a} / ${v.b}`;68 }6970 lt(other: Fract) {71 return this.a * other.b < other.a * this.b;72 }73 eq(other: Fract) {74 return this.a * other.b === other.a * this.b;75 }7677 sqrt() {78 if(this.a < 0n) {79 throw new Error('square root of negative numbers is not supported');80 }8182 if(this.lt(new Fract(2n))) {83 return this;84 }8586 function newtonIteration(n: Fract, x0: Fract): Fract {87 const x1 = rpn(n, x0, '/', x0, '+', new Fract(2n), '/');88 if(x0.eq(x1) || x0.eq(x1.minus(new Fract(1n)))) {89 return x0;90 }91 return newtonIteration(n, x1);92 }9394 return newtonIteration(this, new Fract(1n));95 }96}9798type Op = Fract | '+' | '-' | '*' | '/' | 'dup' | Op[];99function rpn(...ops: (Op)[]) {100 const stack: Fract[] = [];101 for(const op of ops) {102 if(op instanceof Fract) {103 stack.push(op);104 } else if(op === '+') {105 if(stack.length < 2)106 throw new Error('stack underflow');107 const b = stack.pop()!;108 const a = stack.pop()!;109 stack.push(a.plus(b));110 } else if(op === '*') {111 if(stack.length < 2)112 throw new Error('stack underflow');113 const b = stack.pop()!;114 const a = stack.pop()!;115 stack.push(a.mul(b));116 } else if(op === '-') {117 if(stack.length < 2)118 throw new Error('stack underflow');119 const b = stack.pop()!;120 const a = stack.pop()!;121 stack.push(a.minus(b));122 } else if(op === '/') {123 if(stack.length < 2)124 throw new Error('stack underflow');125 const b = stack.pop()!;126 const a = stack.pop()!;127 stack.push(a.div(b));128 } else if(op === 'dup') {129 if(stack.length < 1)130 throw new Error('stack underflow');131 const a = stack.pop()!;132 stack.push(a);133 stack.push(a);134 } else if(Array.isArray(op)) {135 stack.push(rpn(...op));136 } else {137 throw new Error(`unknown operand: ${op}`);138 }139 }140 if(stack.length != 1)141 throw new Error('one element should be left on stack');142 return stack[0]!;143}144145function linearRegression(points: { x: Fract, y: Fract }[]) {146 let sumxy = Fract.ZERO;147 let sumx = Fract.ZERO;148 let sumy = Fract.ZERO;149 let sumx2 = Fract.ZERO;150 const n = points.length;151 for(let i = 0; i < n; i++) {152 const p = points[i];153 sumxy = rpn(p.x, p.y, '*', sumxy, '+');154 sumx = sumx.plus(p.x);155 sumy = sumy.plus(p.y);156 sumx2 = rpn(p.x, p.x, '*', sumx2, '+');157 }158159 const nb = new Fract(BigInt(n));160161 const a = rpn(162 [nb, sumxy, '*', sumx, sumy, '*', '-'],163 [nb, sumx2, '*', sumx, sumx, '*', '-'],164 '/',165 );166 const b = rpn(167 [sumy, a, sumx, '*', '-'],168 nb,169 '/',170 );171172 return {a, b};173}174175const hypothesisLinear = (a: Fract, b: Fract) => (x: Fract) => rpn(x, a, '*', b, '+');176177function _error(points: { x: Fract, y: Fract }[], hypothesis: (a: Fract) => Fract) {178 return points.map(p => {179 const v = hypothesis(p.x);180 const vv = p.y;181182 return rpn(v, vv, '-', 'dup', '*');183 }).reduce((a, b) => a.plus(b), Fract.ZERO).sqrt().div(new Fract(BigInt(points.length)));184}185186async function calibrateWeightToFee(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {187 const alice = await privateKey('//Alice');188 const bob = await privateKey('//Bob');189 const dataPoints = [];190191 {192 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});193 const token = await collection.mintToken(alice, {Substrate: alice.address});194 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);195 await token.transfer(alice, {Substrate: bob.address});196 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);197198 console.log(`\t[NFT transfer] Original price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);199 }200201 const api = helper.getApi();202 const base = (await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt();203 for(let i = -5; i < 5; i++) {204 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(base + base / 1000n * BigInt(i))));205206 const coefficient = new Fract((await api.query.configuration.weightToFeeCoefficientOverride() as any).toBigInt());207 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});208 const token = await collection.mintToken(alice, {Substrate: alice.address});209210 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);211 await token.transfer(alice, {Substrate: bob.address});212 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);213214 const transferPrice = new Fract(aliceBalanceBefore - aliceBalanceAfter);215216 dataPoints.push({x: transferPrice, y: coefficient});217 }218 const {a, b} = linearRegression(dataPoints);219220 const hyp = hypothesisLinear(a, b);221 222223 224 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(1n, 10n), '*'));225 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setWeightToFeeCoefficientOverride(perfectValue.toBigInt())));226227 {228 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});229 const token = await collection.mintToken(alice, {Substrate: alice.address});230 const aliceBalanceBefore = await helper.balance.getSubstrate(alice.address);231 await token.transfer(alice, {Substrate: bob.address});232 const aliceBalanceAfter = await helper.balance.getSubstrate(alice.address);233234 console.log(`\t[NFT transfer] Calibrated price: ${Number(aliceBalanceBefore - aliceBalanceAfter) / Number(helper.balance.getOneTokenNominal())} UNQ`);235 }236}237238async function calibrateMinGasPrice(helper: EthUniqueHelper, privateKey: (account: string) => Promise<IKeyringPair>) {239 const alice = await privateKey('//Alice');240 const caller = await helper.eth.createAccountWithBalance(alice);241 const receiver = helper.eth.createAccount();242 const dataPoints = [];243244 {245 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});246 const token = await collection.mintToken(alice, {Ethereum: caller});247248 const address = helper.ethAddress.fromCollectionId(collection.collectionId);249 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);250251 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));252253 console.log(`\t[ETH NFT transfer] Original price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);254 }255256 const api = helper.getApi();257 258 const base = (await api.query.configuration.minGasPriceOverride() as any).toBigInt();259 for(let i = -8; i < 8; i++) {260 const gasPrice = base + base / 100000n * BigInt(i);261 const gasPriceStr = '0x' + gasPrice.toString(16);262 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(gasPrice)));263264 const coefficient = new Fract((await api.query.configuration.minGasPriceOverride() as any).toBigInt());265 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});266 const token = await collection.mintToken(alice, {Ethereum: caller});267268 const address = helper.ethAddress.fromCollectionId(collection.collectionId);269 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);270271 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})));272273 dataPoints.push({x: transferPrice, y: coefficient});274 }275276 const {a, b} = linearRegression(dataPoints);277278 const hyp = hypothesisLinear(a, b);279 280281 282 const perfectValue = hyp(rpn(new Fract(helper.balance.getOneTokenNominal()), new Fract(15n, 100n), '*'));283 await helper.signTransaction(alice, api.tx.sudo.sudo(api.tx.configuration.setMinGasPriceOverride(perfectValue.toBigInt())));284285 {286 const collection = await helper.nft.mintCollection(alice, {name: 'New', description: 'New collection', tokenPrefix: 'NEW'});287 const token = await collection.mintToken(alice, {Ethereum: caller});288289 const address = helper.ethAddress.fromCollectionId(collection.collectionId);290 const contract = await helper.ethNativeContract.collection(address, 'nft', caller);291292 const cost = await helper.eth.calculateFee({Ethereum: caller}, () => contract.methods.transfer(receiver, token.tokenId).send({from: caller, gas: helper.eth.DEFAULT_GAS}));293294 console.log(`\t[ETH NFT transfer] Calibrated price: ${Number(cost) / Number(helper.balance.getOneTokenNominal())} UNQ`);295 }296}297298299(async () => {300 await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {301 302303 const iterations = 3;304305 console.log('[Calibrate WeightToFee]');306 for(let i = 0; i < iterations; i++) {307 await calibrateWeightToFee(helper, privateKey);308 }309310 console.log();311312 console.log('[Calibrate MinGasPrice]');313 for(let i = 0; i < iterations; i++) {314 await calibrateMinGasPrice(helper, privateKey);315 }316 });317})();