Skip to content
Open
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
1 change: 1 addition & 0 deletions apps/interface/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"lucide-svelte": "^0.441.0",
"ms": "^2.1.3",
"ts-essentials": "^10.0.2",
"webauthn-p256": "^0.0.10",
"viem": "^2.21.54",
"zod": "^3.23.8"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/interface/src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>zkLogin on Base</title>
<title>zkLogin with EIP-7702</title>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
Expand Down
67 changes: 16 additions & 51 deletions apps/interface/src/lib/SendEthCard.svelte
Original file line number Diff line number Diff line change
@@ -1,78 +1,45 @@
<script lang="ts">
import { lib } from "$lib";
import { Ui } from "@repo/ui";
import { createQuery } from "@tanstack/svelte-query";
import { ethers } from "ethers";
import { assert } from "ts-essentials";
import type { Address } from "viem";
import { z } from "zod";
import { relayer } from "./chain";
import { zAddress } from "./utils";
import { ethersSignerToWalletClient, getBundlerClient } from "./viemClients";

let {
jwt,
signer,
disabled,
address,
}: {
jwt: string | undefined;
signer: ethers.Wallet;
disabled: boolean;
address: Address;
} = $props();

let balanceQuery = $derived(
createQuery(
{
queryKey: ["balance", jwt && ethers.id(jwt)],
queryFn: async () => {
let raw: bigint;
if (!jwt) {
raw = 0n;
} else {
const account = await lib.jwtAccount.getAccount(jwt, signer);
raw = await signer.provider!.getBalance(account.address);
}
return `${ethers.formatEther(raw)} ETH`;
},
},
lib.queries.queryClient,
),
);
</script>

<Ui.Card.Root>
<Ui.Card.Header>
<Ui.Card.Title>Send ETH</Ui.Card.Title>
</Ui.Card.Header>
<Ui.Card.Content>
<div>
Balance: <Ui.Query query={$balanceQuery}>
{#snippet success(data)}
{data}
{/snippet}
</Ui.Query>
</div>

<Ui.Form
schema={z.object({
recipient: zAddress(),
amount: z.string(),
})}
initialValues={{
recipient: relayer.address,
amount: "0.00001",
}}
onsubmit={async (data) => {
assert(jwt, "no session");
const bundlerClient = getBundlerClient(
await ethersSignerToWalletClient(signer),
);
const account = await lib.jwtAccount.getAccount(jwt, signer);
const tx = await bundlerClient.sendUserOperation({
account,
calls: [
{
to: data.recipient as Address,
value: ethers.parseEther(data.amount),
},
],
const cred = await lib.webAuthn.getCredential();
assert(cred, "no credential");
const tx = await lib.eip7702.executeTx({
credentialId: cred.id,
address,
to: data.recipient,
value: ethers.parseEther(data.amount),
});
console.log("tx", tx);
lib.queries.invalidateAll();
Ui.toast.success("Transaction sent successfully");
}}
>
Expand All @@ -95,9 +62,7 @@
<Ui.Form.FieldErrors />
</Ui.Form.Field>

<Ui.Form.SubmitButton variant="default">
{disabled ? "Create a session first" : "Send"}
</Ui.Form.SubmitButton>
<Ui.Form.SubmitButton variant="default">Send</Ui.Form.SubmitButton>
{/snippet}
</Ui.Form>
</Ui.Card.Content>
Expand Down
9 changes: 7 additions & 2 deletions apps/interface/src/lib/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@ import type { z } from "zod";
import { zUnionFromArray } from "./utils";

/** @deprecated use {@link ChainService} */
export const chain = baseSepolia;
export const chain = odysseyTestnet;

const RPC_URL = "https://sepolia.base.org";
const RPC_URL = "https://odyssey.ithaca.xyz";
/** @deprecated migrate to viem */
export const provider = new ethers.JsonRpcProvider(RPC_URL);
/** @deprecated migrate to viem */
export const relayer = new ethers.Wallet(
"0x4e560d1db4456119f9256bb65b4321ad54b860882c46b5ecb6ba92ca4d725dad",
provider,
);

const chains = [baseSepolia, odysseyTestnet] as const;

Expand Down
11 changes: 7 additions & 4 deletions apps/interface/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { browser } from "$app/environment";
import { PUBLIC_AUTH_GOOGLE_ID } from "$env/static/public";
import { zklogin } from "@shield-labs/zklogin";
import { QueryClient } from "@tanstack/svelte-query";
import { ChainService, provider } from "./chain.js";
import { JwtAccountService } from "./services/JwtAccountService.js";
import { ChainService } from "./chain.js";
import { Eip7702Service } from "./services/Eip7702Service.js";
import { QueriesService } from "./services/QueriesService.svelte.js";
import { WebAuthnService } from "./services/WebAuthnService.js";
import { publicClient } from "./viemClients.js";

export * from "./viemClients.js";
Expand All @@ -18,9 +19,10 @@ const queryClient = new QueryClient({
});

const chain = new ChainService();
const webAuthn = new WebAuthnService();
const zkLogin = new zklogin.ZkLogin(new zklogin.PublicKeyRegistry(""));
const authProvider = new zklogin.GoogleProvider(PUBLIC_AUTH_GOOGLE_ID);
const jwtAccount = new JwtAccountService(publicClient, provider, zkLogin);
const eip7702 = new Eip7702Service(zkLogin, publicClient, authProvider);
const queries = new QueriesService(authProvider, queryClient);

const APP_NAME = "zkLogin";
Expand All @@ -29,6 +31,7 @@ export const lib = {
queries,
chain,
zkLogin,
eip7702,
authProvider,
jwtAccount,
webAuthn,
};
210 changes: 210 additions & 0 deletions apps/interface/src/lib/services/Eip7702Service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import { chain, provider, relayer } from "$lib/chain";
import { Ui } from "@repo/ui";
import { utils } from "@shield-labs/utils";
import { zklogin } from "@shield-labs/zklogin";
import deployments from "@shield-labs/zklogin-contracts/deployments.json";
import { EoaAccount__factory } from "@shield-labs/zklogin-contracts/typechain-types";
import { ethers } from "ethers";
import { assert } from "ts-essentials";
import {
createWalletClient,
http,
slice,
type Account,
type Chain,
type Client,
type Hex,
} from "viem";
import { signAuthorization } from "viem/experimental";
import { parsePublicKey, sign } from "webauthn-p256";

export class Eip7702Service {
constructor(
private zkLogin: zklogin.ZkLogin,
private client: Client & { chain: Chain },
private authProvider: zklogin.GoogleProvider,
) {}

async requestJwt({ webAuthnPublicKey }: { webAuthnPublicKey: Hex }) {
const nonce = this.#toNonce(webAuthnPublicKey).slice("0x".length);
await this.authProvider.signInWithRedirect({ nonce });
}

async authorize({
account,
jwt,
webAuthnPublicKey,
}: {
account: Account;
jwt: string;
webAuthnPublicKey: Hex;
}) {
if (!(await this.#requestJwtIfInvalid({ jwt, webAuthnPublicKey }))) {
throw new Error("jwt invalid");
}

const contractAddress = deployments[chain.id].contracts
.EoaAccount as `0x${string}`;
const auth = await signAuthorization(this.client, {
account,
contractAddress,
});

const accountClient = createWalletClient({
account: account,
chain: this.client.chain,
transport: http(),
});

const accountData = await this.zkLogin.getAccountDataFromJwt(
jwt,
this.client.chain.id,
);
const hash = await accountClient.writeContract({
abi: EoaAccount__factory.abi,
address: account.address,
functionName: "setAccountId",
args: [parsePublicKey(webAuthnPublicKey), accountData],
authorizationList: [auth],
account: account,
chain: this.client.chain,
});

return hash;
}

async recover({
address,
jwt,
webAuthnPublicKey,
}: {
address: string;
jwt: string;
webAuthnPublicKey: Hex;
}) {
if (!(await this.#requestJwtIfInvalid({ jwt, webAuthnPublicKey }))) {
throw new Error("jwt invalid");
}

await this.zkLogin.publicKeyRegistry.requestPublicKeysUpdate(
this.client.chain.id,
);

const result = await this.zkLogin.proveJwt(
jwt,
this.#toNonce(webAuthnPublicKey).slice("0x".length),
);
assert(result, "jwt invalid");
const { input, proof } = result;

const accContract = this.#toAccountContract(address);
const tx = await accContract.connect(relayer).recover(
{
proof: ethers.hexlify(proof),
jwtIat: input.jwt_iat,
jwtNonce: this.#toNonce(webAuthnPublicKey),
publicKeyHash: input.public_key_hash,
},
parsePublicKey(webAuthnPublicKey),
);
return tx.hash;
}

async executeTx({
credentialId,
address,
to,
value,
}: {
to: string;
value: bigint;
credentialId: string;
address: string;
}) {
const accContract = this.#toAccountContract(address);
const nonce = await accContract.nonce();
const data = "0x";
const digest = ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "address", "bytes", "uint256"],
[nonce, to, data, value],
);
const digestHash = ethers.keccak256(digest) as `0x${string}`;

const signature = await sign({ hash: digestHash, credentialId });
const r = slice(signature.signature, 0, 32);
const s = slice(signature.signature, 32, 64);

console.log(
"balance before",
ethers.formatEther(await provider.provider.getBalance(accContract)),
);

const tx = await accContract
.connect(relayer)
.execute(to, data, value, { r, s }, signature.webauthn);
console.log("tx", tx.hash);
await tx.wait();
console.log(
"balance after",
ethers.formatEther(await provider.provider.getBalance(accContract)),
);
return tx.hash;
}

async #requestJwtIfInvalid({
jwt,
webAuthnPublicKey,
}: {
jwt: string;
webAuthnPublicKey: Hex;
}): Promise<boolean> {
const isValid: boolean = await this.zkLogin.checkJwt(
jwt,
this.#toNonce(webAuthnPublicKey).slice("0x".length),
);
if (isValid) {
return true;
}
Ui.toast.log(
"Sign in again please to link your wallet to your Google account",
);
await utils.sleep("2 sec");
await this.requestJwt({
webAuthnPublicKey,
});
return false;
}

async isWebAuthnPublicKeyCorrect({
address,

webAuthnPublicKey,
}: {
address: string;
webAuthnPublicKey: Hex;
}) {
const accContract = this.#toAccountContract(address);
const publicKeyOnChain = await accContract.webauthnPublicKey();
return (
ethers.concat([
ethers.zeroPadValue(ethers.toBeArray(publicKeyOnChain.x), 32),
ethers.zeroPadValue(ethers.toBeArray(publicKeyOnChain.y), 32),
]) === webAuthnPublicKey
);
}

#toNonce(webAuthnPublicKey: Hex) {
const parsed = parsePublicKey(webAuthnPublicKey);
const hex = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["uint256", "uint256"],
[parsed.x, parsed.y],
),
);
return hex;
}

#toAccountContract(address: string) {
return EoaAccount__factory.connect(address, provider.provider);
}
}
Loading