← Back to list

Building with secp256r1 on Avalanche: Your Complete Guide to Passkey-Enabled Smart Contracts

A hands-on journey into leveraging Avalanche Granite’s secp256r1 capabilities to build the next generation of user-friendly dApps

Joseph Mwangi🔺 · 2025-12-12 21:56 · 0 claps · 33.3 min read
#avalanche #secp256k1
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval CRY · Crypto & Web3

Building with secp256r1 on Avalanche: Your Complete Guide to Passkey-Enabled Smart Contracts

A hands-on journey into leveraging Avalanche Granite’s secp256r1 capabilities to build the next generation of user-friendly dApps

Introduction: Why I’m Excited About This

Hey there! If you’ve been following my previous article on Avalanche Granite, you know I was pretty pumped about the secp256r1 support that’s now live on the network. The response was incredible, and many of you reached out asking the same question: “This sounds amazing, but how do I actually build with it?”

Well, that’s exactly what we’re going to tackle today.

I’ll be honest with you — when I first dove into secp256r1 capabilities, I was intimidated. There’s a lot of cryptography jargon floating around, and it’s easy to get lost in the technical weeds. But here’s what I discovered: once you understand the core concepts and see it in action, it’s actually quite straightforward. And more importantly, it opens up possibilities that frankly, I find revolutionary.

What we’ll build together in this guide:

  • A complete understanding of secp256r1 and why it matters for your projects
  • Your first smart contract that verifies secp256r1 signatures
  • A working passkey-enabled wallet that requires NO seed phrases
  • Real production patterns you can use in your dApps today

By the end of this guide, you’ll be able to build applications that let users authenticate with their fingerprint, Face ID, or hardware security keys — all while maintaining the security and decentralization we love about blockchain. No more asking users to write down 12 random words on a piece of paper. No more “I lost my seed phrase” support tickets.

Sound good? Let’s dive in.

Part 1: Understanding the Fundamentals (I Promise This Won’t Hurt)

What Exactly is secp256r1?

Okay, let’s start with the basics. You’re probably familiar with secp256k1 — it’s the elliptic curve that Ethereum, Bitcoin, and most blockchains use for signatures. It works great, but here’s the catch: it’s not what the rest of the digital world uses.

Your iPhone? Uses secp256r1 (also called P-256). Your laptop’s security chip? secp256r1. WebAuthn and passkeys? You guessed it — secp256r1. Government security standards (NIST)? secp256r1.

So we’ve had this weird situation where blockchain lives in its own cryptographic bubble, separate from virtually every other security standard out there. This meant that if you wanted to use hardware security features built into billions of devices worldwide, you were out of luck.

That’s what Avalanche Granite changed.

secp256r1 vs secp256k1: What’s the Difference?

I’m going to keep this practical rather than diving into elliptic curve mathematics (trust me, we don’t need to go there). Here’s what you need to know:

secp256k1 (the old way):

  • Used by Bitcoin, Ethereum, and most crypto
  • Not supported by mainstream hardware/software
  • Requires custom implementations
  • Great for crypto-native users

secp256r1 (the new capability):

  • Industry standard (NIST P-256)
  • Built into virtually every modern device
  • Supported by browsers, operating systems, hardware security modules
  • Familiar to traditional developers
  • Game changer: enables passkeys and WebAuthn

Both are secure. Both use elliptic curve cryptography. The difference is ecosystem compatibility.

How Does This Work on Avalanche?

Here’s where it gets interesting. Avalanche implemented secp256r1 support through something called precompiles.

Think of precompiles as built-in, super-efficient functions that run at the protocol level. Instead of implementing complex cryptographic operations in Solidity (which would be slow and expensive), Avalanche provides native support that’s:

  • Fast: Optimized at the protocol level
  • Cheap: Lower gas costs than implementing it yourself
  • Secure: Battle-tested implementations
  • Easy to use: Simple function calls from your smart contracts

The specific precompile we’ll work with is at address 0x0000000000000000000000000000000000000100. I know, memorable right? Don't worry, we'll make this easy.

Part 2: The “Aha!” Moment — What You Can Actually Build

Before we get into code, let me show you what’s now possible. This is what gets me excited:

Use Case 1: The Seedless Wallet

Imagine a wallet where users sign in with Face ID. That’s it. No seed phrase to write down, no private key to manage, no “store this in a safe place” warnings. The user’s biometric data never leaves their device, but they can sign blockchain transactions securely.

Use Case 2: Gaming Without Friction

A player wants to try your blockchain game. Instead of:

  1. Installing MetaMask/Core Wallet
  2. Creating a wallet
  3. Writing down seed words
  4. Buying crypto
  5. Finally playing your game

They just:

  1. Click “Sign in with Face ID”
  2. Start playing

Which experience do you think converts better?

Use Case 3: Enterprise Integration

A company wants to use blockchain for supply chain tracking. Their employees already have hardware security keys for corporate login. Now those same keys can sign blockchain transactions. No new infrastructure, no training, seamless integration.

Use Case 4: Social Recovery

Build wallets where users can recover access using their device biometrics plus a social recovery mechanism. Lost your phone? Use your new phone’s Face ID plus approval from trusted contacts. No seed phrase needed anywhere.

These aren’t theoretical — these are things you can build right now with what I’m about to show you.

Part 3: Setting Up Your Development Environment

Alright, let’s get our hands dirty. I’m assuming you have some basic development experience, but I’ll walk you through everything step by step.

What You’ll Need

Here’s my development stack for this tutorial:

- Node.js (v18 or higher)
- npm or yarn
- Hardhat (we'll install this)
- A code editor (I use VS Code)
- MetaMask or Core wallet
- A browser that supports WebAuthn (Chrome, Safari, Firefox, Edge - basically any modern browser)

Step 1: Project Setup

Let me walk you through setting up the project. Open your terminal and run:

mkdir avalanche-secp256r1-demo
cd avalanche-secp256r1-demo
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npx hardhat init

Choose “Create a TypeScript project” when prompted. Accept the defaults.

Step 2: Configure for Avalanche

Now, let’s configure Hardhat to work with Avalanche. Open hardhat.config.ts and update it:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "dotenv/config";

const config: HardhatUserConfig = {
  solidity: {
    version: "0.8.24",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  networks: {
    fuji: {
      url: "https://api.avax-test.network/ext/bc/C/rpc",
      chainId: 43113,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    },
    avalanche: {
      url: "https://api.avax.network/ext/bc/C/rpc",
      chainId: 43114,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : []
    }
  }
};
export default config;

Step 3: Get Test AVAX

You’ll need some test AVAX for deployment. Head to the Avalanche Fuji Faucet and grab some test tokens. It’s instant and free.

Pro tip: Create a .env file for your private key:

PRIVATE_KEY=your_private_key_here

And install dotenv:

npm install dotenv

Don’t forget to add .env to your .gitignore file!

echo ".env" >> .gitignore

Part 4: Your First secp256r1 Smart Contract

Now for the fun part — let’s write our first contract that can verify secp256r1 signatures!

Understanding the Precompile Interface

The secp256r1 precompile on Avalanche accepts:

  • A message hash (32 bytes)
  • The signature (r and s values, 32 bytes each)
  • The public key coordinates (x and y, 32 bytes each)

And returns: true if the signature is valid, false otherwise.

The Basic Verification Contract

Create a new file contracts/PasskeyVerifier.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title PasskeyVerifier
 * @author [Your Name]
 * @notice A contract that verifies secp256r1 signatures using Avalanche's native precompile
 * @dev This enables WebAuthn/Passkey integration for seedless wallet experiences
 */
contract PasskeyVerifier {

    // The secp256r1 precompile address on Avalanche
    address constant SECP256R1_PRECOMPILE = 0x0000000000000000000000000000000000000100;

    // Events for tracking verification attempts
    event SignatureVerified(address indexed user, bytes32 indexed messageHash, bool success);

    /**
     * @notice Verifies a secp256r1 signature
     * @param messageHash The hash of the message that was signed
     * @param r The r component of the signature
     * @param s The s component of the signature
     * @param pubKeyX The x coordinate of the public key
     * @param pubKeyY The y coordinate of the public key
     * @return bool True if signature is valid, false otherwise
     */
    function verifySignature(
        bytes32 messageHash,
        bytes32 r,
        bytes32 s,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) public returns (bool) {

        // Prepare the input for the precompile
        // Format: messageHash || r || s || pubKeyX || pubKeyY
        bytes memory input = abi.encodePacked(
            messageHash,
            r,
            s,
            pubKeyX,
            pubKeyY
        );

        // Call the precompile
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);

        // Check if the call was successful and the signature is valid
        bool isValid = success && result.length > 0 && abi.decode(result, (bool));

        emit SignatureVerified(msg.sender, messageHash, isValid);

        return isValid;
    }

    /**
     * @notice View function to check signature without emitting events
     * @dev Useful for off-chain verification checks
     */
    function checkSignature(
        bytes32 messageHash,
        bytes32 r,
        bytes32 s,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) public view returns (bool) {
        bytes memory input = abi.encodePacked(
            messageHash,
            r,
            s,
            pubKeyX,
            pubKeyY
        );

        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.staticcall(input);

        return success && result.length > 0 && abi.decode(result, (bool));
    }
}

Let me explain what’s happening here:

Line 13: We define the precompile address as a constant. This is your gateway to secp256r1 verification.

Lines 21–27: Our main function takes the five pieces of data needed for verification. In the real world, these would come from a passkey signature.

Lines 30–36: We pack all the data together in the exact format the precompile expects. Order matters here!

Line 39: This is where the magic happens — we call the precompile with our data.

Line 42: We check that the call succeeded and decode the result.

I also included a checkSignature view function (lines 52-71) that you can use for read-only checks without paying gas or emitting events.

Testing Our Contract

Before deploying, let’s write a test. Create test/PasskeyVerifier.test.ts:

import { expect } from "chai";
import { ethers } from "hardhat";
import { PasskeyVerifier } from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";

describe("PasskeyVerifier", function () {
  let verifier: PasskeyVerifier;
  beforeEach(async function () {
    const PasskeyVerifier = await ethers.getContractFactory("PasskeyVerifier");
    verifier = await PasskeyVerifier.deploy();
    await verifier.waitForDeployment();
  });
  it("Should deploy successfully", async function () {
    expect(await verifier.getAddress()).to.be.properAddress;
  });
  it("Should have correct precompile address", async function () {
    // We can't directly access the constant, but we can verify the contract works
    const address = await verifier.getAddress();
    expect(address).to.not.equal(ethers.ZeroAddress);
  });
  // Note: Real signature testing requires actual secp256r1 signatures
  // which we'll generate from the frontend with WebAuthn
  it("Should have verifySignature function", async function () {
    const messageHash = ethers.keccak256(ethers.toUtf8Bytes("test"));
    const r = ethers.randomBytes(32);
    const s = ethers.randomBytes(32);
    const pubKeyX = ethers.randomBytes(32);
    const pubKeyY = ethers.randomBytes(32);
    // This will likely return false as we're using random values
    // but it should not revert
    await expect(
      verifier.verifySignature(messageHash, r, s, pubKeyX, pubKeyY)
    ).to.not.be.reverted;
  });
});

Run the test:

npx hardhat test

You should see all tests passing! Now let’s deploy.

Deploying Your Contract

Create scripts/deploy.ts:

import { ethers } from "hardhat";

async function main() {
  console.log("Deploying PasskeyVerifier to Avalanche Fuji...");

  const [deployer] = await ethers.getSigners();
  console.log("Deploying with account:", deployer.address);

  const balance = await ethers.provider.getBalance(deployer.address);
  console.log("Account balance:", ethers.formatEther(balance), "AVAX");

  const PasskeyVerifier = await ethers.getContractFactory("PasskeyVerifier");
  console.log("Deploying contract...");

  const verifier = await PasskeyVerifier.deploy();
  await verifier.waitForDeployment();

  const address = await verifier.getAddress();
  console.log("PasskeyVerifier deployed to:", address);
  console.log("\nSave this address - you'll need it for the frontend!");
  console.log("\nView on Explorer:");
  console.log(`https://testnet.snowtrace.io/address/${address}`);
}
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Deploy to Fuji testnet:

npx hardhat compile
npx hardhat run scripts/deploy.ts --network fuji

Boom! You’ve just deployed your first secp256r1-enabled contract. Save that contract address — we’re going to use it in the next section.

Part 5: Building a Passkey-Enabled Frontend

Now let’s build something that actually uses passkeys. We’re going to create a web interface that lets users:

  1. Create a passkey using their device biometrics
  2. Sign a message with that passkey
  3. Verify the signature on-chain

This is the foundation of a seedless wallet.

Frontend Setup

In your project root, let’s set up a simple frontend:

mkdir frontend
cd frontend
npm init -y
npm install vite @vitejs/plugin-react react react-dom ethers@6
npm install --save-dev @types/react @types/react-dom typescript
npm install cbor-x

Create frontend/vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    https: true // WebAuthn requires HTTPS (or localhost)
  }
});

Create frontend/tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

Create frontend/tsconfig.node.json:

{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}

Create frontend/index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Avalanche Passkey Demo</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Create frontend/src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);

The Complete Passkey Application

Now, create frontend/src/App.tsx. This is the heart of our application:

import { useState } from 'react';
import { ethers } from 'ethers';
import { decode } from 'cbor-x';

// Your deployed contract address from earlier
const CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS_HERE"; // Replace with your deployed address
const FUJI_CHAIN_ID = 43113;
// Contract ABI
const CONTRACT_ABI = [
  "function verifySignature(bytes32 messageHash, bytes32 r, bytes32 s, bytes32 pubKeyX, bytes32 pubKeyY) public returns (bool)",
  "event SignatureVerified(address indexed user, bytes32 indexed messageHash, bool success)"
];
interface PublicKey {
  x: string;
  y: string;
}
function App() {
  const [status, setStatus] = useState<string>("");
  const [publicKey, setPublicKey] = useState<PublicKey | null>(null);
  const [credentialId, setCredentialId] = useState<ArrayBuffer | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  /**
   * Check if WebAuthn is supported
   */
  const isWebAuthnSupported = (): boolean => {
    return window?.PublicKeyCredential !== undefined &&
           navigator?.credentials?.create !== undefined;
  };
  /**
   * Step 1: Create a new passkey
   * This uses the WebAuthn API to create a credential
   */
  const createPasskey = async () => {
    if (!isWebAuthnSupported()) {
      setStatus("WebAuthn is not supported on this device/browser");
      return;
    }
    try {
      setIsLoading(true);
      setStatus("Creating passkey... Please authenticate with your device");

      // Generate a random challenge
      const challenge = new Uint8Array(32);
      crypto.getRandomValues(challenge);

      // Create the passkey with secp256r1
      const credential = await navigator.credentials.create({
        publicKey: {
          challenge: challenge,
          rp: {
            name: "Avalanche Passkey Demo",
            id: window.location.hostname
          },
          user: {
            id: crypto.getRandomValues(new Uint8Array(16)),
            name: `user-${Date.now()}@avalanche.demo`,
            displayName: "Avalanche Demo User"
          },
          pubKeyCredParams: [
            {
              type: "public-key",
              alg: -7 // ES256 (secp256r1)
            }
          ],
          authenticatorSelection: {
            authenticatorAttachment: "platform", // Use platform authenticator (Face ID, Touch ID, Windows Hello)
            userVerification: "required",
            residentKey: "preferred"
          },
          timeout: 60000,
          attestation: "direct"
        }
      }) as PublicKeyCredential;

      if (!credential) {
        throw new Error("Failed to create credential");
      }

      setCredentialId(credential.rawId);

      // Extract the public key from the credential
      const response = credential.response as AuthenticatorAttestationResponse;
      const publicKey = await extractPublicKey(response);

      setPublicKey(publicKey);
      setStatus(`Passkey created successfully!\n\nPublic Key:\nX: ${publicKey.x.slice(0, 20)}...\nY: ${publicKey.y.slice(0, 20)}...`);

    } catch (error: any) {
      console.error("Create passkey error:", error);
      setStatus(`Error: ${error.message}`);
    } finally {
      setIsLoading(false);
    }
  };

  /**
   * Extract the public key coordinates from the attestation response
   */
  const extractPublicKey = async (response: AuthenticatorAttestationResponse): Promise<PublicKey> => {
    try {
      // Decode the attestation object (CBOR format)
      const attestationObject = decode(new Uint8Array(response.attestationObject));

      // Get the authenticator data
      const authData = attestationObject.authData;

      // The credential public key starts at byte 55 in authData
      // First 32 bytes: rpIdHash
      // Next 4 bytes: flags + signCount
      // Next 16 bytes: AAGUID
      // Next 2 bytes: credentialIdLength
      // After that: credentialId
      // Then: COSE-encoded public key

      const rpIdHashLength = 32;
      const flagsAndCounterLength = 5;
      const aaguidLength = 16;
      const credIdLengthBytes = 2;

      let offset = rpIdHashLength + flagsAndCounterLength + aaguidLength;

      // Get credential ID length
      const credIdLength = (authData[offset] << 8) + authData[offset + 1];
      offset += credIdLengthBytes + credIdLength;

      // The rest is the COSE key
      const coseKeyBytes = authData.slice(offset);
      const coseKey = decode(coseKeyBytes);

      // COSE key format for EC2 (secp256r1):
      // -1: curve (P-256)
      // -2: x coordinate
      // -3: y coordinate

      const xCoord = coseKey.get(-2);
      const yCoord = coseKey.get(-3);

      if (!xCoord || !yCoord) {
        throw new Error("Failed to extract public key coordinates");
      }

      // Convert to hex strings with proper padding
      const x = '0x' + Buffer.from(xCoord).toString('hex').padStart(64, '0');
      const y = '0x' + Buffer.from(yCoord).toString('hex').padStart(64, '0');

      return { x, y };

    } catch (error) {
      console.error("Extract public key error:", error);
      throw new Error("Failed to extract public key from credential");
    }
  };

  /**
   * Step 2: Sign a message with the passkey and verify on-chain
   */
  const signAndVerify = async () => {
    if (!credentialId || !publicKey) {
      setStatus("Please create a passkey first!");
      return;
    }

    try {
      setIsLoading(true);
      setStatus("Preparing message to sign...");

      // The message we want to sign
      const message = "Hello Avalanche! This is a passkey signature.";
      const messageBytes = ethers.toUtf8Bytes(message);
      const messageHash = ethers.keccak256(messageBytes);

      setStatus("Please authenticate with your passkey...");

      // Create the challenge from our message hash
      const challenge = ethers.getBytes(messageHash);

      // Get an assertion (signature) from the passkey
      const assertion = await navigator.credentials.get({
        publicKey: {
          challenge: challenge,
          rpId: window.location.hostname,
          allowCredentials: [{
            id: credentialId,
            type: "public-key",
            transports: ["internal"]
          }],
          userVerification: "required",
          timeout: 60000
        }
      }) as PublicKeyCredential;

      if (!assertion) {
        throw new Error("Failed to get assertion");
      }

      setStatus("Parsing signature...");

      // Extract the signature
      const response = assertion.response as AuthenticatorAssertionResponse;
      const signature = new Uint8Array(response.signature);

      // The signature is in DER format, we need to extract r and s
      const { r, s } = parseDERSignature(signature);

      console.log("Message:", message);
      console.log("Message Hash:", messageHash);
      console.log("Signature r:", r);
      console.log("Signature s:", s);
      console.log("Public Key X:", publicKey.x);
      console.log("Public Key Y:", publicKey.y);

      setStatus("Verifying signature on Avalanche...");

      // Verify on-chain
      await verifyOnChain(messageHash, r, s);

    } catch (error: any) {
      console.error("Sign and verify error:", error);
      setStatus(`Error: ${error.message}`);
    } finally {
      setIsLoading(false);
    }
  };

  /**
   * Parse DER-encoded signature to extract r and s values
   */
  const parseDERSignature = (signature: Uint8Array): { r: string; s: string } => {
    try {
      // DER format: 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s]
      let offset = 0;

      // Check for SEQUENCE tag
      if (signature[offset++] !== 0x30) {
        throw new Error("Invalid DER signature: missing SEQUENCE tag");
      }

      // Skip total length
      offset++;

      // Check for INTEGER tag (r)
      if (signature[offset++] !== 0x02) {
        throw new Error("Invalid DER signature: missing r INTEGER tag");
      }

      // Get r length and value
      const rLength = signature[offset++];
      let r = signature.slice(offset, offset + rLength);
      offset += rLength;

      // Check for INTEGER tag (s)
      if (signature[offset++] !== 0x02) {
        throw new Error("Invalid DER signature: missing s INTEGER tag");
      }

      // Get s length and value
      const sLength = signature[offset++];
      let s = signature.slice(offset, offset + sLength);

      // Remove leading zeros if present (DER adds them for positive numbers)
      while (r.length > 32 && r[0] === 0) {
        r = r.slice(1);
      }
      while (s.length > 32 && s[0] === 0) {
        s = s.slice(1);
      }

      // Pad to 32 bytes if needed
      const rPadded = new Uint8Array(32);
      const sPadded = new Uint8Array(32);
      rPadded.set(r, 32 - r.length);
      sPadded.set(s, 32 - s.length);

      return {
        r: ethers.hexlify(rPadded),
        s: ethers.hexlify(sPadded)
      };
    } catch (error) {
      console.error("Parse DER signature error:", error);
      throw new Error("Failed to parse DER signature");
    }
  };

  /**
   * Step 3: Verify the signature on-chain
   */
  const verifyOnChain = async (messageHash: string, r: string, s: string) => {
    if (!publicKey) {
      throw new Error("No public key available");
    }

    try {
      // Check if MetaMask is installed
      if (!window.ethereum) {
        throw new Error("Please install MetaMask or Core Wallet");
      }

      // Request account access
      await window.ethereum.request({ method: 'eth_requestAccounts' });

      const provider = new ethers.BrowserProvider(window.ethereum);
      const network = await provider.getNetwork();

      // Check if we're on Fuji
      if (Number(network.chainId) !== FUJI_CHAIN_ID) {
        setStatus("Switching to Avalanche Fuji testnet...");

        try {
          await window.ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [{ chainId: '0x' + FUJI_CHAIN_ID.toString(16) }],
          });
        } catch (switchError: any) {
          // This error code indicates that the chain has not been added to MetaMask
          if (switchError.code === 4902) {
            await window.ethereum.request({
              method: 'wallet_addEthereumChain',
              params: [{
                chainId: '0x' + FUJI_CHAIN_ID.toString(16),
                chainName: 'Avalanche Fuji Testnet',
                nativeCurrency: {
                  name: 'AVAX',
                  symbol: 'AVAX',
                  decimals: 18
                },
                rpcUrls: ['https://api.avax-test.network/ext/bc/C/rpc'],
                blockExplorerUrls: ['https://testnet.snowtrace.io/']
              }]
            });
          } else {
            throw switchError;
          }
        }
      }

      const signer = await provider.getSigner();

      // Connect to contract
      const contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);

      setStatus("Sending transaction to blockchain...");

      // Call the verification function
      const tx = await contract.verifySignature(
        messageHash,
        r,
        s,
        publicKey.x,
        publicKey.y
      );

      setStatus(`Transaction sent! Hash: ${tx.hash}\n\nWaiting for confirmation...`);

      const receipt = await tx.wait();

      // Parse the event to check if verification succeeded
      const event = receipt.logs.find((log: any) => {
        try {
          const parsedLog = contract.interface.parseLog(log);
          return parsedLog?.name === 'SignatureVerified';
        } catch {
          return false;
        }
      });

      if (event) {
        const parsedEvent = contract.interface.parseLog(event);
        const success = parsedEvent?.args.success;

        if (success) {
          setStatus(`SUCCESS! Signature verified on-chain!\n\nTransaction: ${receipt.hash}\nView on Explorer: https://testnet.snowtrace.io/tx/${receipt.hash}`);
        } else {
          setStatus(`Signature verification failed on-chain.\n\nTransaction: ${receipt.hash}`);
        }
      } else {
        setStatus(`Transaction confirmed!\n\nHash: ${receipt.hash}\nView on Explorer: https://testnet.snowtrace.io/tx/${receipt.hash}`);
      }

    } catch (error: any) {
      console.error("On-chain verification error:", error);
      throw new Error(`On-chain verification failed: ${error.message}`);
    }
  };
  return (
    <div style={{
      minHeight: '100vh',
      background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
      padding: '40px 20px',
      fontFamily: 'system-ui, -apple-system, sans-serif'
    }}>
      <div style={{
        maxWidth: '700px',
        margin: '0 auto',
        backgroundColor: 'white',
        borderRadius: '20px',
        padding: '40px',
        boxShadow: '0 20px 60px rgba(0,0,0,0.3)'
      }}>
        <div style={{ textAlign: 'center', marginBottom: '30px' }}>
          <h1 style={{
            fontSize: '36px',
            margin: '0 0 10px 0',
            background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
            WebkitBackgroundClip: 'text',
            WebkitTextFillColor: 'transparent',
            backgroundClip: 'text'
          }}>
            Avalanche Passkey Demo
          </h1>
          <p style={{ color: '#666', fontSize: '16px', margin: 0 }}>
            Experience seedless wallet authentication with secp256r1
          </p>
        </div>
        {!isWebAuthnSupported() && (
          <div style={{
            padding: '15px',
            backgroundColor: '#fee',
            border: '1px solid #fcc',
            borderRadius: '8px',
            marginBottom: '20px',
            color: '#c00'
          }}>
            WebAuthn is not supported on this browser/device. Please use a modern browser with biometric authentication support.
          </div>
        )}
        <div style={{
          display: 'grid',
          gridTemplateColumns: publicKey ? '1fr 1fr' : '1fr',
          gap: '15px',
          marginBottom: '30px'
        }}>
          <button 
            onClick={createPasskey}
            disabled={isLoading || !isWebAuthnSupported()}
            style={{
              padding: '16px 24px',
              fontSize: '16px',
              fontWeight: '600',
              backgroundColor: isLoading || !isWebAuthnSupported() ? '#ccc' : '#E84142',
              color: 'white',
              border: 'none',
              borderRadius: '12px',
              cursor: isLoading || !isWebAuthnSupported() ? 'not-allowed' : 'pointer',
              transition: 'all 0.3s ease',
              boxShadow: '0 4px 15px rgba(232, 65, 66, 0.3)',
              opacity: isLoading || !isWebAuthnSupported() ? 0.6 : 1
            }}
            onMouseOver={(e) => {
              if (!isLoading && isWebAuthnSupported()) {
                e.currentTarget.style.transform = 'translateY(-2px)';
                e.currentTarget.style.boxShadow = '0 6px 20px rgba(232, 65, 66, 0.4)';
              }
            }}
            onMouseOut={(e) => {
              e.currentTarget.style.transform = 'translateY(0)';
              e.currentTarget.style.boxShadow = '0 4px 15px rgba(232, 65, 66, 0.3)';
            }}
          >
            {publicKey ? 'Passkey Created' : 'Create Passkey'}
          </button>

          {publicKey && (
            <button 
              onClick={signAndVerify}
              disabled={isLoading}
              style={{
                padding: '16px 24px',
                fontSize: '16px',
                fontWeight: '600',
                backgroundColor: isLoading ? '#ccc' : '#10B981',
                color: 'white',
                border: 'none',
                borderRadius: '12px',
                cursor: isLoading ? 'not-allowed' : 'pointer',
                transition: 'all 0.3s ease',
                boxShadow: '0 4px 15px rgba(16, 185, 129, 0.3)',
                opacity: isLoading ? 0.6 : 1
              }}
              onMouseOver={(e) => {
                if (!isLoading) {
                  e.currentTarget.style.transform = 'translateY(-2px)';
                  e.currentTarget.style.boxShadow = '0 6px 20px rgba(16, 185, 129, 0.4)';
                }
              }}
              onMouseOut={(e) => {
                e.currentTarget.style.transform = 'translateY(0)';
                e.currentTarget.style.boxShadow = '0 4px 15px rgba(16, 185, 129, 0.3)';
              }}
            >
              Sign & Verify On-Chain
            </button>
          )}
        </div>
        {status && (
          <div style={{
            padding: '20px',
            backgroundColor: status.includes('Error') ? '#fee' : status.includes('SUCCESS') ? '#efe' : '#f5f5f5',
            border: `2px solid ${status.includes('Error') ? '#fcc' : status.includes('SUCCESS') ? '#cfc' : '#ddd'}`,
            borderRadius: '12px',
            marginBottom: '20px',
            borderLeft: `6px solid ${status.includes('Error') ? '#E84142' : status.includes('SUCCESS') ? '#10B981' : '#667eea'}`,
            whiteSpace: 'pre-wrap',
            wordBreak: 'break-word'
          }}>
            <div style={{ 
              fontSize: '14px', 
              lineHeight: '1.6',
              color: '#333'
            }}>
              {status}
            </div>
          </div>
        )}

        {publicKey && (
          <div style={{
            padding: '20px',
            backgroundColor: '#f8f9fa',
            borderRadius: '12px',
            border: '1px solid #e0e0e0'
          }}>
            <h3 style={{ 
              margin: '0 0 15px 0', 
              fontSize: '18px',
              color: '#333'
            }}>
              Your Public Key
            </h3>
            <div style={{ 
              fontSize: '12px',
              fontFamily: 'monospace',
              wordBreak: 'break-all',
              lineHeight: '1.6',
              color: '#555'
            }}>
              <div style={{ marginBottom: '10px' }}>
                <strong>X:</strong> <span style={{ color: '#667eea' }}>{publicKey.x}</span>
              </div>
              <div>
                <strong>Y:</strong> <span style={{ color: '#764ba2' }}>{publicKey.y}</span>
              </div>
            </div>
          </div>
        )}
        <div style={{
          marginTop: '30px',
          padding: '20px',
          backgroundColor: '#fff9e6',
          borderRadius: '12px',
          border: '1px solid #ffe066'
        }}>
          <h4 style={{ margin: '0 0 10px 0', color: '#d97706', fontSize: '16px' }}>
            How it works
          </h4>
          <ol style={{ margin: 0, paddingLeft: '20px', fontSize: '14px', color: '#666', lineHeight: '1.8' }}>
            <li>Click "Create Passkey" and authenticate with Face ID, Touch ID, or your device's biometric</li>
            <li>Your device generates a secp256r1 key pair (private key never leaves your device)</li>
            <li>Click "Sign & Verify" to sign a message and verify it on Avalanche blockchain</li>
            <li>The smart contract uses Avalanche's native secp256r1 precompile to verify the signature</li>
          </ol>
        </div>
        <div style={{
          marginTop: '20px',
          padding: '15px',
          backgroundColor: '#e0f2fe',
          borderRadius: '12px',
          fontSize: '13px',
          color: '#0369a1',
          textAlign: 'center'
        }}>
          Connected to Avalanche Fuji Testnet | Contract: {CONTRACT_ADDRESS.slice(0, 6)}...{CONTRACT_ADDRESS.slice(-4)}
        </div>
      </div>
    </div>
  );
}
export default App;

Update frontend/package.json scripts:

{
  "name": "avalanche-passkey-frontend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "ethers": "^6.9.0",
    "cbor-x": "^1.5.4"
  },
  "devDependencies": {
    "@types/react": "^18.2.43",
    "@types/react-dom": "^18.2.17",
    "@vitejs/plugin-react": "^4.2.1",
    "typescript": "^5.2.2",
    "vite": "^5.0.8"
  }
}

Now run the frontend:

cd frontend
npm install
npm run dev

Visit http://localhost:3000 in your browser and try it out!

Important Note: For WebAuthn to work properly, you need HTTPS. On localhost, this is handled automatically by browsers, but for production deployment, you’ll need a proper SSL certificate.

Part 6: Building Advanced Wallet Contracts

Now that you understand the basics, let’s build production-ready wallet contracts that showcase the power of passkey authentication.

Pattern 1: Smart Contract Wallet with Passkey Authentication

Let’s build a smart contract wallet that can be controlled with passkeys. Create contracts/PasskeyWallet.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title PasskeyWallet
 * @notice A smart contract wallet that uses passkey (secp256r1) signatures for authentication
 * @dev This enables seedless, recoverable wallets with biometric authentication
 */
contract PasskeyWallet {

    address constant SECP256R1_PRECOMPILE = 0x0000000000000000000000000000000000000100;

    // Struct to store passkey public keys
    struct PasskeyCredential {
        bytes32 pubKeyX;
        bytes32 pubKeyY;
        bool isActive;
        uint256 addedAt;
    }

    // Wallet owner
    address public owner;

    // Mapping of credential ID hash to public key
    mapping(bytes32 => PasskeyCredential) public credentials;

    // Array of all credential IDs for enumeration
    bytes32[] public credentialIds;

    // Nonce for replay protection
    uint256 public nonce;

    // Events
    event CredentialAdded(bytes32 indexed credentialId, bytes32 pubKeyX, bytes32 pubKeyY);
    event CredentialRemoved(bytes32 indexed credentialId);
    event TransactionExecuted(address indexed to, uint256 value, bytes data, bool success);
    event Received(address indexed from, uint256 value);

    // Modifiers
    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    /**
     * @notice Constructor sets the initial owner
     */
    constructor() {
        owner = msg.sender;
    }

    /**
     * @notice Add a new passkey credential
     * @param credentialId The unique identifier for this credential
     * @param pubKeyX The x coordinate of the public key
     * @param pubKeyY The y coordinate of the public key
     */
    function addCredential(
        bytes32 credentialId,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) external onlyOwner {
        require(!credentials[credentialId].isActive, "Credential already exists");

        credentials[credentialId] = PasskeyCredential({
            pubKeyX: pubKeyX,
            pubKeyY: pubKeyY,
            isActive: true,
            addedAt: block.timestamp
        });

        credentialIds.push(credentialId);

        emit CredentialAdded(credentialId, pubKeyX, pubKeyY);
    }

    /**
     * @notice Remove a passkey credential
     * @param credentialId The credential to remove
     */
    function removeCredential(bytes32 credentialId) external onlyOwner {
        require(credentials[credentialId].isActive, "Credential not active");

        credentials[credentialId].isActive = false;

        emit CredentialRemoved(credentialId);
    }

    /**
     * @notice Execute a transaction using passkey authentication
     * @param to The destination address
     * @param value The amount of ETH to send
     * @param data The transaction data
     * @param credentialId The credential used to sign
     * @param r The r component of the signature
     * @param s The s component of the signature
     */
    function executeWithPasskey(
        address to,
        uint256 value,
        bytes calldata data,
        bytes32 credentialId,
        bytes32 r,
        bytes32 s
    ) external returns (bool) {
        // Get the credential
        PasskeyCredential memory cred = credentials[credentialId];
        require(cred.isActive, "Invalid or inactive credential");

        // Create the message hash
        bytes32 messageHash = keccak256(abi.encodePacked(
            address(this),
            to,
            value,
            data,
            nonce,
            block.chainid
        ));

        // Verify the signature
        bool isValid = verifySignature(messageHash, r, s, cred.pubKeyX, cred.pubKeyY);
        require(isValid, "Invalid signature");

        // Increment nonce for replay protection
        nonce++;

        // Execute the transaction
        (bool success, ) = to.call{value: value}(data);

        emit TransactionExecuted(to, value, data, success);

        return success;
    }

    /**
     * @notice Internal function to verify secp256r1 signatures
     */
    function verifySignature(
        bytes32 messageHash,
        bytes32 r,
        bytes32 s,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) internal returns (bool) {
        bytes memory input = abi.encodePacked(messageHash, r, s, pubKeyX, pubKeyY);
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        return success && result.length > 0 && abi.decode(result, (bool));
    }

    /**
     * @notice Get the number of credentials
     */
    function getCredentialCount() external view returns (uint256) {
        return credentialIds.length;
    }

    /**
     * @notice Check if a credential is active
     */
    function isCredentialActive(bytes32 credentialId) external view returns (bool) {
        return credentials[credentialId].isActive;
    }

    /**
     * @notice Receive ETH
     */
    receive() external payable {
        emit Received(msg.sender, msg.value);
    }

    /**
     * @notice Get wallet balance
     */
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}

This wallet contract allows you to:

  • Add multiple passkey credentials (use different devices)
  • Execute transactions with passkey signatures
  • Built-in replay protection with nonces
  • Remove compromised credentials

Pattern 2: Social Recovery Wallet

One of the most powerful use cases for passkeys is social recovery. Let me show you how to implement it:

Create contracts/SocialRecoveryWallet.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title SocialRecoveryWallet
 * @notice A wallet with passkey authentication and social recovery
 * @dev Lost your device? Guardians can help you recover access
 */
contract SocialRecoveryWallet {

    address constant SECP256R1_PRECOMPILE = 0x0000000000000000000000000000000000000100;

    struct PasskeyCredential {
        bytes32 pubKeyX;
        bytes32 pubKeyY;
        bool isActive;
    }

    struct Guardian {
        address guardianAddress;
        bool isActive;
    }

    struct RecoveryRequest {
        bytes32 newCredentialId;
        bytes32 newPubKeyX;
        bytes32 newPubKeyY;
        uint256 approvalCount;
        mapping(address => bool) approvals;
        uint256 requestedAt;
        bool executed;
    }

    address public owner;
    mapping(bytes32 => PasskeyCredential) public credentials;
    bytes32[] public credentialIds;

    mapping(address => Guardian) public guardians;
    address[] public guardianList;
    uint256 public requiredApprovals;
    uint256 public recoveryTimelock = 2 days;

    mapping(uint256 => RecoveryRequest) public recoveryRequests;
    uint256 public recoveryRequestCount;

    uint256 public nonce;

    event CredentialAdded(bytes32 indexed credentialId);
    event GuardianAdded(address indexed guardian);
    event GuardianRemoved(address indexed guardian);
    event RecoveryInitiated(uint256 indexed requestId, bytes32 newCredentialId);
    event RecoveryApproved(uint256 indexed requestId, address indexed guardian);
    event RecoveryExecuted(uint256 indexed requestId);
    event RecoveryCancelled(uint256 indexed requestId);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    modifier onlyGuardian() {
        require(guardians[msg.sender].isActive, "Not a guardian");
        _;
    }

    constructor(
        bytes32 initialCredentialId,
        bytes32 initialPubKeyX,
        bytes32 initialPubKeyY,
        address[] memory initialGuardians,
        uint256 _requiredApprovals
    ) {
        require(initialGuardians.length >= _requiredApprovals, "Not enough guardians");
        require(_requiredApprovals > 0, "Invalid required approvals");

        owner = msg.sender;

        // Add initial credential
        credentials[initialCredentialId] = PasskeyCredential({
            pubKeyX: initialPubKeyX,
            pubKeyY: initialPubKeyY,
            isActive: true
        });
        credentialIds.push(initialCredentialId);

        // Add guardians
        for (uint256 i = 0; i < initialGuardians.length; i++) {
            guardians[initialGuardians[i]] = Guardian({
                guardianAddress: initialGuardians[i],
                isActive: true
            });
            guardianList.push(initialGuardians[i]);
        }

        requiredApprovals = _requiredApprovals;
    }

    /**
     * @notice Add a new passkey credential
     */
    function addCredential(
        bytes32 credentialId,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) external onlyOwner {
        require(!credentials[credentialId].isActive, "Credential exists");

        credentials[credentialId] = PasskeyCredential({
            pubKeyX: pubKeyX,
            pubKeyY: pubKeyY,
            isActive: true
        });

        credentialIds.push(credentialId);
        emit CredentialAdded(credentialId);
    }

    /**
     * @notice Add a guardian
     */
    function addGuardian(address guardian) external onlyOwner {
        require(!guardians[guardian].isActive, "Already a guardian");
        require(guardian != address(0), "Invalid address");

        guardians[guardian] = Guardian({
            guardianAddress: guardian,
            isActive: true
        });

        guardianList.push(guardian);
        emit GuardianAdded(guardian);
    }

    /**
     * @notice Remove a guardian
     */
    function removeGuardian(address guardian) external onlyOwner {
        require(guardians[guardian].isActive, "Not a guardian");
        require(guardianList.length - 1 >= requiredApprovals, "Would leave too few guardians");

        guardians[guardian].isActive = false;
        emit GuardianRemoved(guardian);
    }

    /**
     * @notice Initiate recovery process
     * @dev Called by a guardian when the owner loses access
     */
    function initiateRecovery(
        bytes32 newCredentialId,
        bytes32 newPubKeyX,
        bytes32 newPubKeyY
    ) external onlyGuardian returns (uint256) {
        uint256 requestId = recoveryRequestCount++;

        RecoveryRequest storage request = recoveryRequests[requestId];
        request.newCredentialId = newCredentialId;
        request.newPubKeyX = newPubKeyX;
        request.newPubKeyY = newPubKeyY;
        request.approvalCount = 1;
        request.approvals[msg.sender] = true;
        request.requestedAt = block.timestamp;
        request.executed = false;

        emit RecoveryInitiated(requestId, newCredentialId);
        emit RecoveryApproved(requestId, msg.sender);

        return requestId;
    }

    /**
     * @notice Approve a recovery request
     */
    function approveRecovery(uint256 requestId) external onlyGuardian {
        RecoveryRequest storage request = recoveryRequests[requestId];

        require(!request.executed, "Already executed");
        require(!request.approvals[msg.sender], "Already approved");

        request.approvals[msg.sender] = true;
        request.approvalCount++;

        emit RecoveryApproved(requestId, msg.sender);
    }

    /**
     * @notice Execute recovery after timelock and sufficient approvals
     */
    function executeRecovery(uint256 requestId) external {
        RecoveryRequest storage request = recoveryRequests[requestId];

        require(!request.executed, "Already executed");
        require(request.approvalCount >= requiredApprovals, "Not enough approvals");
        require(
            block.timestamp >= request.requestedAt + recoveryTimelock,
            "Timelock not passed"
        );

        // Deactivate all old credentials
        for (uint256 i = 0; i < credentialIds.length; i++) {
            credentials[credentialIds[i]].isActive = false;
        }

        // Add the new credential
        credentials[request.newCredentialId] = PasskeyCredential({
            pubKeyX: request.newPubKeyX,
            pubKeyY: request.newPubKeyY,
            isActive: true
        });

        credentialIds.push(request.newCredentialId);
        request.executed = true;

        emit RecoveryExecuted(requestId);
    }

    /**
     * @notice Cancel a recovery request (owner can cancel if they regain access)
     */
    function cancelRecovery(
        uint256 requestId,
        bytes32 credentialId,
        bytes32 r,
        bytes32 s
    ) external {
        RecoveryRequest storage request = recoveryRequests[requestId];
        require(!request.executed, "Already executed");

        // Verify the signature with an active credential
        PasskeyCredential memory cred = credentials[credentialId];
        require(cred.isActive, "Invalid credential");

        bytes32 messageHash = keccak256(abi.encodePacked(
            "CANCEL_RECOVERY",
            address(this),
            requestId,
            nonce++
        ));

        bytes memory input = abi.encodePacked(messageHash, r, s, cred.pubKeyX, cred.pubKeyY);
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        bool isValid = success && result.length > 0 && abi.decode(result, (bool));

        require(isValid, "Invalid signature");

        request.executed = true; // Mark as executed to prevent execution
        emit RecoveryCancelled(requestId);
    }

    /**
     * @notice Execute transaction with passkey
     */
    function executeWithPasskey(
        address to,
        uint256 value,
        bytes calldata data,
        bytes32 credentialId,
        bytes32 r,
        bytes32 s
    ) external returns (bool) {
        PasskeyCredential memory cred = credentials[credentialId];
        require(cred.isActive, "Invalid credential");

        bytes32 messageHash = keccak256(abi.encodePacked(
            address(this),
            to,
            value,
            data,
            nonce,
            block.chainid
        ));

        bytes memory input = abi.encodePacked(messageHash, r, s, cred.pubKeyX, cred.pubKeyY);
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        bool isValid = success && result.length > 0 && abi.decode(result, (bool));

        require(isValid, "Invalid signature");

        nonce++;

        (bool txSuccess, ) = to.call{value: value}(data);
        return txSuccess;
    }

    /**
     * @notice Get active guardians count
     */
    function getActiveGuardianCount() external view returns (uint256) {
        uint256 count = 0;
        for (uint256 i = 0; i < guardianList.length; i++) {
            if (guardians[guardianList[i]].isActive) {
                count++;
            }
        }
        return count;
    }

    receive() external payable {}
}

This social recovery wallet provides:

  • Multiple passkey credentials (use different devices)
  • Guardian-based recovery when you lose your device
  • Timelock protection against malicious guardians
  • Owner can cancel recovery if they regain access

Part 7: Real-World Application Examples

Let me show you how to build practical applications that users will actually want to use.

Example 1: NFT Marketplace with Passkey Login

Create contracts/PasskeyNFTMarketplace.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IERC721 {
    function transferFrom(address from, address to, uint256 tokenId) external;
}
/**
 * @title PasskeyNFTMarketplace
 * @notice Buy NFTs with just your fingerprint - no seed phrase needed
 */
contract PasskeyNFTMarketplace {

    address private constant SECP256R1_PRECOMPILE = 0x0000000000000000000000000000000000000100;

    struct Listing {
        address seller;
        address nftContract;
        uint256 tokenId;
        uint256 price;
        bool active;
    }

    struct UserAccount {
        bytes32 pubKeyX;
        bytes32 pubKeyY;
        uint256 balance;
        uint256 nonce;
        bool exists;
    }

    mapping(bytes32 => UserAccount) public accounts; // credentialId => account
    mapping(uint256 => Listing) public listings;
    uint256 public listingCount;

    event AccountCreated(bytes32 indexed credentialId);
    event Deposited(bytes32 indexed credentialId, uint256 amount);
    event Listed(uint256 indexed listingId, address nftContract, uint256 tokenId, uint256 price);
    event Purchased(uint256 indexed listingId, bytes32 indexed buyer);

    /**
     * @notice Create a passkey account
     */
    function createAccount(
        bytes32 credentialId,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) external {
        require(!accounts[credentialId].exists, "Account exists");

        accounts[credentialId] = UserAccount({
            pubKeyX: pubKeyX,
            pubKeyY: pubKeyY,
            balance: 0,
            nonce: 0,
            exists: true
        });

        emit AccountCreated(credentialId);
    }

    /**
     * @notice Deposit funds to your passkey account
     */
    function deposit(bytes32 credentialId) external payable {
        require(accounts[credentialId].exists, "Account doesn't exist");
        accounts[credentialId].balance += msg.value;
        emit Deposited(credentialId, msg.value);
    }

    /**
     * @notice List an NFT for sale
     */
    function listNFT(
        address nftContract,
        uint256 tokenId,
        uint256 price
    ) external returns (uint256) {
        IERC721(nftContract).transferFrom(msg.sender, address(this), tokenId);

        uint256 listingId = listingCount++;
        listings[listingId] = Listing({
            seller: msg.sender,
            nftContract: nftContract,
            tokenId: tokenId,
            price: price,
            active: true
        });

        emit Listed(listingId, nftContract, tokenId, price);
        return listingId;
    }

    /**
     * @notice Buy an NFT with passkey signature
     * @dev No MetaMask popup needed - just Face ID!
     */
    function buyWithPasskey(
        uint256 listingId,
        bytes32 credentialId,
        bytes32 r,
        bytes32 s
    ) external {
        Listing storage listing = listings[listingId];
        require(listing.active, "Listing not active");

        UserAccount storage account = accounts[credentialId];
        require(account.exists, "Account doesn't exist");
        require(account.balance >= listing.price, "Insufficient balance");

        // Create message hash
        bytes32 messageHash = keccak256(abi.encodePacked(
            "BUY_NFT",
            address(this),
            listingId,
            account.nonce,
            block.chainid
        ));

        // Verify signature
        bytes memory input = abi.encodePacked(
            messageHash, r, s, account.pubKeyX, account.pubKeyY
        );
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        require(success && result.length > 0 && abi.decode(result, (bool)), "Invalid signature");

        // Update state
        account.nonce++;
        account.balance -= listing.price;
        listing.active = false;

        // Transfer NFT to buyer
        IERC721(listing.nftContract).transferFrom(
            address(this),
            msg.sender,
            listing.tokenId
        );

        // Pay seller
        (bool sent, ) = listing.seller.call{value: listing.price}("");
        require(sent, "Payment failed");

        emit Purchased(listingId, credentialId);
    }

    /**
     * @notice Withdraw funds with passkey signature
     */
    function withdrawWithPasskey(
        bytes32 credentialId,
        uint256 amount,
        address payable recipient,
        bytes32 r,
        bytes32 s
    ) external {
        UserAccount storage account = accounts[credentialId];
        require(account.exists, "Account doesn't exist");
        require(account.balance >= amount, "Insufficient balance");

        bytes32 messageHash = keccak256(abi.encodePacked(
            "WITHDRAW",
            address(this),
            amount,
            recipient,
            account.nonce,
            block.chainid
        ));

        bytes memory input = abi.encodePacked(
            messageHash, r, s, account.pubKeyX, account.pubKeyY
        );
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        require(success && result.length > 0 && abi.decode(result, (bool)), "Invalid signature");

        account.nonce++;
        account.balance -= amount;

        (bool sent, ) = recipient.call{value: amount}("");
        require(sent, "Withdrawal failed");
    }
}

Example 2: Gaming Inventory System

Create contracts/PasskeyGameInventory.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
 * @title PasskeyGameInventory
 * @notice Manage in-game items with just biometric authentication
 * @dev Perfect for onboarding Web2 gamers to Web3
 */
contract PasskeyGameInventory {

    address private constant SECP256R1_PRECOMPILE = 0x0000000000000000000000000000000000000100;

    struct Player {
        bytes32 pubKeyX;
        bytes32 pubKeyY;
        uint256 nonce;
        bool exists;
    }

    struct Item {
        uint256 itemId;
        string name;
        uint256 rarity; // 1-5
        uint256 power;
    }

    mapping(bytes32 => Player) public players; // credentialId => player
    mapping(bytes32 => mapping(uint256 => Item)) public inventory; // credentialId => itemId => item
    mapping(bytes32 => uint256[]) public playerItems; // credentialId => itemIds

    uint256 public nextItemId = 1;

    event PlayerRegistered(bytes32 indexed credentialId);
    event ItemMinted(bytes32 indexed credentialId, uint256 itemId, string name);
    event ItemTransferred(bytes32 indexed from, bytes32 indexed to, uint256 itemId);

    /**
     * @notice Register as a player - just use Face ID!
     */
    function register(
        bytes32 credentialId,
        bytes32 pubKeyX,
        bytes32 pubKeyY
    ) external {
        require(!players[credentialId].exists, "Already registered");

        players[credentialId] = Player({
            pubKeyX: pubKeyX,
            pubKeyY: pubKeyY,
            nonce: 0,
            exists: true
        });

        // Give starter items
        _mintItem(credentialId, "Wooden Sword", 1, 10);
        _mintItem(credentialId, "Leather Armor", 1, 5);

        emit PlayerRegistered(credentialId);
    }

    /**
     * @notice Internal mint function
     */
    function _mintItem(
        bytes32 credentialId,
        string memory name,
        uint256 rarity,
        uint256 power
    ) internal returns (uint256) {
        uint256 itemId = nextItemId++;

        inventory[credentialId][itemId] = Item({
            itemId: itemId,
            name: name,
            rarity: rarity,
            power: power
        });

        playerItems[credentialId].push(itemId);

        emit ItemMinted(credentialId, itemId, name);
        return itemId;
    }

    /**
     * @notice Transfer item to another player with passkey signature
     */
    function transferItem(
        bytes32 fromCredentialId,
        bytes32 toCredentialId,
        uint256 itemId,
        bytes32 r,
        bytes32 s
    ) external {
        Player storage fromPlayer = players[fromCredentialId];
        require(fromPlayer.exists, "Sender not registered");
        require(players[toCredentialId].exists, "Recipient not registered");
        require(inventory[fromCredentialId][itemId].itemId != 0, "Item doesn't exist");

        // Create message hash
        bytes32 messageHash = keccak256(abi.encodePacked(
            "TRANSFER_ITEM",
            address(this),
            toCredentialId,
            itemId,
            fromPlayer.nonce,
            block.chainid
        ));

        // Verify signature
        bytes memory input = abi.encodePacked(
            messageHash, r, s, fromPlayer.pubKeyX, fromPlayer.pubKeyY
        );
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        require(success && result.length > 0 && abi.decode(result, (bool)), "Invalid signature");

        // Update nonce
        fromPlayer.nonce++;

        // Transfer item
        Item memory item = inventory[fromCredentialId][itemId];
        delete inventory[fromCredentialId][itemId];
        inventory[toCredentialId][itemId] = item;

        // Update arrays
        _removeFromArray(playerItems[fromCredentialId], itemId);
        playerItems[toCredentialId].push(itemId);

        emit ItemTransferred(fromCredentialId, toCredentialId, itemId);
    }

    /**
     * @notice Craft a new item by consuming others
     */
    function craftItem(
        bytes32 credentialId,
        uint256[] calldata itemIds,
        string memory newItemName,
        bytes32 r,
        bytes32 s
    ) external returns (uint256) {
        Player storage player = players[credentialId];
        require(player.exists, "Not registered");
        require(itemIds.length >= 2, "Need at least 2 items to craft");

        bytes32 messageHash = keccak256(abi.encodePacked(
            "CRAFT_ITEM",
            address(this),
            itemIds,
            newItemName,
            player.nonce,
            block.chainid
        ));

        bytes memory input = abi.encodePacked(
            messageHash, r, s, player.pubKeyX, player.pubKeyY
        );
        (bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
        require(success && result.length > 0 && abi.decode(result, (bool)), "Invalid signature");

        player.nonce++;

        // Calculate new item stats
        uint256 totalPower = 0;
        uint256 maxRarity = 0;

        for (uint256 i = 0; i < itemIds.length; i++) {
            Item memory item = inventory[credentialId][itemIds[i]];
            require(item.itemId != 0, "Item doesn't exist");

            totalPower += item.power;
            if (item.rarity > maxRarity) {
                maxRarity = item.rarity;
            }

            // Burn the item
            delete inventory[credentialId][itemIds[i]];
            _removeFromArray(playerItems[credentialId], itemIds[i]);
        }

        // Create new item with boosted stats
        uint256 newRarity = maxRarity < 5 ? maxRarity + 1 : 5;
        uint256 newPower = totalPower + (totalPower * newRarity / 10);

        return _mintItem(credentialId, newItemName, newRarity, newPower);
    }

    /**
     * @notice Get all items for a player
     */
    function getPlayerItems(bytes32 credentialId) external view returns (Item[] memory) {
        uint256[] memory itemIds = playerItems[credentialId];
        Item[] memory items = new Item[](itemIds.length);

        for (uint256 i = 0; i < itemIds.length; i++) {
            items[i] = inventory[credentialId][itemIds[i]];
        }

        return items;
    }

    /**
     * @notice Helper to remove item from array
     */
    function _removeFromArray(uint256[] storage array, uint256 value) private {
        for (uint256 i = 0; i < array.length; i++) {
            if (array[i] == value) {
                array[i] = array[array.length - 1];
                array.pop();
                break;
            }
        }
    }
}

Part 8: Security Best Practices and Common Pitfalls

Building with secp256r1 and passkeys is powerful, but like any cryptographic system, security is paramount. Let me share the lessons I’ve learned (sometimes the hard way).

Critical Security Considerations

1. Always Validate Signature Components

Never trust that signature components (r, s) are well-formed. Always validate:

function isValidSignatureComponent(bytes32 component) internal pure returns (bool) {
    // secp256r1 curve order
    uint256 n = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551;
    uint256 componentValue = uint256(component);

    // r and s must be in range [1, n-1]
    return componentValue > 0 && componentValue < n;
}

2. Implement Replay Protection

Always include:

  • Nonce (prevents replay of old signatures)
  • Chain ID (prevents cross-chain replay)
  • Contract address (prevents cross-contract replay)
  • Expiration timestamp (optional but recommended)
function createMessageHash(
    string memory action,
    address contractAddr,
    uint256 nonce,
    uint256 chainId,
    uint256 expiration
) internal pure returns (bytes32) {
    return keccak256(abi.encodePacked(
        action,
        contractAddr,
        nonce,
        chainId,
        expiration,
        block.timestamp < expiration
    ));
}

3. Handle Malleability

secp256r1 signatures can be malleable. Always check that s is in the lower half of the curve order:

function isLowS(bytes32 s) internal pure returns (bool) {
    uint256 HALF_N = 0x7FFFFFFF800000007FFFFFFFFFFFFFFFDE737D56D38BCF4279DCE5617E3192A8;
    return uint256(s) <= HALF_N;
}

4. Secure Key Storage on Frontend

The private key never leaves the user’s device, but you still need to protect the public key and credential ID:

// Store in IndexedDB, not localStorage
const storeCredential = async (credentialId: ArrayBuffer, publicKey: PublicKey) => {
    const db = await openDB('passkey-store', 1, {
        upgrade(db) {
            db.createObjectStore('credentials');
        }
    });

    await db.put('credentials', {
        id: bufferToBase64(credentialId),
        publicKey,
        createdAt: Date.now()
    }, 'main');
};

5. WebAuthn Authenticator Data Validation

Always validate the authenticator data flags:

function validateAuthenticatorData(authData: ArrayBuffer): boolean {
    const flags = new Uint8Array(authData)[32];

    // Bit 0: User Present (UP)
    // Bit 2: User Verified (UV)
    const userPresent = (flags & 0x01) !== 0;
    const userVerified = (flags & 0x04) !== 0;

    return userPresent && userVerified;
}

Common Pitfalls and How to Avoid Them

Pitfall 1: Incorrect Message Hashing

Wrong:

// DON'T hash the message again in the contract
bytes32 messageHash = keccak256(abi.encodePacked(message));

Right:

// The frontend should send the hash that was actually signed
// WebAuthn signs: SHA256(authData || clientDataHash)
// where clientDataHash = SHA256(clientDataJSON)
bytes32 messageHash = _hashFromFrontend;

Pitfall 2: Not Checking Precompile Success

Wrong:

(bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
return abi.decode(result, (bool)); // DANGEROUS!

Right:

(bool success, bytes memory result) = SECP256R1_PRECOMPILE.call(input);
require(success, "Precompile call failed");
require(result.length > 0, "Empty result");
require(abi.decode(result, (bool)), "Invalid signature");

Pitfall 3: Credential ID Collisions

Wrong:

mapping(bytes32 => PublicKey) public keys;
// Using a simple hash of credential ID

Right:

// Use the full credential ID or a proper hash with domain separation
mapping(bytes32 => PublicKey) public keys;

function registerCredential(bytes calldata credentialId, ...) external {
    bytes32 credKey = keccak256(abi.encodePacked(
        "CREDENTIAL",
        address(this),
        credentialId
    ));
    // ...
}

Pitfall 4: Gas Limit Issues

The precompile call is efficient, but still costs gas. Always set appropriate gas limits:

// Estimate: ~100k gas for signature verification
function verifyWithGasCheck(
    bytes32 messageHash,
    bytes32 r,
    bytes32 s,
    bytes32 pubKeyX,
    bytes32 pubKeyY
) external returns (bool) {
    require(gasleft() >= 120000, "Insufficient gas");
    // ... verification logic
}

Testing Checklist

Before deploying to production, test these scenarios:

  • [ ] Valid signature verification (happy path)
  • [ ] Invalid signature rejection
  • [ ] Replay attack prevention
  • [ ] Cross-chain replay prevention
  • [ ] Expired signature handling
  • [ ] Malformed signature components
  • [ ] Gas consumption under various conditions
  • [ ] Multiple credentials per user
  • [ ] Credential rotation
  • [ ] Recovery scenarios

Monitoring and Incident Response

Set up monitoring for:

event SecurityAlert(
    string alertType,
    address indexed user,
    bytes32 indexed credentialId,
    uint256 timestamp
);

function _checkSecurityConditions() internal {
    // Monitor for suspicious patterns
    if (nonce jumped by more than expected) {
        emit SecurityAlert("SUSPICIOUS_NONCE", msg.sender, credId, block.timestamp);
    }

    if (too many failed verifications) {
        emit SecurityAlert("BRUTE_FORCE_ATTEMPT", msg.sender, credId, block.timestamp);
    }
}

Part 9: Production Deployment Guide

You’ve built something amazing. Now let’s deploy it safely to production.

Pre-Deployment Checklist

Smart Contract Readiness

  • [ ] All contracts audited by reputable firm
  • [ ] Comprehensive test coverage (>90%)
  • [ ] Gas optimization complete
  • [ ] Emergency pause mechanism implemented
  • [ ] Upgrade strategy defined (if using proxies)
  • [ ] Events properly emitted for monitoring
  • [ ] Documentation complete
  • [ ] Deployment scripts tested on Fuji

Frontend Readiness

  • [ ] Tested on all major browsers (Chrome, Safari, Firefox, Edge)
  • [ ] Mobile testing complete (iOS Safari, Android Chrome)
  • [ ] Error handling comprehensive
  • [ ] User feedback clear and helpful
  • [ ] Loading states implemented
  • [ ] HTTPS configured properly
  • [ ] Analytics and monitoring set up
  • [ ] Backup authentication method available

Security Verification

  • [ ] Security best practices followed
  • [ ] No hardcoded credentials
  • [ ] Environment variables properly managed
  • [ ] Rate limiting implemented
  • [ ] Input validation complete
  • [ ] Fallback mechanisms tested
  • [ ] Incident response plan documented
  • [ ] Bug bounty program considered

Deployment Script

Create scripts/deploy-production.ts:

import { ethers } from "hardhat";
import * as fs from "fs";

async function main() {
  console.log("Production Deployment to Avalanche Mainnet");
  console.log("============================================");

  // Verify we're on mainnet
  const network = await ethers.provider.getNetwork();
  if (network.chainId !== 43114n) {
    throw new Error("CRITICAL: Must deploy to Avalanche Mainnet only! Current chain ID: " + network.chainId);
  }

  const [deployer] = await ethers.getSigners();
  console.log("\nDeployer Address:", deployer.address);

  const balance = await ethers.provider.getBalance(deployer.address);
  console.log("Balance:", ethers.formatEther(balance), "AVAX");

  // Require minimum balance
  if (balance < ethers.parseEther("1")) {
    throw new Error("CRITICAL: Insufficient balance! Need at least 1 AVAX");
  }

  // Confirm deployment
  console.log("\nPRODUCTION DEPLOYMENT - FINAL CONFIRMATION");
  console.log("Press Ctrl+C to cancel, or wait 10 seconds to continue...");
  await new Promise(resolve => setTimeout(resolve, 10000));

  console.log("\nDeploying PasskeyVerifier...");
  const PasskeyVerifier = await ethers.getContractFactory("PasskeyVerifier");
  const verifier = await PasskeyVerifier.deploy();
  await verifier.waitForDeployment();

  const verifierAddress = await verifier.getAddress();
  console.log("PasskeyVerifier deployed:", verifierAddress);

  // Wait for more confirmations on mainnet
  console.log("\nWaiting for 5 confirmations...");
  await verifier.deploymentTransaction()?.wait(5);
  console.log("Confirmed");

  console.log("\nDeploying PasskeyWallet...");
  const PasskeyWallet = await ethers.getContractFactory("PasskeyWallet");
  const wallet = await PasskeyWallet.deploy();
  await wallet.waitForDeployment();

  const walletAddress = await wallet.getAddress();
  console.log("PasskeyWallet deployed:", walletAddress);

  await wallet.deploymentTransaction()?.wait(5);
  console.log("Confirmed");

  // Save deployment information
  const deploymentInfo = {
    network: "avalanche-mainnet",
    chainId: 43114,
    deployer: deployer.address,
    timestamp: new Date().toISOString(),
    contracts: {
      PasskeyVerifier: {
        address: verifierAddress,
        txHash: verifier.deploymentTransaction()?.hash
      },
      PasskeyWallet: {
        address: walletAddress,
        txHash: wallet.deploymentTransaction()?.hash
      }
    }
  };

  const filename = `deployment-mainnet-${Date.now()}.json`;
  fs.writeFileSync(filename, JSON.stringify(deploymentInfo, null, 2));

  console.log("\nDeployment Complete!");
  console.log("================================");
  console.log("\nContract Addresses:");
  console.log("PasskeyVerifier:", verifierAddress);
  console.log("PasskeyWallet:", walletAddress);
  console.log("\nExplorer Links:");
  console.log("PasskeyVerifier:", `https://snowtrace.io/address/${verifierAddress}`);
  console.log("PasskeyWallet:", `https://snowtrace.io/address/${walletAddress}`);
  console.log("\nDeployment info saved to:", filename);
  console.log("\nNext Steps:");
  console.log("1. Verify contracts on Snowtrace");
  console.log("2. Update frontend with contract addresses");
  console.log("3. Run integration tests");
  console.log("4. Monitor transactions");
}
main().catch((error) => {
  console.error("\nDEPLOYMENT FAILED:");
  console.error(error);
  process.exit(1);
});

Deployment Process

  1. Final Testing on Fuji
# Run full test suite
npx hardhat test

# Deploy to Fuji one last time
npx hardhat run scripts/deploy.ts --network fuji
# Run integration tests
npm run test:integration
  1. Deploy to Mainnet
# Ensure you have the correct private key in .env
# NEVER commit .env to version control!

# Deploy
npx hardhat run scripts/deploy-production.ts --network avalanche
# Verify on Snowtrace
npx hardhat verify --network avalanche CONTRACT_ADDRESS
  1. Post-Deployment
# Update frontend environment variables
echo "VITE_PASSKEY_VERIFIER_ADDRESS=0x..." > frontend/.env.production
echo "VITE_PASSKEY_WALLET_ADDRESS=0x..." >> frontend/.env.production

# Build frontend
cd frontend
npm run build
# Deploy frontend to your hosting provider
# (Vercel, Netlify, AWS, etc.)

Monitoring Your Deployment

Set up monitoring with tools like:

  • The Graph: Index your contract events
  • Tenderly: Monitor transactions and set up alerts
  • Defender: Automated security monitoring
  • Snowtrace API: Track contract interactions

Example monitoring script:

import { ethers } from "ethers";

const VERIFIER_ADDRESS = "YOUR_CONTRACT_ADDRESS";
const RPC_URL = "https://api.avax.network/ext/bc/C/rpc";
async function monitorContract() {
    const provider = new ethers.JsonRpcProvider(RPC_URL);
    const verifier = new ethers.Contract(
        VERIFIER_ADDRESS,
        ["event SignatureVerified(address indexed user, bytes32 indexed messageHash, bool success)"],
        provider
    );

    verifier.on("SignatureVerified", (user, messageHash, success, event) => {
        console.log(`Verification: ${success ? 'SUCCESS' : 'FAILED'}`);
        console.log(`User: ${user}`);
        console.log(`Tx: ${event.log.transactionHash}`);

        // Send to your monitoring system
        if (!success) {
            alertSecurityTeam(`Failed verification from ${user}`);
        }
    });

    console.log("Monitoring started...");
}
monitorContract();

Part 10: Conclusion — What You’ve Accomplished

You’ve just completed a comprehensive journey into building with secp256r1 on Avalanche. Let’s recap what you now have in your developer toolkit:

Skills You’ve Mastered

Foundation Knowledge

  • Deep understanding of secp256r1 vs secp256k1
  • WebAuthn and passkey authentication flows
  • Avalanche’s precompile architecture
  • Cryptographic signature verification

Smart Contract Development

  • Basic signature verification contracts
  • Production-ready wallet contracts
  • Multi-signature authentication systems
  • Social recovery implementations
  • Gaming inventory systems
  • Marketplace contracts with passkey auth

Frontend Integration

  • WebAuthn API implementation
  • Biometric authentication flows
  • Signature generation and parsing
  • DER signature format handling
  • Web3 integration with ethers.js
  • Error handling and user experience

Security Expertise

  • Replay attack prevention
  • Signature malleability handling
  • Gas optimization techniques
  • Input validation strategies
  • Monitoring and incident response

Production Deployment

  • Testing strategies and coverage
  • Deployment automation
  • Network configuration
  • Post-deployment monitoring
  • Maintenance best practices

The Impact of Your Work

What you’ve built represents more than just code — it’s infrastructure for the next billion blockchain users.

For Users:

  • No more seed phrases to manage
  • Familiar biometric authentication
  • Reduced anxiety about wallet security
  • Seamless onboarding experience
  • Recovery without centralized custodians

For Businesses:

  • Lower user acquisition costs
  • Reduced support burden
  • Higher conversion rates
  • Enterprise-compatible security
  • Regulatory compliance friendly

For the Ecosystem:

  • Bridges Web2 and Web3
  • Reduces barrier to entry
  • Enables new use cases
  • Attracts mainstream developers
  • Advances adoption metrics

Real-World Applications You Can Build Now

Consumer Applications

  • Seedless mobile wallets
  • Social media with crypto payments
  • E-commerce with blockchain loyalty
  • Gaming with true asset ownership
  • Digital identity systems

Enterprise Solutions

  • Supply chain tracking
  • Document verification
  • Asset tokenization platforms
  • Internal blockchain tools
  • Partner collaboration systems

DeFi Platforms

  • User-friendly DEX interfaces
  • Lending with biometric auth
  • Staking dashboards
  • Portfolio management tools
  • Cross-chain bridges

Next Steps for Your Journey

Immediate Actions

  1. Deploy your contracts to Fuji testnet
  2. Share your demo with potential users
  3. Gather feedback on user experience
  4. Iterate based on real usage
  5. Plan your mainnet deployment

Short-Term Goals

  1. Add advanced features (multi-device, recovery)
  2. Integrate with existing dApps
  3. Build your specific use case
  4. Optimize gas costs further
  5. Expand test coverage

Long-Term Vision

  1. Scale to production users
  2. Build a community around your dApp
  3. Contribute to the ecosystem
  4. Share your learnings
  5. Help others adopt secp256r1

Resources for Continued Learning

Official Documentation

Community

  • Avalanche Discord: Join for support and discussions
  • Avalanche Forum: Share your projects
  • GitHub: Contribute to open-source projects
  • Twitter: Follow Avalanche developers

Advanced Topics

  • Account abstraction (ERC-4337)
  • Multi-chain deployments
  • Zero-knowledge proofs integration
  • Quantum-resistant considerations

A Personal Note

When I started exploring secp256r1 on Avalanche, I was skeptical. Could it really make that much difference? After building these applications and watching users interact with them, I’m convinced this is transformative.

The moment a non-crypto user realizes they just made a blockchain transaction using only their fingerprint — no wallet installation, no seed phrase, no visible blockchain complexity — that’s when you see the future of this technology.

We’re not just building better wallets. We’re building the infrastructure that brings blockchain to everyone.

The Path Forward

Every user who doesn’t have to manage a seed phrase is a victory for adoption. Every developer who builds with secp256r1 is pushing the ecosystem forward. Every transaction authenticated with a fingerprint brings us closer to mainstream blockchain integration.

The tools are ready. The network is live. The users are waiting.

Now go build something that changes how people interact with blockchain. Test thoroughly, deploy confidently, and iterate based on user feedback.

Staying Connected

The secp256r1 ecosystem on Avalanche is growing rapidly. New patterns, tools, and best practices emerge regularly. Stay connected with the community, share your experiences, and help others on their journey.

Remember: every project starts with a single deployment. Make it count.

Acknowledgments

This technology builds on the work of countless developers, researchers, and community members who believed in making blockchain accessible to everyone. Thank you to:

  • The Avalanche team for implementing secp256r1 support
  • The WebAuthn working group for establishing standards
  • The open-source community for tools and libraries
  • Early adopters who provide feedback and improvements
  • Every developer pushing blockchain adoption forward

Further Reading

Technical Deep Dives

  • “Elliptic Curve Cryptography Explained”
  • “WebAuthn Specification (W3C)”
  • “Avalanche Precompiles Technical Reference”
  • “Smart Contract Security Best Practices”

Use Case Studies

  • “Building Seedless Wallets: Lessons Learned”
  • “Enterprise Blockchain with Passkeys”
  • “Gaming on Blockchain Without Seed Phrases”
  • “DeFi for Non-Crypto Users”

All this coming soon check them out soon.


메타데이터
post_id
075f1cb71ea6
slug
building-with-secp256r1-on-avalanche-your-complete-guide-to-passkey-enabled-smart-contracts-075f1cb71ea6
url
https://medium.com/@Joseph_Mwangi/building-with-secp256r1-on-avalanche-your-complete-guide-to-passkey-enabled-smart-contracts-075f1cb71ea6
canonical_url
https://medium.com/@Joseph_Mwangi/building-with-secp256r1-on-avalanche-your-complete-guide-to-passkey-enabled-smart-contracts-075f1cb71ea6
author_url
https://medium.com/@Joseph_Mwangi
status
ok
fetched_at
2026-07-07 04:41:59