-
Notifications
You must be signed in to change notification settings - Fork 232
feat: publish passkety-bundler #2500
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
base: main
Are you sure you want to change the base?
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,161 @@ | ||
| name: Passkey Bundler Docker | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| tags: | ||
| - "passkey-bundler/v*" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| concurrency: | ||
| group: passkey-bundler-docker-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| jobs: | ||
| changes: | ||
| name: Detect changes | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| bundler: ${{ steps.determine.outputs.bundler }} | ||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Determine whether to build | ||
| id: determine | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | ||
| echo "bundler=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then | ||
| echo "bundler=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| before="${{ github.event.before }}" | ||
| if [[ -z "${before}" || "${before}" == "0000000000000000000000000000000000000000" ]]; then | ||
| echo "bundler=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| if git diff --name-only "${before}" "${GITHUB_SHA}" -- "passkey-bundler" ".github/workflows/passkey-bundler-docker.yml" | grep -q .; then | ||
| echo "bundler=true" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "bundler=false" >> "$GITHUB_OUTPUT" | ||
|
|
||
| build: | ||
| name: Build (${{ matrix.build.platform }}) | ||
| needs: [changes] | ||
| if: | | ||
| github.event_name == 'workflow_dispatch' || | ||
| startsWith(github.ref, 'refs/tags/passkey-bundler/v') || | ||
| needs.changes.outputs.bundler == 'true' | ||
| strategy: | ||
| fail-fast: true | ||
| matrix: | ||
| build: | ||
| - platform: linux/amd64 | ||
| runner: ubuntu-22.04 | ||
| - platform: linux/arm64 | ||
| runner: ubuntu-22.04-arm | ||
| runs-on: ${{ matrix.build.runner }} | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Prepare env vars | ||
| run: | | ||
| ARCH="$(echo "${{ matrix.build.platform }}" | cut -d '/' -f 2)" | ||
| echo "ARCH=$ARCH" >> "$GITHUB_ENV" | ||
| echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV" | ||
| OWNER="${{ github.repository_owner }}" | ||
| OWNER="${OWNER,,}" | ||
| echo "IMAGE=ghcr.io/${OWNER}/passkey-bundler" >> "$GITHUB_ENV" | ||
| if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then | ||
| VERSION="${GITHUB_REF#refs/tags/passkey-bundler/v}" | ||
| echo "TAG_PREFIX=$VERSION" >> "$GITHUB_ENV" | ||
| else | ||
| echo "TAG_PREFIX=main" >> "$GITHUB_ENV" | ||
| fi | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Login to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Build and push (arch) | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: passkey-bundler | ||
| push: true | ||
| platforms: ${{ matrix.build.platform }} | ||
| tags: ${{ env.IMAGE }}:${{ env.TAG_PREFIX }}-${{ env.ARCH }} | ||
|
|
||
| merge: | ||
| name: Create multi-arch image | ||
| needs: [build] | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Prepare env vars | ||
| run: | | ||
| echo "SHORT_SHA=${GITHUB_SHA::7}" >> "$GITHUB_ENV" | ||
| OWNER="${{ github.repository_owner }}" | ||
| OWNER="${OWNER,,}" | ||
| echo "IMAGE=ghcr.io/${OWNER}/passkey-bundler" >> "$GITHUB_ENV" | ||
| if [[ "${GITHUB_REF}" == refs/tags/passkey-bundler/v* ]]; then | ||
| VERSION="${GITHUB_REF#refs/tags/passkey-bundler/v}" | ||
| echo "TAG_PREFIX=$VERSION" >> "$GITHUB_ENV" | ||
| echo "IS_RELEASE=true" >> "$GITHUB_ENV" | ||
| else | ||
| echo "TAG_PREFIX=main" >> "$GITHUB_ENV" | ||
| echo "IS_RELEASE=false" >> "$GITHUB_ENV" | ||
| fi | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Login to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ghcr.io | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Create and push manifest list | ||
| run: | | ||
| set -euo pipefail | ||
| if [[ "${IS_RELEASE}" == "true" ]]; then | ||
| docker buildx imagetools create \ | ||
| --tag "${IMAGE}:${TAG_PREFIX}" \ | ||
| --tag "${IMAGE}:latest" \ | ||
| "${IMAGE}:${TAG_PREFIX}-amd64" \ | ||
| "${IMAGE}:${TAG_PREFIX}-arm64" | ||
| else | ||
| docker buildx imagetools create \ | ||
| --tag "${IMAGE}:main" \ | ||
| --tag "${IMAGE}:sha-${SHORT_SHA}" \ | ||
| "${IMAGE}:main-amd64" \ | ||
| "${IMAGE}:main-arm64" | ||
| fi |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -362,3 +362,4 @@ debug_container.dot | |
| AGENTS.md | ||
| precompile.test | ||
| tx_log.json | ||
| passkey-bundler/.tmp | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,78 @@ import os from "node:os" | |
| import path from "node:path" | ||
| import { test } from "node:test" | ||
|
|
||
| import { loadConfig } from "../src/config" | ||
| import { RateLimiter } from "../src/rateLimiter" | ||
| import { SqliteStore } from "../src/store" | ||
| import { requiredPrefund } from "../src/userop" | ||
| import { NonceManager } from "../src/submission" | ||
| import { calldataGasCost, estimatePreVerificationGas } from "../src/validation" | ||
|
|
||
| function withEnv<T>(updates: Record<string, string | undefined>, fn: () => T): T { | ||
| const previous: Record<string, string | undefined> = {} | ||
| for (const [key, value] of Object.entries(updates)) { | ||
| previous[key] = process.env[key] | ||
| if (value === undefined) { | ||
| delete process.env[key] | ||
| } else { | ||
| process.env[key] = value | ||
| } | ||
| } | ||
|
|
||
| try { | ||
| return fn() | ||
| } finally { | ||
| for (const [key, value] of Object.entries(previous)) { | ||
| if (value === undefined) { | ||
| delete process.env[key] | ||
| } else { | ||
| process.env[key] = value | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const bundlerEnvKeys = [ | ||
| "BUNDLER_CONFIG", | ||
| "BUNDLER_MODE", | ||
| "RPC_URL", | ||
| "JSON_RPC_ENDPOINT", | ||
| "ENTRY_POINT", | ||
| "CHAIN_ID", | ||
| "BUNDLER_PRIVATE_KEY", | ||
| "BENEFICIARY", | ||
| "BUNDLER_PORT", | ||
| "METRICS_PORT", | ||
| "MAX_BODY_BYTES", | ||
| "BUNDLER_REQUIRE_AUTH", | ||
| "RATE_LIMIT", | ||
| "BUNDLER_API_KEYS", | ||
| "MAX_QUEUE", | ||
| "QUEUE_CONCURRENCY", | ||
| "LOG_LEVEL", | ||
| "ENABLE_PASSKEY_HELPERS", | ||
| "DB_URL", | ||
| "VALIDATION_ENABLED", | ||
| "GAS_BUMP", | ||
| "GAS_BUMP_WEI", | ||
| "PREFUND_ENABLED", | ||
| "MAX_PREFUND_WEI", | ||
| "PREFUND_ALLOWLIST", | ||
| "SUBMISSION_TIMEOUT_MS", | ||
| "FINALITY_BLOCKS", | ||
| "RECEIPT_LIMIT", | ||
| "RECEIPT_POLL_INTERVAL_MS", | ||
| ] as const | ||
|
|
||
| function withBundlerEnv<T>( | ||
| updates: Partial<Record<(typeof bundlerEnvKeys)[number], string | undefined>>, | ||
| fn: () => T, | ||
| ): T { | ||
| const reset: Record<string, string | undefined> = {} | ||
| for (const key of bundlerEnvKeys) reset[key] = undefined | ||
| return withEnv({ ...reset, ...updates }, fn) | ||
| } | ||
|
|
||
| test("rate limiter enforces window", () => { | ||
| const limiter = new RateLimiter(2, 1000) | ||
| assert.equal(limiter.allow("a"), true) | ||
|
|
@@ -148,3 +214,41 @@ test("sqlite store persists userOp records and receipts", async () => { | |
|
|
||
| fs.rmSync(dir, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| test("testnet mode defaults authRequired=true and requires API keys", () => { | ||
| const entryPoint = "0x" + "11".repeat(20) | ||
| const privateKey = "0x" + "22".repeat(32) | ||
|
|
||
| withBundlerEnv( | ||
| { | ||
| BUNDLER_MODE: "testnet", | ||
| ENTRY_POINT: entryPoint, | ||
| BUNDLER_PRIVATE_KEY: privateKey, | ||
| DB_URL: "sqlite:./data/bundler.sqlite", | ||
| }, | ||
| () => { | ||
| assert.throws( | ||
| () => loadConfig(), | ||
| /Testnet mode requires API keys \(set BUNDLER_API_KEYS\) or disable authRequired/, | ||
| ) | ||
| }, | ||
| ) | ||
| }) | ||
|
Comment on lines
+218
to
+236
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 test writes to a hardcoded database path ( test("testnet mode defaults authRequired=true and requires API keys", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "passkey-bundler-"))
try {
const entryPoint = "0x" + "11".repeat(20)
const privateKey = "0x" + "22".repeat(32)
withBundlerEnv(
{
BUNDLER_MODE: "testnet",
ENTRY_POINT: entryPoint,
BUNDLER_PRIVATE_KEY: privateKey,
DB_URL: `sqlite:${path.join(dir, "bundler.sqlite")}`,
},
() => {
assert.throws(
() => loadConfig(),
/Testnet mode requires API keys \(set BUNDLER_API_KEYS\) or disable authRequired/,
)
},
)
} finally {
fs.rmSync(dir, { recursive: true, force: true })
}
}) |
||
|
|
||
| test("testnet mode respects BUNDLER_REQUIRE_AUTH=false", () => { | ||
| const entryPoint = "0x" + "11".repeat(20) | ||
| const privateKey = "0x" + "22".repeat(32) | ||
|
|
||
| const config = withBundlerEnv( | ||
| { | ||
| BUNDLER_MODE: "testnet", | ||
| ENTRY_POINT: entryPoint, | ||
| BUNDLER_PRIVATE_KEY: privateKey, | ||
| DB_URL: "sqlite:./data/bundler.sqlite", | ||
| BUNDLER_REQUIRE_AUTH: "false", | ||
| }, | ||
| () => loadConfig(), | ||
| ) | ||
|
|
||
| assert.equal(config.authRequired, false) | ||
| }) | ||
|
Comment on lines
+238
to
+254
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. Similar to the previous test, this test writes to a hardcoded database path. This should be updated to use a temporary directory to ensure test isolation and prevent leaving artifacts. test("testnet mode respects BUNDLER_REQUIRE_AUTH=false", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "passkey-bundler-"))
try {
const entryPoint = "0x" + "11".repeat(20)
const privateKey = "0x" + "22".repeat(32)
const config = withBundlerEnv(
{
BUNDLER_MODE: "testnet",
ENTRY_POINT: entryPoint,
BUNDLER_PRIVATE_KEY: privateKey,
DB_URL: `sqlite:${path.join(dir, "bundler.sqlite")}`,
BUNDLER_REQUIRE_AUTH: "false",
},
() => loadConfig(),
)
assert.equal(config.authRequired, false)
} finally {
fs.rmSync(dir, { recursive: true, force: true })
}
}) |
||
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.
This check for an empty
normalizedstring is redundant. Ifnormalizedis an empty string, it won't be found in either of the subsequentincludes()checks, and the function will correctly returnundefinedat the end. Removing this line makes the function more concise without changing its behavior.