Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions examples/demo-app/scripts/deploy-msa-anvil.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ FACTORY=$(echo "$DEPLOY_OUTPUT" | grep "MSAFactory:" | awk '{print $2}')
echo ""
echo "📦 Deploying MockPaymaster..."
cd "$CONTRACTS_DIR"
PAYMASTER_OUTPUT=$(forge create test/mocks/MockPaymaster.sol:MockPaymaster --rpc-url "$RPC_URL" --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --broadcast 2>&1)
ANVIL_ACCOUNT_0_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
PAYMASTER_OUTPUT=$(forge create test/mocks/MockPaymaster.sol:MockPaymaster --rpc-url "$RPC_URL" --private-key "$ANVIL_ACCOUNT_0_KEY" --broadcast 2>&1)
echo "$PAYMASTER_OUTPUT"
PAYMASTER=$(echo "$PAYMASTER_OUTPUT" | grep "Deployed to:" | awk '{print $3}')

Expand All @@ -53,7 +54,6 @@ echo "MockPaymaster deployed to: $PAYMASTER"
# Fund the paymaster with ETH from Anvil account #0 (has plenty of ETH)
echo ""
echo "💰 Funding paymaster with 10 ETH..."
ANVIL_ACCOUNT_0_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
cast send "$PAYMASTER" --value 10ether --private-key "$ANVIL_ACCOUNT_0_KEY" --rpc-url "$RPC_URL" 2>&1 || echo "Fund transfer initiated"

# Deposit the paymaster's ETH into the EntryPoint
Expand Down
2 changes: 1 addition & 1 deletion packages/auth-server/components/PrividiumLogin.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="h-full flex flex-col justify-center px-4">
<div class="max-w-md mx-auto w-full">
<AppAccountLogo class="dark:text-neutral-100 h-16 md:h-20 mb-14" />
<AppAccountLogo class="dark:text-neutral-100 h-16 md:h-20 mb-14 mx-auto" />
<!-- Error display -->
<CommonHeightTransition :opened="!!error">
<p class="pb-3 text-sm text-error-300 text-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ const { respond, deny } = useRequestsStore();
const { responseInProgress, requestChain } = storeToRefs(useRequestsStore());
const { address } = storeToRefs(useAccountStore());
const { getClient } = useClientStore();
const runtimeConfig = useRuntimeConfig();

const confirmConnection = () => {
respond(async () => {
const client = getClient({ chainId: requestChain.value!.id });
return {
result: constructReturn(client.account.address, client.chain.id),
result: constructReturn({
address: client.account.address,
chainId: client.chain.id,
prividiumMode: runtimeConfig.public.prividiumMode,
prividiumProxyUrl: runtimeConfig.public.prividium?.rpcUrl || "",
}),
};
});
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const { responseInProgress, requestChainId } = storeToRefs(useRequestsStore());
const { createAccount } = useAccountCreate(requestChainId);
const { respond, deny } = useRequestsStore();
const { getClient } = useClientStore();
const runtimeConfig = useRuntimeConfig();

const defaults = {
expiresAt: BigInt(Math.floor(Date.now() / 1000) + 60 * 60 * 24), // 24 hours
Expand Down Expand Up @@ -287,14 +288,16 @@ const confirmConnection = async () => {
});

response = {
result: constructReturn(
accountData!.address,
accountData!.chainId,
{
result: constructReturn({
address: accountData!.address,
chainId: accountData!.chainId,
session: {
sessionConfig: accountData!.sessionConfig!,
sessionKey: accountData!.sessionKey!,
},
),
prividiumMode: runtimeConfig.public.prividiumMode,
prividiumProxyUrl: runtimeConfig.public.prividium?.rpcUrl || "",
}),
};
} else {
// create a new session for the existing account
Expand Down Expand Up @@ -329,11 +332,13 @@ const confirmConnection = async () => {
});

response = {
result: constructReturn(
client.account.address,
client.chain.id,
result: constructReturn({
address: client.account.address,
chainId: client.chain.id,
session,
),
prividiumMode: runtimeConfig.public.prividiumMode,
prividiumProxyUrl: runtimeConfig.public.prividium?.rpcUrl || "",
}),
};
}
} catch (error) {
Expand Down
9 changes: 8 additions & 1 deletion packages/auth-server/pages/confirm/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,16 @@
key="sign-typed-data"
/>
<ViewsConfirmationSend
v-else
v-else-if="requestMethod === 'eth_sendTransaction'"
key="confirmation"
/>
<div
v-else
key="unsupported-method"
class="flex h-full items-center justify-center"
>
<p>Unsupported request method.</p>
</div>
</TransitionGroup>
</template>

Expand Down
8 changes: 0 additions & 8 deletions packages/auth-server/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
},
"dependsOn": ["^build", "demo-app:deploy-contracts-erc4337"]
},
"dev:with-api": {
"executor": "nx:run-commands",
"options": {
"cwd": "packages/auth-server",
"commands": ["pnpm run copy:snarkjs && PORT=3002 nuxt dev", "pnpm nx run auth-server-api:dev"]
},
"dependsOn": ["^build"]
},
"dev:no-deploy": {
"executor": "nx:run-commands",
"options": {
Expand Down
16 changes: 14 additions & 2 deletions packages/auth-server/stores/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,22 @@ export const useClientStore = defineStore("client", () => {
const contracts = contractsByChain[chainId];
const publicClient = getPublicClient({ chainId });

// In prividium mode, use prividium transport for bundler as well
// Get transport from existing prividium instance - it auto-routes bundler methods
const bundlerTransport = runtimeConfig.public.prividiumMode
? (() => {
const prividiumTransport = prividiumAuthStore.getTransport();
if (!prividiumTransport) {
throw new Error("Prividium transport not available. User may need to authenticate.");
}
return prividiumTransport;
})()
: http(contracts.bundlerUrl || "http://localhost:4337");

return createBundlerClient({
client: publicClient,
chain,
transport: http(contracts.bundlerUrl || "http://localhost:4337"),
transport: bundlerTransport,
userOperation: {
async estimateFeesPerGas() {
const feesPerGas = await publicClient.estimateFeesPerGas();
Expand All @@ -160,7 +172,7 @@ export const useClientStore = defineStore("client", () => {
const contracts = contractsByChain[chainId];
const bundlerClient = getBundlerClient({ chainId });

const finalPaymasterAddress = paymasterAddress ?? (usePaymaster ? contracts.testPaymaster : undefined);
const finalPaymasterAddress = paymasterAddress as Address | undefined ?? (usePaymaster ? contracts.testPaymaster : undefined);

const client = createPasskeyClient({
account: {
Expand Down
21 changes: 19 additions & 2 deletions packages/auth-server/utils/constructReturn.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import type { SessionSpec } from "zksync-sso-4337/client";
import type { AuthServerRpcSchema, ExtractReturnType } from "zksync-sso-4337/client-auth-server";

export const constructReturn = (address: `0x${string}`, chainId: number, session?: { sessionKey: `0x${string}`; sessionConfig: SessionSpec }): ExtractReturnType<"eth_requestAccounts", AuthServerRpcSchema> => {
type ConstructReturnOptions = {
address: `0x${string}`;
chainId: number;
session?: { sessionKey: `0x${string}`; sessionConfig: SessionSpec };
prividiumMode: boolean;
prividiumProxyUrl: string;
};

export const constructReturn = ({
address,
chainId,
session,
prividiumMode,
prividiumProxyUrl,
}: ConstructReturnOptions): ExtractReturnType<"eth_requestAccounts", AuthServerRpcSchema> => {
return {
account: {
address,
Expand All @@ -22,7 +36,10 @@ export const constructReturn = (address: `0x${string}`, chainId: number, session
},
},
contracts: contractsByChain[chain.id],
bundlerUrl: contractsByChain[chain.id].bundlerUrl,
bundlerUrl: prividiumMode
? prividiumProxyUrl
: contractsByChain[chain.id].bundlerUrl || "",
prividiumMode,
})),
};
};
20 changes: 16 additions & 4 deletions packages/sdk-4337/src/client-auth-server/Signer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Address, type Chain, createPublicClient, createWalletClient, custom, type Hash, http, type RpcSchema as RpcSchemaGeneric, type SendTransactionParameters, type Transport, type WalletClient } from "viem";
import { type Address, type Chain, createPublicClient, createWalletClient, custom, type Hash, http, type RpcSchema as RpcSchemaGeneric, type SendTransactionParameters, toHex, type Transport, type WalletClient } from "viem";
import { type BundlerClient, createBundlerClient } from "viem/account-abstraction";

import type { PaymasterConfig } from "../actions/sendUserOperation.js";
Expand Down Expand Up @@ -143,9 +143,8 @@ export class Signer implements SignerInterface {
return this.bundlerClients[chainId];
}

// Try to create bundler client from chainsInfo if bundlerUrl is available
const chainInfo = this.chainsInfo.find((c) => c.id === chainId);
if (!chainInfo?.bundlerUrl) {
if (!chainInfo) {
return undefined;
}

Expand All @@ -154,6 +153,16 @@ export class Signer implements SignerInterface {
return undefined;
}

// In prividium mode, use transport from constructor; otherwise use bundlerUrl
const bundlerTransport = chainInfo.prividiumMode
? this.transports[chainId]
: http(chainInfo.bundlerUrl);

if (!bundlerTransport) {
console.error(`Prividium mode requires a transport for chain ${chainId}`);
return undefined;
}

const publicClient = createPublicClient({
chain,
transport: this.transports[chain.id] || http(),
Expand All @@ -162,7 +171,7 @@ export class Signer implements SignerInterface {
this.bundlerClients[chain.id] = createBundlerClient({
client: publicClient,
chain,
transport: http(chainInfo.bundlerUrl),
transport: bundlerTransport,
userOperation: {
// Use fixed gas values matching old Rust SDK implementation
// (old SDK used: 2M callGas, 2M verificationGas, 1M preVerificationGas)
Expand Down Expand Up @@ -333,6 +342,9 @@ export class Signer implements SignerInterface {
case "eth_accounts": {
return this.accounts as ExtractReturnType<TMethod>;
}
case "eth_chainId": {
return toHex(this.chain.id) as ExtractReturnType<TMethod>;
}
default:
return undefined;
}
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-4337/src/client-auth-server/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ export type AuthServerRpcSchema = [
id: Chain["id"];
capabilities: Record<string, unknown>;
contracts: SessionRequiredContracts;
bundlerUrl?: string;
bundlerUrl: string;
prividiumMode: boolean;
}[];
};
},
Expand Down
61 changes: 59 additions & 2 deletions packages/sdk-4337/src/connector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import {
} from "@wagmi/core";
import type { Compute } from "@wagmi/core/internal";
import {
type Abi,
type AbiFunction,
type AbiStateMutability,
type Address,
type Client,
getAddress,
Expand All @@ -18,7 +21,7 @@ import {
import type { BundlerClient } from "viem/account-abstraction";

import type { PaymasterConfig } from "../actions/sendUserOperation.js";
import type { SessionClient, SessionPreferences } from "../client/index.js";
import type { PartialCallPolicy, SessionClient, SessionPreferences } from "../client/index.js";
import type { ProviderInterface } from "../client-auth-server/index.js";
import { WalletProvider } from "../client-auth-server/WalletProvider.js";
import type { AppMetadata, Communicator } from "../communicator/interface.js";
Expand All @@ -42,6 +45,30 @@ export type ZksyncSsoConnectorOptions = {
paymaster?: Address | PaymasterConfig;
};

function filterContractCallsAbi(contractCalls: PartialCallPolicy[]): PartialCallPolicy[] {
const allowedStateMutability: AbiStateMutability[] = ["nonpayable", "payable"];

return contractCalls.map((call) => {
const matchingFunction = (call.abi as Abi).find(
(item): item is AbiFunction =>
item.type === "function"
&& item.name === call.functionName
&& allowedStateMutability.includes(item.stateMutability),
);

if (!matchingFunction) {
throw new Error(
`Function "${call.functionName}" not found in ABI for contract ${call.address}. Only nonpayable/payable functions are allowed.`,
);
}

return {
...call,
abi: [matchingFunction],
};
});
}

export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => {
let walletProvider: ProviderInterface | undefined;

Expand Down Expand Up @@ -160,14 +187,37 @@ export const zksyncSsoConnector = (parameters: ZksyncSsoConnectorOptions) => {
}
}

// Process session to filter ABI to only the specified function
let processedSession = parameters.session;
if (parameters.session) {
if (typeof parameters.session === "function") {
const originalSessionFn = parameters.session;
processedSession = async () => {
const sessionConfig = await originalSessionFn();
if (sessionConfig.contractCalls) {
return {
...sessionConfig,
contractCalls: filterContractCallsAbi(sessionConfig.contractCalls),
};
}
return sessionConfig;
};
} else if (parameters.session.contractCalls) {
processedSession = {
...parameters.session,
contractCalls: filterContractCallsAbi(parameters.session.contractCalls),
};
}
}

walletProvider = parameters.provider ?? new WalletProvider({
metadata: {
name: parameters.metadata?.name,
icon: parameters.metadata?.icon,
configData: parameters.metadata?.configData,
},
authServerUrl: parameters.authServerUrl,
session: parameters.session,
session: processedSession,
transports: config.transports,
bundlerClients: parameters.bundlerClients,
chains: config.chains,
Expand Down Expand Up @@ -257,11 +307,13 @@ export const isSsoSessionClientConnected = async<

try {
// Check if this is a ZKsync SSO connector with our custom _getClient method
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (typeof (connection.connector as any)._getClient !== "function") {
return false;
}

// Use the custom _getClient method to get our custom client
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = await (connection.connector as any)._getClient(parameters);
return isSsoSessionClient(client);
} catch {
Expand All @@ -284,10 +336,12 @@ export const getConnectedSsoSessionClient = async<
}

// Check if this is a ZKsync SSO connector with our custom _getClient method
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (typeof (connection.connector as any)._getClient !== "function") {
throw new Error("Connector does not support getClient method. Make sure you're using the ZKsync SSO connector.");
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const client = await (connection.connector as any)._getClient(parameters);

if (!isSsoSessionClient(client)) {
Expand All @@ -297,3 +351,6 @@ export const getConnectedSsoSessionClient = async<
const sessionClient = client as unknown as GetConnectedSsoClientReturnType<config, chainId>;
return sessionClient;
};

// Re-export callPolicy utility for convenient access
export { callPolicy } from "../client/index.js";
Loading