Implement ethereum ABI encode and decode in subgraph (#103)

* Implement ethereum ABI encode in subgraph

* Implement ethereum ABI decoding host API

* Implement ABI encode decode for array type

* Implement ABI encode decode for bytes type
This commit is contained in:
nikugogoi 2021-12-30 13:12:32 +05:30 committed by GitHub
parent 9b1aa29afd
commit 8b913af93f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 306 additions and 17 deletions

View File

@ -71,7 +71,7 @@ describe('call handler in mapping code', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: { block: dummyEventData.block } }, { block: dummyEventData.block },
filePath, filePath,
dummyGraphData dummyGraphData
); );

View File

@ -88,7 +88,7 @@ describe('eden wasm loader tests', async () => {
db, db,
indexer, indexer,
provider, provider,
{ event: { block: dummyEventData.block } }, { block: dummyEventData.block },
filePath, filePath,
data data
)); ));
@ -203,7 +203,7 @@ describe('eden wasm loader tests', async () => {
({ exports } = await instantiate(db, ({ exports } = await instantiate(db,
indexer, indexer,
provider, provider,
{ event: { block: dummyEventData.block } }, { block: dummyEventData.block },
filePath, filePath,
data data
)); ));
@ -316,7 +316,7 @@ describe('eden wasm loader tests', async () => {
db, db,
indexer, indexer,
provider, provider,
{ event: { block: dummyEventData.block } }, { block: dummyEventData.block },
filePath, filePath,
data data
)); ));

View File

@ -0,0 +1,70 @@
//
// Copyright 2021 Vulcanize, Inc.
//
import path from 'path';
import { expect } from 'chai';
import { BaseProvider } from '@ethersproject/providers';
import { instantiate } from './loader';
import { getTestDatabase, getTestIndexer, getTestProvider } from '../test/utils';
import { Database } from './database';
import { Indexer } from '../test/utils/indexer';
describe('ethereum ABI encode decode', () => {
let exports: any;
let db: Database;
let indexer: Indexer;
let provider: BaseProvider;
let encoded: string;
before(async () => {
db = getTestDatabase();
indexer = getTestIndexer();
provider = getTestProvider();
});
it('should load the subgraph example wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/example1/build/Example1/Example1.wasm');
const instance = await instantiate(
db,
indexer,
provider,
{},
filePath
);
exports = instance.exports;
const { _start } = exports;
// Important to call _start for built subgraphs on instantiation!
// TODO: Check api version https://github.com/graphprotocol/graph-node/blob/6098daa8955bdfac597cec87080af5449807e874/runtime/wasm/src/module/mod.rs#L533
_start();
});
it('should encode data', async () => {
const { testEthereumEncode, __getString } = exports;
const encodedPtr = await testEthereumEncode();
encoded = __getString(encodedPtr);
expect(encoded)
.to
.equal('0x0000000000000000000000000000000000000000000000000000000000000420583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945000000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000003f0000000000000000000000000000000000000000000000000000000000000001');
});
it('should decode data', async () => {
const { testEthereumDecode, __getString, __getArray, __newString } = exports;
const encodedString = await __newString(encoded);
const decodedArrayPtr = await testEthereumDecode(encodedString);
const decodedArray = __getArray(decodedArrayPtr);
const [addressString, bytesString, bigInt1String, bigInt2String, boolString] = decodedArray.map((value: any) => __getString(value));
expect(addressString).to.equal('0x0000000000000000000000000000000000000420');
expect(bytesString).to.equal('0x583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945');
expect(bigInt1String).to.equal('62');
expect(bigInt2String).to.equal('63');
expect(boolString).to.equal('true');
});
});

View File

@ -49,7 +49,7 @@ describe('eth-call wasm tests', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: { block: dummyEventData.block } }, { block: dummyEventData.block },
filePath, filePath,
data data
); );

View File

@ -32,7 +32,7 @@ describe('wasm loader tests', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: {} }, {},
filePath filePath
); );
@ -109,7 +109,7 @@ describe('wasm loader tests', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: {} }, {},
module module
); );

View File

@ -23,7 +23,8 @@ import {
Block, Block,
fromEthereumValue, fromEthereumValue,
toEthereumValue, toEthereumValue,
resolveEntityFieldConflicts resolveEntityFieldConflicts,
getEthereumTypes
} from './utils'; } from './utils';
import { Database } from './database'; import { Database } from './database';
@ -194,6 +195,27 @@ export const instantiate = async (
console.log('eth_call error', err); console.log('eth_call error', err);
return null; 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);
} }
}, },
conversion: { conversion: {
@ -593,6 +615,7 @@ export const instantiate = async (
const Address: any = instanceExports.Address as any; const Address: any = instanceExports.Address as any;
const ethereum: any = instanceExports.ethereum as any; const ethereum: any = instanceExports.ethereum as any;
const Entity: any = instanceExports.Entity as any; const Entity: any = instanceExports.Entity as any;
const ByteArray: any = instanceExports.ByteArray as any;
return instance; return instance;
}; };

View File

@ -42,7 +42,7 @@ describe('numbers wasm tests', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: {} }, {},
filePath filePath
); );
exports = instance.exports; exports = instance.exports;

View File

@ -31,7 +31,7 @@ describe('typeConversion wasm tests', () => {
db, db,
indexer, indexer,
provider, provider,
{ event: {} }, {},
filePath filePath
); );
exports = instance.exports; exports = instance.exports;

View File

@ -63,6 +63,80 @@ export interface EventData {
eventIndex: number; eventIndex: number;
} }
export const getEthereumTypes = async (instanceExports: any, value: any): Promise<any> => {
const {
__getArray,
Bytes,
ethereum
} = instanceExports;
const kind = await value.kind;
switch (kind) {
case EthereumValueKind.ADDRESS:
return 'address';
case EthereumValueKind.BOOL:
return 'bool';
case EthereumValueKind.STRING:
return 'string';
case EthereumValueKind.BYTES:
return 'bytes';
case EthereumValueKind.FIXED_BYTES: {
const bytesPtr = await value.toBytes();
const bytes = await Bytes.wrap(bytesPtr);
const length = await bytes.length;
return `bytes${length}`;
}
case EthereumValueKind.INT:
return 'int256';
case EthereumValueKind.UINT: {
return 'uint256';
}
case EthereumValueKind.ARRAY: {
const valuesPtr = await value.toArray();
const [firstValuePtr] = await __getArray(valuesPtr);
const firstValue = await ethereum.Value.wrap(firstValuePtr);
const type = await getEthereumTypes(instanceExports, firstValue);
return `${type}[]`;
}
case EthereumValueKind.FIXED_ARRAY: {
const valuesPtr = await value.toArray();
const values = await __getArray(valuesPtr);
const firstValue = await ethereum.Value.wrap(values[0]);
const type = await getEthereumTypes(instanceExports, firstValue);
return `${type}[${values.length}]`;
}
case EthereumValueKind.TUPLE: {
let values = await value.toTuple();
values = await __getArray(values);
const typePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return getEthereumTypes(instanceExports, value);
});
const types = await Promise.all(typePromises);
return `tuple(${types.join(',')})`;
}
default:
break;
}
};
/** /**
* Method to get value from graph-ts ethereum.Value wasm instance. * Method to get value from graph-ts ethereum.Value wasm instance.
* @param instanceExports * @param instanceExports
@ -71,9 +145,12 @@ export interface EventData {
*/ */
export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => { export const fromEthereumValue = async (instanceExports: any, value: any): Promise<any> => {
const { const {
__getArray,
__getString, __getString,
BigInt, BigInt,
Address Address,
Bytes,
ethereum
} = instanceExports; } = instanceExports;
const kind = await value.kind; const kind = await value.kind;
@ -91,9 +168,15 @@ export const fromEthereumValue = async (instanceExports: any, value: any): Promi
return Boolean(bool); return Boolean(bool);
} }
case EthereumValueKind.STRING: {
const stringPtr = await value.toString();
return __getString(stringPtr);
}
case EthereumValueKind.BYTES: case EthereumValueKind.BYTES:
case EthereumValueKind.FIXED_BYTES: { case EthereumValueKind.FIXED_BYTES: {
const bytes = await value.toBytes(); const bytesPtr = await value.toBytes();
const bytes = await Bytes.wrap(bytesPtr);
const bytesStringPtr = await bytes.toHexString(); const bytesStringPtr = await bytes.toHexString();
return __getString(bytesStringPtr); return __getString(bytesStringPtr);
} }
@ -107,6 +190,31 @@ export const fromEthereumValue = async (instanceExports: any, value: any): Promi
return BigNumber.from(bigIntString); return BigNumber.from(bigIntString);
} }
case EthereumValueKind.ARRAY:
case EthereumValueKind.FIXED_ARRAY: {
const valuesPtr = await value.toArray();
const values = __getArray(valuesPtr);
const valuePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return fromEthereumValue(instanceExports, value);
});
return Promise.all(valuePromises);
}
case EthereumValueKind.TUPLE: {
let values = await value.toTuple();
values = await __getArray(values);
const valuePromises = values.map(async (value: any) => {
value = await ethereum.Value.wrap(value);
return fromEthereumValue(instanceExports, value);
});
return Promise.all(valuePromises);
}
default: default:
break; break;
} }
@ -131,7 +239,26 @@ export const toEthereumValue = async (instanceExports: any, output: utils.ParamT
id_of_type: getIdOfType id_of_type: getIdOfType
} = instanceExports; } = instanceExports;
const { type } = output; const { type, baseType, arrayChildren } = output;
// For array type.
if (baseType === 'array') {
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
// Get values for array elements.
const ethereumValuePromises = value.map(
async (value: any) => toEthereumValue(
instanceExports,
arrayChildren,
value
)
);
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
const ethereumValuesArray = await __newArray(arrayEthereumValueId, ethereumValues);
return ethereum.Value.fromArray(ethereumValuesArray);
}
// For tuple type. // For tuple type.
if (type === 'tuple') { if (type === 'tuple') {
@ -140,10 +267,10 @@ export const toEthereumValue = async (instanceExports: any, output: utils.ParamT
// Get values for struct elements. // Get values for struct elements.
const ethereumValuePromises = output.components const ethereumValuePromises = output.components
.map( .map(
async (component: utils.ParamType) => toEthereumValue( async (component: utils.ParamType, index) => toEthereumValue(
instanceExports, instanceExports,
component, component,
value[component.name] value[index]
) )
); );

View File

@ -1,4 +1,4 @@
import { Address, log, BigInt, BigDecimal, ByteArray, dataSource, ethereum } from '@graphprotocol/graph-ts'; import { Address, log, BigInt, BigDecimal, ByteArray, dataSource, ethereum, Bytes } from '@graphprotocol/graph-ts';
import { import {
Example1, Example1,
@ -440,3 +440,67 @@ export function testBigIntToHex (value: string): string[] {
return [res1, res2]; return [res1, res2];
} }
export function testEthereumEncode (): string {
const address = ethereum.Value.fromAddress(Address.fromString('0x0000000000000000000000000000000000000420'));
const bigInt1 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(62));
const bigInt2 = ethereum.Value.fromUnsignedBigInt(BigInt.fromI32(63));
const bool = ethereum.Value.fromBoolean(true);
const bytes = ethereum.Value.fromFixedBytes(
Bytes.fromByteArray(
ByteArray.fromHexString('0x583bc7e1bc4799a225663353b82eb36d925399e6ef2799a6a95909f5ab8ac945')
)
);
const fixedSizedArray = ethereum.Value.fromFixedSizedArray([
bigInt1,
bigInt2
]);
const tupleArray: Array<ethereum.Value> = [
fixedSizedArray,
bool
];
const tuple = ethereum.Value.fromTuple(changetype<ethereum.Tuple>(tupleArray));
const token: Array<ethereum.Value> = [
address,
bytes,
tuple
];
const encoded = ethereum.encode(ethereum.Value.fromTuple(changetype<ethereum.Tuple>(token)))!;
log.debug('encoded: {}', [encoded.toHex()]);
return encoded.toHex();
}
export function testEthereumDecode (encoded: string): string[] {
const decoded = ethereum.decode('(address,bytes32,(uint256[2],bool))', Bytes.fromByteArray(ByteArray.fromHexString(encoded)));
const tupleValues = decoded!.toTuple();
const decodedAddress = tupleValues[0].toAddress();
const decodedBytes = tupleValues[1].toBytes();
const decodedTuple = tupleValues[2].toTuple();
const decodedFixedSizedArray = decodedTuple[0].toArray();
const decodedBigInt1 = decodedFixedSizedArray[0].toBigInt();
const decodedBigInt2 = decodedFixedSizedArray[1].toBigInt();
const decodedBool = decodedTuple[1].toBoolean();
log.debug('decoded address: {}', [decodedAddress.toHex()]);
log.debug('decoded bytes: {}', [decodedBytes.toHex()]);
log.debug('decoded bigInt1: {}', [decodedBigInt1.toString()]);
log.debug('decoded bigInt2: {}', [decodedBigInt2.toString()]);
log.debug('decoded bool: {}', [decodedBool.toString()]);
return [
decodedAddress.toHex(),
decodedBytes.toHex(),
decodedBigInt1.toString(),
decodedBigInt2.toString(),
decodedBool.toString()
];
}

View File

@ -21,6 +21,7 @@ export const getDummyEventData = async (): Promise<EventData> => {
const ethersBlock = await provider.getBlock(blockNumber); const ethersBlock = await provider.getBlock(blockNumber);
const block = { const block = {
headerId: 0,
blockHash: ethersBlock.hash, blockHash: ethersBlock.hash,
blockNumber: ethersBlock.number.toString(), blockNumber: ethersBlock.number.toString(),
timestamp: '0', timestamp: '0',
@ -41,7 +42,11 @@ export const getDummyEventData = async (): Promise<EventData> => {
hash: ZERO_HASH, hash: ZERO_HASH,
index: 0, index: 0,
from: ZERO_ADDRESS, from: ZERO_ADDRESS,
to: ZERO_ADDRESS to: ZERO_ADDRESS,
value: '0',
gasLimit: '0',
gasPrice: '0',
input: ZERO_HASH
}; };
return { return {