UNPKG

@gemini-wallet/core

Version:

Core SDK for Gemini Wallet integration with popup communication

260 lines (228 loc) 8.88 kB
import { type Address, encodeAbiParameters, encodeFunctionData, encodePacked, getCreate2Address, type Hex, keccak256, } from "viem"; // WebAuthn validator data structure export interface WebAuthnValidatorData { pubKeyX: bigint; pubKeyY: bigint; } // Parameters for calculating wallet address export interface CalculateWalletAddressParams { publicKey: Hex; // Combined 64-byte hex string (32 bytes X + 32 bytes Y) credentialId: string; // Base64URL encoded credential ID index?: bigint; // Optional, defaults to 0 } // Embedded contract addresses for Horizon deployment const CONTRACT_ADDRESSES = { ACCOUNT_IMPLEMENTATION: "0x0006050168DE255a8672ACaD4821e721CBA44337" as const, ATTESTER: "0x000474392a9cd86a4687354f1Ce2964B52e97484" as const, BOOTSTRAPPER: "0x00000000D3254452a909E4eeD47455Af7E27C289" as const, FACTORY: "0x00E58DF70FaB983a324c4C068c82d20407579FaC" as const, REGISTRY: "0x000000000069E2a187AEFFb852bF3cCdC95151B2" as const, WEBAUTHN_VALIDATOR: "0xbA45a2BFb8De3D24cA9D7F1B551E14dFF5d690Fd" as const, }; /** * Calculate smart wallet address from public key and credential ID * This handles all validation and setup internally */ export function calculateWalletAddress( params: CalculateWalletAddressParams, ): Address { const { publicKey, credentialId, index = 0n } = params; // Validate input if (!publicKey.startsWith("0x") || publicKey.length !== 130) { throw new Error( "Invalid public key: must be 64-byte hex string (0x + 128 chars)", ); } // Extract X and Y coordinates const pubKeyX = `0x${publicKey.slice(2, 66)}` as Hex; const pubKeyY = `0x${publicKey.slice(66, 130)}` as Hex; // Convert to WebAuthnValidatorData const webAuthnData: WebAuthnValidatorData = { pubKeyX: BigInt(pubKeyX), pubKeyY: BigInt(pubKeyY), }; // Validate the key is on the secp256r1 curve if (!validateWebAuthnKey(webAuthnData)) { throw new Error( "Invalid WebAuthn key: coordinates are not on secp256r1 curve", ); } // Calculate authenticator ID hash from credential ID const authenticatorIdHash = generateAuthenticatorIdHash(credentialId); // Use the internal calculation with embedded addresses return calculateAddressInternal({ authenticatorIdHash, index, webAuthnData, }); } /** * Generate authenticator ID hash from credential ID */ export function generateAuthenticatorIdHash(credentialId: string): Hex { // Convert base64url to bytes const padding = "=".repeat((4 - (credentialId.length % 4)) % 4); const base64 = credentialId.replace(/-/g, "+").replace(/_/g, "/") + padding; const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return keccak256(bytes); } /** * Validate WebAuthn public key offchain * Mirrors the contract's _validateWebAuthnKey function */ export function validateWebAuthnKey( webAuthnData: WebAuthnValidatorData, ): boolean { const SECP256R1_P = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn; const SECP256R1_B = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604bn; const { pubKeyX, pubKeyY } = webAuthnData; // Check if coordinates are valid if ( pubKeyX === 0n || pubKeyY === 0n || pubKeyX >= SECP256R1_P || pubKeyY >= SECP256R1_P ) { return false; } // Validate curve membership: Y² ≡ X³ - 3X + B (mod P) const ySquared = (pubKeyY * pubKeyY) % SECP256R1_P; const xCubed = (pubKeyX * pubKeyX * pubKeyX) % SECP256R1_P; const threeX = (3n * pubKeyX) % SECP256R1_P; const rightSide = (xCubed + SECP256R1_P - threeX + SECP256R1_B) % SECP256R1_P; return ySquared === rightSide; } /** * Internal calculation method using embedded contract addresses */ function calculateAddressInternal(params: { webAuthnData: WebAuthnValidatorData; authenticatorIdHash: Hex; index: bigint; }): Address { const { webAuthnData, authenticatorIdHash, index } = params; // Use embedded contract addresses const factoryAddress = CONTRACT_ADDRESSES.FACTORY; const accountImplementation = CONTRACT_ADDRESSES.ACCOUNT_IMPLEMENTATION; const webAuthnValidator = CONTRACT_ADDRESSES.WEBAUTHN_VALIDATOR; const attester = CONTRACT_ADDRESSES.ATTESTER; const bootstrapper = CONTRACT_ADDRESSES.BOOTSTRAPPER; const registry = CONTRACT_ADDRESSES.REGISTRY; // Generate cross-chain consistent salt (same as contract) const salt = keccak256( encodePacked( ["uint256", "uint256", "bytes32", "uint256"], [webAuthnData.pubKeyX, webAuthnData.pubKeyY, authenticatorIdHash, index], ), ); // Prepare validator initialization data (WebAuthnValidatorData + authenticatorIdHash) const validatorInitData = encodeAbiParameters( [ { components: [ { name: "pubKeyX", type: "uint256" }, { name: "pubKeyY", type: "uint256" }, ], type: "tuple", }, { type: "bytes32" }, ], [webAuthnData, authenticatorIdHash], ); // Create RegistryConfig struct const registryConfig = { attesters: [attester], registry, threshold: 1n, }; // Encode the bootstrap call const bootstrapCall = encodeFunctionData({ abi: [ { inputs: [ { name: "validator", type: "address" }, { name: "validatorInitData", type: "bytes" }, { components: [ { name: "registry", type: "address" }, { name: "attesters", type: "address[]" }, { name: "threshold", type: "uint8" }, ], name: "registryConfig", type: "tuple", }, ], name: "initNexusWithSingleValidator", type: "function", }, ], args: [webAuthnValidator, validatorInitData, registryConfig], functionName: "initNexusWithSingleValidator", }); // Format initialization data as expected by ProxyLib const initData = encodeAbiParameters( [{ type: "address" }, { type: "bytes" }], [bootstrapper, bootstrapCall], ); // Calculate CREATE2 address using the same logic as ProxyLib.predictProxyAddress return predictProxyAddress( accountImplementation, salt, initData, factoryAddress, ); } /** * Predicts the proxy address using CREATE2 * Mirrors ProxyLib.predictProxyAddress functionality exactly */ function predictProxyAddress( implementation: Address, salt: Hex, initData: Hex, deployer: Address, ): Address { // Encode the call to INexus.initializeAccount with initData const initializeCall = encodeFunctionData({ abi: [ { inputs: [{ name: "data", type: "bytes" }], name: "initializeAccount", type: "function", }, ], args: [initData], functionName: "initializeAccount", }); // Encode constructor arguments for NexusProxy const constructorArgs = encodeAbiParameters( [{ type: "address" }, { type: "bytes" }], [implementation, initializeCall], ); // Calculate initCodeHash using actual compiled NexusProxy creation bytecode const nexusProxyCreationCode = "0x60806040526102c8803803806100148161018c565b92833981016040828203126101885781516001600160a01b03811692909190838303610188576020810151906001600160401b03821161018857019281601f8501121561018857835161006e610069826101c5565b61018c565b9481865260208601936020838301011161018857815f926020809301865e8601015260017f90b772c2cb8a51aa7a8a65fc23543c6d022d5b3f8e2b92eed79fba7eef8293005d823b15610176577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b031916821790557fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b5f80a282511561015e575f8091610146945190845af43d15610156573d91610137610069846101c5565b9283523d5f602085013e6101e0565b505b6040516089908161023f8239f35b6060916101e0565b50505034156101485763b398979f60e01b5f5260045ffd5b634c9c8ce360e01b5f5260045260245ffd5b5f80fd5b6040519190601f01601f191682016001600160401b038111838210176101b157604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116101b157601f01601f191660200190565b9061020457508051156101f557805190602001fd5b63d6bda27560e01b5f5260045ffd5b81511580610235575b610215575090565b639996b31560e01b5f9081526001600160a01b0391909116600452602490fd5b50803b1561020d56fe608060405236156051577f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545f9081906001600160a01b0316368280378136915af43d5f803e15604d573d5ff35b3d5ffd5b00fea264697066735822122041b5f70a351952142223f22504ca7b4e6d975f3a302d114ff820442fcf815ac264736f6c634300081b0033" as const; const initCodeHash = keccak256( encodePacked(["bytes", "bytes"], [nexusProxyCreationCode, constructorArgs]), ); // Standard CREATE2 formula return getCreate2Address({ bytecodeHash: initCodeHash, from: deployer, salt, }); }