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:
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.
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
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.
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
import { nodePolyfills } from "vite-plugin-node-polyfills";
export default {
plugins: [nodePolyfills()],
};
Create a Bitcoin wallet
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.
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>
// ...
);
}