Skip to content
This repository has been archived by the owner on Jun 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #35 from LiskHQ/feature/q96-number-system
Browse files Browse the repository at this point in the history
Implement Q96 number format - Closes #20
  • Loading branch information
emiliolisk committed Sep 12, 2022
2 parents b97c47d + 88fcbe3 commit 21b705f
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 4 deletions.
48 changes: 45 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"@oclif/plugin-autocomplete": "0.3.0",
"@oclif/plugin-help": "3.2.2",
"axios": "0.21.2",
"bigint-buffer": "^1.1.5",
"fs-extra": "9.1.0",
"inquirer": "8.0.0",
"lisk-commander": "^6.0.0-alpha.0",
Expand Down
2 changes: 1 addition & 1 deletion src/app/modules/dex/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface FeeTiers {
[id: number]: number;
}

export type Q96 = BigInt;
export type Q96 = bigint;
export type TokenID = Buffer;
export type PoolID = Buffer;
export type PositionID = Buffer;
Expand Down
140 changes: 140 additions & 0 deletions src/app/modules/dex/utils/q96.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
/*
* Copyright © 2022 Lisk Foundation
*
* See the LICENSE file at the top-level directory of this distribution
* for licensing information.
*
* Unless otherwise agreed in a custom licensing agreement with the Lisk Foundation,
* no part of this software, including this file, may be copied, modified,
* propagated, or distributed except according to the terms contained in the
* LICENSE file.
*
* Removal or modification of this copyright notice is prohibited.
*/

import {
toBufferBE
} from 'bigint-buffer';
import {
Q96
} from '../types';
import {
MAX_NUM_BYTES_Q96
} from '../constants';
import {
ONE,
TWO,
N_96
} from "./mathConstants";


//Fixed Point Arithmetic
export const numberToQ96 = (r: bigint): Q96 => {
const _exp: bigint = TWO ** N_96;
const _r = BigInt(r);
const num: bigint = _r * _exp;

return num;
}

export const roundDownQ96 = (a: Q96): bigint => a >> N_96

export const roundUpQ96 = (a: Q96): bigint => {
const _x = ONE << N_96;
const _y = a % _x;

const _z = Number(_y);

if (_z === 0) {
return a >> N_96;
}

const _r = a >> N_96;
return _r + ONE;

}


// Arithmetic
export const addQ96 = (a: Q96, b: Q96): Q96 => a + b

export const subQ96 = (a: Q96, b: Q96): Q96 => {
if (a >= b) {
return a - b;
}

throw new Error ("Result can't be negative");
}

export const mulQ96 = (a: Q96, b: Q96): Q96 => {
const _x: bigint = a * b;
return _x >> N_96;
}

export const divQ96 = (a: Q96, b: Q96): Q96 => {
const _x: Q96 = a << N_96;
return _x / b;
}

export const mulDivQ96 = (a: Q96, b: Q96, c: Q96): bigint => {
const _x: bigint = a * b;
const _y: bigint = _x << N_96;
const _z: bigint = _y / c;

return roundDownQ96(_z);
}

export const mulDivRoundUpQ96 = (a: Q96, b: Q96, c: Q96): bigint => {
const _x: bigint = a * b;
const _y: bigint = _x << N_96;
const _z: bigint = _y / c;

return roundUpQ96(_z)
}

export const q96ToInt = (a: Q96): bigint => roundDownQ96(a)

export const q96ToIntRoundUp = (a: Q96): bigint => roundUpQ96(a)

export const invQ96 = (a: Q96): Q96 => {
const _x: bigint = ONE << N_96;
return divQ96(_x, a);
}


// Q96 Encoding and Decoding
export const bytesToQ96 = (numberBytes: Buffer): Q96 => {
if (numberBytes.length > MAX_NUM_BYTES_Q96) {
throw new Error();
}

const _hex: string[] = [];

for (let i = 0; i < numberBytes.length; i++) {
let current = numberBytes[i] < 0 ? numberBytes[i] + 256 : numberBytes[i];
_hex.push((current >>> 4).toString(16));
_hex.push((current & 0xF).toString(16));
}

const _hex_bi = _hex.join("");

//return big-endian decoding of bytes
return BigInt(`0x${_hex_bi}`);
}

export const q96ToBytes = (numberQ96: Q96): Buffer => {

const _hex: string = numberQ96.toString(16);
let _byteArr: number[] = [];

for (let c = 0; c < _hex.length; c += 2) {
_byteArr.push(parseInt(_hex.substring(c, 2), 16));
}

if (_byteArr.length > MAX_NUM_BYTES_Q96) {
throw new Error("Overflow when serializing a Q96 number")
}

// return result padded to length NUM_BYTES_Q96 with zero bytes
return toBufferBE(BigInt(`0x${_hex}`), MAX_NUM_BYTES_Q96) //big-endian encoding of numberQ96 as integer
}
69 changes: 69 additions & 0 deletions test/unit/modules/dex/q96.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
addQ96,
divQ96,
numberToQ96,
mulQ96,
subQ96,
invQ96,
bytesToQ96,
q96ToBytes,
q96ToInt,
mulDivQ96,
q96ToIntRoundUp
} from '../../../../src/app/modules/dex/utils/q96';

import {
Q96
} from '../../../../src/app/modules/dex/types';

import {
MAX_SQRT_RATIO,
MAX_UINT_64
} from '../../../../src/app/modules/dex/constants';

describe('DexQ96Module', () => {
describe('constructor', () => {
const testValueMax: Q96 = numberToQ96(MAX_SQRT_RATIO);
const testValueMaxUint: Q96 = numberToQ96(BigInt(MAX_UINT_64));
const testValue2: Q96 = numberToQ96(BigInt(2));

it('add', async () => {
expect(q96ToInt(addQ96(testValueMaxUint, testValue2))).toBe(BigInt("18446744073709551617"));
});

it('sub', async () => {
expect(q96ToInt(subQ96(testValueMax, testValue2))).toBe(BigInt("1461446703529909599612049957420313862569572983182"));
});

it('mul', async () => {
expect(q96ToInt(mulQ96(testValueMaxUint, testValue2))).toBe(BigInt("36893488147419103230"));
});

it('div', async () => {
const one = numberToQ96(BigInt(1));
const div = divQ96(one, testValue2);
expect(q96ToInt(div)).toEqual(BigInt("0"));
expect(q96ToIntRoundUp(div)).toEqual(BigInt("1"));

const test = mulQ96(testValue2, testValueMaxUint);
expect(divQ96(test, (testValueMaxUint))).toEqual(testValue2);

const zero = numberToQ96(BigInt(1));
expect(divQ96(testValue2, zero)).toThrow();
});

it('mulDiv', async () => {
const test = divQ96(testValueMax, testValueMax);
expect(mulDivQ96((test), numberToQ96(BigInt('4')), numberToQ96(BigInt('4')))).toEqual(test);
});

it('invQ96', async () => {
const three = numberToQ96(BigInt(3)); // 3 is an odd number, not nicely invertible in binary
expect(q96ToInt(invQ96(invQ96(three)))).toEqual(BigInt("3"));
});

it('bytesToQ96 and q96ToBytes', async () => {
expect(bytesToQ96(q96ToBytes(testValueMaxUint))).toBe(testValueMaxUint)
});
});
});
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"target": "es2019",
"module": "commonjs",
Expand Down

0 comments on commit 21b705f

Please sign in to comment.