From cd08acc3adf89cfd0f5d0e53cb0e84f326e2d97e Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:02:48 +0100 Subject: [PATCH 1/7] bump typescript target to es2022 Fixes build errors from the ox package (transitive dep of viem) which ships raw .ts files requiring es2021+ features (replaceAll, override). Co-Authored-By: Claude Opus 4.6 --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 5eb2d4d..5baea98 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, "module": "commonjs" /* Specify what module code is generated. */, "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, "declaration": true, From 5e11c8e6c1448baa43d92bfac74771fcf5bba857 Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:03:05 +0100 Subject: [PATCH 2/7] replace ethers Wallet.createRandom with viem in 5 examples Use generatePrivateKey + privateKeyToAccount from viem/accounts instead of ethers Wallet.createRandom() in multisig, recovery, simulate-with-tenderly, eip-7702 sponsored-gas, and nested-safe-accounts. Co-Authored-By: Claude Opus 4.6 --- ...eoa-to-7702-smart-account-sponsored-gas.ts | 8 ++++---- multisig/multisig.ts | 10 ++++++---- nested-safe-accounts/nested-safe-accounts.ts | 20 +++++++++++-------- recovery/recovery.ts | 11 +++++----- .../simulate-with-tenderly.ts | 7 ++++--- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/eip-7702/upgrade-eoa-to-7702-smart-account-sponsored-gas.ts b/eip-7702/upgrade-eoa-to-7702-smart-account-sponsored-gas.ts index cf3d36e..5ca9187 100644 --- a/eip-7702/upgrade-eoa-to-7702-smart-account-sponsored-gas.ts +++ b/eip-7702/upgrade-eoa-to-7702-smart-account-sponsored-gas.ts @@ -6,7 +6,7 @@ import { createAndSignEip7702DelegationAuthorization, CandidePaymaster, } from "abstractionkit"; -import { Wallet } from 'ethers'; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; async function main(): Promise { //get values from .env @@ -14,10 +14,10 @@ async function main(): Promise { const chainId = BigInt(process.env.CHAIN_ID as string) const bundlerUrl = process.env.BUNDLER_URL as string const nodeUrl = process.env.NODE_URL as string; - - const eoaDelegator = Wallet.createRandom(); + + const eoaDelegatorPrivateKey = generatePrivateKey(); + const eoaDelegator = privateKeyToAccount(eoaDelegatorPrivateKey); const eoaDelegatorPublicAddress = eoaDelegator.address; - const eoaDelegatorPrivateKey = eoaDelegator.privateKey; const paymasterUrl = process.env.PAYMASTER_URL as string; const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID as string; diff --git a/multisig/multisig.ts b/multisig/multisig.ts index 74aad5e..322a412 100644 --- a/multisig/multisig.ts +++ b/multisig/multisig.ts @@ -9,7 +9,7 @@ import { CandidePaymaster, } from "abstractionkit"; -import { Wallet } from "ethers"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; async function main(): Promise { //get values from .env @@ -21,8 +21,10 @@ async function main(): Promise { const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID; - const owner1 = Wallet.createRandom(); - const owner2 = Wallet.createRandom(); + const owner1PrivateKey = generatePrivateKey(); + const owner1 = privateKeyToAccount(owner1PrivateKey); + const owner2PrivateKey = generatePrivateKey(); + const owner2 = privateKeyToAccount(owner2PrivateKey); //initializeNewAccount only needed when the smart account //have not been deployed yet for its first useroperation. @@ -98,7 +100,7 @@ async function main(): Promise { //privateKeys userOperation.signature = smartAccount.signUserOperation( userOperation, - [owner1.privateKey, owner2.privateKey], + [owner1PrivateKey, owner2PrivateKey], chainId ) console.log(userOperation) diff --git a/nested-safe-accounts/nested-safe-accounts.ts b/nested-safe-accounts/nested-safe-accounts.ts index d62933f..1a0ad27 100644 --- a/nested-safe-accounts/nested-safe-accounts.ts +++ b/nested-safe-accounts/nested-safe-accounts.ts @@ -1,5 +1,5 @@ import * as dotenv from 'dotenv' -import * as ethers from 'ethers' +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { SafeAccountV0_3_0 as SafeAccount, @@ -20,11 +20,15 @@ async function main(): Promise { const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID as string; /*subaccount1 signers*/ - const signer1Subaccount1 = ethers.Wallet.createRandom(); - const signer2Subaccount1 = ethers.Wallet.createRandom(); + const signer1Subaccount1PrivateKey = generatePrivateKey(); + const signer1Subaccount1 = privateKeyToAccount(signer1Subaccount1PrivateKey); + const signer2Subaccount1PrivateKey = generatePrivateKey(); + const signer2Subaccount1 = privateKeyToAccount(signer2Subaccount1PrivateKey); /*subaccount2 signers*/ - const signer1Subaccount2 = ethers.Wallet.createRandom(); - const signer2Subaccount2 = ethers.Wallet.createRandom(); + const signer1Subaccount2PrivateKey = generatePrivateKey(); + const signer1Subaccount2 = privateKeyToAccount(signer1Subaccount2PrivateKey); + const signer2Subaccount2PrivateKey = generatePrivateKey(); + const signer2Subaccount2 = privateKeyToAccount(signer2Subaccount2PrivateKey); /* initialize subaccounts */ const subAccount1 = SafeAccount.initializeNewAccount( @@ -66,7 +70,7 @@ async function main(): Promise { subAccount1DeployMainAccountUserOperation.signature = subAccount1.signUserOperation( subAccount1DeployMainAccountUserOperation, - [signer1Subaccount1.privateKey, signer2Subaccount1.privateKey], + [signer1Subaccount1PrivateKey, signer2Subaccount1PrivateKey], chainId, ) @@ -176,7 +180,7 @@ async function main(): Promise { subAccount1UserOperation.signature = subAccount1.signUserOperation( subAccount1UserOperation, - [signer1Subaccount1.privateKey, signer2Subaccount1.privateKey], + [signer1Subaccount1PrivateKey, signer2Subaccount1PrivateKey], chainId, ) let subAccount2UserOperation = await subAccount2.createUserOperation( @@ -190,7 +194,7 @@ async function main(): Promise { subAccount2UserOperation.signature = subAccount2.signUserOperation( subAccount2UserOperation, - [signer1Subaccount2.privateKey, signer2Subaccount2.privateKey], + [signer1Subaccount2PrivateKey, signer2Subaccount2PrivateKey], chainId, ) /***********************************/ diff --git a/recovery/recovery.ts b/recovery/recovery.ts index 5e4ed41..545d92e 100644 --- a/recovery/recovery.ts +++ b/recovery/recovery.ts @@ -7,7 +7,7 @@ import { SocialRecoveryModule } from "abstractionkit"; -import { Wallet } from "ethers"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; async function main(): Promise { //get values from .env @@ -18,13 +18,14 @@ async function main(): Promise { const paymasterUrl = process.env.PAYMASTER_URL as string; const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID as string; - const safeOwner = Wallet.createRandom(); + const safeOwnerPrivateKey = generatePrivateKey(); + const safeOwner = privateKeyToAccount(safeOwnerPrivateKey); const ownerPublicAddress = process.env.PUBLIC_ADDRESS || safeOwner.address as string - const ownerPrivateKey = process.env.PRIVATE_KEY || safeOwner.privateKey as string + const ownerPrivateKey = process.env.PRIVATE_KEY || safeOwnerPrivateKey as string - const guardianWallet = Wallet.createRandom(); - const guardianPublicAddress = guardianWallet.address as string; + const guardian = privateKeyToAccount(generatePrivateKey()); + const guardianPublicAddress = guardian.address as string; //initializeNewAccount only needed when the smart account //have not been deployed yet for its first useroperation. diff --git a/simulate-with-tenderly/simulate-with-tenderly.ts b/simulate-with-tenderly/simulate-with-tenderly.ts index 33285c8..a11de41 100644 --- a/simulate-with-tenderly/simulate-with-tenderly.ts +++ b/simulate-with-tenderly/simulate-with-tenderly.ts @@ -1,5 +1,5 @@ import * as dotenv from 'dotenv' -import { Wallet } from "ethers"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; import { SafeAccountV0_3_0, MetaTransaction, @@ -14,9 +14,10 @@ async function main(): Promise { const bundlerUrl = process.env.BUNDLER_URL as string const nodeUrl = process.env.NODE_URL as string - const owner = Wallet.createRandom(); + const ownerPrivateKeyGenerated = generatePrivateKey(); + const owner = privateKeyToAccount(ownerPrivateKeyGenerated); const ownerPublicAddress = process.env.PUBLIC_ADDRESS || owner.address as string - const ownerPrivateKey = process.env.PRIVATE_KEY || owner.privateKey as string + const ownerPrivateKey = process.env.PRIVATE_KEY || ownerPrivateKeyGenerated as string const tenderlyAccountSlug = ''; const tenderlyProjectSlug = ''; From 4cc3947f7c1c7fd162a6e4f7439e97c07e0fb330 Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:03:44 +0100 Subject: [PATCH 3/7] replace ethers Contract and JsonRpcProvider with viem in spend-permission Use createPublicClient + client.readContract instead of ethers Contract and JsonRpcProvider for the ERC-20 balanceOf check. Co-Authored-By: Claude Opus 4.6 --- spend-permission/spend-permission.ts | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/spend-permission/spend-permission.ts b/spend-permission/spend-permission.ts index de54776..47bf0f6 100644 --- a/spend-permission/spend-permission.ts +++ b/spend-permission/spend-permission.ts @@ -5,15 +5,10 @@ import { CandidePaymaster, ZeroAddress } from "abstractionkit"; -import { - Wallet, - Contract, - JsonRpcProvider, -} from "ethers"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { createPublicClient, http, parseAbi } from "viem"; -const ERC20_ABI = [ - "function balanceOf(address owner) view returns (uint256)", -]; +const ERC20_ABI = parseAbi(["function balanceOf(address owner) view returns (uint256)"]); async function main(): Promise { //get values from .env @@ -30,18 +25,22 @@ async function main(): Promise { const sourceOwnerPrivateKey = process.env.PRIVATE_KEY as string; // delegate account owner - const delegateOwner = Wallet.createRandom(); + const delegateOwnerPrivateKey = generatePrivateKey(); + const delegateOwner = privateKeyToAccount(delegateOwnerPrivateKey); const delegateOwnerPublicAddress = delegateOwner.address; - const delegateOwnerPrivateKey = delegateOwner.privateKey; // source safe account const sourceSafeAccount = SafeAccount.initializeNewAccount( [sourceOwnerPublicAddress], { c2Nonce: 0n } ); - const provider = new JsonRpcProvider(nodeUrl); - const tokenContract = new Contract(allowanceToken, ERC20_ABI, provider); - const sourceSafeAccountBalance = await tokenContract.balanceOf(sourceSafeAccount.accountAddress); + const client = createPublicClient({ transport: http(nodeUrl) }); + const sourceSafeAccountBalance = await client.readContract({ + address: allowanceToken as `0x${string}`, + abi: ERC20_ABI, + functionName: 'balanceOf', + args: [sourceSafeAccount.accountAddress as `0x${string}`], + }); if (sourceSafeAccountBalance <= 2n) { console.log("Please fund the Safe Account with some tokens first"); From e68a1c8c5f55c1bfb98fb2849e2e0110a078af59 Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:03:51 +0100 Subject: [PATCH 4/7] replace ethers signing and provider with viem in eip-7702 wallet-signed Use viem sign() + parseSignature for delegation authorization and UserOperation signing. Replace JsonRpcProvider with createPublicClient. Co-Authored-By: Claude Opus 4.6 --- ...eoa-to-7702-smart-account-wallet-signed.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/eip-7702/upgrade-eoa-to-7702-smart-account-wallet-signed.ts b/eip-7702/upgrade-eoa-to-7702-smart-account-wallet-signed.ts index df203ab..457c2a5 100644 --- a/eip-7702/upgrade-eoa-to-7702-smart-account-wallet-signed.ts +++ b/eip-7702/upgrade-eoa-to-7702-smart-account-wallet-signed.ts @@ -7,7 +7,8 @@ import { createEip7702DelegationAuthorizationHash, createUserOperationHash, } from "abstractionkit"; -import { JsonRpcProvider, toBeHex, Wallet } from 'ethers'; +import { generatePrivateKey, privateKeyToAccount, sign } from "viem/accounts"; +import { createPublicClient, http, toHex } from "viem"; async function main(): Promise { try { @@ -17,9 +18,10 @@ async function main(): Promise { const bundlerUrl = process.env.BUNDLER_URL as string const nodeUrl = process.env.NODE_URL as string; - const provider = new JsonRpcProvider(nodeUrl); + const client = createPublicClient({ transport: http(nodeUrl) }); - const eoaDelegator = Wallet.createRandom(provider); + const eoaDelegatorPrivateKey = generatePrivateKey(); + const eoaDelegator = privateKeyToAccount(eoaDelegatorPrivateKey); const eoaDelegatorPublicAddress = eoaDelegator.address; const paymasterUrl = process.env.PAYMASTER_URL as string; const sponsorshipPolicyId = process.env.SPONSORSHIP_POLICY_ID as string; @@ -58,7 +60,7 @@ async function main(): Promise { } ); - const nonce = await eoaDelegator.getNonce(); + const nonce = await client.getTransactionCount({ address: eoaDelegatorPublicAddress }); const eip7702DelegationAuthorizationHash = createEip7702DelegationAuthorizationHash( chainId, @@ -66,17 +68,15 @@ async function main(): Promise { BigInt(nonce) ); - const signedHash = eoaDelegator.signingKey.sign( - eip7702DelegationAuthorizationHash, - ); + const delegationSig = await sign({ hash: eip7702DelegationAuthorizationHash as `0x${string}`, privateKey: eoaDelegatorPrivateKey }); userOperation.eip7702Auth = { - chainId: toBeHex(chainId), + chainId: toHex(chainId), address: smartAccount.delegateeAddress, - nonce: toBeHex(nonce), - yParity: toBeHex(signedHash.yParity), - r: signedHash.r, - s: signedHash.s + nonce: toHex(nonce), + yParity: toHex(delegationSig.v === 27n ? 0 : 1), + r: delegationSig.r, + s: delegationSig.s }; const paymaster = new CandidePaymaster(paymasterUrl); @@ -91,7 +91,7 @@ async function main(): Promise { chainId, ); - userOperation.signature = eoaDelegator.signingKey.sign(userOperationHash).serialized; + userOperation.signature = await sign({ hash: userOperationHash as `0x${string}`, privateKey: eoaDelegatorPrivateKey, to: 'hex' }); let sendUserOperationResponse = await smartAccount.sendUserOperation( userOperation, bundlerUrl From 8028e73f5785ae646888717a0459c82c62408a57 Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:04:04 +0100 Subject: [PATCH 5/7] replace ethers and @noble/curves with viem and node:crypto in webauthn Replace ethers utility functions with viem equivalents (keccak256, sha256, toHex, hexToBytes, toBytes). Replace @noble/curves/p256 with node:crypto for P-256 key generation and signing. Simplify binary protocol construction by using Buffer.concat with an explicit buildAuthenticatorData helper instead of abusing encodePacked for binary data. Use native base64url encoding. Fix Buffer pool bug in extractPublicKey where DataView was constructed without byteOffset, reading garbage from the shared 8KB ArrayBuffer pool. Co-Authored-By: Claude Opus 4.6 --- passkeys/webauthn.ts | 180 +++++++++++++++++++++---------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/passkeys/webauthn.ts b/passkeys/webauthn.ts index 80ac17d..612bfe1 100644 --- a/passkeys/webauthn.ts +++ b/passkeys/webauthn.ts @@ -8,9 +8,9 @@ * [1]: */ -import { p256 } from '@noble/curves/p256' -import { ethers, BytesLike } from 'ethers' -import CBOR from 'cbor' +import * as crypto from 'node:crypto' +import { keccak256, sha256, toHex, hexToBytes, toBytes, maxUint256, type Hex } from 'viem' +import * as CBOR from 'cbor' export interface CredentialCreationOptions { publicKey: PublicKeyCredentialCreationOptions @@ -69,7 +69,7 @@ export interface PublicKeyCredential { } /** - * The authenticator's response to a client’s request for the creation of a new public key credential. + * The authenticator's response to a client's request for the creation of a new public key credential. * See . */ export interface AuthenticatorAttestationResponse { @@ -78,7 +78,7 @@ export interface AuthenticatorAttestationResponse { } /** - * The authenticator's response to a client’s request generation of a new authentication assertion given the WebAuthn Relying Party's challenge. + * The authenticator's response to a client's request generation of a new authentication assertion given the WebAuthn Relying Party's challenge. * See . */ export interface AuthenticatorAssertionResponse { @@ -89,40 +89,66 @@ export interface AuthenticatorAssertionResponse { } class Credential { - public id: string - public pk: bigint + public id: Hex + public privateKey: crypto.KeyObject + private publicKeyUncompressed: Uint8Array // 65 bytes: 0x04 || x || y constructor( public rp: string, public user: Uint8Array, ) { - this.pk = p256.utils.normPrivateKeyToScalar(p256.utils.randomPrivateKey()) - this.id = ethers.dataSlice(ethers.keccak256(ethers.dataSlice(p256.getPublicKey(this.pk, false), 1)), 12) + const keyPair = crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' }) + this.privateKey = keyPair.privateKey + + // Export uncompressed public key (0x04 || x || y) + const pubJwk = keyPair.publicKey.export({ format: 'jwk' }) + const x = Buffer.from(pubJwk.x!, 'base64url') + const y = Buffer.from(pubJwk.y!, 'base64url') + this.publicKeyUncompressed = new Uint8Array(Buffer.concat([Buffer.from([0x04]), x, y])) + + // Credential ID = last 20 bytes of keccak256(pubkey without 0x04 prefix) + const pubKeyHash = keccak256(toHex(this.publicKeyUncompressed.slice(1))) + this.id = `0x${pubKeyHash.slice(26)}` as Hex // skip "0x" + 24 hex chars = 12 bytes } /** * Computes the COSE encoded public key for this credential. * See . - * - * @returns Hex-encoded COSE-encoded public key */ - public cosePublicKey(): string { - const pubk = p256.getPublicKey(this.pk, false) - const x = pubk.subarray(1, 33) - const y = pubk.subarray(33, 65) + public cosePublicKey(): Buffer { + const x = this.publicKeyUncompressed.subarray(1, 33) + const y = this.publicKeyUncompressed.subarray(33, 65) - // const key = new Map() - // key.set(-1, 1) // crv = P-256 key.set(-2, b2ab(x)) key.set(-3, b2ab(y)) - // key.set(1, 2) // kty = EC2 - key.set(3, -7) // alg = ES256 (Elliptic curve signature with SHA-256) + key.set(3, -7) // alg = ES256 + return CBOR.encode(key) + } +} - return ethers.hexlify(CBOR.encode(key)) +/** + * Build authenticator data as a binary buffer. + * See + */ +function buildAuthenticatorData( + rpId: string, + flags: number, + signCount: number, + attestedCredentialData?: Buffer, +): Buffer { + const rpIdHash = Buffer.from(hexToBytes(sha256(toBytes(rpId)))) + const flagsBuf = Buffer.from([flags]) + const signCountBuf = Buffer.alloc(4) + signCountBuf.writeUInt32BE(signCount) + + const parts = [rpIdHash, flagsBuf, signCountBuf] + if (attestedCredentialData) { + parts.push(attestedCredentialData) } + return Buffer.concat(parts) } export class WebAuthnCredentials { @@ -131,9 +157,6 @@ export class WebAuthnCredentials { /** * This is a shim for `navigator.credentials.create` method. * See . - * - * @param options The public key credential creation options. - * @returns A public key credential with an attestation response. */ public create({ publicKey }: CredentialCreationOptions): PublicKeyCredential { if (!publicKey.pubKeyCredParams.some(({ alg }) => alg === -7)) { @@ -143,7 +166,6 @@ export class WebAuthnCredentials { const credential = new Credential(publicKey.rp.id, publicKey.user.id) this.#credentials.push(credential) - // const clientData = { type: 'webauthn.create', challenge: base64UrlEncode(publicKey.challenge).replace(/=*$/, ''), @@ -153,31 +175,27 @@ export class WebAuthnCredentials { const userVerification = publicKey.userVerification ?? 'preferred' const userVerificationFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x01 - // - const attestationObject = { - authData: ethers.getBytes( - ethers.solidityPacked( - ['bytes32', 'uint8', 'uint32', 'bytes16', 'uint16', 'bytes', 'bytes'], - [ - ethers.sha256(ethers.toUtf8Bytes(publicKey.rp.id)), - 0x40 + userVerificationFlag, // flags = attested_data + user_present - 0, // signCount - `0x${'42'.repeat(16)}`, // aaguid - ethers.dataLength(credential.id), - credential.id, - credential.cosePublicKey(), - ], - ), - ), - fmt: 'none', - attStmt: {}, - } + // Build attested credential data: aaguid (16) + credIdLen (2) + credId + coseKey + const aaguid = Buffer.alloc(16, 0x42) + const credIdBytes = Buffer.from(hexToBytes(credential.id)) + const credIdLen = Buffer.alloc(2) + credIdLen.writeUInt16BE(credIdBytes.length) + const attestedCredentialData = Buffer.concat([aaguid, credIdLen, credIdBytes, credential.cosePublicKey()]) + + const authData = buildAuthenticatorData( + publicKey.rp.id, + 0x40 + userVerificationFlag, // flags = attested_data + user_present + 0, + attestedCredentialData, + ) + + const attestationObject = { authData, fmt: 'none', attStmt: {} } return { id: base64UrlEncode(credential.id), - rawId: ethers.getBytes(credential.id), + rawId: hexToBytes(credential.id), response: { - clientDataJSON: b2ab(ethers.toUtf8Bytes(JSON.stringify(clientData))), + clientDataJSON: b2ab(Buffer.from(JSON.stringify(clientData))), attestationObject: b2ab(CBOR.encode(attestationObject)), }, type: 'public-key', @@ -187,19 +205,15 @@ export class WebAuthnCredentials { /** * This is a shim for `navigator.credentials.get` method. * See . - * - * @param options The public key credential request options. - * @returns A public key credential with an assertion response. */ get({ publicKey }: CredentialRequestOptions): PublicKeyCredential { const credential = publicKey.allowCredentials - .flatMap(({ id }) => this.#credentials.filter((c) => c.rp === publicKey.rpId && c.id === ethers.hexlify(id))) + .flatMap(({ id }) => this.#credentials.filter((c) => c.rp === publicKey.rpId && c.id === toHex(id))) .at(0) if (credential === undefined) { throw new Error('credential not found') } - // const clientData = { type: 'webauthn.get', challenge: base64UrlEncode(publicKey.challenge).replace(/=*$/, ''), @@ -208,37 +222,21 @@ export class WebAuthnCredentials { const userVerification = publicKey.userVerification ?? 'preferred' const userVerificationFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x01 - // - // Note that we use a constant 0 value for signCount to simplify things: - // > If the authenticator does not implement a signature counter, let the signature counter - // > value remain constant at zero. - const authenticatorData = ethers.solidityPacked( - ['bytes32', 'uint8', 'uint32'], - [ - ethers.sha256(ethers.toUtf8Bytes(publicKey.rpId)), - userVerificationFlag, // flags = user_present - 0, // signCount - ], - ) - // - // - const signature = p256.sign( - ethers.getBytes(ethers.concat([authenticatorData, ethers.sha256(ethers.toUtf8Bytes(JSON.stringify(clientData)))])), - credential.pk, - { - lowS: false, - prehash: true, - }, - ) + const authenticatorData = buildAuthenticatorData(publicKey.rpId, userVerificationFlag, 0) + + // Sign: authenticatorData || sha256(clientDataJSON) + const clientDataHash = Buffer.from(hexToBytes(sha256(toBytes(JSON.stringify(clientData))))) + const dataToSign = Buffer.concat([authenticatorData, clientDataHash]) + const derSignature = crypto.sign('sha256', dataToSign, credential.privateKey) return { id: base64UrlEncode(credential.id), - rawId: ethers.getBytes(credential.id), + rawId: hexToBytes(credential.id), response: { - clientDataJSON: b2ab(ethers.toUtf8Bytes(JSON.stringify(clientData))), - authenticatorData: b2ab(ethers.getBytes(authenticatorData)), - signature: b2ab(signature.toDERRawBytes(false)), + clientDataJSON: b2ab(Buffer.from(JSON.stringify(clientData))), + authenticatorData: b2ab(authenticatorData), + signature: b2ab(derSignature), userHandle: credential.user, }, type: 'public-key', @@ -248,15 +246,16 @@ export class WebAuthnCredentials { /** * Encode bytes using the Base64 URL encoding. - * * See - * - * @param data data to encode to `base64url` - * @returns the `base64url` encoded data as a string. */ -export function base64UrlEncode(data: BytesLike | ArrayBufferLike): string { - const bytes = ethers.isBytesLike(data) ? data : new Uint8Array(data) - return ethers.encodeBase64(bytes).replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, '') +export function base64UrlEncode(data: Hex | Uint8Array | ArrayBufferLike): string { + if (typeof data === 'string') { + return Buffer.from(hexToBytes(data)).toString('base64url') + } + if (data instanceof Uint8Array) { + return Buffer.from(data).toString('base64url') + } + return Buffer.from(new Uint8Array(data)).toString('base64url') } function b2ab(buf: Uint8Array): ArrayBuffer { @@ -269,11 +268,12 @@ function b2ab(buf: Uint8Array): ArrayBuffer { */ export function extractPublicKey(response: AuthenticatorAttestationResponse): { x: bigint; y: bigint } { const attestationObject = CBOR.decode(response.attestationObject) - const authDataView = new DataView(attestationObject.authData.buffer) + const authData: Buffer = attestationObject.authData + const authDataView = new DataView(authData.buffer, authData.byteOffset, authData.byteLength) const credentialIdLength = authDataView.getUint16(53) - const cosePublicKey = attestationObject.authData.slice(55 + credentialIdLength) + const cosePublicKey = authData.subarray(55 + credentialIdLength) const key: Map = CBOR.decode(cosePublicKey) - const bn = (bytes: Uint8Array) => BigInt(ethers.hexlify(bytes)) + const bn = (bytes: Uint8Array) => BigInt(toHex(bytes)) return { x: bn(key.get(-2) as Uint8Array), y: bn(key.get(-3) as Uint8Array), @@ -296,7 +296,7 @@ export function extractClientDataFields(response: AuthenticatorAssertionResponse } const [, fields] = match - return ethers.hexlify(ethers.toUtf8Bytes(fields)) + return toHex(toBytes(fields)) } /** @@ -328,12 +328,12 @@ export function extractSignature(response: AuthenticatorAssertionResponse): [big const len = view.getUint8(offset + 1) const start = offset + 2 const end = start + len - const n = BigInt(ethers.hexlify(new Uint8Array(view.buffer.slice(start, end)))) - check(n < ethers.MaxUint256) + const n = BigInt(toHex(new Uint8Array(view.buffer.slice(start, end)))) + check(n < maxUint256) return [n, end] as const } const [r, sOffset] = readInt(2) const [s] = readInt(sOffset) return [r, s] -} \ No newline at end of file +} From da189b5631e8361ab2fc145697843df003da1141 Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:04:14 +0100 Subject: [PATCH 6/7] replace ethers with viem in passkey consumer files Use viem hexToBytes, keccak256, toBytes, numberToBytes instead of ethers getBytes, id, toBeArray in passkeys/index.ts, passkeys/passkeys-v0.2.1.ts, and chain-abstraction/add-owner-passkey.ts. Co-Authored-By: Claude Opus 4.6 --- chain-abstraction/add-owner-passkey.ts | 11 ++++++----- passkeys/index.ts | 8 ++++---- passkeys/passkeys-v0.2.1.ts | 8 ++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/chain-abstraction/add-owner-passkey.ts b/chain-abstraction/add-owner-passkey.ts index 41dc532..8821a8c 100644 --- a/chain-abstraction/add-owner-passkey.ts +++ b/chain-abstraction/add-owner-passkey.ts @@ -16,7 +16,8 @@ */ import * as dotenv from 'dotenv' -import * as ethers from 'ethers' +import { hexToBytes, keccak256, toBytes, numberToBytes } from 'viem' +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts" import { SafeMultiChainSigAccount as SafeAccount, AllowAllPaymaster, @@ -64,11 +65,11 @@ async function main(): Promise { id: 'safe.global', }, user: { - id: ethers.getBytes(ethers.id('chain-abstraction-demo')), + id: hexToBytes(keccak256(toBytes('chain-abstraction-demo'))), name: 'demo-user', displayName: 'Demo User', }, - challenge: ethers.toBeArray(Date.now()), + challenge: numberToBytes(Date.now()), pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }, }) @@ -84,7 +85,7 @@ async function main(): Promise { console.log(" Public key X:", publicKey.x.toString().slice(0, 20) + "...") // Generate a new owner address to add (using random address for demo) - const newOwnerAddress = ethers.Wallet.createRandom().address + const newOwnerAddress = privateKeyToAccount(generatePrivateKey()).address console.log("\nPasskey owner (signer):", credential.id.slice(0, 20) + "...") console.log("New owner to add:", newOwnerAddress) @@ -157,7 +158,7 @@ async function main(): Promise { // In a browser, this would trigger device biometrics const assertion = navigator.credentials.get({ publicKey: { - challenge: ethers.getBytes(multiChainHash), + challenge: hexToBytes(multiChainHash as `0x${string}`), rpId: 'safe.global', allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], userVerification: UserVerificationRequirement.required, diff --git a/passkeys/index.ts b/passkeys/index.ts index 1eac5a2..fceeb69 100644 --- a/passkeys/index.ts +++ b/passkeys/index.ts @@ -11,7 +11,7 @@ */ import * as dotenv from 'dotenv' -import * as ethers from 'ethers' +import { hexToBytes, keccak256, toBytes, numberToBytes } from 'viem' import { SafeAccountV0_3_0 as SafeAccount, MetaTransaction, @@ -68,11 +68,11 @@ async function main(): Promise { id: 'safe.global', }, user: { - id: ethers.getBytes(ethers.id('chucknorris')), + id: hexToBytes(keccak256(toBytes('chucknorris'))), name: 'chucknorris', displayName: 'Chuck Norris', }, - challenge: ethers.toBeArray(Date.now()), + challenge: numberToBytes(Date.now()), pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }, }) @@ -150,7 +150,7 @@ async function main(): Promise { // Simulate passkey authentication (biometric prompt in real browser) const assertion = navigator.credentials.get({ publicKey: { - challenge: ethers.getBytes(safeInitOpHash), + challenge: hexToBytes(safeInitOpHash as `0x${string}`), rpId: 'safe.global', allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], userVerification: UserVerificationRequirement.required, diff --git a/passkeys/passkeys-v0.2.1.ts b/passkeys/passkeys-v0.2.1.ts index d978e2b..c5f17a3 100644 --- a/passkeys/passkeys-v0.2.1.ts +++ b/passkeys/passkeys-v0.2.1.ts @@ -16,7 +16,7 @@ */ import * as dotenv from 'dotenv' -import * as ethers from 'ethers' +import { hexToBytes, keccak256, toBytes, numberToBytes } from 'viem' import { SafeAccountV0_3_0 as SafeAccount, MetaTransaction, @@ -92,11 +92,11 @@ async function main(): Promise { id: 'safe.global', }, user: { - id: ethers.getBytes(ethers.id('chucknorris')), + id: hexToBytes(keccak256(toBytes('chucknorris'))), name: 'chucknorris', displayName: 'Chuck Norris', }, - challenge: ethers.toBeArray(Date.now()), + challenge: numberToBytes(Date.now()), pubKeyCredParams: [{ type: 'public-key', alg: -7 }], }, }) @@ -182,7 +182,7 @@ async function main(): Promise { // Simulate passkey authentication (biometric prompt in real browser) const assertion = navigator.credentials.get({ publicKey: { - challenge: ethers.getBytes(safeInitOpHash), + challenge: hexToBytes(safeInitOpHash as `0x${string}`), rpId: 'safe.global', allowCredentials: [{ type: 'public-key', id: new Uint8Array(credential.rawId) }], userVerification: UserVerificationRequirement.required, From 89a9f392823ec806d6087cbaaec2f74e03e2fd1e Mon Sep 17 00:00:00 2001 From: sednaoui Date: Sat, 7 Mar 2026 13:32:44 +0100 Subject: [PATCH 7/7] fix webauthn flags and rawId type in webauthn shim Always set UP (User Present) bit in authenticator data flags per the WebAuthn spec. Previously when UV was required, UP was cleared (0x44 instead of 0x45). This was inherited from the original Safe repo code. Wrap rawId with b2ab() to return ArrayBuffer matching the PublicKeyCredential interface, consistent with all other ArrayBuffer fields. Co-Authored-By: Claude Opus 4.6 --- passkeys/webauthn.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/passkeys/webauthn.ts b/passkeys/webauthn.ts index 612bfe1..8ae8626 100644 --- a/passkeys/webauthn.ts +++ b/passkeys/webauthn.ts @@ -173,7 +173,7 @@ export class WebAuthnCredentials { } const userVerification = publicKey.userVerification ?? 'preferred' - const userVerificationFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x01 + const uvFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x00 // Build attested credential data: aaguid (16) + credIdLen (2) + credId + coseKey const aaguid = Buffer.alloc(16, 0x42) @@ -184,7 +184,7 @@ export class WebAuthnCredentials { const authData = buildAuthenticatorData( publicKey.rp.id, - 0x40 + userVerificationFlag, // flags = attested_data + user_present + 0x41 | uvFlag, // flags = AT (0x40) + UP (0x01) + optionally UV (0x04) 0, attestedCredentialData, ) @@ -193,7 +193,7 @@ export class WebAuthnCredentials { return { id: base64UrlEncode(credential.id), - rawId: hexToBytes(credential.id), + rawId: b2ab(hexToBytes(credential.id)), response: { clientDataJSON: b2ab(Buffer.from(JSON.stringify(clientData))), attestationObject: b2ab(CBOR.encode(attestationObject)), @@ -221,9 +221,9 @@ export class WebAuthnCredentials { } const userVerification = publicKey.userVerification ?? 'preferred' - const userVerificationFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x01 + const uvFlag = userVerification === UserVerificationRequirement.required ? 0x04 : 0x00 - const authenticatorData = buildAuthenticatorData(publicKey.rpId, userVerificationFlag, 0) + const authenticatorData = buildAuthenticatorData(publicKey.rpId, 0x01 | uvFlag, 0) // Sign: authenticatorData || sha256(clientDataJSON) const clientDataHash = Buffer.from(hexToBytes(sha256(toBytes(JSON.stringify(clientData))))) @@ -232,7 +232,7 @@ export class WebAuthnCredentials { return { id: base64UrlEncode(credential.id), - rawId: hexToBytes(credential.id), + rawId: b2ab(hexToBytes(credential.id)), response: { clientDataJSON: b2ab(Buffer.from(JSON.stringify(clientData))), authenticatorData: b2ab(authenticatorData),