Skip to content

Commit e5ec435

Browse files
committed
feat: forward arbitrary args to tempo
1 parent dc2bd84 commit e5ec435

File tree

5 files changed

+145
-52
lines changed

5 files changed

+145
-52
lines changed

.changeset/icy-lizards-judge.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"prool": patch
3+
---
4+
5+
Modified `Instance.tempo` to forward arbitrary arguments.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"check:types": "tsc",
88
"dev": "zile dev",
99
"postinstall": "pnpm dev",
10-
"test": "vitest --testTimeout=10000"
10+
"test": "vitest --testTimeout=100000"
1111
},
1212
"devDependencies": {
1313
"@biomejs/biome": "^2.3.8",

src/instances/tempo.ts

Lines changed: 79 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,43 +6,60 @@ import {
66
Wait,
77
} from 'testcontainers'
88
import * as Instance from '../Instance.js'
9+
import { deepAssign, toArgs } from '../internal/utils.js'
910
import { execa } from '../processes/execa.js'
1011

1112
export function command(parameters: tempo.Parameters): string[] {
12-
const { faucet, port } = parameters
13-
const dataDir = path.join(os.tmpdir(), '.prool', `tempo.${port}`)
13+
const { blockMaxTransactions, blockTime, mnemonic, port, ...rest } =
14+
parameters
15+
16+
const datadir = path.join(os.tmpdir(), '.prool', `tempo.${port}`)
17+
const defaultParameters = {
18+
authrpc: {
19+
port: port! + 30,
20+
},
21+
datadir,
22+
dev: [
23+
true,
24+
{
25+
blockTime: blockTime ?? '50ms',
26+
...(blockMaxTransactions ? { blockMaxTransactions } : {}),
27+
...(mnemonic ? { mnemonic } : {}),
28+
},
29+
],
30+
engine: {
31+
disablePrecompileCache: true,
32+
legacyStateRoot: true,
33+
},
34+
faucet: {
35+
address: [
36+
'0x20c0000000000000000000000000000000000000',
37+
'0x20c0000000000000000000000000000000000001',
38+
'0x20c0000000000000000000000000000000000002',
39+
'0x20c0000000000000000000000000000000000003',
40+
],
41+
amount: '1000000000000',
42+
enabled: true,
43+
privateKey:
44+
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
45+
},
46+
http: {
47+
addr: '0.0.0.0',
48+
api: 'all',
49+
corsdomain: '*',
50+
port: port!,
51+
},
52+
port: port! + 10,
53+
ws: {
54+
port: port! + 20,
55+
},
56+
}
57+
1458
return [
1559
'node',
16-
`--authrpc.port=${port! + 30}`,
17-
`--datadir=${dataDir}`,
18-
'--dev',
19-
`--dev.block-time=${parameters?.blockTime ?? '50ms'}`,
20-
'--engine.disable-precompile-cache',
21-
'--engine.legacy-state-root',
22-
'--faucet.address',
23-
...(faucet?.addresses ?? [
24-
'0x20c0000000000000000000000000000000000000',
25-
'0x20c0000000000000000000000000000000000001',
26-
'0x20c0000000000000000000000000000000000002',
27-
'0x20c0000000000000000000000000000000000003',
28-
]),
29-
`--faucet.amount=${faucet?.amount ?? '1000000000000'}`,
30-
'--faucet.enabled',
31-
`--faucet.private-key=${faucet?.privateKey ?? '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'}`,
32-
'--http',
33-
'--http.addr=0.0.0.0',
34-
'--http.api=all',
35-
'--http.corsdomain=*',
36-
`--http.port=${port!}`,
37-
`--port=${port! + 10}`,
38-
'--txpool.basefee-max-count=10000000000000',
39-
'--txpool.basefee-max-size=10000',
40-
'--txpool.max-account-slots=500000',
41-
'--txpool.pending-max-count=10000000000000',
42-
'--txpool.pending-max-size=10000',
43-
'--txpool.queued-max-count=10000000000000',
44-
'--txpool.queued-max-size=10000',
45-
`--ws.port=${port! + 20}`,
60+
...toArgs(deepAssign(defaultParameters, rest), {
61+
arraySeparator: null,
62+
}),
4663
]
4764
}
4865

@@ -89,7 +106,7 @@ export const tempo = Instance.define((parameters?: tempo.Parameters) => {
89106
env: {
90107
RUST_LOG,
91108
},
92-
})`${[binary, ...command({ ...parameters, port })]}`,
109+
})`${[binary, ...command({ ...args, port })]}`,
93110
{
94111
...options,
95112
// Resolve when the process is listening via "RPC HTTP server started" message.
@@ -124,6 +141,28 @@ export declare namespace tempo {
124141
* Interval between blocks.
125142
*/
126143
blockTime?: string | undefined
144+
/**
145+
* How many transactions to mine per block
146+
*/
147+
blockMaxTransactions?: number | undefined
148+
/**
149+
* Path to a configuration file.
150+
*/
151+
config?: string | undefined
152+
/**
153+
* The chain this node is running.
154+
* Possible values are either a built-in chain or the path to a chain specification file.
155+
*
156+
* Built-in chains:
157+
* - testnet
158+
*
159+
* @default "testnet"
160+
*/
161+
chain?: string | undefined
162+
/**
163+
* The path to the data dir for all reth files and subdirectories.
164+
*/
165+
datadir?: string | undefined
127166
/**
128167
* Faucet options.
129168
*/
@@ -160,11 +199,16 @@ export declare namespace tempo {
160199
* Host the server will listen on.
161200
*/
162201
host?: string | undefined
202+
/**
203+
* Derive dev accounts from a fixed mnemonic instead of random ones.
204+
* @default "test test test test test test test test test test test junk"
205+
*/
206+
mnemonic?: string | undefined
163207
/**
164208
* Port the server will listen on.
165209
*/
166210
port?: number | undefined
167-
}
211+
} & Record<string, unknown>
168212
}
169213

170214
/**
@@ -218,7 +262,7 @@ export const tempoDocker = Instance.define(
218262
])
219263
.withName(containerName)
220264
.withEnvironment({ RUST_LOG })
221-
.withCommand(command({ ...parameters, port }))
265+
.withCommand(command({ ...args, port }))
222266
.withWaitStrategy(Wait.forLogMessage(/RPC HTTP server started/))
223267
.withLogConsumer((stream) => {
224268
stream.on('data', (data) => {

src/internal/utils.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ test.each([
3030
[[{ foo: 0 }], ['--foo', '0']],
3131
[[{ foo: 1 }], ['--foo', '1']],
3232
[[{ foo: 1n }], ['--foo', '1']],
33+
[[{ foo: 1n }], ['--foo', '1']],
3334
[[{ foo: 'bar', baz: 1 }], ['--foo', 'bar', '--baz', '1']],
3435
[[{ fooBar: 'test' }], ['--foo-bar', 'test']],
3536
[[{ foo: ['bar', 'baz'] }], ['--foo', 'bar,baz']],
3637
[[{ foo: { barBaz: 'test' } }], ['--foo.bar-baz', 'test']],
38+
[[{ foo: [true, { barBaz: 'test' }] }], ['--foo', '--foo.bar-baz', 'test']],
3739
[[{ foo: { barBaz: ['test', 'test2'] } }], ['--foo.bar-baz', 'test,test2']],
3840
[
3941
[{ fooBar: 'test' }, { casing: 'snake' }],
@@ -43,6 +45,18 @@ test.each([
4345
[{ foo: { barBaz: 'test' } }, { casing: 'snake' }],
4446
['--foo.bar_baz', 'test'],
4547
],
48+
[
49+
[{ foo: ['bar', 'baz'] }, { arraySeparator: null }],
50+
['--foo', 'bar', 'baz'],
51+
],
52+
[
53+
[{ foo: { bar: ['baz', 'qux'] } }, { arraySeparator: null }],
54+
['--foo.bar', 'baz', 'qux'],
55+
],
56+
[
57+
[{ foo: ['bar', 'baz'] }, { arraySeparator: null }],
58+
['--foo', 'bar', 'baz'],
59+
],
4660
] as [Parameters<typeof toArgs>, string[]][])('toArgs(%o) -> %o', ([
4761
input,
4862
options,

src/internal/utils.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,26 @@ export function stripColors(message: string) {
5050
* @returns The command line arguments.
5151
*/
5252
export function toArgs(
53-
obj: {
54-
[key: string]:
55-
| Record<string, any>
56-
| string
57-
| readonly string[]
58-
| boolean
59-
| number
60-
| bigint
61-
| undefined
62-
},
63-
options: { casing: 'kebab' | 'snake' } = { casing: 'kebab' },
64-
) {
65-
const { casing } = options
53+
obj: Record<string, unknown>,
54+
options: {
55+
arraySeparator?: string | null | undefined
56+
casing?: 'kebab' | 'snake' | undefined
57+
} = {},
58+
): string[] {
59+
const { arraySeparator = ',', casing = 'kebab' } = options
6660
return Object.entries(obj).flatMap(([key, value]) => {
6761
if (value === undefined) return []
6862

69-
if (Array.isArray(value)) return [toFlagCase(key), value.join(',')]
63+
if (Array.isArray(value)) {
64+
if (value[0] === true)
65+
return [toFlagCase(key), ...toArgs({ [key]: value[1] }, options)]
66+
const arrayValue = (() => {
67+
if (arraySeparator === null) return value
68+
return value.join(arraySeparator)
69+
})()
70+
71+
return [toFlagCase(key), arrayValue].flat()
72+
}
7073

7174
if (typeof value === 'object' && value !== null) {
7275
return Object.entries(value).flatMap(([subKey, subValue]) => {
@@ -75,7 +78,7 @@ export function toArgs(
7578
`${key}.${subKey}`,
7679
casing === 'kebab' ? '-' : '_',
7780
)
78-
return [flag, Array.isArray(subValue) ? subValue.join(',') : subValue]
81+
return toArgs({ [flag.slice(2)]: subValue }, options)
7982
})
8083
}
8184

@@ -84,7 +87,7 @@ export function toArgs(
8487
if (value === false) return [flag, 'false']
8588
if (value === true) return [flag]
8689

87-
const stringified = value.toString()
90+
const stringified = value?.toString() ?? ''
8891
if (stringified === '') return [flag]
8992

9093
return [flag, stringified]
@@ -106,3 +109,30 @@ export function toFlagCase(str: string, separator = '-') {
106109
}
107110
return `--${keys.join('.')}`
108111
}
112+
113+
function isPlainObject(value: unknown): value is Record<string, unknown> {
114+
return Object.prototype.toString.call(value) === '[object Object]'
115+
}
116+
117+
export function deepAssign(
118+
target: Record<string, unknown>,
119+
...sources: Record<string, unknown>[]
120+
): Record<string, unknown> {
121+
for (const source of sources) {
122+
if (!isPlainObject(source)) continue
123+
124+
for (const key of Reflect.ownKeys(source)) {
125+
const srcVal = source[key as keyof typeof source]
126+
const tgtVal = target[key as keyof typeof target]
127+
128+
// If both are plain objects, merge recursively
129+
if (isPlainObject(srcVal) && isPlainObject(tgtVal)) {
130+
deepAssign(tgtVal, srcVal)
131+
continue
132+
}
133+
// Otherwise, assign a deep clone of the source value
134+
;(target as any)[key] = structuredClone(srcVal)
135+
}
136+
}
137+
return target
138+
}

0 commit comments

Comments
 (0)