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

difftreelog

source

tests/src/calibrate.ts10.3 KiBsourcehistory
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, '+');144145// JS has no builtin function to calculate sqrt of bigint146// https://stackoverflow.com/a/53684036/6190169147function 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  // 0.1 UNQ194  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  // const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt();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  // 0.15 UNQ252  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    // Subsequent runs reduce error, as price line is not actually straight, this is a curve271272    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})();