git.delta.rocks / unique-network / refs/commits / 1a014777c169

difftreelog

source

tests/src/calibrate.ts10.7 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  // This is a workaround to beat the lack of precision of the `Number` type.130  // We divide `BigInt`s. But since it is an integer division, we should take care of the precision on our own.131  // After the division we can convert the result back to the `Number` and then we set the correct precision.132  // It is crucial to have the correct slope for the regression line.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, '+');149150// JS has no builtin function to calculate sqrt of bigint151// https://stackoverflow.com/a/53684036/6190169152function 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  // 0.1 UNQ199  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  // const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt();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  // 0.15 UNQ257  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    // Subsequent runs reduce error, as price line is not actually straight, this is a curve276277    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})();