git.delta.rocks / unique-network / refs/commits / d561386d7f33

difftreelog

source

js-packages/scripts/calibrate.ts11.1 KiBsourcehistory
1import type {IKeyringPair} from '@polkadot/types/types';2import {usingEthPlaygrounds} from '@unique/tests/eth/util/index.js';3import {EthUniqueHelper} from '@unique/tests//eth/util/playgrounds/unique.dev.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, '+');177178// function error(points: { x: Fract, y: Fract }[], hypothesis: (a: Fract) => Fract) {179//   return points.map(p => {180//     const v = hypothesis(p.x);181//     const vv = p.y;182183//     return rpn(v, vv, '-', 'dup', '*');184//   }).reduce((a, b) => a.plus(b), Fract.ZERO).sqrt().div(new Fract(BigInt(points.length)));185// }186187async 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  // console.log(`\t[NFT transfer] Error: ${_error(dataPoints, hyp).toNumber()}`);223224  // 0.1 UNQ225  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  // const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt();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  // console.log(`\t[ETH NFT transfer] Error: ${_error(dataPoints, hyp).toNumber()}`);281282  // 0.15 UNQ283  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}298299// eslint-disable-next-line @typescript-eslint/no-floating-promises300(async () => {301  await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {302    // Subsequent runs reduce error, as price line is not actually straight, this is a curve303304    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})();