Skip to content

Commit 604ffca

Browse files
committed
Add async and fetch support to interpreters
1 parent 5784e07 commit 604ffca

File tree

15 files changed

+400
-96
lines changed

15 files changed

+400
-96
lines changed

.changeset/warm-spiders-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@3loop/transaction-interpreter': minor
3+
---
4+
5+
Add fetch and async calls support to quickjs and eval interpreters with extra config parameter. Add extra optional 'context' field to all interpreted tx

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"dependencies": {
1212
"@3loop/transaction-decoder": "workspace:*",
1313
"@3loop/transaction-interpreter": "workspace:*",
14-
"@jitl/quickjs-singlefile-browser-release-sync": "^0.29.2",
14+
"@jitl/quickjs-singlefile-browser-release-sync": "^0.31.0",
1515
"@monaco-editor/react": "^4.6.0",
1616
"@radix-ui/react-collapsible": "^1.1.2",
1717
"@radix-ui/react-dialog": "^1.1.4",

apps/web/src/lib/interpreter.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import variant from '@jitl/quickjs-singlefile-browser-release-sync'
1212

1313
const config = Layer.succeed(QuickjsConfig, {
1414
variant: variant,
15-
runtimeConfig: { timeout: 1000 },
15+
runtimeConfig: {
16+
timeout: 1000,
17+
useFetch: true,
18+
},
1619
})
1720

1821
const layer = Layer.provide(QuickjsInterpreterLive, config)
@@ -26,10 +29,13 @@ export interface Interpretation {
2629
export async function applyInterpreter(
2730
decodedTx: DecodedTransaction,
2831
interpreter: Interpreter,
32+
interpretAsUserAddress?: string,
2933
): Promise<Interpretation> {
3034
const runnable = Effect.gen(function* () {
3135
const interpreterService = yield* TransactionInterpreter
32-
const interpretation = yield* interpreterService.interpretTx(decodedTx, interpreter)
36+
const interpretation = yield* interpreterService.interpretTransaction(decodedTx, interpreter, {
37+
interpretAsUserAddress,
38+
})
3339
return interpretation
3440
}).pipe(Effect.provide(layer))
3541

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { genericSwapInterpreter } from './std.js'
2+
import type { InterpretedTransaction } from '../src/types.js'
3+
import type { DecodedTransaction } from '@3loop/transaction-decoder'
4+
5+
export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
6+
return genericSwapInterpreter(event)
7+
}
8+
9+
export const contracts = []

packages/transaction-interpreter/interpreters/routers.ts renamed to packages/transaction-interpreter/interpreters/dexes.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
1-
import type { InterpretedTransaction } from '@/types.js'
1+
import type { InterpretedTransaction } from '../src/types.js'
22
import type { DecodedTransaction } from '@3loop/transaction-decoder'
33
import { genericSwapInterpreter } from './std.js'
4+
import { InterpreterOptions } from '../src/types.js'
5+
6+
export function transformEvent(event: DecodedTransaction, options?: InterpreterOptions): InterpretedTransaction {
7+
if (options?.interpretAsUserAddress) {
8+
return genericSwapInterpreter({
9+
...event,
10+
fromAddress: options.interpretAsUserAddress,
11+
})
12+
}
413

5-
export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
614
return genericSwapInterpreter(event)
715
}
816

packages/transaction-interpreter/interpreters/std.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -234,21 +234,21 @@ export const formatNumber = (numberString: string, precision = 3): string => {
234234
if ((integerPart && integerPart.length < 3 && !decimalPart) || (decimalPart && decimalPart.startsWith('000')))
235235
return numberString
236236

237+
// Apply rounding first to get the correct integer and decimal parts
238+
const rounded = num.toFixed(precision)
239+
const [roundedIntegerPart, roundedDecimalPart] = rounded.split('.')
240+
237241
// Format the integer part manually
238242
let formattedIntegerPart = ''
239-
for (let i = 0; i < integerPart.length; i++) {
240-
if (i > 0 && (integerPart.length - i) % 3 === 0) {
243+
for (let i = 0; i < roundedIntegerPart.length; i++) {
244+
if (i > 0 && (roundedIntegerPart.length - i) % 3 === 0) {
241245
formattedIntegerPart += ','
242246
}
243-
formattedIntegerPart += integerPart[i]
247+
formattedIntegerPart += roundedIntegerPart[i]
244248
}
245249

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

253253
return formattedIntegerPart + '.' + formattedDecimalPart
254254
}
@@ -284,7 +284,7 @@ export function displayAssets(assets: Payment[]) {
284284
// Categorization Functions
285285

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

289289
const minted = assetsMinted(event.transfers, event.fromAddress)
290290
const burned = assetsBurned(event.transfers, event.fromAddress)
@@ -352,8 +352,8 @@ export function genericSwapInterpreter(event: DecodedTransaction): InterpretedTr
352352
type: 'swap',
353353
action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]),
354354
context: {
355-
sent: [netSent[0]],
356-
received: [netReceived[0]],
355+
netSent: [netSent[0]],
356+
netReceived: [netReceived[0]],
357357
},
358358
}
359359

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

401401
if (uniqueAssets.size === 1) {
402-
const amount = minted.reduce((acc, asset) => acc + Number(asset.amount), 0)
402+
const amount = minted.reduce((acc: number, asset: AssetTransfer) => acc + Number(asset.amount), 0)
403403
return {
404404
...newEvent,
405405
type: 'mint',

packages/transaction-interpreter/interpreters/zeroEx.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
import { assetsReceived, genericInterpreter, displayAsset, getNetTransfers, filterTransfers } from './std.js'
2-
import type { InterpretedTransaction } from '@/types.js'
2+
import type { InterpretedTransaction, InterpreterOptions } from '../src/types.js'
33
import type { DecodedTransaction } from '@3loop/transaction-decoder'
44

5-
export function transformEvent(event: DecodedTransaction): InterpretedTransaction {
5+
export function transformEvent(rawEvent: DecodedTransaction, options?: InterpreterOptions): InterpretedTransaction {
6+
const event = options?.interpretAsUserAddress
7+
? {
8+
...rawEvent,
9+
fromAddress: options.interpretAsUserAddress,
10+
}
11+
: rawEvent
12+
613
const newEvent = genericInterpreter(event)
714

815
if (newEvent.type !== 'unknown') return newEvent
@@ -50,8 +57,8 @@ export function transformEvent(event: DecodedTransaction): InterpretedTransactio
5057
type: 'swap',
5158
action: 'Swapped ' + displayAsset(netSent[0]) + ' for ' + displayAsset(netReceived[0]),
5259
context: {
53-
sent: [netSent[0]],
54-
received: [netReceived[0]],
60+
netSent: [netSent[0]],
61+
netReceived: [netReceived[0]],
5562
},
5663
assetsReceived: assetsReceived(
5764
filteredTransfers.filter((t) => receivedTokens.includes(t.address)),

packages/transaction-interpreter/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"eslint-config-prettier": "^8.10.0",
7070
"fast-glob": "^3.3.2",
7171
"prettier": "^2.8.8",
72-
"quickjs-emscripten": "^0.29.1",
72+
"quickjs-emscripten": "^0.31.0",
7373
"rimraf": "^6.0.1",
7474
"tsup": "^7.2.0",
7575
"tsx": "^4.19.0",
Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { stringify } from './helpers/stringify.js'
22
import type { DecodedTransaction } from '@3loop/transaction-decoder'
33
import { Effect, Layer } from 'effect'
4-
import { Interpreter } from './types.js'
4+
import { Interpreter, InterpreterOptions } from './types.js'
55
import { getInterpreter } from './interpreters.js'
66
import { TransactionInterpreter } from './interpreter.js'
7+
import { InterpreterError } from './quickjs.js'
78

8-
function localEval(code: string, input: string) {
9+
async function localEval(code: string, input: string) {
910
const fn = new Function(`with(this) { ${code}; return transformEvent(${input}) }`)
10-
return fn.call({})
11+
const result = fn.call({})
12+
13+
// Check if result is a promise and await it
14+
if (result && typeof result.then === 'function') {
15+
return await result
16+
}
17+
18+
return result
1119
}
1220

13-
const make = Effect.succeed({
21+
const make = {
1422
// NOTE: We could export this separately to allow bundling the interpreters separately
1523
findInterpreter: (decodedTx: DecodedTransaction) => {
1624
if (!decodedTx.toAddress) return undefined
@@ -24,29 +32,38 @@ const make = Effect.succeed({
2432
}
2533
},
2634
interpretTx: (
27-
decodedTx: DecodedTransaction,
35+
decodedTransaction: DecodedTransaction,
2836
interpreter: Interpreter,
2937
options?: {
3038
interpretAsUserAddress?: string
3139
},
3240
) =>
33-
Effect.sync(() => {
34-
// TODO: add ability to surpress warning on acknowledge
35-
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')
36-
37-
let input
38-
if (options?.interpretAsUserAddress) {
39-
input = stringify({
40-
...decodedTx,
41-
fromAddress: options.interpretAsUserAddress,
42-
})
43-
} else {
44-
input = stringify(decodedTx)
45-
}
46-
47-
const result = localEval(interpreter.schema, input)
48-
return result
41+
Effect.tryPromise({
42+
try: async () => {
43+
// TODO: add ability to surpress warning on acknowledge
44+
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')
45+
const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
46+
const result = await localEval(interpreter.schema, input)
47+
return result
48+
},
49+
catch: (error) => new InterpreterError(error),
4950
}).pipe(Effect.withSpan('TransactionInterpreter.interpretTx')),
50-
})
5151

52-
export const EvalInterpreterLive = Layer.scoped(TransactionInterpreter, make)
52+
interpretTransaction: (
53+
decodedTransaction: DecodedTransaction,
54+
interpreter: Interpreter,
55+
options?: InterpreterOptions,
56+
) =>
57+
Effect.tryPromise({
58+
try: async () => {
59+
// TODO: add ability to surpress warning on acknowledge
60+
Effect.logWarning('Using eval in production can result in security vulnerabilities. Use at your own risk.')
61+
const input = stringify(decodedTransaction) + (options ? `,${stringify(options)}` : '')
62+
const result = await localEval(interpreter.schema, input)
63+
return result
64+
},
65+
catch: (error) => new InterpreterError(error),
66+
}).pipe(Effect.withSpan('TransactionInterpreter.interpretTransaction')),
67+
}
68+
69+
export const EvalInterpreterLive = Layer.scoped(TransactionInterpreter, Effect.succeed(make))

packages/transaction-interpreter/src/QuickjsConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface RuntimeConfig {
55
timeout?: number
66
memoryLimit?: number
77
maxStackSize?: number
8+
useFetch?: boolean
89
}
910

1011
export interface QuickjsConfig {

0 commit comments

Comments
 (0)