Learn how to build on the decentralized web
MPC Wallet
React (Web) Guide

Tutorial for a wallet using MPC

The goal of this tutorial is to create a simple web app that demonstrate how to use the pier MPC library:

  • ⛏️ using a simple create web app using vite
  • 🚧 using fixed username and password for authentication
  • 📝 user can sign transactions
  • ⛓️ user can interact with ETH and BTC

Full repository on GitHub (opens in a new tab) Demo deployed on Vercel (opens in a new tab)

Initialize a new app

npm create vite@latest MyMPCApp -- --template react-ts
cd MyMPCApp
npm install
npm run dev

Add dependencies

Add dependencies for the pier MPC library

npm i @pier-wallet/mpc-lib vite-plugin-node-polyfills

Exclude pier wasm from the vite dev server:

// vite.config.ts
import { nodePolyfills } from "vite-plugin-node-polyfills";
 
export default defineConfig({
  // ...
  plugins: [react(), nodePolyfills()],
  optimizeDeps: {
    exclude: ["@pier-wallet/mpc-ecdsa-wasm"],
  },
});

Generate a key share

Create a simple page with a button to sign in (MPC is guarded with an authentication wall) and a button to generate a key share:

App.tsx
import { KeyShare, SessionKind, PierMpcVaultSdk } from "@pier-wallet/mpc-lib";
import { createPierMpcSdkWasm } from "@pier-wallet/mpc-lib/wasm";
import { useEffect, useState } from "react";
 
const pierMpc = new PierMpcVaultSdk(createPierMpcSdkWasm());
export default function App() {
  const [isLoading, setIsLoading] = useState(false);
  const [keyShare, setKeyShare] = useState<KeyShare | null>(null);
 
  const generateKeyShare = async () => {
    // we'll add this later
  };
 
  return (
    <div>
      <button
        disabled={isLoading}
        onClick={async () => {
          setIsLoading(true);
          try {
            await pierMpc.auth.signInWithPassword({
              email: "mpc-lib-test@example.com",
              password: "123456",
            });
          } finally {
            setIsLoading(false);
          }
 
          console.log("signed in as test user");
        }}
      >
        Sign in as Test User
      </button>
      <button onClick={generateKeyShare} disabled={isLoading}>
        Generate Key Share
      </button>
      <div>PublicKey: {keyShare?.publicKey.join(",")}</div>
    </div>
  );
}

Now, let's add the key share generation logic. This involves complex math and will take around 4 seconds.

App.tsx
export default function App() {
  // ...
  const [keyShare, setKeyShare] = useState<KeyShare | null>(null);
  const generateKeyShare = async () => {
    setIsLoading(true);
    try {
      console.log("generating local key share...");
      const localKeyShare = await pierMpc.generateKeyShare();
 
      console.log("local key share generated.", localKeyShare.publicKey);
      setKeyShare(localKeyShare);
    } catch (e) {
      console.error(e);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    // ...
    <button onClick={generateKeyShare} disabled={isLoading}>
      Generate Key Share
    </button>
    // ...
  );
}

Ethereum wallet

Install ethers to work with Ethereum - ethereum wallet is based on ethers v5 and implements ethers.Signer API.

npm i ethers@^5.7.2

Create a wallet

App.tsx
import { PierMpcEthereumWallet } from "@pier-wallet/mpc-lib/ethers-v5";
import { ethers } from "ethers";
 
// REMARK: Use should use your own ethers provider - this is just for demo purposes
const ethereumProvider = new ethers.providers.JsonRpcProvider(
  "https://ethereum-sepolia.publicnode.com",
);
 
export default function App() {
  // ...
 
  const [ethWallet, setEthWallet] = useState<PierMpcEthereumWallet | null>(
    null,
  );
  useEffect(() => {
    if (!keyShare) return;
    (async () => {
      const signConnection = await pierMpc.establishConnection(
        SessionKind.SIGN,
      );
      const ethWallet = new PierMpcEthereumWallet(
        keyShare,
        signConnection,
        pierMpc,
        ethereumProvider,
      );
      setEthWallet(ethWallet);
    })();
  }, [keyShare]);
 
  return (
    // ...
    <div>ETH Address: {ethWallet?.address}</div>
    // ...
  );
}

Send Ethereum Transaction

Important: you need to fund the address that is shown on the screen on the ethereum sepolia blockchain. You can use https://sepoliafaucet.com/ (opens in a new tab) to do this

Important: you might expect failing transactions if you don't pick a stable RPC provider like alchemy

Important: This transaction signing will take around 4 seconds.

App.tsx
export default function App() {
  //  ...
  const sendEthereumTransaction = async () => {
    if (!ethWallet) return;
 
    setIsLoading(true);
    try {
      // send 1/10 of the balance to a zero address
      const receiver = ethers.constants.AddressZero;
      const balance = await ethWallet.getBalance();
      const amountToSend = balance.div(10);
 
      // sign the transaction locally & send it to the network once we have the full signature
      const tx = await ethWallet.sendTransaction({
        to: receiver,
        value: amountToSend,
      });
      console.log("tx", tx.hash);
    } catch (e) {
      console.error(e);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    // ...
    <button onClick={sendEthereumTransaction} disabled={isLoading}>
      Send Ethereum
    </button>
    // ...
  );
}

Bitcoin wallet

Polyfills

⚠️

PierMpcBitcoinWallet is based on bitcore-lib (opens in a new tab) which is a node.js library, so we need to polyfill some node.js globals.

If you don't want to use bitcore-lib, you can sign raw Bitcoin sighashes using pierMpc.sign.

npm i vite-plugin-node-polyfills -D
vite.config.ts
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default {
  plugins: [nodePolyfills()],
};

Create a Bitcoin wallet

App.tsx
import {
  PierMpcBitcoinWallet,
  PierMpcBitcoinWalletNetwork,
} from "@pier-wallet/mpc-lib/bitcoin";
// ...
 
export default function App() {
  // ...
  const [btcWallet, setBtcWallet] = useState<PierMpcBitcoinWallet | null>(null);
  useEffect(() => {
    if (!keyShare) return;
    (async () => {
      const signConnection = await pierMpc.establishConnection(
        SessionKind.SIGN,
      );
      const btcWallet = new PierMpcBitcoinWallet(
        keyShare,
        PierMpcBitcoinWalletNetwork.Testnet,
        signConnection,
        pierMpc,
      );
      setBtcWallet(btcWallet);
    })();
  }, [keyShare]);
 
  return (
    // ...
    <div>BTC Address: {btcWallet?.address}</div>
    // ...
  );
}

Send Bitcoin Transaction

Important: you need to fund the address that is shown on the screen on the bitcoin test blockchain. You can use https://bitcoinfaucet.uo1.net/send.php (opens in a new tab) to do this

Transaction signing involves complex math and will take around 4 seconds.

App.tsx
export default function App() {
  // ...
 
  const sendBitcoinTransaction = async () => {
    if (!btcWallet) return;
 
    setIsLoading(true);
    try {
      const receiver = "tb1qw2c3lxufxqe2x9s4rdzh65tpf4d7fssjgh8nv6"; // testnet faucet
      const amountToSend = 800n; // 0.00000800 BTC = 800 satoshi
      const feePerByte = 1n; // use a fee provider to get a more accurate fee estimate - otherwise check minimum fee manually
 
      // create a transaction request
      const txRequest = await btcWallet.populateTransaction({
        to: receiver,
        value: amountToSend,
        feePerByte,
      });
 
      // sign the transaction locally & send it to the network once we have the full signature
      const tx = await btcWallet.sendTransaction(txRequest);
      console.log("tx", tx.hash);
    } catch (e) {
      console.error(e);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    // ...
    <button onClick={sendBitcoinTransaction} disabled={isLoading}>
      Send Bitcoin
    </button>
    // ...
  );
}