This repository has been archived by the owner on Jul 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 19
add BTC transactions page #35
Closed
Closed
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
withNumber.isNaN
to avoid type coercion issues.Committable suggestion
Tools
Biome