Skip to content

swenthebuilder/portkey-client

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Passkey PRF embedded crypto wallet 🔑🚪

Create passkey-secured, self-custodial crypto wallets & sign transactions straight from the browser.

npm version bundle size license

Portkey brings the power of FIDO2 / WebAuthn passkeys to web3. Portkey uses Passkeys PRF extension to encrypt and decrypt keys for signing. Think Turnkey, but without a third-party HSM: the private key never leaves the user’s device and is never decryptable by your app, browser extensions, or Portkey itself.


✨ Highlights

Feature Description
🔒 Isolated signing environment A cross-origin, CSP-locked iframe (“Vault”) handles all key material & cryptography.
🪪 Passkey PRF encryption Private keys are encrypted with the WebAuthn PRF extension; only the user’s passkey can decrypt.
🏡 Self-hosted Vault Deploy the Vault on your own sub-domain → no vendor lock-in, phishing-resistant, CSP-hardened.
🧩 React-first API <BackgroundIframeProvider />, ready-made <PortkeyButton />, and usePortkeyWatcher() hook.
🌐 All chains supported Ethereum (raw & EIP-712), Solana, Hyperliquid, and easy extensibility.
⚡️ Session re-use Optional allowSessionSigning flag caches the derived AES key in a closure for ~5 min → multiple signatures without additional passkey prompts.
🕶 Zero snooping Vault lives in a different origin + sandboxed + document.domain locked. You can’t read it—even in devtools.

🏗 Architecture

sequenceDiagram
    participant UI as React App
    participant PK as Portkey React SDK
    participant VAULT as Vault Iframe (cross-origin)
    participant AUTH as Platform Authenticator (passkey)

    UI->>PK: <PortkeyButton command="sign">
    PK->>VAULT: postMessage({ command: "sign" })
    VAULT--)UI: "ready" ✅
    UI->>AUTH: navigator.credentials.get({ prf })
    AUTH-->>VAULT: PRF(salt) // shared secret
    VAULT->>VAULT: AES-GCM decrypt private key
    VAULT->>VAULT: Sign tx / typed data
    VAULT->>PK: postMessage({ signedTx })
    PK-->>UI: resolve onSigned callback
Loading

Cross-origin isolation prevents the parent from touching VAULT.document.

PRF extension = deterministic, credential-scoped HKDF; no user secrets cross domain.

Optional session key (in-memory, AES-encrypted) reduces UX friction.

❤️ Acknowledgements

  • ethers.js & @solana/web3.js for crypto
  • Design inspired by Turnkey

“The best password is no password” → the best wallet is no seed phrase.
Welcome to passkey-powered web3 with Portkey.


🚀 Quick Start

We have NEXT and Client side demos in /examples.

1. Install

npm i portkey-client        # or pnpm add / yarn add

2. Wrap your app

import React from "react";
import { BackgroundIframeProvider } from "portkey-client";

export default function App() {
  return (
    <BackgroundIframeProvider initialSrc="https://vault.yourapp.xyz">
      <YourRoutesAndPages />
    </BackgroundIframeProvider>
  );
}

3. Create a wallet (signup)

import { PortkeyButton } from "portkey-client";

export function Signup() {
  return (
    <PortkeyButton
      label="Create Wallet"
      command="signup"
      buttonType="signup"
      origin="https://vault.yourapp.xyz"
      className="my-4"
    />
  );
}

PortkeyButton automatically:

Moves and shows the Vault iframe on top of itself.

Sends the signup command.

Waits for the Vault to return { wallet, passkey }.

4. Listen for results

import { usePortkeyWatcher } from "portkey-client";

export function GlobalPortkeyEvents() {
  usePortkeyWatcher((msg) => {
    if (msg.command === "signup" && msg.result) {
      console.log("✅ New wallet:", msg.result.wallet);
      // ➡️ Persist `wallet.cipherText / iv / salt` server-side
    }
  }, "https://vault.yourapp.xyz");

  return null; // invisible listener
}

5. Sign a transaction

  1. Sign a transaction (with )
import { PortkeyButton } from "portkey-client";

export function SignTxButton({ tx, pubkey, vault }) {
  return (
    <PortkeyButton
      label="Sign Transaction"
      command="sign"
      buttonType="sign"
      origin="https://vault.yourapp.xyz"
      data={{
        transaction: {
          pubkey,
          vault,
          data: tx,
        },
      }}
      className="my-4"
    />
  );
}

This will:

Render a button that activates the Vault iframe.

Send the sign command along with the transaction data.

Wait for the Vault to return a signed transaction.


🛠 API Reference

<BackgroundIframeProvider initialSrc?>
Prop Type Default Description
initialSrc string "about:blank" The Vault URL
children ReactNode Your app content

Exposes context:

{
  iframeRef: RefObject<HTMLIFrameElement>;
  setIframeSrc(src: string): void;
  moveIframeTo(el: HTMLElement | null): void;
}
<PortkeyButton /> (UI helper)
Prop Type Required Description
label string ✔︎ Button text
buttonType "signup" | "signEthTx" | "signSolTx" ✔︎ What to do on click
command string ✔︎ Command sent to iframe
origin string ✔︎ Vault origin (e.g. https://vault.foo.xyz)
data Record<string, any> Optional payload
hide boolean Render invisible, iframe stays alive
createWallet(options)
createWallet({
  iframe,
  vaultOrigin,
  jwt,
  pubkey,
  onResult(wallet),
  onError(error),
});

Creates both an Ethereum & Solana wallet encrypted with the passkey PRF.

signEthereumTransaction(options)

Same signature as above, plus transactionBase64.
Returns { signedTx }.

signSolanaTransaction(options)

Same signature as above, plus transactionBase64.
Returns { signedTx } (Base64 of a VersionedTransaction).

usePortkeyWatcher(handler, allowedOrigin)
usePortkeyWatcher((msg) => {
  // msg.command === "signup", "signedEthereumTransaction", etc.
}, "https://vault.foo.xyz");

Typed guard for window.postMessage events.


⚙️ Advanced

Keep-alive session key

{ allowSessionSigning: true } // default: false
  • Derives AES key once
  • Re-encrypts it in memory
  • Auto-expires after 5 min of inactivity

Custom UI (no preset button)

const { iframeRef, setIframeSrc, moveIframeTo } = useBackgroundIframe();

function MyBeautifulCTA() {
  const ref = React.useRef<HTMLButtonElement>(null);

  const onClick = () => {
    if (!ref.current) return;
    setIframeSrc("https://vault.foo.xyz?chain=eth");
    moveIframeTo(ref.current);

    iframeRef.current?.contentWindow?.postMessage(
      { command: "signEthereumTransaction", data: myTx },
      "https://vault.foo.xyz"
    );
  };

  return <button ref={ref} onClick={onClick}>Pay 0.01 ETH</button>;
}

🛡 Security Model

  • Origin Isolation: Vault is served from vault.yourapp.xyz, while your app runs on app.yourapp.xyz.
  • CSP hardened: Scripts only allowed from trusted CDNs.
  • Access-blocked iframe: document.domain and top access disabled.
  • PRF-based key derivation: No brute-force vector; passkey is required.
  • No remote logging: All logs stay inside the Vault iframe.

See vault/index.html for CSP, COOP/COEP, and nonce usage.


🏗 Deploying the Vault

cd packages/vault
pnpm build
rsync -a dist/ user@server:/var/www/vault

DNS → vault.yourapp.xyz, HTTPS required.

Add these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Content-Security-Policy: default-src 'self';

📄 License

MIT © Boole Digital – Use at your own risk. Experimental software; audit pending.


About

A native crypto wallet that uses passkey PRF to create and manage user accounts—no extensions, no seed phrases, no third-party custodians. Just your passkey, your browser, your wallet.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • JavaScript 80.2%
  • HTML 13.6%
  • TypeScript 6.2%