diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c3cacd13..b13a53278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,7 +98,7 @@ jobs: - name: Install Playwright Chromium Browser run: pnpm exec playwright install chromium working-directory: examples/demo-app - - name: Run e2e tests + - name: Run ERC4337 e2e tests run: pnpm nx e2e:erc4337 demo-app - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} @@ -106,7 +106,31 @@ jobs: name: demo-app-4337-playwright-report path: examples/demo-app/playwright-report/ retention-days: 3 - + + # Start Auth Server API as a long-lived background service (port 3005) + - name: Start Auth Server API + run: | + nohup pnpm nx dev auth-server-api > /tmp/auth-server-api.log 2>&1 & + echo $! > /tmp/auth-server-api.pid + echo "Waiting for Auth Server API on 3005..." + timeout 60 bash -c 'until nc -z localhost 3005; do echo "Wait-API: Waiting..."; sleep 1; done' + echo "Auth Server API ready." + + - name: Show Auth Server API logs on failure + if: failure() + run: | + echo "--- auth-server-api.log (tail) ---" + tail -200 /tmp/auth-server-api.log || true + + - name: Run demo-only e2e tests (session + passkey) + run: pnpm nx e2e:demo-only demo-app + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: demo-app-demo-only-playwright-report + path: examples/demo-app/playwright-report/ + retention-days: 3 + # e2e-nft-quest: # runs-on: ubuntu-latest # defaults: @@ -166,4 +190,4 @@ jobs: # with: # name: nft-quest-playwright-report # path: examples/nft-quest/playwright-report/ - # retention-days: 3 \ No newline at end of file + # retention-days: 3 diff --git a/docs/paymaster-implementation-plan.md b/docs/paymaster-implementation-plan.md new file mode 100644 index 000000000..490022e9f --- /dev/null +++ b/docs/paymaster-implementation-plan.md @@ -0,0 +1,254 @@ +# Paymaster Implementation Plan + +## Overview + +Add General Paymaster support to the ZKsync SSO auth server and SDK, enabling +gas sponsorship for both regular transactions and session creation. Paymaster +sponsorship works transparently without requiring UI changes. + +## Requirements + +### 1. Paymaster Type + +- **General Paymaster** (simple sponsorship model) +- Uses existing `createGeneralPaymaster()` handler from SDK +- TestPaymaster contract already exists at + `packages/erc4337-contracts/src/test/TestPaymaster.sol` + +### 2. UI Requirements + +- **No extra display** — Paymaster works transparently in the background +- No fee comparison UI, no sponsorship badges +- Existing fee displays remain unchanged + +### 3. Session Integration + +- **No automatic session + paymaster coupling** +- Sessions and paymasters are independent features +- **Can create sessions WITH paymaster enabled** — session creation transaction + can be sponsored +- Session fee limits remain independent of paymaster (track actual gas spent, + not sponsored amounts) + +### 4. Test Requirements + +- **Use unfunded accounts** to prove paymaster sponsorship works +- Transactions that would fail without paymaster must succeed with it +- Fund test paymaster in `beforeAll()` hook, reuse single instance across tests +- Add negative test case: verify unfunded account fails without paymaster + +### 5. Configuration Options + +- **Support both**: paymaster address (simple) and handler function (advanced) +- Address-only → automatically use General Paymaster handler +- Custom handler → supports future ZyFi/custom integrations + +## Architecture Changes + +### SDK-4337 Connector + +```typescript +// packages/sdk-4337/src/connector/index.ts +export interface ZksyncSsoConnectorParameters { + // ... existing params + paymaster?: Address | CustomPaymasterHandler; +} +``` + +### Client Creation Flow + +``` +zksyncSsoConnector({ paymaster: "0x..." }) + └→ getClient({ paymasterHandler }) + └→ createSmartAccountClient({ paymasterHandler }) + └→ bundler uses handler in getTransactionWithPaymasterData() +``` + +### Test Architecture + +``` +beforeAll: + 1. Deploy TestPaymaster contract + 2. Fund paymaster with 10 ETH from rich account + 3. Export paymaster address to test config + +Tests: + 1. Create unfunded account (skip fundAccount() call) + 2. Verify balance = 0 + 3. Send transaction WITH paymaster → Success + 4. Send transaction WITHOUT paymaster → Fail (insufficient funds) + 5. Create session WITH paymaster → Success + 6. Execute session transaction WITH paymaster → Success +``` + +## Implementation Tasks + +### 1. SDK-4337 Connector Enhancement + +- [ ] Add `paymaster?: Address | CustomPaymasterHandler` to connector parameters +- [ ] Convert address to handler using `createGeneralPaymaster()` if needed +- [ ] Pass handler through to client creation + +**Files**: + +- `packages/sdk-4337/src/connector/index.ts` +- `packages/sdk-4337/src/connector/types.ts` + +### 2. Client Creation Threading + +- [ ] Accept `paymasterHandler` parameter in `getClient()` +- [ ] Thread handler to `createSmartAccountClient()` +- [ ] Ensure handler is applied in bundler's transaction preparation + +**Files**: + +- `packages/sdk-4337/src/client-auth-server/index.ts` +- `packages/sdk-4337/src/client-auth-server/client.ts` + +### 3. Demo-App Paymaster Deployment + +- [ ] Create deployment script for TestPaymaster +- [ ] Export deployed address to `contracts-anvil.json` +- [ ] Add funding helper in test utilities + +**Files**: + +- `examples/demo-app/scripts/deploy-paymaster.ts` (new) +- `examples/demo-app/contracts-anvil.json` + +### 4. Demo-App Connector Configuration + +- [ ] Add paymaster address constant +- [ ] Create `zksyncConnectorWithPaymaster` instance +- [ ] Optionally add UI toggle for testing (button to connect with/without + paymaster) + +**Files**: + +- `examples/demo-app/pages/index.vue` + +### 5. E2E Test Suite: Paymaster Flows + +- [ ] Add `beforeAll` hook to deploy and fund paymaster +- [ ] Helper: `createUnfundedAccount()` (skip funding step) +- [ ] Test: "Unfunded account transaction fails without paymaster" +- [ ] Test: "Unfunded account transaction succeeds with paymaster" +- [ ] Test: "Unfunded account creates session with paymaster" +- [ ] Test: "Session transaction uses paymaster successfully" + +**Files**: + +- `examples/demo-app/tests/create-account.spec.ts` (add new test suite) +- Or create new file: `examples/demo-app/tests/paymaster.spec.ts` + +## Key Technical Details + +### Paymaster Handler Flow + +```typescript +// SDK already has infrastructure: +export type CustomPaymasterHandler = ( + args: CustomPaymasterParameters, +) => Promise; + +// Response includes: +interface CustomPaymasterHandlerResponse { + paymaster: Address; + paymasterInput: Hex; + // Optional fee overrides: + maxFeePerGas?: bigint; + maxPriorityFeePerGas?: bigint; +} +``` + +### General Paymaster Handler + +```typescript +// Already implemented in packages/sdk-4337/src/handlers/general.ts +export function createGeneralPaymaster( + paymaster: Address, +): CustomPaymasterHandler { + return async (_): Promise => { + return { + paymaster, + paymasterInput: getGeneralPaymasterInput({ innerInput: "0x" }), + }; + }; +} +``` + +### TestPaymaster Contract + +```solidity +// Already exists at packages/erc4337-contracts/src/test/TestPaymaster.sol +contract TestPaymaster is IPaymaster { + function validatePaymasterUserOp(...) external pure returns (...) { + return ("", 0); // Always approves + } + + function postOp(...) external {} + + receive() external payable {} // Can receive funds +} +``` + +## Test Validation Strategy + +### Proof of Sponsorship + +1. **Initial State**: Create account with balance = 0 ETH +2. **Without Paymaster**: Attempt 0.1 ETH transfer → Expect failure + (insufficient funds) +3. **With Paymaster**: Same transfer → Expect success +4. **Post-Transaction**: Verify balance still ≈ 0 ETH (paymaster covered gas) + +### Session + Paymaster Flow + +1. **Create Session**: Unfunded account + paymaster → Session created + successfully +2. **Execute Transaction**: Use session to send ETH → Success without passkey +3. **Balance Check**: Account balance remains at 0 (all gas sponsored) + +### Error Detection + +```typescript +// Check for insufficient funds error +catch (error) { + const errorMessage = error.cause?.details || error.message; + expect(errorMessage).toMatch(/insufficient funds|insufficient balance/i); +} +``` + +## Acceptance Criteria + +- [ ] Connector accepts `paymaster: Address` parameter +- [ ] Connector accepts `paymaster: CustomPaymasterHandler` parameter +- [ ] Unfunded account (0 ETH) can execute transactions with paymaster +- [ ] Session creation works with paymaster on unfunded account +- [ ] Unfunded account transactions fail without paymaster (negative test) +- [ ] No breaking changes to existing flows (paymaster is optional) +- [ ] E2E tests pass consistently on local anvil +- [ ] No UI changes required (transparent operation) + +## Future Enhancements (Out of Scope) + +- [ ] UI indicator for paymaster sponsorship status +- [ ] Fee comparison display (with/without sponsorship) +- [ ] ZyFi paymaster integration for conditional sponsorship +- [ ] Paymaster selection UI (multiple paymasters) +- [ ] Session-level paymaster configuration (inherit from session) +- [ ] Analytics/tracking of sponsored transaction volume + +## References + +- **TestPaymaster Contract**: + `packages/erc4337-contracts/src/test/TestPaymaster.sol` +- **Paymaster Handlers**: `packages/sdk-4337/src/handlers/` +- **Current E2E Tests**: `examples/demo-app/tests/create-account.spec.ts` +- **Demo App**: `examples/demo-app/pages/index.vue` +- **SDK Connector**: `packages/sdk-4337/src/connector/index.ts` + +--- + +**Created**: December 12, 2025 +**Status**: Ready for Implementation diff --git a/examples/demo-app/components/SessionCreator.vue b/examples/demo-app/components/SessionCreator.vue index cfcb368a6..c18cc070b 100644 --- a/examples/demo-app/components/SessionCreator.vue +++ b/examples/demo-app/components/SessionCreator.vue @@ -190,6 +190,7 @@ async function createSessionOnChain() { address: props.accountAddress as Address, signerPrivateKey: props.eoaPrivateKey as `0x${string}`, eoaValidatorAddress: props.eoaValidatorAddress as Address, + entryPointAddress: contracts.entryPoint as Address, }); // eslint-disable-next-line no-console diff --git a/examples/demo-app/components/SessionTransactionSender.vue b/examples/demo-app/components/SessionTransactionSender.vue index 325a92687..d60e5cf54 100644 --- a/examples/demo-app/components/SessionTransactionSender.vue +++ b/examples/demo-app/components/SessionTransactionSender.vue @@ -187,6 +187,7 @@ async function sendTransaction() { bundlerClient, chain, transport: http(), + entryPointAddress: contracts.entryPoint as Address, }); // If an allowed recipient is configured, enforce it matches the selected target diff --git a/examples/demo-app/pages/index.vue b/examples/demo-app/pages/index.vue index 52cdac2c3..294a87962 100644 --- a/examples/demo-app/pages/index.vue +++ b/examples/demo-app/pages/index.vue @@ -5,17 +5,33 @@ + +