Skip to content
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

fix: auth server and sdk when not using sessions #45

Merged
merged 6 commits into from
Dec 22, 2024
Merged
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
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -52,6 +52,17 @@ jobs:
run: pnpm run deploy
working-directory: packages/contracts

- name: Install zksync-foundry
run: |
wget -qc https://github.com/matter-labs/foundry-zksync/releases/download/nightly/foundry_nightly_linux_amd64.tar.gz -O - | tar -xz
./forge -V && ./cast -V
sudo mv ./forge /usr/local/bin/
sudo mv ./cast /usr/local/bin/
forge -V && cast -V

- name: Deploy Demo-App contracts
run: pnpm nx deploy-contracts demo-app

# Run E2E tests
- name: Install Playwright Chromium Browser
run: pnpm exec playwright install chromium
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@ node_modules/

# era-test-node
era_test_node.log
anvil-zksync.log
foundryup-zksync
cache/
zkout/

package-lock.json
yarn.lock
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -125,13 +125,19 @@ This monorepo is comprised of the following packages, products, and examples:
[workspace protocol](https://pnpm.io/workspaces#workspace-protocol-workspace)
to link SDK in the new folder.

3. Start a local node:
3. Install `foundry-zksync`:

```bash
curl -L https://raw.githubusercontent.com/matter-labs/foundry-zksync/main/install-foundry-zksync | bash
```

4. Start a local node:

```bash
npx zksync-cli dev start
```

4. Compile and deploy contracts to the local node:
5. Compile and deploy contracts to the local node:

```bash
# Compile and deploy contracts
@@ -140,11 +146,9 @@ This monorepo is comprised of the following packages, products, and examples:
pnpm run deploy
```

5. Start the demo application:
6. Start the demo application:

```bash
# Go back to root folder to start demo app
cd ../..
pnpm nx dev demo-app
```

1 change: 1 addition & 0 deletions examples/demo-app/.gitignore
Original file line number Diff line number Diff line change
@@ -22,3 +22,4 @@ logs
.env
.env.*
!.env.example
forge-output.json
5 changes: 3 additions & 2 deletions examples/demo-app/package.json
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@matterlabs/zksync-contracts": "^0.6.1",
"@nuxtjs/google-fonts": "^3.2.0",
"@pinia/nuxt": "^0.5.5",
"@simplewebauthn/browser": "^10.0.0",
@@ -19,8 +20,8 @@
"viem": "2.21.14",
"vue": "^3.4.21",
"wagmi": "^2.12.17",
"zksync-sso": "workspace:*",
"zksync-ethers": "^6.15.0"
"zksync-ethers": "^6.15.0",
"zksync-sso": "workspace:*"
},
"devDependencies": {
"@nuxt/eslint": "^0.5.7",
84 changes: 69 additions & 15 deletions examples/demo-app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -4,11 +4,18 @@
ZKsync SSO Demo
</h1>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
@click="address ? disconnectWallet() : connectWallet()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mr-4"
@click="address ? disconnectWallet() : connectWallet(false)"
>
{{ address ? "Disconnect" : "Connect" }}
</button>
<button
v-if="!address"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
@click="address ? disconnectWallet() : connectWallet(true)"
>
Connect w/ Session
</button>
<div
v-if="address"
class="mt-4"
@@ -23,12 +30,20 @@
</div>
<button
v-if="address"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-3"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-3 mr-4 disabled:bg-slate-300"
:disabled="isSendingEth"
@click="sendTokens()"
@click="sendTokens(false)"
>
Send 0.1 ETH
</button>
<button
v-if="address"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded mt-3 disabled:bg-slate-300"
:disabled="isSendingEth"
@click="sendTokens(true)"
>
Send 0.1 ETH w/ Paymaster
</button>

<div
v-if="errorMessage"
@@ -45,11 +60,13 @@ import { zksyncSsoConnector } from "zksync-sso/connector";
import { zksyncInMemoryNode } from "@wagmi/core/chains";
import { createWalletClient, http, parseEther, type Address } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { getGeneralPaymasterInput } from "viem/zksync";
import PaymasterContract from "../forge-output.json";

const chain = zksyncInMemoryNode;

const testTransferTarget = "0x55bE1B079b53962746B2e86d12f158a41DF294A6";
const zksyncConnector = zksyncSsoConnector({
const zksyncConnectorWithSession = zksyncSsoConnector({
authServerUrl: "http://localhost:3002/confirm",
session: {
feeLimit: parseEther("0.1"),
@@ -61,6 +78,9 @@ const zksyncConnector = zksyncSsoConnector({
],
},
});
const zksyncConnector = zksyncSsoConnector({
authServerUrl: "http://localhost:3002/confirm",
});
const wagmiConfig = createConfig({
chains: [chain],
connectors: [zksyncConnector],
@@ -84,10 +104,20 @@ const fundAccount = async () => {
transport: http(),
});

await richClient.sendTransaction({
let transactionHash = await richClient.sendTransaction({
to: address.value,
value: parseEther("1"),
});
// FIXME: When not using sessions, sendTransaction returns a map and not a string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((transactionHash as any).value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transactionHash = (transactionHash as any).value;
}

await waitForTransactionReceipt(wagmiConfig, {
hash: transactionHash,
});
};

watchAccount(wagmiConfig, {
@@ -118,11 +148,11 @@ watch(address, async () => {
balance.value = currentBalance;
}, { immediate: true });

const connectWallet = async () => {
const connectWallet = async (useSession: boolean) => {
try {
errorMessage.value = "";
connect(wagmiConfig, {
connector: zksyncConnector,
connector: useSession ? zksyncConnectorWithSession : zksyncConnector,
chainId: chain.id,
});
} catch (error) {
@@ -133,25 +163,45 @@ const connectWallet = async () => {
};

const disconnectWallet = async () => {
errorMessage.value = "";
await disconnect(wagmiConfig);
};

const sendTokens = async () => {
const sendTokens = async (usePaymaster: boolean) => {
if (!address.value) return;

errorMessage.value = "";
isSendingEth.value = true;
try {
const transactionHash = await sendTransaction(wagmiConfig, {
to: testTransferTarget,
value: parseEther("0.1"),
});
let transactionHash;

if (usePaymaster) {
transactionHash = await sendTransaction(wagmiConfig, {
to: testTransferTarget,
value: parseEther("0.1"),
paymaster: PaymasterContract.deployedTo as `0x${string}`,
paymasterInput: getGeneralPaymasterInput({ innerInput: "0x" }),
});
} else {
transactionHash = await sendTransaction(wagmiConfig, {
to: testTransferTarget,
value: parseEther("0.1"),
});
}

// FIXME: When not using sessions, sendTransaction returns a map and not a string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((transactionHash as any).value !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transactionHash = (transactionHash as any).value;
}

const receipt = await waitForTransactionReceipt(wagmiConfig, {
hash: transactionHash,
});
balance.value = await getBalance(wagmiConfig, {
address: address.value,
});

const receipt = await waitForTransactionReceipt(wagmiConfig, { hash: transactionHash });
if (receipt.status === "reverted") throw new Error("Transaction reverted");
} catch (error) {
// eslint-disable-next-line no-console
@@ -162,6 +212,10 @@ const sendTokens = async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transactionFailureDetails = (error as any).cause?.cause?.data?.originalError?.cause?.details;
}
if (!transactionFailureDetails) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
transactionFailureDetails = (error as any).cause?.details;
}

if (transactionFailureDetails) {
errorMessage.value = transactionFailureDetails;
22 changes: 20 additions & 2 deletions examples/demo-app/project.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,8 @@
"prefix": "Demo-App:"
}
]
}
},
"dependsOn": ["deploy-contracts"]
},
"build": {
"executor": "nx:run-commands",
@@ -28,8 +29,24 @@
"command": "pnpm nuxt generate"
}
]
},
"dependsOn": ["deploy-contracts"]
},
"build-contracts": {
"executor": "nx:run-commands",
"options": {
"cwd": "examples/demo-app",
"command": "forge build smart-contracts/DemoPaymaster.sol --root . --zksync"
}
},
"deploy-contracts": {
"executor": "nx:run-commands",
"options": {
"cwd": "examples/demo-app",
"command": "forge create smart-contracts/DemoPaymaster.sol:DemoPaymaster --private-key 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 --rpc-url http://localhost:8011 --root . --chain 260 --zksync --json 2>&1 | sed -n 's/.*\\({.*}\\).*/\\1/p' > forge-output.json && ADDRESS=$(sed -n 's/.*\"deployedTo\":\"\\([^\"]*\\)\".*/\\1/p' forge-output.json) && echo $ADDRESS && cast send --private-key 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 $ADDRESS --rpc-url http://localhost:8011 --value 0.1ether"
},
"dependsOn": ["build-contracts"]
},
"build:local": {
"executor": "nx:run-commands",
"options": {
@@ -60,7 +77,8 @@
"options": {
"cwd": "examples/demo-app",
"command": "pnpm exec playwright install chromium"
}
},
"dependsOn": ["deploy-contracts"]
},
"e2e": {
"executor": "nx:run-commands",
65 changes: 65 additions & 0 deletions examples/demo-app/smart-contracts/DemoPaymaster.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
/// !!! !!!
/// !!! THIS IS FOR DEMO PURPOSES ONLY !!!
/// !!! !!!
/// !!! DO NOT COPY THIS PAYMASTER !!!
/// !!! FOR PRODUCTION APPLICATIONS !!!
/// !!! !!!
/// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

import { IPaymaster, ExecutionResult, PAYMASTER_VALIDATION_SUCCESS_MAGIC } from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymaster.sol";
import { IPaymasterFlow } from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IPaymasterFlow.sol";
import { TransactionHelper, Transaction } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";

import "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";

/// @author Matter Labs
/// @notice DO NOT USE THIS FOR PRODUCTION. This contract does not include any validations other than using the paymaster general flow.
contract DemoPaymaster is IPaymaster {
modifier onlyBootloader() {
require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method");
// Continue execution if called from the bootloader.
_;
}

function validateAndPayForPaymasterTransaction(
bytes32,
bytes32,
Transaction calldata _transaction
) external payable onlyBootloader returns (bytes4 magic, bytes memory context) {
// By default we consider the transaction as accepted.
magic = PAYMASTER_VALIDATION_SUCCESS_MAGIC;
require(_transaction.paymasterInput.length >= 4, "The standard paymaster input must be at least 4 bytes long");

bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]);
require(paymasterInputSelector == IPaymasterFlow.general.selector, "Unsupported paymaster flow");

// Note, that while the minimal amount of ETH needed is tx.gasPrice * tx.gasLimit,
// neither paymaster nor account are allowed to access this context variable.
uint256 requiredETH = _transaction.gasLimit * _transaction.maxFeePerGas;

// The bootloader never returns any data, so it can safely be ignored here.
(bool success, ) = payable(BOOTLOADER_FORMAL_ADDRESS).call{ value: requiredETH }("");
require(success, "Failed to transfer tx fee to the Bootloader. Paymaster balance might not be enough.");
}

function postTransaction(
bytes calldata _context,
Transaction calldata _transaction,
bytes32,
bytes32,
ExecutionResult _txResult,
uint256 _maxRefundedGas
) external payable override onlyBootloader {}

function withdraw(address payable _to) external {
uint256 balance = address(this).balance;
(bool success, ) = _to.call{ value: balance }("");
require(success, "Failed to withdraw funds from paymaster.");
}

receive() external payable {}
}
282 changes: 271 additions & 11 deletions examples/demo-app/tests/create-account.spec.ts
Original file line number Diff line number Diff line change
@@ -44,9 +44,9 @@ test.beforeEach(async ({ page }) => {
await expect(page.getByText("ZKsync SSO Demo")).toBeVisible();
});

test("Create account, session key, and send ETH", async ({ page }) => {
test("Create account w/ session and send ETH", async ({ page }) => {
// Click the Connect button
await page.getByRole("button", { name: "Connect" }).click();
await page.getByRole("button", { name: "Connect w/ Session", exact: true }).click();

// Ensure popup is displayed
await page.waitForTimeout(2000);
@@ -64,7 +64,68 @@ test("Create account, session key, and send ETH", async ({ page }) => {
// NOTE: This needs to be done for every page of every test that uses WebAuthn
const client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
const result = await client.send("WebAuthn.addVirtualAuthenticator", {
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});

// Click Sign Up
await popup.getByTestId("signup").click();

// Add session
await expect(popup.getByText("Authorize ZKsync SSO Demo")).toBeVisible();
await expect(popup.getByText("Act on your behalf")).toBeVisible();
await expect(popup.getByText("Expires tomorrow")).toBeVisible();
await expect(popup.getByText("Permissions")).toBeVisible();
await popup.getByTestId("connect").click();

// Waits for session to complete and popup to close
await page.waitForTimeout(2000);

// Check address/balance is shown
await expect(page.getByText("Disconnect")).toBeVisible();
await expect(page.getByText("Balance:")).toBeVisible();
const startBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");

// Send some eth
await page.getByRole("button", { name: "Send 0.1 ETH", exact: true }).click();
await expect(page.getByRole("button", { name: "Send 0.1 ETH", exact: true })).toBeEnabled();
const endBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");
await expect(startBalance, "Balance after transfer should be ~0.1 ETH less")
.toBeGreaterThan(endBalance + 0.1);
});

test("Create account w/ session and send ETH w/ paymaster", async ({ page }) => {
// Click the Connect button
await page.getByRole("button", { name: "Connect w/ Session", exact: true }).click();

// Ensure popup is displayed
await page.waitForTimeout(2000);
const popup = page.context().pages()[1];
await expect(popup.getByText("Connect to")).toBeVisible();
popup.on("console", (msg) => {
if (msg.type() === "error")
console.log(`Auth server error console: "${msg.text()}"`);
});
popup.on("pageerror", (exception) => {
console.log(`Auth server uncaught exception: "${exception}"`);
});

// Setup webauthn a Chrome Devtools Protocol session
// NOTE: This needs to be done for every page of every test that uses WebAuthn
const client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
@@ -74,8 +135,6 @@ test("Create account, session key, and send ETH", async ({ page }) => {
automaticPresenceSimulation: true,
},
});
const authenticatorId = result.authenticatorId;
console.log(`WebAuthn Authenticator ID: ${authenticatorId}`);

// Click Sign Up
await popup.getByTestId("signup").click();
@@ -92,20 +151,221 @@ test("Create account, session key, and send ETH", async ({ page }) => {

// Check address/balance is shown
await expect(page.getByText("Disconnect")).toBeVisible();
const address = (await page.getByText("Connected Address:").innerText())
.replace("Connected Address: ", "");
console.log(`Public Address: ${address}`);
await expect(page.getByText("Balance:")).toBeVisible();
const startBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");

// Send some eth w/ paymaster
await page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true }).click();
await expect(page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true })).toBeEnabled();
const endBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");
await expect(startBalance, "Balance after transfer should be 0.1 ETH less (no fees)")
.toEqual(endBalance + 0.1);
});

test("Create passkey account and send ETH", async ({ page }) => {
// Click the Connect button
await page.getByRole("button", { name: "Connect", exact: true }).click();

// Ensure popup is displayed
await page.waitForTimeout(2000);
let popup = page.context().pages()[1];
await expect(popup.getByText("Connect to")).toBeVisible();
popup.on("console", (msg) => {
if (msg.type() === "error")
console.log(`Auth server error console: "${msg.text()}"`);
});
popup.on("pageerror", (exception) => {
console.log(`Auth server uncaught exception: "${exception}"`);
});

// Setup webauthn a Chrome Devtools Protocol session
// NOTE: This needs to be done for every page of every test that uses WebAuthn
let client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
let newCredential = null;
client.on("WebAuthn.credentialAdded", (credentialAdded) => {
console.log("New Passkey credential added");
console.log(`Authenticator ID: ${credentialAdded.authenticatorId}`);
console.log(`Credential: ${credentialAdded.credential}`);
newCredential = credentialAdded.credential;
});

// Click Sign Up
await popup.getByTestId("signup").click();

// Confirm access to your account
await expect(popup.getByText("Connect to ZKsync SSO Demo")).toBeVisible();
await expect(popup.getByText("localhost:3004")).toBeVisible();
await expect(popup.getByText("Let it see your address, balance and activity")).toBeVisible();
await popup.getByTestId("connect").click();

// Waits for session to complete and popup to close
await page.waitForTimeout(2000);

// Check address/balance is shown
await expect(page.getByText("Disconnect")).toBeVisible();
await expect(page.getByText("Balance:")).toBeVisible();
const startBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");

// Send some eth
await page.getByRole("button", { name: "Send 0.1 ETH" }).click();
await expect(page.getByRole("button", { name: "Send 0.1 ETH" })).toBeEnabled();
await page.getByRole("button", { name: "Send 0.1 ETH", exact: true }).click();

// Wait for Auth Server to pop back up
await page.waitForTimeout(2000);
popup = page.context().pages()[1];

// We need to recreate the virtual authenticator to match the previous one
client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
const result = await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
await expect(newCredential).not.toBeNull();
await client.send("WebAuthn.addCredential", {
authenticatorId: result.authenticatorId,
credential: newCredential!,
});

// Confirm the transfer
await expect(popup.getByText("-0.1")).toBeVisible();
await expect(popup.getByText("Sending to")).toBeVisible();
await expect(popup.getByText("0x55b...4A6")).toBeVisible();
await expect(popup.getByText("Fees")).toBeVisible();
await popup.getByTestId("confirm").click();

// Wait for confirmation to complete and popup to close
await page.waitForTimeout(2000);

// Confirm transfer completed and balance updated
await expect(page.getByRole("button", { name: "Send 0.1 ETH", exact: true })).toBeEnabled();
const endBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");
await expect(startBalance, "Balance after transfer should be ~0.1 ETH less")
.toBeGreaterThanOrEqual(endBalance + 0.1);
.toBeGreaterThan(endBalance + 0.1);
});

test("Create passkey account and send ETH w/ paymaster", async ({ page }) => {
// Click the Connect button
await page.getByRole("button", { name: "Connect", exact: true }).click();

// Ensure popup is displayed
await page.waitForTimeout(2000);
let popup = page.context().pages()[1];
await expect(popup.getByText("Connect to")).toBeVisible();
popup.on("console", (msg) => {
if (msg.type() === "error")
console.log(`Auth server error console: "${msg.text()}"`);
});
popup.on("pageerror", (exception) => {
console.log(`Auth server uncaught exception: "${exception}"`);
});

// Setup webauthn a Chrome Devtools Protocol session
// NOTE: This needs to be done for every page of every test that uses WebAuthn
let client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
let newCredential = null;
client.on("WebAuthn.credentialAdded", (credentialAdded) => {
console.log("New Passkey credential added");
console.log(`Authenticator ID: ${credentialAdded.authenticatorId}`);
console.log(`Credential: ${credentialAdded.credential}`);
newCredential = credentialAdded.credential;
});

// Click Sign Up
await popup.getByTestId("signup").click();

// Confirm access to your account
await expect(popup.getByText("Connect to ZKsync SSO Demo")).toBeVisible();
await expect(popup.getByText("localhost:3004")).toBeVisible();
await expect(popup.getByText("Let it see your address, balance and activity")).toBeVisible();
await popup.getByTestId("connect").click();

// Waits for session to complete and popup to close
await page.waitForTimeout(2000);

// Check address/balance is shown
await expect(page.getByText("Disconnect")).toBeVisible();
await expect(page.getByText("Balance:")).toBeVisible();
const startBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");

// Send some eth w/ paymaster
await page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true }).click();

// Wait for Auth Server to pop back up
await page.waitForTimeout(2000);
popup = page.context().pages()[1];

// We need to recreate the virtual authenticator to match the previous one
client = await popup.context().newCDPSession(popup);
await client.send("WebAuthn.enable");
const result = await client.send("WebAuthn.addVirtualAuthenticator", {
options: {
protocol: "ctap2",
transport: "usb",
hasResidentKey: true,
hasUserVerification: true,
isUserVerified: true,
automaticPresenceSimulation: true,
},
});
await expect(newCredential).not.toBeNull();
await client.send("WebAuthn.addCredential", {
authenticatorId: result.authenticatorId,
credential: newCredential!,
});

// Confirm the transfer
await expect(popup.getByText("-0.1")).toBeVisible();
await expect(popup.getByText("Sending to")).toBeVisible();
await expect(popup.getByText("0x55b...4A6")).toBeVisible();
await expect(popup.getByText("Fees")).toBeVisible();
await popup.getByTestId("confirm").click();

// Wait for confirmation to complete and popup to close
await page.waitForTimeout(2000);

// Confirm transfer completed and balance updated
await expect(page.getByRole("button", { name: "Send 0.1 ETH w/ Paymaster", exact: true })).toBeEnabled();
const endBalance = +(await page.getByText("Balance:").innerText())
.replace("Balance: ", "")
.replace(" ETH", "");
await expect(startBalance, "Balance after transfer should be 0.1 ETH less (no fees)")
.toEqual(endBalance + 0.1);
});
2 changes: 1 addition & 1 deletion packages/auth-server/components/views/Auth.vue
Original file line number Diff line number Diff line change
@@ -66,7 +66,7 @@ const registerAccount = async () => {
if (!session.value) {
// no session defined
await createAccount();
if (!createAccountError) {
if (!createAccountError.value) {
navigateTo("/confirm/connect");
}
} else {
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@
data-testid="connect"
@click="confirmConnection()"
>
Connect
{{ isLoggedIn ? 'Connect' : 'Create' }}
</ZkButton>
</ZkHighlightWrapper>
</div>
4 changes: 3 additions & 1 deletion packages/sdk/src/client/passkey/client.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { passkeyHashSignatureResponseFormat } from "../../utils/passkey.js";
import { toPasskeyAccount } from "./account.js";
import { requestPasskeyAuthentication } from "./actions/passkey.js";
import { type ZksyncSsoPasskeyActions, zksyncSsoPasskeyActions } from "./decorators/passkey.js";
import { zksyncSsoPasskeyWalletActions } from "./decorators/wallet.js";

export function createZksyncPasskeyClient<
transport extends Transport,
@@ -47,7 +48,8 @@ export function createZksyncPasskeyClient<
.extend(publicActions)
.extend(walletActions)
.extend(eip712WalletActions())
.extend(zksyncSsoPasskeyActions);
.extend(zksyncSsoPasskeyActions)
.extend(zksyncSsoPasskeyWalletActions);
return client;
}

52 changes: 52 additions & 0 deletions packages/sdk/src/client/passkey/decorators/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { type Account, bytesToHex, type Chain, formatTransaction, type Transport, type WalletActions } from "viem";
import { deployContract, getAddresses, getChainId, sendRawTransaction, signMessage, signTypedData, writeContract } from "viem/actions";
import { signTransaction, type ZksyncEip712Meta } from "viem/zksync";

import { sendEip712Transaction } from "../../session/actions/sendEip712Transaction.js";
import type { ClientWithZksyncSsoPasskeyData } from "../client.js";

export type ZksyncSsoPasskeyWalletActions<chain extends Chain, account extends Account> = Omit<
WalletActions<chain, account>, "addChain" | "getPermissions" | "requestAddresses" | "requestPermissions" | "switchChain" | "watchAsset" | "prepareTransactionRequest"
>;

export function zksyncSsoPasskeyWalletActions<
transport extends Transport,
chain extends Chain,
account extends Account,
>(client: ClientWithZksyncSsoPasskeyData<transport, chain>): ZksyncSsoPasskeyWalletActions<chain, account> {
return {
deployContract: (args) => deployContract(client, args),
getAddresses: () => getAddresses(client),
getChainId: () => getChainId(client),
sendRawTransaction: (args) => sendRawTransaction(client, args),
sendTransaction: async (args) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const unformattedTx: any = Object.assign({}, args);

if ("eip712Meta" in unformattedTx) {
const eip712Meta = unformattedTx.eip712Meta as ZksyncEip712Meta;
unformattedTx.gasPerPubdata = eip712Meta.gasPerPubdata ? BigInt(eip712Meta.gasPerPubdata) : undefined;
unformattedTx.factoryDeps = eip712Meta.factoryDeps;
unformattedTx.customSignature = eip712Meta.customSignature;
unformattedTx.paymaster = eip712Meta.paymasterParams?.paymaster;
unformattedTx.paymasterInput = eip712Meta.paymasterParams?.paymasterInput ? bytesToHex(new Uint8Array(eip712Meta.paymasterParams?.paymasterInput)) : undefined;
delete unformattedTx.eip712Meta;
}

const formatters = client.chain?.formatters;
const format = formatters?.transaction?.format || formatTransaction;

const tx = {
...format(unformattedTx),
type: "eip712",
};

return await sendEip712Transaction(client, tx);
},
signMessage: (args) => signMessage(client, args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
signTransaction: (args) => signTransaction(client, args as any),
signTypedData: (args) => signTypedData(client, args),
writeContract: (args) => writeContract(client, args),
};
}
77 changes: 65 additions & 12 deletions pnpm-lock.yaml