Skip to content

Commit

Permalink
mock ethereum calls
Browse files Browse the repository at this point in the history
  • Loading branch information
Nicholas Rodrigues Lordello authored and nlordell committed Sep 29, 2020
1 parent 252fa7a commit fca044f
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 27 deletions.
65 changes: 65 additions & 0 deletions tests/tokens.test.ts
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,
})
})
})
14 changes: 14 additions & 0 deletions tests/wasm/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const EventDefinitions = {
amount: Ethereum.ValueKind.Uint,
batchId: Ethereum.ValueKind.Uint,
},
TokenListing: {
token: Ethereum.ValueKind.Address,
id: Ethereum.ValueKind.Uint,
},
} as const

export type EventNames = keyof typeof EventDefinitions
Expand All @@ -36,6 +40,16 @@ const EntityDefinitions = {
createEpoch: Store.ValueKind.BigInt,
txHash: Store.ValueKind.Bytes,
},
Token: {
id: Store.ValueKind.String,
address: Store.ValueKind.Bytes,
fromBatchId: Store.ValueKind.BigInt,
symbol: { optional: Store.ValueKind.String },
decimals: { optional: Store.ValueKind.BigInt },
name: { optional: Store.ValueKind.String },
createEpoch: Store.ValueKind.BigInt,
txHash: Store.ValueKind.Bytes,
},
} as const

export type EntityNames = keyof typeof EntityDefinitions
Expand Down
11 changes: 10 additions & 1 deletion tests/wasm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import fs from 'fs'
import path from 'path'
import { Module, Runtime } from './runtime'
import { CallHandler } from './runtime/chain'
import { fromEntityData, toEntityData, toEvent, EntityNames, EntityData, EventData, EventMetadata } from './definitions'

// NOTE: Use `readFileSync` here so that we pay the price of reading the Wasm
Expand All @@ -16,7 +17,7 @@ const MODULE_WASM = fs.readFileSync(path.join(__dirname, '../../build/BatchExcha
const MODULE = Module.compile('BatchExchange.wasm', MODULE_WASM)

export class Mappings {
private constructor(private runtime: Runtime) {}
private constructor(private readonly runtime: Runtime) {}

public static async load(): Promise<Mappings> {
const runtime = await Runtime.instantiate(await MODULE)
Expand All @@ -27,6 +28,10 @@ export class Mappings {
this.runtime.eventHandler('onDeposit', toEvent('Deposit', deposit, meta))
}

public onTokenListing(tokenListing: EventData<'TokenListing'>, meta?: EventMetadata): void {
this.runtime.eventHandler('onTokenListing', toEvent('TokenListing', tokenListing, meta))
}

public getEntity<T extends EntityNames>(name: T, id: string): EntityData<T> | null {
const entity = this.runtime.getEntity(name, id)
if (entity === null) {
Expand All @@ -40,4 +45,8 @@ export class Mappings {
const entity = fromEntityData(name, data)
this.runtime.setEntity(name, id, entity)
}

public setCallHandler(call: CallHandler): void {
this.runtime.setEth({ call })
}
}
100 changes: 98 additions & 2 deletions tests/wasm/runtime/abi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class Abi {
}

private readArray<T>(ptr: Pointer, reader: (value: Pointer) => T): T[] | null {
if (ptr === null) {
if (ptr === 0) {
return null
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}`)
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/wasm/runtime/chain.ts
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}`)
},
}
65 changes: 47 additions & 18 deletions tests/wasm/runtime/entities.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { fromHex, toHex } from './convert'
import { Entity, Entry, Value, ValueKind } from './store'

export type RecursiveValueKind = Exclude<ValueKind, ValueKind.Array> | readonly [RecursiveValueKind]
export type Definition = Record<string, RecursiveValueKind>
export type SimpleValueKind = Exclude<ValueKind, ValueKind.Array | ValueKind.Null>
export type EntityValueKind = SimpleValueKind | readonly [SimpleValueKind] | { readonly optional: SimpleValueKind }
export type Definition = Record<string, EntityValueKind>

export type ValueOf<T> = T extends ValueKind.Array
export type RawValueOf<T> = T extends ValueKind.Array | ValueKind.Null
? never
: T extends ValueKind.Bytes
? string
: T extends readonly [infer S]
? ValueOf<S>[]
: Extract<Value, { kind: T }>['data']
export type ValueOf<T> = T extends readonly [infer S]
? RawValueOf<S>[]
: T extends { readonly optional: infer S }
? RawValueOf<S> | null
: RawValueOf<T>

export type Data<T> = {
[K in keyof T]: ValueOf<T[K]>
Expand Down Expand Up @@ -40,26 +44,35 @@ export function fromData<D extends Definition>(definition: D, data: Data<D>): En
return { entries }
}

function isRecursive(kind: RecursiveValueKind): kind is readonly [RecursiveValueKind] {
function isArray(kind: EntityValueKind): kind is readonly [SimpleValueKind] {
return Array.isArray(kind)
}

function coerceFromValue(value: Value, kind: RecursiveValueKind): unknown {
const unexpectedKindError = (expectedKind: ValueKind) => {
const n = (kind: ValueKind) => ValueKind[kind] || 'unknown'
return new Error(`expected ${n(expectedKind)} store value but got ${n(value.kind)}`)
}
function isOptional(kind: EntityValueKind): kind is { readonly optional: SimpleValueKind } {
return typeof kind === 'object' && 'optional' in kind
}

if (isRecursive(kind)) {
function coerceFromValue(value: Value, kind: EntityValueKind): unknown {
if (isArray(kind)) {
if (value.kind !== ValueKind.Array) {
throw unexpectedKindError(ValueKind.Array)
throw unexpectedKindError(value, ValueKind.Array)
}
return value.data.map((value) => coerceFromValue(value, kind[0]))
return value.data.map((value) => coerceFromSimpleValue(value, kind[0]))
} else if (isOptional(kind)) {
if (value.kind === ValueKind.Null) {
return null
}
return coerceFromSimpleValue(value, kind.optional)
} else {
return coerceFromSimpleValue(value, kind)
}
}

function coerceFromSimpleValue(value: Value, kind: SimpleValueKind): unknown {
if (value.kind !== kind) {
throw unexpectedKindError(kind)
throw unexpectedKindError(value, kind)
}

switch (value.kind) {
case ValueKind.Bytes:
return toHex(value.data)
Expand All @@ -68,14 +81,30 @@ function coerceFromValue(value: Value, kind: RecursiveValueKind): unknown {
}
}

function coerceToValue(data: unknown, kind: RecursiveValueKind): Value {
if (isRecursive(kind)) {
function unexpectedKindError(value: Value, expectedKind: ValueKind) {
if (value.kind !== expectedKind) {
const n = (kind: ValueKind) => ValueKind[kind] || 'unknown'
return new Error(`expected ${n(expectedKind)} store value but got ${n(value.kind)}`)
}
}

function coerceToValue(data: unknown, kind: EntityValueKind): Value {
if (isArray(kind)) {
return {
kind: ValueKind.Array,
data: (data as unknown[]).map((data) => coerceToValue(data, kind[0])),
data: (data as unknown[]).map((data) => coerceToSimpleValue(data, kind[0])),
}
} else if (isOptional(kind)) {
if (data === null) {
return { kind: ValueKind.Null, data: null }
}
return coerceToSimpleValue(data, kind.optional)
} else {
return coerceToSimpleValue(data, kind)
}
}

function coerceToSimpleValue(data: unknown, kind: SimpleValueKind): Value {
switch (kind) {
case ValueKind.Bytes:
return { kind, data: fromHex(data as string) }
Expand Down
8 changes: 8 additions & 0 deletions tests/wasm/runtime/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ export interface Event {
transaction: Transaction
parameters: EventParam[]
}

export interface Call {
contractName: string
contractAddress: Address
functionName: string
functionSignature: string
functionParams: Value[]
}
Loading

0 comments on commit fca044f

Please sign in to comment.