Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable metamask snap #2

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const BtcBridgeForm = ({
if (e.code === 4001) {
toast.error('User rejected the request');
} else {
toast.error('Something went wrong. Please try again later.');
toast.error(e.message);
}
}, []);

Expand Down
138 changes: 94 additions & 44 deletions packages/sats-wagmi/src/connectors/mm-snap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import { BIP32Factory } from 'bip32';
import * as bitcoin from 'bitcoinjs-lib';
import { Psbt } from 'bitcoinjs-lib';
import bs58check from 'bs58check';
import { base64, hex } from '@scure/base';
/* @ts-ignore */

import { BitcoinScriptType, WalletNetwork } from '../types';
import { SnapError } from '../utils';

import { SatsConnector } from './base';
import { PsbtInputAccounts, SatsConnector } from './base';

const getLibNetwork = (network: Network): WalletNetwork => {
switch (network) {
case 'main':
return 'mainnet';
case 'test':
return 'testnet';
}
};

const getSnapNetwork = (network: WalletNetwork): Network => {
switch (network) {
Expand Down Expand Up @@ -46,8 +56,14 @@ function addressFromExtPubKey(xyzpub: string, network: bitcoin.Network) {
return bitcoin.payments.p2wpkh({ pubkey, network }).address;
}

const DEFAULT_BIP32_PATH = "m/84'/1'/0'/0/0";
// const hardcodedScriptType = BitcoinScriptType.P2WPKH;
const getDefaultBip32Path = (scriptType: BitcoinScriptType, network: Network): string => {
switch (scriptType) {
case BitcoinScriptType.P2WPKH:
return `m/84'/${network === 'main' ? '0' : '1'}'/0'/0/0`;
}
};

const DEFAULT_SCRIPT_TYPE = BitcoinScriptType.P2WPKH;

interface ExtendedPublicKey {
xpub: string;
Expand Down Expand Up @@ -88,25 +104,29 @@ class MMSnapConnector extends SatsConnector {
const result: any = await ethereum.request({
method: 'wallet_requestSnaps',
params: {
[snapId]: {}
[snapId]: {
version: '2.2.1'
}
}
});

const hasError = !!result?.snaps?.[snapId]?.error;

if (hasError) {
throw new Error('Failed Connect');
}
// eslint-disable-next-line no-console
console.log('Using snap version:', result?.[snapId]?.version);
} finally {
// Switch in case current network is testnet
if (this.snapNetwork === 'test') {
await this.updateNetworkInSnap();
const mappedNetwork = getLibNetwork(this.snapNetwork);

if (mappedNetwork !== this.network) {
const expectedNetwork = getSnapNetwork(this.network);

// Switch in case current network is wrong
await this.updateNetworkInSnap(expectedNetwork);
}

this.extendedPublicKey = await this.getExtendedPublicKey();
this.publicKey = await this.getPublicKey();
// Set the address to P2WPKH
this.address = addressFromExtPubKey(this.extendedPublicKey.xpub, await this.getNetwork());
this.paymentAddress = this.address;
}
}

Expand All @@ -132,7 +152,7 @@ class MMSnapConnector extends SatsConnector {
method: 'btc_getPublicExtendedKey',
params: {
network: this.snapNetwork,
scriptType: BitcoinScriptType.P2WPKH
scriptType: DEFAULT_SCRIPT_TYPE
}
}
}
Expand All @@ -155,7 +175,7 @@ class MMSnapConnector extends SatsConnector {
// convert to xpub/tpub before getting pubkey
const forcedXpub = anyPubToXpub(this.extendedPublicKey.xpub, await this.getNetwork());

// child is m/84'/1'/0'/0/0 (same as DEFAULT_BIP32_PATH)
// child is m/84'/0'/0'/0/0
const pubkey = bip32.fromBase58(forcedXpub, network).derive(0).derive(0).publicKey;

return pubkey.toString('hex');
Expand Down Expand Up @@ -333,9 +353,9 @@ class MMSnapConnector extends SatsConnector {
params: {
psbt: psbt.toBase64(),
network: this.snapNetwork,
scriptType: BitcoinScriptType.P2WPKH,
scriptType: DEFAULT_SCRIPT_TYPE,
inputIndex,
path: DEFAULT_BIP32_PATH
path: getDefaultBip32Path(DEFAULT_SCRIPT_TYPE, this.snapNetwork)
}
}
}
Expand Down Expand Up @@ -370,35 +390,65 @@ class MMSnapConnector extends SatsConnector {
throw error;
}
}
// FIXME: Refactor using btc-signer
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async signPsbt(): Promise<string> {
throw new Error('Not implemented');
// const scriptType = BitcoinScriptType;
// try {
//
// return (await ethereum.request({
// method: 'wallet_invokeSnap',
// params: {
// snapId,
// request: {
// method: 'btc_signPsbt',
// params: {
// psbt: psbt,
// network: this.snapNetwork,
// scriptType
// }
// }
// }
// })) as Promise<{ txId: string; txHex: string }>;
// } catch (err: any) {
// const error = new SnapError(err?.message || 'Sign PSBT failed');

// throw error;
// }

async signPsbt(psbtHex: string, _psbtInputAccounts: PsbtInputAccounts[]): Promise<string> {
const psbt = bitcoin.Psbt.fromHex(psbtHex);
const masterFingerprint = Buffer.from((await this.getMasterFingerprint()) as any, 'hex');
const publicKey = Buffer.from(this.publicKey!, 'hex');
const bip32Path = getDefaultBip32Path(DEFAULT_SCRIPT_TYPE, this.snapNetwork);

psbt.data.inputs.forEach(
(psbtInput) =>
(psbtInput.bip32Derivation = [
{
masterFingerprint: masterFingerprint,
path: bip32Path,
pubkey: publicKey
}
])
);

// add this so the current validation works, in a future version of the snap
// we will change it to accept op_return without specifying bip32Derivation
psbt.data.outputs.forEach(
(psbtOutput) =>
(psbtOutput.bip32Derivation = [
{
masterFingerprint: masterFingerprint,
path: getDefaultBip32Path(DEFAULT_SCRIPT_TYPE, this.snapNetwork),
pubkey: publicKey
}
])
);

try {
const psbtBase64 = (await ethereum.request({
method: 'wallet_invokeSnap',
params: {
snapId,
request: {
method: 'btc_signPsbt',
params: {
psbt: psbt.toBase64(),
network: this.snapNetwork,
scriptType: DEFAULT_SCRIPT_TYPE,
opts: {
autoFinalize: false
}
}
}
}
})) as string;

return hex.encode(base64.decode(psbtBase64));
} catch (err: any) {
const error = new SnapError(err?.message || 'Could not sign psbt');

throw error;
}
}

async updateNetworkInSnap() {
async updateNetworkInSnap(expectedNetwork: Network) {
try {
return await ethereum.request({
method: 'wallet_invokeSnap',
Expand All @@ -408,7 +458,7 @@ class MMSnapConnector extends SatsConnector {
method: 'btc_network',
params: {
action: 'set',
network: this.snapNetwork
network: expectedNetwork
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/sats-wagmi/src/connectors/unisat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const getUnisatNetwork = (network: WalletNetwork): Network => {
switch (network) {
default:
case 'mainnet':
return 'testnet';
return 'livenet';
case 'testnet':
return 'testnet';
}
Expand Down Expand Up @@ -103,12 +103,12 @@ class UnisatConnector extends SatsConnector {
await window.unisat.switchNetwork(expectedNetwork);
}

const [accounts, publickKey] = await Promise.all([window.unisat.requestAccounts(), window.unisat.getPublicKey()]);
const [accounts, publicKey] = await Promise.all([window.unisat.requestAccounts(), window.unisat.getPublicKey()]);

this.address = accounts[0];
this.paymentAddress = accounts[0];
this.ordinalsAddress = accounts[0];
this.publicKey = publickKey;
this.publicKey = publicKey;

window.unisat.on('accountsChanged', this.changeAccount);
}
Expand Down
7 changes: 3 additions & 4 deletions packages/sats-wagmi/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BitcoinNetwork } from '@gobob/types';
import { FC, ReactNode, createContext, useCallback, useContext, useEffect, useState } from 'react';
import { useLocalStorage } from '@uidotdev/usehooks';

import { LeatherConnector, UnisatConnector, XverseConnector } from './connectors';
import { LeatherConnector, MMSnapConnector, UnisatConnector, XverseConnector } from './connectors';
import { SatsConnector } from './connectors/base';
import { LocalStorageKeys } from './constants';

Expand Down Expand Up @@ -54,10 +54,9 @@ const SatsWagmiConfig: FC<SatsWagmiConfigProps> = ({ children, network = 'mainne

readyConnectors.push(unisat);

// TODO: to be enabled when metamask snap is tested on mainnet
// const mmSnap = new MMSnapConnector(network);
const mmSnap = new MMSnapConnector(network);

// readyConnectors.push(mmSnap);
readyConnectors.push(mmSnap);

const leather = new LeatherConnector(network);

Expand Down
7 changes: 5 additions & 2 deletions packages/utils/src/utxo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export async function createTransferWithOpReturn(
electrsClient.getFeeEstimate(CONFIRMATION_TARGET)
]);

// eslint-disable-next-line no-console
console.log('Fee rate:', feeRate);

if (confirmedUtxos.length === 0) {
throw new Error('No confirmed UTXOs');
}
Expand Down Expand Up @@ -226,14 +229,14 @@ export function getInputFromUtxoAndTransaction(
amount: output.amount!
}
};
const witnessMixin = transaction.hasWitnesses ? witnessUtxo : nonWitnessUtxo;
const witnessMixin = transaction.hasWitnesses ? { ...witnessUtxo, ...nonWitnessUtxo } : nonWitnessUtxo;

// Construct inputs based on the script type
const input = {
txid: utxo.txid,
index: utxo.vout,
...scriptMixin, // Maybe adds the redeemScript and/or witnessScript
...witnessMixin // Adds the witnessUtxo or nonWitnessUtxo
...witnessMixin // Adds the witnessUtxo and/or nonWitnessUtxo
};

// eslint-disable-next-line no-console
Expand Down