From b04f6f2fbaf436332762f193288bc5adb2d8c098 Mon Sep 17 00:00:00 2001 From: prathamesh0 <42446521+prathamesh0@users.noreply.github.com> Date: Thu, 25 Nov 2021 16:19:45 +0530 Subject: [PATCH] Implement bigDecimal host API (#62) * Handle negative bigInt in store host API * Increase size of bigInt in fromString API to support UInt256 * Implement bigDecimal divideBy host API * Implement bigDecimal plus, minus and times host API * Implement bigInt dividedByDecimal and mod host API * Change BN js version to that used by ethers js --- packages/graph-node/package.json | 1 + packages/graph-node/src/loader.ts | 156 +++++++++++++++--- packages/graph-node/src/numbers.test.ts | 101 +++++++++--- .../test/subgraph/example1/src/mapping.ts | 93 ++++++++++- packages/graph-node/test/utils/index.ts | 9 +- 5 files changed, 304 insertions(+), 56 deletions(-) diff --git a/packages/graph-node/package.json b/packages/graph-node/package.json index 872f5413..6b5cada3 100644 --- a/packages/graph-node/package.json +++ b/packages/graph-node/package.json @@ -41,6 +41,7 @@ "@vulcanize/assemblyscript": "0.0.1", "@vulcanize/ipld-eth-client": "^0.1.0", "@vulcanize/util": "^0.1.0", + "bn.js": "^4.11.9", "debug": "^4.3.1", "decimal.js": "^10.3.1", "fs-extra": "^10.0.0", diff --git a/packages/graph-node/src/loader.ts b/packages/graph-node/src/loader.ts index 4d1ae908..8d58920a 100644 --- a/packages/graph-node/src/loader.ts +++ b/packages/graph-node/src/loader.ts @@ -4,7 +4,6 @@ import assert from 'assert'; import fs from 'fs/promises'; -import loader from '@vulcanize/assemblyscript/lib/loader'; import { utils, BigNumber, @@ -14,7 +13,9 @@ import { } from 'ethers'; import Decimal from 'decimal.js'; import JSONbig from 'json-bigint'; +import BN from 'bn.js'; +import loader from '@vulcanize/assemblyscript/lib/loader'; import { IndexerInterface } from '@vulcanize/util'; import { TypeId } from './types'; @@ -23,6 +24,18 @@ import { Database } from './database'; const NETWORK_URL = 'http://127.0.0.1:8081'; +// Size (in bytes) of the BN used in bigInt store host API. +// The BN is being stored as a byte array in wasm memory in 2's compliment representation and interpreted as such in other APIs. +// 33 bytes is chosen so that it can support: +// - Int256 (32 bytes sufficient) +// - UInt256 (33 bytes required as we are storing the 2's compliment) +const BN_SIZE = 33; + +// Endianness of BN used in bigInt store host API. +// Negative bigInt is being stored in wasm in 2's compliment, 'le' representation. +// (for eg. bigInt.fromString(negativeI32Value)) +const BN_ENDIANNESS = 'le'; + type idOfType = (TypeId: number) => number interface DataSource { @@ -185,8 +198,14 @@ export const instantiate = async (database: Database, indexer: IndexerInterface, 'typeConversion.bigIntToString': (bigInt: number) => { const bigIntByteArray = __getArray(bigInt); - const bigNumber = BigNumber.from(bigIntByteArray); - const ptr = __newString(bigNumber.toString()); + + // Create a BN with 'le' endianness. + const bigNumber = new BN(bigIntByteArray, BN_ENDIANNESS); + + // Convert BN from two's compliment and to string. + const bigNumberString = bigNumber.fromTwos(bigIntByteArray.length * 8).toString(); + + const ptr = __newString(bigNumberString); return ptr; }, @@ -210,19 +229,22 @@ export const instantiate = async (database: Database, indexer: IndexerInterface, }, numbers: { 'bigDecimal.dividedBy': async (x: number, y: number) => { - console.log('numbers bigDecimal.dividedBy'); + // Creating decimal x. + const xBigDecimal = await BigDecimal.wrap(x); + const xStringPtr = await xBigDecimal.toString(); + const xDecimal = new Decimal(__getString(xStringPtr)); - const bigDecimaly = BigDecimal.wrap(y); + // Create decimal y. + const yBigDecimal = await BigDecimal.wrap(y); + const yStringPtr = await yBigDecimal.toString(); + const yDecimal = new Decimal(__getString(yStringPtr)); - const digitsPtr = await bigDecimaly.digits; - const yDigitsBigIntArray = __getArray(digitsPtr); - const yDigits = BigNumber.from(yDigitsBigIntArray); + // Performing the decimal division operation. + const divResult = xDecimal.dividedBy(yDecimal); + const ptr = await __newString(divResult.toString()); + const divResultBigDecimal = await BigDecimal.fromString(ptr); - const expPtr = await bigDecimaly.exp; - const yExpBigIntArray = __getArray(expPtr); - const yExp = BigNumber.from(yExpBigIntArray); - - console.log('y digits and exp', yDigits, yExp); + return divResultBigDecimal; }, 'bigDecimal.toString': async (bigDecimal: number) => { const bigDecimalInstance = BigDecimal.wrap(bigDecimal); @@ -265,21 +287,73 @@ export const instantiate = async (database: Database, indexer: IndexerInterface, return bigDecimal; }, - 'bigDecimal.plus': () => { - console.log('bigDecimal.plus'); + 'bigDecimal.plus': async (x: number, y: number) => { + // Create decimal x string. + const xBigDecimal = await BigDecimal.wrap(x); + const xStringPtr = await xBigDecimal.toString(); + const xDecimalString = __getString(xStringPtr); + + // Create decimal y string. + const yBigDecimal = await BigDecimal.wrap(y); + const yStringPtr = await yBigDecimal.toString(); + const yDecimalString = __getString(yStringPtr); + + // Perform the decimal sum operation. + const sumResult = Decimal.sum(xDecimalString, yDecimalString); + const ptr = await __newString(sumResult.toString()); + const sumResultBigDecimal = await BigDecimal.fromString(ptr); + + return sumResultBigDecimal; }, - 'bigDecimal.minus': () => { - console.log('bigDecimal.minus'); + 'bigDecimal.minus': async (x: number, y: number) => { + // Create decimal x string. + const xBigDecimal = await BigDecimal.wrap(x); + const xStringPtr = await xBigDecimal.toString(); + const xDecimalString = __getString(xStringPtr); + + // Create decimal y string. + const yBigDecimal = await BigDecimal.wrap(y); + const yStringPtr = await yBigDecimal.toString(); + const yDecimalString = __getString(yStringPtr); + + // Perform the decimal sub operation. + const subResult = Decimal.sub(xDecimalString, yDecimalString); + const ptr = await __newString(subResult.toString()); + const subResultBigDecimal = await BigDecimal.fromString(ptr); + + return subResultBigDecimal; }, - 'bigDecimal.times': () => { - console.log('bigDecimal.times'); + 'bigDecimal.times': async (x: number, y: number) => { + // Create decimal x string. + const xBigDecimal = await BigDecimal.wrap(x); + const xStringPtr = await xBigDecimal.toString(); + const xDecimalString = __getString(xStringPtr); + + // Create decimal y string. + const yBigDecimal = await BigDecimal.wrap(y); + const yStringPtr = await yBigDecimal.toString(); + const yDecimalString = __getString(yStringPtr); + + // Perform the decimal mul operation. + const mulResult = Decimal.mul(xDecimalString, yDecimalString); + const ptr = await __newString(mulResult.toString()); + const mulResultBigDecimal = await BigDecimal.fromString(ptr); + + return mulResultBigDecimal; }, 'bigInt.fromString': async (s: number) => { const string = __getString(s); - const bigNumber = BigNumber.from(string); - const hex = bigNumber.toHexString(); - const bytes = utils.arrayify(hex); + + // Create a BN in 2's compliment representation. + // Need to use BN as ethers.BigNumber: + // Doesn't store -ve numbers in 2's compilment form + // Stores in big endian form. + let bigNumber = new BN(string); + bigNumber = bigNumber.toTwos(BN_SIZE * 8); + + // Create an array out of BN in 'le' endianness. + const bytes = bigNumber.toArray(BN_ENDIANNESS, BN_SIZE); const uint8ArrayId = await getIdOfType(TypeId.Uint8Array); const ptr = await __newArray(uint8ArrayId, bytes); @@ -347,11 +421,41 @@ export const instantiate = async (database: Database, indexer: IndexerInterface, return quotientBigInt; }, - 'bigInt.dividedByDecimal': () => { - console.log('bigInt.dividedByDecimal'); + 'bigInt.dividedByDecimal': async (x: number, y: number) => { + // Create a decimal out of bigInt x. + const xBigInt = await BigInt.wrap(x); + const xStringPtr = await xBigInt.toString(); + const xDecimal = new Decimal(__getString(xStringPtr)); + + // Create decimal y. + const yBigDecimal = await BigDecimal.wrap(y); + const yStringPtr = await yBigDecimal.toString(); + const yDecimal = new Decimal(__getString(yStringPtr)); + + // Perform the decimal division operation. + const divResult = xDecimal.dividedBy(yDecimal); + const ptr = await __newString(divResult.toString()); + const divResultBigDecimal = await BigDecimal.fromString(ptr); + + return divResultBigDecimal; }, - 'bigInt.mod': () => { - console.log('bigInt.mod'); + 'bigInt.mod': async (x: number, y: number) => { + // Create a bigNumber x. + const xBigInt = await BigInt.wrap(x); + const xStringPtr = await xBigInt.toString(); + const xBigNumber = BigNumber.from(__getString(xStringPtr)); + + // Create a bigNumber y. + const yBigInt = await BigInt.wrap(y); + const yStringPtr = await yBigInt.toString(); + const yBigNumber = BigNumber.from(__getString(yStringPtr)); + + // Perform the bigNumber mod operation. + const remainder = xBigNumber.mod(yBigNumber); + const ptr = await __newString(remainder.toString()); + const remainderBigInt = BigInt.fromString(ptr); + + return remainderBigInt; }, 'bigInt.bitOr': () => { console.log('bigInt.bitOr'); diff --git a/packages/graph-node/src/numbers.test.ts b/packages/graph-node/src/numbers.test.ts index 60a9d53a..8995e27c 100644 --- a/packages/graph-node/src/numbers.test.ts +++ b/packages/graph-node/src/numbers.test.ts @@ -29,20 +29,28 @@ describe('numbers wasm tests', () => { }); describe('should execute bigInt fromString API', () => { - let testBigIntFromString: any, __getString: any, __newString: any; + let testBigIntFromString: any, testBigIntWithI32: any, __getString: any, __newString: any, __getArray: any; before(() => { - ({ testBigIntFromString, __getString, __newString } = exports); + ({ testBigIntFromString, testBigIntWithI32, __getString, __newString, __getArray } = exports); }); it('should get bigInt for positive numbers', async () => { - const ptr = await testBigIntFromString(await __newString('123')); - expect(__getString(ptr)).to.equal('123'); + const ptr = await testBigIntFromString(await __newString('923567899898')); + expect(__getString(ptr)).to.equal('923567899898'); }); - xit('should get bigInt for negative numbers', async () => { - const ptr = await testBigIntFromString(await __newString('-123')); - expect(__getString(ptr)).to.equal('-123'); + it('should get bigInt for negative numbers', async () => { + const ptr = await testBigIntFromString(await __newString('-1506556')); + expect(__getString(ptr)).to.equal('-1506556'); + }); + + it('should give equal values for bigInt fromString and fromI32', async () => { + const ptr = await testBigIntWithI32(await __newString('-1506556')); + const ptrs = __getArray(ptr); + + expect(__getString(ptrs[0])).to.equal(__getString(ptrs[1])); + expect(__getString(ptrs[2])).to.equal('0'); }); }); @@ -74,11 +82,46 @@ describe('numbers wasm tests', () => { expect(__getString(ptr)).to.equal('100'); }); - it('should execute bigDecimal toString API', async () => { - const { testBigDecimalToString, __getString } = exports; + describe('should execute bigInt dividedByDecimal API', async () => { + let testBigIntDividedByDecimal: any, __newString: any, __getString: any; - const ptr = await testBigDecimalToString(); - expect(__getString(ptr)).to.equal('1000000000000000000'); + before(() => { + ({ testBigIntDividedByDecimal, __newString, __getString } = exports); + }); + + it('should execute bigInt dividedByDecimal for positive dividend and positive divisor', async () => { + const ptr = await testBigIntDividedByDecimal(await __newString('2315432122132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('42366323478.725506672'); + }); + + it('should execute bigInt dividedByDecimal for negative dividend and positive divisor', async () => { + const ptr = await testBigIntDividedByDecimal(await __newString('-2315432122132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('-42366323478.725506672'); + }); + + it('should execute bigInt dividedByDecimal for positive dividend and negative divisor', async () => { + const ptr = await testBigIntDividedByDecimal(await __newString('2315432122132354'), await __newString('-54652.65645')); + expect(__getString(ptr)).to.equal('-42366323478.725506672'); + }); + + it('should execute bigInt dividedByDecimal for negative dividend and negative divisor', async () => { + const ptr = await testBigIntDividedByDecimal(await __newString('-2315432122132354'), await __newString('-54652.65645')); + expect(__getString(ptr)).to.equal('42366323478.725506672'); + }); + }); + + it('should execute bigInt mod API', async () => { + const { testBigIntMod, __getString, __newString } = exports; + + const ptr = await testBigIntMod(await __newString('2315432122132354'), await __newString('5465265645')); + expect(__getString(ptr)).to.equal('1283174719'); + }); + + it('should execute bigDecimal toString API', async () => { + const { testBigDecimalToString, __newString, __getString } = exports; + + const ptr = await testBigDecimalToString(await __newString('-5032485723458348569331745849735.3343434634691214453454356561')); + expect(__getString(ptr)).to.equal('-5032485723458348569331745849735.3343434634691214453454356561'); }); describe('should execute bigDecimal fromString API', () => { @@ -93,17 +136,37 @@ describe('numbers wasm tests', () => { expect(__getString(ptr)).to.equal('43210'); }); - xit('should get bigDecimal for numbers with decimals', async () => { - const ptr = await testBigDecimalFromString(await __newString('5032485723458348569331745.33434346346912144534543')); - expect(__getString(ptr)).to.equal('5032485723458348569331745.33434346346912144534543'); + it('should get bigDecimal for numbers with decimals', async () => { + const ptr = await testBigDecimalFromString(await __newString('-5032485723458348569331745849735.3343434634691214453454356561')); + expect(__getString(ptr)).to.equal('-5032485723458348569331745849735.3343434634691214453454356561'); }); }); - xit('should execute bigDecimal dividedBy API', () => { - const { testBigDecimalDividedBy, __getString } = exports; + it('should execute bigDecimal plus API', async () => { + const { testBigDecimalPlus, __getString, __newString } = exports; - const ptr = testBigDecimalDividedBy(); - expect(__getString(ptr)).to.equal('10000000000000000'); - console.log(__getString(ptr)); + const ptr = await testBigDecimalPlus(await __newString('231543212.2132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('231597864.8696854'); + }); + + it('should execute bigDecimal minus API', async () => { + const { testBigDecimalMinus, __getString, __newString } = exports; + + const ptr = await testBigDecimalMinus(await __newString('231543212.2132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('231488559.5567854'); + }); + + it('should execute bigDecimal times API', async () => { + const { testBigDecimalTimes, __getString, __newString } = exports; + + const ptr = await testBigDecimalTimes(await __newString('231543212.2132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('12654451630419.398459'); + }); + + it('should execute bigDecimal dividedBy API', async () => { + const { testBigDecimalDividedBy, __getString, __newString } = exports; + + const ptr = await testBigDecimalDividedBy(await __newString('231543212.2132354'), await __newString('54652.65645')); + expect(__getString(ptr)).to.equal('4236.6323478725506672'); }); }); diff --git a/packages/graph-node/test/subgraph/example1/src/mapping.ts b/packages/graph-node/test/subgraph/example1/src/mapping.ts index 1315eee4..b4f3bde7 100644 --- a/packages/graph-node/test/subgraph/example1/src/mapping.ts +++ b/packages/graph-node/test/subgraph/example1/src/mapping.ts @@ -171,13 +171,12 @@ export function testStringToH160 (): string { return res; } -export function testBigDecimalToString (): string { +export function testBigDecimalToString (value: string): string { log.debug('In test bigDecimalToString', []); - const bigInt = BigInt.fromString('1000000000000000000'); - const bigDecimal = bigInt.toBigDecimal(); + const bigDecimal = BigDecimal.fromString(value); const res = bigDecimal.toString(); - log.debug('typeConversion.bigIntToString from hex result: {}', [res]); + log.debug('typeConversion.bigIntToString result: {}', [res]); return res; } @@ -192,20 +191,54 @@ export function testBigDecimalFromString (value: string): string { return res; } -export function testBigDecimalDividedBy (): string { +export function testBigDecimalDividedBy (value1: string, value2: string): string { log.debug('In test bigDecimal.dividedBy', []); - const bigInt1 = BigInt.fromString('1000000000000000000'); - const bigInt2 = BigInt.fromString('100'); + const bigDecimal1 = BigDecimal.fromString(value1); + const bigDecimal2 = BigDecimal.fromString(value2); - const bigDecimal1 = new BigDecimal(bigInt1); - const bigDecimal2 = new BigDecimal(bigInt2); const res = bigDecimal1 / bigDecimal2; log.debug('bigDecimal.dividedBy result: {}', [res.toString()]); return res.toString(); } +export function testBigDecimalPlus (value1: string, value2: string): string { + log.debug('In test bigDecimal.plus', []); + + const bigDecimal1 = BigDecimal.fromString(value1); + const bigDecimal2 = BigDecimal.fromString(value2); + + const res = bigDecimal1 + bigDecimal2; + log.debug('bigDecimal.plus result: {}', [res.toString()]); + + return res.toString(); +} + +export function testBigDecimalMinus (value1: string, value2: string): string { + log.debug('In test bigDecimal.minus', []); + + const bigDecimal1 = BigDecimal.fromString(value1); + const bigDecimal2 = BigDecimal.fromString(value2); + + const res = bigDecimal1 - bigDecimal2; + log.debug('bigDecimal.minus result: {}', [res.toString()]); + + return res.toString(); +} + +export function testBigDecimalTimes (value1: string, value2: string): string { + log.debug('In test bigDecimal.times', []); + + const bigDecimal1 = BigDecimal.fromString(value1); + const bigDecimal2 = BigDecimal.fromString(value2); + + const res = bigDecimal1 * bigDecimal2; + log.debug('bigDecimal.times result: {}', [res.toString()]); + + return res.toString(); +} + export function testBigIntPlus (): string { log.debug('In test bigInt.plus', []); @@ -250,6 +283,28 @@ export function testBigIntDividedBy (): string { return res.toString(); } +export function testBigIntDividedByDecimal (value1: string, value2: string): string { + log.debug('In test bigInt.dividedByDecimal', []); + + const bigInt = BigInt.fromString(value1); + const bigDecimal = BigDecimal.fromString(value2); + + const res = bigInt.divDecimal(bigDecimal); + log.debug('bigInt.dividedByDecimal result: {}', [res.toString()]); + return res.toString(); +} + +export function testBigIntMod (value1: string, value2: string): string { + log.debug('In test bigInt.mod', []); + + const bigInt1 = BigInt.fromString(value1); + const bigInt2 = BigInt.fromString(value2); + + const res = bigInt1.mod(bigInt2); + log.debug('bigInt.mod result: {}', [res.toString()]); + return res.toString(); +} + export function testBigIntFromString (value: string): string { log.debug('In test bigInt.fromString', []); @@ -259,3 +314,23 @@ export function testBigIntFromString (value: string): string { return res; } + +export function testBigIntWithI32 (value: string): string[] { + log.debug('In testBigIntWithI32', []); + + const variableI32: i32 = parseInt(value) as i32; + + const bigInt1 = BigInt.fromI32(variableI32); + const bigInt2 = BigInt.fromString(value); + + const res1 = bigInt1.toString(); + log.debug('bigInt.FromString result 1: {}', [res1]); + + const res2 = bigInt2.toString(); + log.debug('bigInt.FromString result 2: {}', [res2]); + + const res3 = BigInt.compare(bigInt1, bigInt2).toString(); + log.debug('bigInt.FromString result 3: {}', [res3]); + + return [res1, res2, res3]; +} diff --git a/packages/graph-node/test/utils/index.ts b/packages/graph-node/test/utils/index.ts index a2c0341a..618e8415 100644 --- a/packages/graph-node/test/utils/index.ts +++ b/packages/graph-node/test/utils/index.ts @@ -17,7 +17,11 @@ export const getDummyEventData = (): EventData => { stateRoot: ZERO_HASH, td: ZERO_HASH, txRoot: ZERO_HASH, - receiptRoot: ZERO_HASH + receiptRoot: ZERO_HASH, + uncleHash: ZERO_HASH, + difficulty: '0', + gasLimit: '0', + gasUsed: '0' }; const tx = { @@ -30,7 +34,8 @@ export const getDummyEventData = (): EventData => { return { block, tx, - eventParams: [], + inputs: [], + event: {}, eventIndex: 0 }; };