@gemini-wallet/core
Version:
Core SDK for Gemini Wallet integration with popup communication
260 lines (228 loc) • 8.88 kB
text/typescript
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,
});
}