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

feat: Support OKX extension wallet to send Bitcoin. #39

Closed
wants to merge 6 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
227 changes: 227 additions & 0 deletions app/btcintegration/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
"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" | "OkxWallet"

interface ConnectedAddressData {
address: string
pubKey: string
}

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

declare global {
interface Window {
unisat: any
okxwallet: 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"
// const tss = "bc1pej42ahnflkmprmqhl4fk3jafsd6chuax3knpl5g59ht533mk4s9qahk00n"
if (contractAddress.length !== 42)
return alert("Not a valid contract address")
if (isUndefined(amount) || isNaN(amount))
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
case "OkxWallet":
await callOkxWallet(params)
break
}
}

const callOkxWallet = async (params: Params) => {
if (!window.okxwallet) return alert("OKX wallet not installed")
const okxwallet = window.okxwallet
const account = await okxwallet?.bitcoinTestnet?.connect()
if (!account) return alert("No account found")
try {
const txHash = await okxwallet.bitcoinTestnet.send(
{
from: account.address,
to: params.tss,
value: params.amount / 1e8,
memo: `0x${params.contract}${params.message}`,
memoPos: 1,
}
);
return alert(`Broadcasted a transaction: ${txHash}`)
} catch (e: any) {
throw new Error(e.message || "Error sending OKX Bitcoin transaction")
}

}

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, 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>
<option value="OkxWallet">OkxWallet</option>
</select>
<Button
size="sm"
className="mt-4"
onClick={() => {
sendTransaction()
}}
>
Send transaction
</Button>
</div>
</div>
</div>
</div>
)
}

export default BtcIntegration
125 changes: 125 additions & 0 deletions app/btcintegration/xverse-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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
) {
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.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
}

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