-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add mocked ethereum call implementation #108
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { expect } from 'chai' | ||
import { Mappings } from './wasm' | ||
import { ValueKind } from './wasm/runtime/ethereum' | ||
|
||
describe('onTokenListing', function () { | ||
it('creates a token', async () => { | ||
const mappings = await Mappings.load() | ||
mappings.setCallHandler((call) => { | ||
const callName = `${call.contractName}.${call.functionName}` | ||
switch (callName) { | ||
case `Erc20.symbol`: | ||
return [{ kind: ValueKind.String, data: 'TEST' }] | ||
case `Erc20.name`: | ||
return [{ kind: ValueKind.String, data: 'Test Token' }] | ||
case `Erc20.decimals`: | ||
return [{ kind: ValueKind.Uint, data: 18n }] | ||
default: | ||
throw new Error(`unexpected contract call ${callName}`) | ||
} | ||
}) | ||
|
||
mappings.onTokenListing( | ||
{ | ||
id: 42, | ||
token: '0x1337133713371337133713371337133713371337', | ||
}, | ||
{ | ||
block: { | ||
timestamp: 42 * 300, | ||
}, | ||
transaction: { | ||
hash: `0x${'42'.repeat(32)}`, | ||
}, | ||
}, | ||
) | ||
|
||
expect(mappings.getEntity('Token', '42')).to.deep.equal({ | ||
id: '42', | ||
address: '0x1337133713371337133713371337133713371337', | ||
fromBatchId: 42n, | ||
symbol: 'TEST', | ||
decimals: 18n, | ||
name: 'Test Token', | ||
createEpoch: 42n * 300n, | ||
txHash: `0x${'42'.repeat(32)}`, | ||
}) | ||
}) | ||
|
||
it('accepts tokens without details', async () => { | ||
const mappings = await Mappings.load() | ||
mappings.setCallHandler(() => null) | ||
|
||
mappings.onTokenListing({ | ||
id: 0, | ||
token: `0x${'00'.repeat(20)}`, | ||
}) | ||
|
||
const { symbol, name, decimals } = mappings.getEntity('Token', '0')! | ||
expect({ symbol, name, decimals }).to.deep.equal({ | ||
symbol: null, | ||
name: null, | ||
decimals: null, | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,7 +49,7 @@ export class Abi { | |
} | ||
|
||
private readArray<T>(ptr: Pointer, reader: (value: Pointer) => T): T[] | null { | ||
if (ptr === null) { | ||
if (ptr === 0) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be caught by the typechecker (cf open issue), maybe we can use a lint rule for now to catch these: https://github.com/microsoft/TypeScript/issues/11920https://palantir.github.io/tslint/rules/strict-type-predicates/ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're using ESLint-TypeScript (since, from what I understand, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🕵️ |
||
return null | ||
} | ||
|
||
|
@@ -92,6 +92,94 @@ export class Abi { | |
return fromBytesLE(bytes) | ||
} | ||
|
||
public readEthereumCall(ptr: Pointer): Ethereum.Call | null { | ||
if (ptr === 0) { | ||
return null | ||
} | ||
|
||
const contractName = this.readString(this.getWord(ptr)) | ||
if (contractName === null) { | ||
throw new Error('null call contract name') | ||
} | ||
const contractAddress = this.readUint8Array(this.getWord(ptr + 4)) | ||
if (contractAddress === null) { | ||
throw new Error('null call contract address') | ||
} | ||
|
||
const functionName = this.readString(this.getWord(ptr + 8)) | ||
if (functionName === null) { | ||
throw new Error('null call function name') | ||
} | ||
const functionSignature = this.readString(this.getWord(ptr + 12)) | ||
if (functionSignature === null) { | ||
throw new Error('null call function signature') | ||
} | ||
const functionParams = this.readArray(this.getWord(ptr + 16), (ptr) => { | ||
const value = this.readEthereumValue(ptr) | ||
if (value === null) { | ||
throw new Error('null call function parameter value') | ||
} | ||
return value | ||
}) | ||
if (functionParams === null) { | ||
throw new Error('null call function parameters') | ||
} | ||
|
||
return { contractName, contractAddress, functionName, functionSignature, functionParams } | ||
} | ||
|
||
private readEthereumValue(ptr: Pointer): Ethereum.Value | null { | ||
if (ptr === 0) { | ||
return null | ||
} | ||
|
||
const kind = this.getWord(ptr) | ||
const payload = Number(this.view.getBigInt64(ptr + 8, LE)) | ||
|
||
let data: unknown | ||
switch (kind) { | ||
case Ethereum.ValueKind.Address: | ||
case Ethereum.ValueKind.FixedBytes: | ||
case Ethereum.ValueKind.Bytes: | ||
data = this.readUint8Array(payload) | ||
break | ||
case Ethereum.ValueKind.Int: | ||
case Ethereum.ValueKind.Uint: | ||
data = this.readBigInt(payload) | ||
break | ||
case Ethereum.ValueKind.Bool: | ||
switch (payload) { | ||
case 0: | ||
data = false | ||
break | ||
case 1: | ||
data = true | ||
break | ||
default: | ||
throw new Error(`invalid boolean value ${payload}`) | ||
} | ||
break | ||
case Ethereum.ValueKind.String: | ||
data = this.readString(payload) | ||
break | ||
case Ethereum.ValueKind.FixedArray: | ||
case Ethereum.ValueKind.Array: | ||
case Ethereum.ValueKind.Tuple: | ||
data = this.readArray(payload, (ptr) => { | ||
const value = this.readStoreValue(ptr) | ||
if (value === null) { | ||
throw new Error('invalid null ethereum value in array') | ||
} | ||
return value | ||
}) | ||
break | ||
default: | ||
throw new Error(`invalid store value ${kind}`) | ||
} | ||
|
||
return { kind, data } as Ethereum.Value | ||
} | ||
|
||
public readStoreEntity(ptr: Pointer): Store.Entity | null { | ||
if (ptr === 0) { | ||
return null | ||
|
@@ -308,7 +396,7 @@ export class Abi { | |
case Ethereum.ValueKind.FixedArray: | ||
case Ethereum.ValueKind.Array: | ||
case Ethereum.ValueKind.Tuple: | ||
payload = this.writeArray(value.data, (value) => this.writeEthereumValue(value)) | ||
payload = this.writeEthereumValues(value.data) | ||
break | ||
default: | ||
throw new Error(`invalid ethereum value ${value}`) | ||
|
@@ -321,6 +409,14 @@ export class Abi { | |
return ptr | ||
} | ||
|
||
public writeEthereumValues(values: Ethereum.Value[] | null): Pointer { | ||
if (values === null) { | ||
return 0 | ||
} | ||
|
||
return this.writeArray(values, (value) => this.writeEthereumValue(value)) | ||
} | ||
|
||
public writeStoreEntity(value: Store.Entity | null): Pointer { | ||
if (value === null) { | ||
return 0 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { Call, Value } from './ethereum' | ||
|
||
export type CallHandler = (call: Call) => Value[] | null | ||
|
||
export interface IEthereum { | ||
call: CallHandler | ||
} | ||
|
||
export const DEFAULT_ETHEREUM: IEthereum = { | ||
call: ({ contractName, functionSignature }: Call) => { | ||
throw new Error(`unexpected ethereum call ${contractName}.${functionSignature}`) | ||
}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I assume in then long term we can just pass in the Abis and perform the calls automatically?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct! The idea is to have something like we have for the events and entities, except for events. Ultimately, I think all of this should be auto-generated based on the
subgraph.yaml
definition.