// // Copyright 2021 Vulcanize, Inc. // import assert from 'assert'; import fs from 'fs/promises'; import { utils, BigNumber, Contract, ContractInterface } from 'ethers'; import BN from 'bn.js'; import debug from 'debug'; import { BaseProvider } from '@ethersproject/providers'; import loader from '@vulcanize/assemblyscript/lib/loader'; import { IndexerInterface, GraphDecimal, getGraphDigitsAndExp, jsonBigIntStringReplacer } from '@vulcanize/util'; import { TypeId, Level } from './types'; import { Block, fromEthereumValue, toEthereumValue, resolveEntityFieldConflicts, getEthereumTypes, jsonFromBytes, getStorageValueType } from './utils'; import { Database } from './database'; // 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 export interface GraphData { abis?: {[key: string]: ContractInterface}; dataSource: { network: string; name: string; }; } export interface Context { block?: Block contractAddress?: string } const log = debug('vulcanize:graph-node'); export const instantiate = async ( database: Database, indexer: IndexerInterface, provider: BaseProvider, context: Context, filePathOrModule: string | WebAssembly.Module, data: GraphData ): Promise => { const { abis = {}, dataSource } = data; let source = filePathOrModule; if (!(filePathOrModule instanceof WebAssembly.Module)) { source = await fs.readFile(filePathOrModule); } const imports: WebAssembly.Imports = { index: { 'store.get': async (entity: number, id: number) => { const entityName = __getString(entity); const entityId = __getString(id); assert(context.block); console.time(`time:loader#index.store.get-db-${entityName}`); const entityData = await database.getEntity(entityName, entityId, context.block.blockHash); console.timeEnd(`time:loader#index.store.get-db-${entityName}`); if (!entityData) { return null; } assert(indexer.getEntityTypesMap); const entityTypesMap = indexer.getEntityTypesMap(); const entityTypes = entityTypesMap.get(entityName); assert(entityTypes); return database.toGraphEntity(instanceExports, entityName, entityData, entityTypes); }, 'store.set': async (entity: number, id: number, data: number) => { const entityName = __getString(entity); const entityInstance = await Entity.wrap(data); assert(context.block); let dbData = await database.fromGraphEntity(instanceExports, context.block, entityName, entityInstance); console.time(`time:loader#index.store.set-db-${entityName}`); await database.saveEntity(entityName, dbData); console.timeEnd(`time:loader#index.store.set-db-${entityName}`); // Resolve any field name conflicts in the dbData for auto-diff. dbData = resolveEntityFieldConflicts(dbData); // Prepare the diff data. const diffData: any = { state: {} }; assert(indexer.getRelationsMap); const result = Array.from(indexer.getRelationsMap().entries()) .find(([key]) => key.name === entityName); if (result) { // Update dbData if relations exist. const [_, relations] = result; // Update relation fields for diff data to be similar to GQL query entities. Object.entries(relations).forEach(([relation, { isArray, isDerived }]) => { if (isDerived || !dbData[relation]) { // Field is not present in dbData for derived relations return; } if (isArray) { dbData[relation] = dbData[relation] .map((id: string) => ({ id })) .sort((a: any, b: any) => a.id.localeCompare(b.id)); } else { dbData[relation] = { id: dbData[relation] }; } }); } // JSON stringify and parse data for handling unknown types when encoding. // For example, decimal.js values are converted to string in the diff data. diffData.state[entityName] = { // Using custom replacer to store bigints as string values to be encoded by IPLD dag-cbor. // TODO: Parse and store as native bigint by using Type encoders in IPLD dag-cbor encode. // https://github.com/rvagg/cborg#type-encoders [dbData.id]: JSON.parse(JSON.stringify(dbData, jsonBigIntStringReplacer)) }; // Create an auto-diff. assert(indexer.createDiffStaged); assert(context.contractAddress); await indexer.createDiffStaged(context.contractAddress, context.block.blockHash, diffData); }, 'log.log': (level: number, msg: number) => { log('log %s | %s', Level[level], __getString(msg)); }, 'crypto.keccak256': async (input: number) => { const byteArray = await ByteArray.wrap(input); const hexStringPtr = await byteArray.toHexString(); const hexString = __getString(hexStringPtr); const keccak256 = utils.keccak256(hexString); const keccak256Ptr = await __newString(keccak256); return ByteArray.fromHexString(keccak256Ptr); }, 'test.asyncMethod': async () => { console.log('before timer start'); await new Promise(resolve => { setTimeout(() => { resolve(1); }, 3000); }); console.log('after timer complete'); return 123; } }, ethereum: { 'ethereum.call': async (call: number) => { const smartContractCall = await ethereum.SmartContractCall.wrap(call); const contractAddressPtr = await smartContractCall.contractAddress; const contractAddress = await Address.wrap(contractAddressPtr); const contractNamePtr = await smartContractCall.contractName; const contractName = __getString(contractNamePtr); const functionNamePtr = await smartContractCall.functionName; const functionName = __getString(functionNamePtr); const functionSignaturePtr = await smartContractCall.functionSignature; const functionSignature = __getString(functionSignaturePtr); const functionParamsPtr = await smartContractCall.functionParams; let functionParams = __getArray(functionParamsPtr); console.log('ethereum.call params'); console.log('functionSignature:', functionSignature); const abi = abis[contractName]; const contractAddressStringPtr = await contractAddress.toHexString(); const contract = new Contract(__getString(contractAddressStringPtr), abi, provider); try { const functionParamsPromise = functionParams.map(async param => { const ethereumValue = await ethereum.Value.wrap(param); return fromEthereumValue(instanceExports, ethereumValue); }); functionParams = await Promise.all(functionParamsPromise); assert(context.block); // TODO: Check for function overloading. console.time(`time:loader#ethereum.call-${functionName}`); let result = await contract[functionName](...functionParams, { blockTag: context.block.blockHash }); console.timeEnd(`time:loader#ethereum.call-${functionName}`); // Using function signature does not work. const { outputs } = contract.interface.getFunction(functionName); assert(outputs); // If method returns a single value, ethers returns it directly compared to returning multiple values in an array. if (outputs.length === 1) { // Put result in an array to map with the outputs array from abi. result = [result]; } const resultPtrArrayPromise = outputs.map( async ( output: any, index: number ) => toEthereumValue( instanceExports, output, result[index] ) ); const resultPtrArray: any[] = await Promise.all(resultPtrArrayPromise); const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue); const res = await __newArray(arrayEthereumValueId, resultPtrArray); return res; } catch (err: any) { log('eth_call error', err.message); return null; } }, 'ethereum.encode': async (token: number) => { const ethValue = await ethereum.Value.wrap(token); const data = await fromEthereumValue(instanceExports, ethValue); const type = await getEthereumTypes(instanceExports, ethValue); const encoded = utils.defaultAbiCoder.encode([type], [data]); const encodedString = await __newString(encoded); return ByteArray.fromHexString(encodedString); }, 'ethereum.decode': async (types: number, data: number) => { const typesString = __getString(types); const byteArray = await ByteArray.wrap(data); const bytesHex = await byteArray.toHex(); const dataString = __getString(bytesHex); const [decoded] = utils.defaultAbiCoder.decode([typesString], dataString); return toEthereumValue(instanceExports, utils.ParamType.from(typesString), decoded); }, 'ethereum.storageValue': async (variable: number, mappingKeys: number) => { assert(context.contractAddress); const addressStringPtr = await __newString(context.contractAddress); const addressString = __getString(addressStringPtr); const variableString = __getString(variable); const mappingKeyPtrs = __getArray(mappingKeys); const mappingKeyPromises = mappingKeyPtrs.map(async mappingKeyPtr => { const ethereumValue = await ethereum.Value.wrap(mappingKeyPtr); return fromEthereumValue(instanceExports, ethereumValue); }); const mappingKeyValues = await Promise.all(mappingKeyPromises); const storageLayout = indexer.storageLayoutMap.get(dataSource.name); assert(storageLayout); assert(context.block); console.time(`time:loader#ethereum.storageValue-${variableString}`); const result = await indexer.getStorageValue( storageLayout, context.block.blockHash, addressString, variableString, ...mappingKeyValues ); console.timeEnd(`time:loader#ethereum.storageValue-${variableString}`); const storageValueType = getStorageValueType(storageLayout, variableString, mappingKeyValues); return toEthereumValue( instanceExports, storageValueType, result.value ); } }, conversion: { 'typeConversion.stringToH160': async (s: number) => { const string = __getString(s); const address = utils.getAddress(string); const byteArray = utils.arrayify(address); const uint8ArrayId = await getIdOfType(TypeId.Uint8Array); const ptr = __newArray(uint8ArrayId, byteArray); return ptr; }, 'typeConversion.bigIntToString': (bigInt: number) => { const bigIntByteArray = __getArray(bigInt); // 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; }, 'typeConversion.bigIntToHex': async (bigInt: number) => { const bigIntInstance = await ASBigInt.wrap(bigInt); const bigIntString = await bigIntInstance.toString(); const bigNumber = BigNumber.from(__getString(bigIntString)); const bigNumberHex = bigNumber.toHexString(); return __newString(bigNumberHex); }, 'typeConversion.bytesToHex': async (bytes: number) => { const byteArray = __getArray(bytes); const hexString = utils.hexlify(byteArray); const ptr = await __newString(hexString); return ptr; }, 'typeConversion.bytesToString': async (bytes: number) => { const byteArray = __getArray(bytes); const string = utils.toUtf8String(byteArray); const ptr = await __newString(string); return ptr; }, 'typeConversion.bytesToBase58': async (n: number) => { const uint8Array = __getArray(n); const string = utils.base58.encode(uint8Array); const ptr = await __newString(string); return ptr; } }, numbers: { 'bigDecimal.dividedBy': async (x: number, y: number) => { // Creating decimal x. const xBigDecimal = await BigDecimal.wrap(x); const xStringPtr = await xBigDecimal.toString(); const xDecimalString = __getString(xStringPtr); const xDecimal = new GraphDecimal(xDecimalString); // Create decimal y. const yBigDecimal = await BigDecimal.wrap(y); const yStringPtr = await yBigDecimal.toString(); const yDecimalString = __getString(yStringPtr); // Performing the decimal division operation. const divResult = xDecimal.dividedBy(yDecimalString); const ptr = await __newString(divResult.toString()); const divResultBigDecimal = await BigDecimal.fromString(ptr); return divResultBigDecimal; }, 'bigDecimal.toString': async (bigDecimal: number) => { const bigDecimalInstance = BigDecimal.wrap(bigDecimal); const digitsPtr = await bigDecimalInstance.digits; const digitsBigInt = ASBigInt.wrap(digitsPtr); const expPtr = await bigDecimalInstance.exp; const expBigInt = ASBigInt.wrap(expPtr); const digitsStringPtr = await digitsBigInt.toString(); const digits = __getString(digitsStringPtr); const expStringPtr = await expBigInt.toString(); const exp = __getString(expStringPtr); const decimal = new GraphDecimal(`${digits}e${exp}`); const ptr = __newString(decimal.toFixed()); return ptr; }, 'bigDecimal.fromString': async (s: number) => { const string = __getString(s); // Creating a decimal using custom decimal implementation. const decimal = new GraphDecimal(string); // Get digits string and exp using decimal 'd' and 'e' properties. const { digits, exp } = getGraphDigitsAndExp(decimal.value.d, decimal.value.e); // Create a digits BigInt using digits string and decimal sign 's' property. const digitsBigNumber = BigNumber.from(digits); const signBigNumber = BigNumber.from(decimal.value.s); const digitsStringPtr = await __newString(digitsBigNumber.mul(signBigNumber).toString()); const digitsBigInt = await ASBigInt.fromString(digitsStringPtr); // Create an exp BigInt. const expStringPtr = await __newString(exp.toString()); const expBigInt = await ASBigInt.fromString(expStringPtr); // Create a BigDecimal using digits and exp BigInts. const bigDecimal = await BigDecimal.__new(digitsBigInt); bigDecimal.exp = expBigInt; return bigDecimal; }, '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); const xDecimal = new GraphDecimal(xDecimalString); // Create decimal y string. const yBigDecimal = await BigDecimal.wrap(y); const yStringPtr = await yBigDecimal.toString(); const yDecimalString = __getString(yStringPtr); // Perform the decimal plus operation. const sumResult = xDecimal.plus(yDecimalString); const ptr = await __newString(sumResult.toString()); const sumResultBigDecimal = await BigDecimal.fromString(ptr); return sumResultBigDecimal; }, '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); const xDecimal = new GraphDecimal(xDecimalString); // Create decimal y string. const yBigDecimal = await BigDecimal.wrap(y); const yStringPtr = await yBigDecimal.toString(); const yDecimalString = __getString(yStringPtr); // Perform the decimal minus operation. const subResult = xDecimal.minus(yDecimalString); const ptr = await __newString(subResult.toString()); const subResultBigDecimal = await BigDecimal.fromString(ptr); return subResultBigDecimal; }, '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); const xDecimal = new GraphDecimal(xDecimalString); // Create decimal y string. const yBigDecimal = await BigDecimal.wrap(y); const yStringPtr = await yBigDecimal.toString(); const yDecimalString = __getString(yStringPtr); // Perform the decimal times operation. const mulResult = xDecimal.times(yDecimalString); const ptr = await __newString(mulResult.toString()); const mulResultBigDecimal = await BigDecimal.fromString(ptr); return mulResultBigDecimal; }, 'bigInt.fromString': async (s: number) => { const string = __getString(s); // The BN is being stored as a byte array in wasm memory in 2's compliment representation and interpreted as such in other APIs. // 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); // Size (in bytes) of the BN stored. // Add an extra byte to the BNs byte length to allow for 2's compiment. const bnSize = bigNumber.byteLength() + 1; bigNumber = bigNumber.toTwos(bnSize * 8); // Create a byte array out of BN in 'le' endianness. const bytes = bigNumber.toArray(BN_ENDIANNESS, bnSize); const uint8ArrayId = await getIdOfType(TypeId.Uint8Array); const ptr = await __newArray(uint8ArrayId, bytes); const bigInt = await ASBigInt.fromSignedBytes(ptr); return bigInt; }, 'bigInt.plus': async (x: number, y: number) => { const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); const sum = xBigNumber.add(yBigNumber); const ptr = await __newString(sum.toString()); const sumBigInt = await ASBigInt.fromString(ptr); return sumBigInt; }, 'bigInt.minus': async (x: number, y: number) => { const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); const diff = xBigNumber.sub(yBigNumber); const ptr = await __newString(diff.toString()); const diffBigInt = ASBigInt.fromString(ptr); return diffBigInt; }, 'bigInt.times': async (x: number, y: number) => { const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); const product = xBigNumber.mul(yBigNumber); const ptr = await __newString(product.toString()); const productBigInt = ASBigInt.fromString(ptr); return productBigInt; }, 'bigInt.dividedBy': async (x: number, y: number) => { const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); const quotient = xBigNumber.div(yBigNumber); const ptr = await __newString(quotient.toString()); const quotientBigInt = ASBigInt.fromString(ptr); return quotientBigInt; }, 'bigInt.dividedByDecimal': async (x: number, y: number) => { // Create a decimal out of bigInt x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xDecimal = new GraphDecimal(__getString(xStringPtr)); // Create decimal y. const yBigDecimal = await BigDecimal.wrap(y); const yStringPtr = await yBigDecimal.toString(); const yDecimal = new GraphDecimal(__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': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Create a bigNumber y. const yBigInt = await ASBigInt.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 = ASBigInt.fromString(ptr); return remainderBigInt; }, 'bigInt.bitOr': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Create a bigNumber y. const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); // Perform the bigNumber bit or operation. const res = xBigNumber.or(yBigNumber); const ptr = await __newString(res.toString()); const resBigInt = ASBigInt.fromString(ptr); return resBigInt; }, 'bigInt.bitAnd': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Create a bigNumber y. const yBigInt = await ASBigInt.wrap(y); const yStringPtr = await yBigInt.toString(); const yBigNumber = BigNumber.from(__getString(yStringPtr)); // Perform the bigNumber bit and operation. const res = xBigNumber.and(yBigNumber); const ptr = await __newString(res.toString()); const resBigInt = ASBigInt.fromString(ptr); return resBigInt; }, 'bigInt.leftShift': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Perform the bigNumber left shift operation. const res = xBigNumber.shl(y); const ptr = await __newString(res.toString()); const resBigInt = ASBigInt.fromString(ptr); return resBigInt; }, 'bigInt.rightShift': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Perform the bigNumber right shift operation. const res = xBigNumber.shr(y); const ptr = await __newString(res.toString()); const resBigInt = ASBigInt.fromString(ptr); return resBigInt; }, 'bigInt.pow': async (x: number, y: number) => { // Create a bigNumber x. const xBigInt = await ASBigInt.wrap(x); const xStringPtr = await xBigInt.toString(); const xBigNumber = BigNumber.from(__getString(xStringPtr)); // Perform the bigNumber pow operation. const res = xBigNumber.pow(y); const ptr = await __newString(res.toString()); const resBigInt = ASBigInt.fromString(ptr); return resBigInt; } }, datasource: { 'dataSource.address': async () => { assert(context.contractAddress); const addressStringPtr = await __newString(context.contractAddress); return Address.fromString(addressStringPtr); }, 'dataSource.context': async () => { // TODO: Implement use in data source templates. // https://thegraph.com/docs/en/developer/create-subgraph-hosted/#data-source-context return Entity.__new(); }, 'dataSource.network': async () => { assert(dataSource); return __newString(dataSource.network); }, 'dataSource.create': async (name: number, params: number) => { const [addressStringPtr] = __getArray(params); const addressString = __getString(addressStringPtr); const contractKind = __getString(name); assert(indexer.watchContract); assert(context.block); await indexer.watchContract(utils.getAddress(addressString), contractKind, true, Number(context.block.blockNumber)); } }, json: { 'json.fromBytes': async (bytes: number) => { return jsonFromBytes(instanceExports, bytes); }, 'json.try_fromBytes': async (bytes: number) => { try { const jsonValue = await jsonFromBytes(instanceExports, bytes); return JSONResult.__new(jsonValue); } catch (error) { log('json.try_fromBytes error', error); return JSONResult.__new(null); } }, // TODO: Number methods do not work as 64bit values are not supported in js. // Tried solution in https://github.com/AssemblyScript/assemblyscript/issues/117#issuecomment-531556954 'json.toI64': async (decimal: number) => { return BigInt(__getString(decimal)); }, 'json.toU64': async (decimal: number) => { return BigInt(__getString(decimal)); }, 'json.toF64': async (decimal: number) => { return BigInt(__getString(decimal)); }, // TODO: Debug toBigInt not working. 'json.toBigInt': async (decimal: number) => { return ASBigInt.fromString(decimal); } } }; const instance = await loader.instantiate(source, imports); const { exports: instanceExports } = instance; const { __getString, __newString, __getArray, __newArray } = instanceExports; // TODO: Assign from types file generated by graph-cli const getIdOfType: idOfType = instanceExports.id_of_type as idOfType; const BigDecimal: any = instanceExports.BigDecimal as any; const ASBigInt: any = instanceExports.BigInt as any; const Address: any = instanceExports.Address as any; const ethereum: any = instanceExports.ethereum as any; const Entity: any = instanceExports.Entity as any; const ByteArray: any = instanceExports.ByteArray as any; const JSONResult: any = instanceExports.JSONResult as any; return instance; };