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 all 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
199 changes: 199 additions & 0 deletions app/btcintegration/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"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
}

export type Params = {
contract: string
message: string
amount: number
tss: string
}

declare global {
interface Window {
unisat: any
}
}

const BtcIntegration = () => {
const [contractAddress, setContractAddress] = useState("")
const [message, setMessage] = useState("")
const [amount, setAmount] = useState<number | undefined>()
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
}
Comment on lines +33 to +63
Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid Redeclaring 'Wallet'

The Wallet type is redeclared. Consider renaming it to avoid conflicts.

- type Wallet = "XDefi" | "UniSat" | "XVerse"
+ type WalletType = "XDefi" | "UniSat" | "XVerse"

Committable suggestion was skipped due to low confidence.

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)

}

const callXDefi = async (params: 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: Error, res: Response) => {
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: 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: 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.psbtB64, result.utxoCnt, 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(Number(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 as Wallet)
}}
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
133 changes: 133 additions & 0 deletions app/btcintegration/xverse-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { base64, hex } from "@scure/base"
import * as btc from "micro-btc-signer"
import Wallet, { RpcErrorCode } from "sats-connect"

import { Params } from "./page"

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: Params
): Promise<{ psbtB64: string; utxoCnt: number }> {
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,
})

let utxoCnt = 0

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,
})
utxoCnt += 1
})

const changeAddress = senderAddress

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

const opReturn = btc.Script.encode(["RETURN", Buffer.from(memo, "utf8")])
tx.addOutputAddress(recipientAddress, BigInt(params.amount), bitcoinTestnet)
tx.addOutput({
script: opReturn,
amount: BigInt(0),
})
tx.addOutputAddress(changeAddress, BigInt(800), bitcoinTestnet)

const psbt = tx.toPSBT(0)

const psbtB64 = base64.encode(psbt)

return { psbtB64, utxoCnt }
}
Comment on lines +34 to +91
Copy link
Contributor

Choose a reason for hiding this comment

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

Typographical Error in Parameter Name

The parameter publickkey should be corrected to publicKey.

- publickkey: string,
+ publicKey: string,
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
async function createTransaction(
publickkey: string,
senderAddress: string,
params: Params
): Promise<{ psbtB64: string; utxoCnt: number }> {
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,
})
let utxoCnt = 0
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,
})
utxoCnt += 1
})
const changeAddress = senderAddress
const memo = `${params.contract}${params.message}`.toLowerCase()
const opReturn = btc.Script.encode(["RETURN", Buffer.from(memo, "utf8")])
tx.addOutputAddress(recipientAddress, BigInt(params.amount), bitcoinTestnet)
tx.addOutput({
script: opReturn,
amount: BigInt(0),
})
tx.addOutputAddress(changeAddress, BigInt(800), bitcoinTestnet)
const psbt = tx.toPSBT(0)
const psbtB64 = base64.encode(psbt)
return { psbtB64, utxoCnt }
}
async function createTransaction(
publicKey: string,
senderAddress: string,
params: Params
): Promise<{ psbtB64: string; utxoCnt: number }> {
const publicKey = hex.decode(publicKey)
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,
})
let utxoCnt = 0
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,
})
utxoCnt += 1
})
const changeAddress = senderAddress
const memo = `${params.contract}${params.message}`.toLowerCase()
const opReturn = btc.Script.encode(["RETURN", Buffer.from(memo, "utf8")])
tx.addOutputAddress(recipientAddress, BigInt(params.amount), bitcoinTestnet)
tx.addOutput({
script: opReturn,
amount: BigInt(0),
})
tx.addOutputAddress(changeAddress, BigInt(800), bitcoinTestnet)
const psbt = tx.toPSBT(0)
const psbtB64 = base64.encode(psbt)
return { psbtB64, utxoCnt }
}


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

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

const sigInputs = new Array(utxoCnt).fill(0, 0, utxoCnt).map((_, i) => i)

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

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