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

difftreelog

source

tests/src/calibrate.ts11.0 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).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  // console.log(`\t[NFT transfer] Error: ${_error(dataPoints, hyp).toNumber()}`);222223  // 0.1 UNQ224  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  // const defaultCoeff = (api.consts.configuration.defaultMinGasPrice as any).toBigInt();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  // console.log(`\t[ETH NFT transfer] Error: ${_error(dataPoints, hyp).toNumber()}`);280281  // 0.15 UNQ282  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}297298// eslint-disable-next-line @typescript-eslint/no-floating-promises299(async () => {300  await usingEthPlaygrounds(async (helper: EthUniqueHelper, privateKey) => {301    // Subsequent runs reduce error, as price line is not actually straight, this is a curve302303    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})();