Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

add BTC transactions page #35

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
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
186 changes: 186 additions & 0 deletions app/btcintegration/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"use client"

import React, { useState } from "react"
import { isUndefined } from "lodash"
import Wallet, { AddressPurpose } from "sats-connect"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

import { createTransaction, signPsbt } from "./xverse-utils"

type Wallet = "XDefi" | "UniSat" | "XVerse"

interface ConnectedAddressData {
address: string
pubKey: string
}

const BtcIntegration = () => {
const [contractAddress, setContractAddress] = useState("")
const [message, setMessage] = useState("")
const [amount, setAmount] = useState()
const [selectedWallet, setSelectedWallet] = useState<Wallet>("XDefi")

const sendTransaction = async () => {
const tss = "tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur"
if (contractAddress.length !== 42)
return alert("Not a valid contract address")
if (isUndefined(amount) || isNaN(amount))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Number.isNaN for Type Safety

Replace isNaN with Number.isNaN to avoid type coercion issues.

- if (isUndefined(amount) || isNaN(amount))
+ if (isUndefined(amount) || Number.isNaN(amount))
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isUndefined(amount) || isNaN(amount))
if (isUndefined(amount) || Number.isNaN(amount))
Tools
Biome

[error] 43-43: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

return alert("Amount must be a number")

const params = {
contract: contractAddress.slice(2),
message: message.slice(2),
amount,
tss,
}

switch (selectedWallet) {
case "XDefi":
await callXDefi(params)
break
case "UniSat":
await callUniSat(params)
break
case "XVerse":
await callXverse(params)
break
}
}

const callXDefi = async (params) => {
if (!window.xfi) return alert("XDEFI wallet not installed")
const wallet = window.xfi
window.xfi.bitcoin.changeNetwork("testnet")
const account = (await wallet?.bitcoin?.getAccounts())?.[0]
if (!account) return alert("No account found")
const tx = {
method: "transfer",
params: [
{
feeRate: 10,
from: account,
recipient: params.tss,
amount: {
amount: params.amount,
decimals: 8,
},
memo: `hex::${params.contract}${params.message}`,
},
],
}
window.xfi.bitcoin.request(tx, (err, res) => {
if (err) {
return alert(`Couldn't send transaction, ${JSON.stringify(err)}`)
} else if (res) {
return alert(`Broadcasted a transaction, ${JSON.stringify(res)}`)
}
})
}

const callUniSat = async (params) => {
if (!window.unisat) return alert("Unisat wallet not installed")
try {
await window.unisat.requestAccounts()
const memos = [`${params.contract}${params.message}`.toLowerCase()]
const tx = await window.unisat.sendBitcoin(params.tss, params.amount, {
memos,
})
return alert(`Broadcasted a transaction: ${JSON.stringify(tx)}`)
} catch (e) {
return alert(`Couldn't send transaction, ${JSON.stringify(e)}`)
}
}

const callXverse = async (params) => {
const response = await Wallet.request("getAccounts", {
purposes: [AddressPurpose.Payment],
message: "Test app wants to know your addresses!",
})

if (response.status == "success") {
const result = await createTransaction(
response.result[0].publicKey,
response.result[0].address,
params
)

await signPsbt(result, response.result[0].address)
} else {
alert("wallet connection failed")
}
}

return (
<div className="grid sm:grid-cols-3 gap-x-10 mt-12">
<div className="sm:col-span-2 overflow-x-auto">
<div className="flex items-center justify-start gap-2 mb-6">
<h1 className="leading-10 text-2xl font-bold tracking-tight pl-4">
BTC Integration
</h1>
</div>
<div className="pl-10 px-3 flex flex-col gap-6">
<div>
<Label>Amount in satoshis</Label>
<Input
type="number"
value={amount}
onChange={(e) => {
setAmount(e.target.value)
}}
placeholder="0"
/>
</div>
<div>
<Label>Omnichain contract address</Label>
<Input
type="text"
value={contractAddress}
onChange={(e) => {
setContractAddress(e.target.value)
}}
placeholder="0xc79EA..."
/>
</div>
<div>
<Label>Contract call parameters</Label>
<Input
type="text"
value={message}
onChange={(e) => {
setMessage(e.target.value)
}}
placeholder="0x3724C..."
/>
</div>

<div>
<select
onChange={(e) => {
setSelectedWallet(e.target.value)
}}
className="block my-2"
>
<option value="XDefi">XDEFI</option>
<option value="UniSat">Unisat</option>
<option value="XVerse">Xverse</option>
</select>
<Button
size="sm"
className="mt-4"
onClick={() => {
sendTransaction()
}}
>
Send transaction
</Button>
</div>
</div>
</div>
</div>
)
}

export default BtcIntegration
123 changes: 123 additions & 0 deletions app/btcintegration/xverse-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { base64, hex } from "@scure/base"
import * as btc from "micro-btc-signer"
import Wallet, { RpcErrorCode } from "sats-connect"

const bitcoinTestnet = {
bech32: "tb",
pubKeyHash: 0x6f,
scriptHash: 0xc4,
wif: 0xef,
}

async function fetchUtxo(address: string): Promise<any[]> {
try {
const response = await fetch(
`https://mempool.space/testnet/api/address/${address}/utxo`
)
if (!response.ok) {
throw new Error("Failed to fetch UTXO")
}
const utxos: any[] = await response.json()

if (utxos.length === 0) {
throw new Error("0 Balance")
}
return utxos
} catch (error) {
console.error("Error fetching UTXO:", error)
throw error
}
}

async function createTransaction(
publickkey: string,
senderAddress: string,
params
) {
const publicKey = hex.decode(publickkey)

const p2wpkh = btc.p2wpkh(publicKey, bitcoinTestnet)
const p2sh = btc.p2sh(p2wpkh, bitcoinTestnet)

const recipientAddress = "tb1qy9pqmk2pd9sv63g27jt8r657wy0d9ueeh0nqur"
if (!senderAddress) {
throw new Error("Error: no sender address")
}
if (!recipientAddress) {
throw new Error("Error: no recipient address in ENV")
}

const output = await fetchUtxo(senderAddress)

const tx = new btc.Transaction({
allowUnknowOutput: true,
})

output.forEach((utxo) => {
tx.addInput({
txid: utxo.txid,
index: utxo.vout,
witnessUtxo: {
script: p2sh.script,
amount: BigInt(utxo.value),
},
witnessScript: p2sh.witnessScript,
redeemScript: p2sh.redeemScript,
})
})

const changeAddress = senderAddress

const memo = `${params.contract}${params.message}`.toLowerCase()

const opReturn = btc.Script.encode(["RETURN", Buffer.from(memo, "utf8")])

tx.addOutput({
script: opReturn,
amount: BigInt(0),
})
tx.addOutputAddress(recipientAddress, BigInt(params.amount), bitcoinTestnet)
tx.addOutputAddress(changeAddress, BigInt(800), bitcoinTestnet)

const psbt = tx.toPSBT(0)

const psbtB64 = base64.encode(psbt)

return psbtB64
}

async function signPsbt(psbtBase64: string, senderAddress: string) {
// Get the PSBT Base64 from the input

if (!psbtBase64) {
alert("Please enter a valid PSBT Base64 string.")
return
}

try {
const response = await Wallet.request("signPsbt", {
psbt: psbtBase64,
allowedSignHash: btc.SignatureHash.ALL,
broadcast: true,
signInputs: {
[senderAddress]: [0],
},
})

if (response.status === "success") {
alert("PSBT signed successfully!")
} else {
if (response.error.code === RpcErrorCode.USER_REJECTION) {
alert("Request canceled by user")
} else {
console.error("Error signing PSBT:", response.error)
alert("Error signing PSBT: " + response.error.message)
}
}
} catch (err) {
console.error("Unexpected error:", err)
alert("Error while signing")
}
}

export { createTransaction, signPsbt }
Loading