Skip to content

Commit

Permalink
add bitcoin example for the guide
Browse files Browse the repository at this point in the history
  • Loading branch information
rtomas committed Jan 1, 2025
1 parent fab9e01 commit a8f7e2e
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 11 deletions.
2 changes: 2 additions & 0 deletions react/react-bitcoin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@bitcoinerlab/secp256k1": "^1.2.0",
"@reown/appkit": "1.6.2",
"@reown/appkit-adapter-bitcoin": "1.6.2",
"bitcoinjs-lib": "^6.1.7",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
24 changes: 22 additions & 2 deletions react/react-bitcoin/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createAppKit } from '@reown/appkit/react'
import { useState } from 'react'
import { networks, projectId, metadata, bitcoinAdapter } from './config'
import { ActionButtonList } from './components/ActionButtonList'
import { InfoList } from './components/InfoList'
Expand All @@ -20,20 +21,39 @@ createAppKit({
})

export function App() {
/* sendSignPSBT: (hash: string ) => void;
sendSignMsg: (hash: string) => void;
sendSendTx: (hash: string) => void; */

const [psbt, setPSBT] = useState<string>("");
const [signedMsg, setSignedMsg] = useState<string>("");
const [txHash, setTxHash] = useState<string>("");

const receivePSBT = (hash: string) => {
setPSBT(hash);
};

const receiveSignedMsg = (signedMsg: string) => {
setSignedMsg(signedMsg); // Update the state with the transaction hash
};

const receiveTxHash= (hash: string) => {
setTxHash(hash)
}

return (
<div className={"pages"}>
<img src="/reown.svg" alt="Reown" style={{ width: '150px', height: '150px' }} />
<h1>AppKit bitcoin React dApp Example</h1>
<appkit-button />
<ActionButtonList />
<ActionButtonList sendSignPSBT={receivePSBT} sendSignMsg={receiveSignedMsg} sendSendTx={receiveTxHash} />
<div className="advice">
<p>
This projectId only works on localhost. <br/>
Go to <a href="https://cloud.reown.com" target="_blank" className="link-button" rel="Reown Cloud">Reown Cloud</a> to get your own.
</p>
</div>
<InfoList />
<InfoList psbt={psbt} signedMsg={signedMsg} txHash={txHash} />
</div>
)
}
Expand Down
75 changes: 67 additions & 8 deletions react/react-bitcoin/src/components/ActionButtonList.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { useDisconnect, useAppKit, useAppKitNetwork } from '@reown/appkit/react'
import { useDisconnect, useAppKit, useAppKitNetwork, useAppKitProvider, useAppKitAccount } from '@reown/appkit/react'
import type { BitcoinConnector } from '@reown/appkit-adapter-bitcoin'
import { networks } from '../config'
import { createPSBT } from '../utils/BitcoinUtil';

export const ActionButtonList = () => {
interface ActionButtonListProps {
sendSignPSBT: (hash: string ) => void;
sendSignMsg: (hash: string) => void;
sendSendTx: (hash: string) => void;
}


export const ActionButtonList = ({ sendSignPSBT, sendSignMsg, sendSendTx }: ActionButtonListProps) => {
const { disconnect } = useDisconnect();
const { open } = useAppKit();
const { switchNetwork } = useAppKitNetwork();
const { switchNetwork, caipNetwork } = useAppKitNetwork();
const { isConnected, address } = useAppKitAccount();
const { walletProvider } = useAppKitProvider<BitcoinConnector>('bip122')

const handleDisconnect = async () => {
try {
Expand All @@ -13,11 +24,59 @@ export const ActionButtonList = () => {
console.error("Failed to disconnect:", error);
}
};

// function to sing a msg
const handleSignMsg = async () => {
if (!walletProvider || !address) throw Error('user is disconnected')

// raise the modal to sign the message
const signature = await walletProvider.signMessage({
address,
message: "Hello Reown AppKit!"
});

sendSignMsg(signature);
}

// function to send a tx
const handleSendTx = async () => {
if (!walletProvider || !address) throw Error('user is disconnected')
const recipientAddress = address;

const signature = await walletProvider.sendTransfer({
recipient: recipientAddress,
amount: "1000"
})

sendSendTx(signature);
}

// function to sign a PSBT
const handleSignPSBT = async () => {
if (!walletProvider || !address || !caipNetwork) throw Error('user is disconnected');
const amount = 10000;
const recipientAddress = address;

const params = await createPSBT(caipNetwork, amount, address, recipientAddress);

params.broadcast = false // change to true to broadcast the tx

const signature = await walletProvider.signPSBT(params)
sendSignPSBT(signature.psbt);
}

return (
<div >
<button onClick={() => open()}>Open</button>
<button onClick={handleDisconnect}>Disconnect</button>
<button onClick={() => switchNetwork(networks[1]) }>Switch</button>
</div>
<>
{isConnected ? (
<div >
<button onClick={() => open()}>Open</button>
<button onClick={handleDisconnect}>Disconnect</button>
<button onClick={() => switchNetwork(networks[1]) }>Switch</button>
<button onClick={handleSignMsg}>Sign msg</button>
<button onClick={handleSignPSBT}>Sign PSBT</button>
<button onClick={handleSendTx}>Send tx</button>
</div>
) : null}
</>
)
}
32 changes: 31 additions & 1 deletion react/react-bitcoin/src/components/InfoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import {
useWalletInfo
} from '@reown/appkit/react'

export const InfoList = () => {
interface InfoListProps {
psbt: string;
signedMsg: string;
txHash: string;
}

export const InfoList = ({psbt, signedMsg, txHash}: InfoListProps) => {
const { themeMode, themeVariables } = useAppKitTheme();
const state = useAppKitState();
const {address, caipAddress, isConnected, status} = useAppKitAccount();
Expand All @@ -20,6 +26,30 @@ export const InfoList = () => {

return (
< >
{psbt && (
<section>
<h2>PSBT</h2>
<pre>
Hash: {psbt}<br />
</pre>
</section>
)}
{txHash && (
<section>
<h2>Sign Tx</h2>
<pre>
Hash: {txHash}<br />
</pre>
</section>
)}
{signedMsg && (
<section>
<h2>Sign MSG</h2>
<pre>
{signedMsg}<br />
</pre>
</section>
)}
<section>
<h2>useAppKit</h2>
<pre>
Expand Down
176 changes: 176 additions & 0 deletions react/react-bitcoin/src/utils/BitcoinUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type { CaipNetwork, CaipNetworkId } from '@reown/appkit'
import { bitcoinTestnet } from '@reown/appkit/networks'
import type { BitcoinConnector } from '@reown/appkit-adapter-bitcoin'
import { Psbt, networks as bitcoinNetworks, address as bitcoinAddress, payments as bitcoinPayments, type Network, initEccLib } from 'bitcoinjs-lib';
import * as bitcoinPSBTUtils from 'bitcoinjs-lib/src/psbt/psbtutils'
import ecc from '@bitcoinerlab/secp256k1'

initEccLib(ecc);

export type CreateSignPSBTParams = {
senderAddress: string
recipientAddress: string
network: CaipNetwork
amount: number
utxos: UTXO[]
feeRate: number
memo?: string
}

export type UTXO = {
txid: string
vout: number
value: number
status: {
confirmed: boolean
block_height: number
block_hash: string
block_time: number
}
}
export const calculateChange = (utxos: UTXO[], amount: number, feeRate: number): number => {
const inputSum = utxos.reduce((sum, utxo) => sum + utxo.value, 0)
/**
* 10 bytes: This is an estimated fixed overhead for the transaction.
* 148 bytes: This is the average size of each input (UTXO).
* 34 bytes: This is the size of each output.
* The multiplication by 2 indicates that there are usually two outputs in a typical transaction (one for the recipient and one for change)
*/
const estimatedSize = 10 + 148 * utxos.length + 34 * 2
const fee = estimatedSize * feeRate
const change = inputSum - amount - fee

return change
}

export const getBitcoinNetwork = (networkId: CaipNetworkId): Network => {
return isTestnet(networkId) ? bitcoinNetworks.testnet : bitcoinNetworks.bitcoin
}

export const isTestnet = (networkId: CaipNetworkId): boolean => {
return networkId === bitcoinTestnet.caipNetworkId
}

export const getFeeRate = async () => {
const defaultFeeRate = 2
try {
const response = await fetch('https://mempool.space/api/v1/fees/recommended')
if (response.ok) {
const data = await response.json()

if (data?.fastestFee) {
return parseInt(data.fastestFee, 10)
}
}
} catch (e) {
// eslint-disable-next-line no-console
console.error('Error fetching fee rate', e)
}

return defaultFeeRate
}

//
// Get the utxos ... List of unspent transactions that the sender has
//
export const getUTXOs = async (address: string, isTestnet: boolean = false): Promise<UTXO[]> => {
const response = await fetch(
`https://mempool.space${isTestnet ? '/testnet' : ''}/api/address/${address}/utxo`
)
return await response.json();
}

//
// Get the payment by address ... The type of address that the sender has
//
export const getPaymentByAddress = (
address: string,
network: bitcoinNetworks.Network
): bitcoinPayments.Payment => {
const output = bitcoinAddress.toOutputScript(address, network)

if (bitcoinPSBTUtils.isP2MS(output)) {
return bitcoinPayments.p2ms({ output, network })
} else if (bitcoinPSBTUtils.isP2PK(output)) {
return bitcoinPayments.p2pk({ output, network })
} else if (bitcoinPSBTUtils.isP2PKH(output)) {
return bitcoinPayments.p2pkh({ output, network })
} else if (bitcoinPSBTUtils.isP2WPKH(output)) {
return bitcoinPayments.p2wpkh({ output, network })
} else if (bitcoinPSBTUtils.isP2WSHScript(output)) {
return bitcoinPayments.p2wsh({ output, network })
} else if (bitcoinPSBTUtils.isP2SHScript(output)) {
return bitcoinPayments.p2sh({ output, network })
} else if (bitcoinPSBTUtils.isP2TR(output)) {
return bitcoinPayments.p2tr({ output, network })
}

throw new Error('Unsupported payment type')
}
//
// Create a psbt ... The PSBT that will be signed by the sender in the wallet
//
export const createPSBT = async (caipNetwork: CaipNetwork, amount: number, address: string, recipientAddress: string): Promise<BitcoinConnector.SignPSBTParams> => {
// get the bitcoin network from our caipNetwork
const network = getBitcoinNetwork(caipNetwork.caipNetworkId)
// get the payment by address ... this is the type of address that the sender has
const payment = getPaymentByAddress(address, network)
// get the utxos ... this is the list of unspent transactions that the sender has
const utxos = await getUTXOs(address, isTestnet(caipNetwork.caipNetworkId))
// get the fee rate ... this is the fee per byte
const feeRate = await getFeeRate()
// calculate the change ... this is the amount of satoshis that will be sent back to the sender
const change = calculateChange(utxos, amount, feeRate)
// the memo is the message that will be embedded in the transaction
const memo = "Hello Reown AppKit!";

const psbt = new Psbt({network: network});

// check if the payment output is valid
if (!payment.output) throw new Error('Invalid payment output');
// check if the change is greater than 0 ... this means the sender has enough funds
//if (change < 0) throw new Error('Insufficient funds');

if (change > 0) {
psbt.addOutput({
address: address,
value: change //BigInt(change)
})
}

// add the inputs to the psbt
for (const utxo of utxos) {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: payment.output,
value: utxo.value
}
})
}

// add the output to the psbt ... this is the recipient address and the amount of satoshis that will be sent to the recipient
psbt.addOutput({
address: recipientAddress,
value: amount
})

if (memo) {
const data = Buffer.from(memo, 'utf8')
const embed = bitcoinPayments.embed({ data: [data] })

if (!embed.output) throw new Error('Invalid embed output');

psbt.addOutput({
script: embed.output,
value: 0
})
}

return {
psbt: psbt.toBase64(),
signInputs: [],
broadcast: false
}
}

0 comments on commit a8f7e2e

Please sign in to comment.