Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/warm-spiders-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-interpreter': minor
---

Add fetch and async calls support to quickjs and eval interpreters with extra config parameter. Add extra optional 'context' field to all interpreted tx
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"@3loop/transaction-decoder": "workspace:*",
"@3loop/transaction-interpreter": "workspace:*",
"@jitl/quickjs-singlefile-browser-release-sync": "^0.29.2",
"@jitl/quickjs-singlefile-browser-release-sync": "^0.31.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.4",
Expand Down
10 changes: 8 additions & 2 deletions apps/web/src/lib/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import variant from '@jitl/quickjs-singlefile-browser-release-sync'

const config = Layer.succeed(QuickjsConfig, {
variant: variant,
runtimeConfig: { timeout: 1000 },
runtimeConfig: {
timeout: 1000,
useFetch: true,
},
})

const layer = Layer.provide(QuickjsInterpreterLive, config)
Expand All @@ -26,10 +29,13 @@ export interface Interpretation {
export async function applyInterpreter(
decodedTx: DecodedTransaction,
interpreter: Interpreter,
interpretAsUserAddress?: string,
): Promise<Interpretation> {
const runnable = Effect.gen(function* () {
const interpreterService = yield* TransactionInterpreter
const interpretation = yield* interpreterService.interpretTx(decodedTx, interpreter)
const interpretation = yield* interpreterService.interpretTransaction(decodedTx, interpreter, {
interpretAsUserAddress,
})
return interpretation
}).pipe(Effect.provide(layer))

Expand Down
9 changes: 9 additions & 0 deletions packages/transaction-interpreter/interpreters/aerodrom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { genericSwapInterpreter } from './std.js'
import type { InterpretedTransaction } from '../src/types.js'
import type { DecodedTransaction } from '@3loop/transaction-decoder'

export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
return genericSwapInterpreter(event)
}

export const contracts = []
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import type { InterpretedTransaction } from '@/types.js'
import type { InterpretedTransaction } from '../src/types.js'
import type { DecodedTransaction } from '@3loop/transaction-decoder'
import { genericSwapInterpreter } from './std.js'
import { InterpreterOptions } from '../src/types.js'

export function transformEvent(event: DecodedTransaction, options?: InterpreterOptions): InterpretedTransaction {
if (options?.interpretAsUserAddress) {
return genericSwapInterpreter({
...event,
fromAddress: options.interpretAsUserAddress,
})
}

export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
return genericSwapInterpreter(event)
}

Expand Down
28 changes: 14 additions & 14 deletions packages/transaction-interpreter/interpreters/std.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,21 +234,21 @@ export const formatNumber = (numberString: string, precision = 3): string => {
if ((integerPart && integerPart.length < 3 && !decimalPart) || (decimalPart && decimalPart.startsWith('000')))
return numberString

// Apply rounding first to get the correct integer and decimal parts
const rounded = num.toFixed(precision)
const [roundedIntegerPart, roundedDecimalPart] = rounded.split('.')

// Format the integer part manually
let formattedIntegerPart = ''
for (let i = 0; i < integerPart.length; i++) {
if (i > 0 && (integerPart.length - i) % 3 === 0) {
for (let i = 0; i < roundedIntegerPart.length; i++) {
if (i > 0 && (roundedIntegerPart.length - i) % 3 === 0) {
formattedIntegerPart += ','
}
formattedIntegerPart += integerPart[i]
formattedIntegerPart += roundedIntegerPart[i]
}

// Format the decimal part
const formattedDecimalPart = decimalPart
? parseFloat('0.' + decimalPart)
.toFixed(precision)
.split('.')[1]
: '00'
// Use the already-rounded decimal part
const formattedDecimalPart = roundedDecimalPart || '000'

return formattedIntegerPart + '.' + formattedDecimalPart
}
Expand Down Expand Up @@ -284,7 +284,7 @@ export function displayAssets(assets: Payment[]) {
// Categorization Functions

export function isSwap(event: DecodedTransaction): boolean {
if (event.transfers.some((t) => t.type !== 'ERC20' && t.type !== 'native')) return false
if (event.transfers.some((t: Asset) => t.type !== 'ERC20' && t.type !== 'native')) return false

const minted = assetsMinted(event.transfers, event.fromAddress)
const burned = assetsBurned(event.transfers, event.fromAddress)
Expand Down Expand Up @@ -352,8 +352,8 @@ export function genericSwapInterpreter(event: DecodedTransaction): InterpretedTr
type: 'swap',
action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]),
context: {
sent: [netSent[0]],
received: [netReceived[0]],
netSent: [netSent[0]],
netReceived: [netReceived[0]],
},
}

Expand Down Expand Up @@ -396,10 +396,10 @@ export function genericInterpreter(event: DecodedTransaction): InterpretedTransa
//batch mint
if (minted.length > 1) {
const price = newEvent.assetsSent.length === 1 ? newEvent.assetsSent[0] : undefined
const uniqueAssets = new Set(minted.map((asset) => asset.asset.address))
const uniqueAssets = new Set(minted.map((asset: AssetTransfer) => asset.asset.address))

if (uniqueAssets.size === 1) {
const amount = minted.reduce((acc, asset) => acc + Number(asset.amount), 0)
const amount = minted.reduce((acc: number, asset: AssetTransfer) => acc + Number(asset.amount), 0)
return {
...newEvent,
type: 'mint',
Expand Down
15 changes: 11 additions & 4 deletions packages/transaction-interpreter/interpreters/zeroEx.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { assetsReceived, genericInterpreter, displayAsset, getNetTransfers, filterTransfers } from './std.js'
import type { InterpretedTransaction } from '@/types.js'
import type { InterpretedTransaction, InterpreterOptions } from '../src/types.js'
import type { DecodedTransaction } from '@3loop/transaction-decoder'

export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
export function transformEvent(rawEvent: DecodedTransaction, options?: InterpreterOptions): InterpretedTransaction {
const event = options?.interpretAsUserAddress
? {
...rawEvent,
fromAddress: options.interpretAsUserAddress,
}
: rawEvent

const newEvent = genericInterpreter(event)

if (newEvent.type !== 'unknown') return newEvent
Expand Down Expand Up @@ -50,8 +57,8 @@ export function transformEvent(event: DecodedTransaction): InterpretedTransactio
type: 'swap',
action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]),
context: {
sent: [netSent[0]],
received: [netReceived[0]],
netSent: [netSent[0]],
netReceived: [netReceived[0]],
},
assetsReceived: assetsReceived(
filteredTransfers.filter((t) => receivedTokens.includes(t.address)),
Expand Down
2 changes: 1 addition & 1 deletion packages/transaction-interpreter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"eslint-config-prettier": "^8.10.0",
"fast-glob": "^3.3.2",
"prettier": "^2.8.8",
"quickjs-emscripten": "^0.29.1",
"quickjs-emscripten": "^0.31.0",
"rimraf": "^6.0.1",
"tsup": "^7.2.0",
"tsx": "^4.19.0",
Expand Down
63 changes: 40 additions & 23 deletions packages/transaction-interpreter/src/EvalInterpreter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { stringify } from './helpers/stringify.js'
import type { DecodedTransaction } from '@3loop/transaction-decoder'
import { Effect, Layer } from 'effect'
import { Interpreter } from './types.js'
import { Interpreter, InterpreterOptions } from './types.js'
import { getInterpreter } from './interpreters.js'
import { TransactionInterpreter } from './interpreter.js'
import { InterpreterError } from './quickjs.js'

function localEval(code: string, input: string) {
async function localEval(code: string, input: string) {
const fn = new Function(`with(this) { ${code}; return transformEvent(${input}) }`)
return fn.call({})
const result = fn.call({})

// Check if result is a promise and await it
if (result && typeof result.then === 'function') {
return await result
}

return result
}

const make = Effect.succeed({
const make = {
// NOTE: We could export this separately to allow bundling the interpreters separately
findInterpreter: (decodedTx: DecodedTransaction) => {
if (!decodedTx.toAddress) return undefined
Expand All @@ -24,29 +32,38 @@ const make = Effect.succeed({
}
},
interpretTx: (
decodedTx: DecodedTransaction,
decodedTransaction: DecodedTransaction,
interpreter: Interpreter,
options?: {
interpretAsUserAddress?: string
},
) =>
Effect.sync(() => {
// TODO: add ability to surpress warning on acknowledge
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')

let input
if (options?.interpretAsUserAddress) {
input = stringify({
...decodedTx,
fromAddress: options.interpretAsUserAddress,
})
} else {
input = stringify(decodedTx)
}

const result = localEval(interpreter.schema, input)
return result
Effect.tryPromise({
try: async () => {
// TODO: add ability to surpress warning on acknowledge
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')
const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
const result = await localEval(interpreter.schema, input)
return result
},
catch: (error) => new InterpreterError(error),
}).pipe(Effect.withSpan('TransactionInterpreter.interpretTx')),
})

export const EvalInterpreterLive = Layer.scoped(TransactionInterpreter, make)
interpretTransaction: (
decodedTransaction: DecodedTransaction,
interpreter: Interpreter,
options?: InterpreterOptions,
) =>
Effect.tryPromise({
try: async () => {
// TODO: add ability to surpress warning on acknowledge
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')
const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
const result = await localEval(interpreter.schema, input)
return result
},
catch: (error) => new InterpreterError(error),
}).pipe(Effect.withSpan('TransactionInterpreter.interpretTransaction')),
}

export const EvalInterpreterLive = Layer.scoped(TransactionInterpreter, Effect.succeed(make))
1 change: 1 addition & 0 deletions packages/transaction-interpreter/src/QuickjsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface RuntimeConfig {
timeout?: number
memoryLimit?: number
maxStackSize?: number
useFetch?: boolean
}

export interface QuickjsConfig {
Expand Down
48 changes: 33 additions & 15 deletions packages/transaction-interpreter/src/QuickjsInterpreter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { stringify } from './helpers/stringify.js'
import type { DecodedTransaction } from '@3loop/transaction-decoder'
import { Effect, Layer } from 'effect'
import { Interpreter } from './types.js'
import { Interpreter, InterpreterOptions } from './types.js'
import { getInterpreter } from './interpreters.js'
import { QuickjsVM } from './quickjs.js'
import { TransactionInterpreter } from './interpreter.js'
import { QuickjsConfig } from './QuickjsConfig.js'

const make = Effect.gen(function* () {
const vm = yield* QuickjsVM
const config = yield* QuickjsConfig

return {
// NOTE: We could export this separately to allow bundling the interpreters separately
findInterpreter: (decodedTx: DecodedTransaction) => {
if (!decodedTx.toAddress) return undefined

Expand All @@ -22,24 +22,42 @@ const make = Effect.gen(function* () {
id: `${decodedTx.chainID}:${decodedTx.toAddress}`,
}
},

interpretTx: (
decodedTx: DecodedTransaction,
decodedTransaction: DecodedTransaction,
interpreter: Interpreter,
options?: { interpretAsUserAddress?: string },
) =>
Effect.gen(function* () {
let input
if (options?.interpretAsUserAddress) {
input = stringify({
...decodedTx,
fromAddress: options.interpretAsUserAddress,
})
} else {
input = stringify(decodedTx)
}
const result = yield* vm.eval(interpreter.schema + '\n' + 'transformEvent(' + input + ')')
const vm = yield* QuickjsVM

const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
const code = interpreter.schema + '\n' + 'transformEvent(' + input + ')'
const result = yield* vm.eval(code)
return result
}).pipe(
Effect.withSpan('TransactionInterpreter.interpretTx'),
Effect.scoped,
Effect.provideService(QuickjsConfig, config),
),

interpretTransaction: (
decodedTransaction: DecodedTransaction,
interpreter: Interpreter,
options?: InterpreterOptions,
) =>
Effect.gen(function* () {
const vm = yield* QuickjsVM

const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
const code = interpreter.schema + '\n' + 'transformEvent(' + input + ')'
const result = yield* vm.eval(code)
return result
}).pipe(Effect.withSpan('TransactionInterpreter.interpretTx')),
}).pipe(
Effect.withSpan('TransactionInterpreter.interpretTransaction'),
Effect.scoped,
Effect.provideService(QuickjsConfig, config),
),
}
})

Expand Down
9 changes: 8 additions & 1 deletion packages/transaction-interpreter/src/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import type { DecodedTransaction } from '@3loop/transaction-decoder'
import { Context, Effect } from 'effect'
import { InterpretedTransaction, Interpreter } from './types.js'
import { InterpretedTransaction, Interpreter, InterpreterOptions } from './types.js'
import { InterpreterError } from './quickjs.js'

export interface TransactionInterpreter {
readonly findInterpreter: (decodedTx: DecodedTransaction) => Interpreter | undefined

readonly interpretTx: (
decodedTx: DecodedTransaction,
interpreter: Interpreter,
options?: {
interpretAsUserAddress?: string
},
) => Effect.Effect<InterpretedTransaction, InterpreterError, never>

readonly interpretTransaction: (
decodedTransaction: DecodedTransaction,
interpreter: Interpreter,
options?: InterpreterOptions,
) => Effect.Effect<InterpretedTransaction, InterpreterError, never>
}

export const TransactionInterpreter = Context.GenericTag<TransactionInterpreter>('@3loop/TransactionInterpreter')
Loading
Loading