Oviato Connect
Signing Transactions
Send transactions and sign PSBTs using @oviato/connect
Learn how to send cryptocurrency transfers and sign Bitcoin PSBTs using @oviato/connect in React applications.
Overview
@oviato/connect provides two main transaction capabilities:
- Send Transfers - Simple cryptocurrency transfers across all supported networks
- Sign PSBTs - Sign Partially Signed Bitcoin Transactions (Bitcoin only)
Send Transfers
Send cryptocurrency to any address:
Basic Usage
"use client";
import { useState } from "react";
import { useOviConnect } from "@oviato/connect/client";
import { sendTransfer } from "@oviato/connect";
export default function SendTransfer() {
const { session } = useOviConnect();
const [txid, setTxid] = useState("");
const handleSend = async () => {
const result = await sendTransfer({
to: "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
amount: 10000, // 10,000 satoshis = 0.0001 BTC
fee: 1000, // 1,000 satoshis fee
});
if (result.status === "success") {
setTxid(result.data.txid);
console.log("Explorer:", result.data.explorerUrl);
}
};
if (!session) {
return <div>Please connect your wallet first</div>;
}
return (
<div>
<button onClick={handleSend}>Send Transfer</button>
{txid && (
<div>
<p>Transaction ID: {txid}</p>
</div>
)}
</div>
);
}Complete Transfer Form
Here's a complete example with form inputs and validation:
"use client";
import { useState } from "react";
import { useOviConnect } from "@oviato/connect/client";
import { sendTransfer, rpc } from "@oviato/connect";
export default function TransferForm() {
const { session } = useOviConnect();
const [recipient, setRecipient] = useState("");
const [amount, setAmount] = useState("");
const [fee, setFee] = useState("1000");
const [txid, setTxid] = useState("");
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [balance, setBalance] = useState<number | null>(null);
// Load balance
const loadBalance = async () => {
if (!session) return;
const result = await rpc.getBalance({ address: session.address });
if (result.result) {
setBalance(result.result.balance);
}
};
// Call on mount
useState(() => {
loadBalance();
});
const handleSend = async () => {
// Validation
if (!recipient.trim()) {
setError("Please enter recipient address");
return;
}
const amountNum = parseInt(amount);
const feeNum = parseInt(fee);
if (isNaN(amountNum) || amountNum <= 0) {
setError("Please enter valid amount");
return;
}
if (balance !== null && amountNum + feeNum > balance) {
setError("Insufficient balance");
return;
}
// Confirm with user
const confirmed = confirm(
`Send ${amountNum} satoshis to ${recipient}?\nFee: ${feeNum} satoshis`
);
if (!confirmed) return;
setIsLoading(true);
setError("");
setTxid("");
const result = await sendTransfer({
to: recipient,
amount: amountNum,
fee: feeNum,
});
if (result.status === "success") {
setTxid(result.data.txid);
setRecipient("");
setAmount("");
await loadBalance(); // Refresh balance
} else {
setError(result.message || "Transfer failed");
}
setIsLoading(false);
};
if (!session) {
return (
<div>
<p>Please connect your wallet to send transfers</p>
</div>
);
}
return (
<div className="space-y-4">
<div>
<h2>Send Transfer</h2>
<p>From: {session.address}</p>
<p>
Balance: {balance !== null ? `${balance} satoshis` : "Loading..."}
</p>
</div>
<div>
<label htmlFor="recipient">Recipient Address:</label>
<input
id="recipient"
type="text"
value={recipient}
onChange={(e) => setRecipient(e.target.value)}
placeholder="bc1q..."
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="amount">Amount (satoshis):</label>
<input
id="amount"
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="10000"
className="w-full p-2 border rounded"
/>
</div>
<div>
<label htmlFor="fee">Fee (satoshis):</label>
<input
id="fee"
type="number"
value={fee}
onChange={(e) => setFee(e.target.value)}
placeholder="1000"
className="w-full p-2 border rounded"
/>
</div>
<button
onClick={handleSend}
disabled={isLoading || !recipient || !amount}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isLoading ? "Sending..." : "Send Transfer"}
</button>
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded">
Error: {error}
</div>
)}
{txid && (
<div className="p-3 bg-green-100 rounded">
<h3 className="font-bold">Transaction Sent!</h3>
<p className="break-all font-mono text-sm">TX ID: {txid}</p>
</div>
)}
</div>
);
}Transfer Options
type TransferOptions = {
to: string; // Recipient address
amount: number; // Amount in smallest unit
fee: number; // Fee in smallest unit
memo?: string; // Optional memo (not supported on all networks)
};Amount Units by Network
| Network | Unit | Example |
|---|---|---|
| Bitcoin | Satoshis | 10000 = 0.0001 BTC |
| Ethereum | Wei | 1000000000000000000 = 1 ETH |
Sign PSBTs (Bitcoin Only)
Sign Partially Signed Bitcoin Transactions for advanced Bitcoin use cases:
Basic Usage
import { signPsbt } from "@oviato/connect";
const result = await signPsbt({
psbt: "cHNidP8BAH0CAAAAA...", // Base64 encoded PSBT
signInputs: [0, 1], // Indices to sign
broadcast: true, // Auto-broadcast
});
if (result.status === "success") {
console.log("Signed PSBT:", result.data.psbt);
if (result.data.txid) {
console.log("Broadcast TX:", result.data.txid);
}
}PSBT Signing Component
"use client";
import { useState } from "react";
import { useOviConnect } from "@oviato/connect/client";
import { signPsbt } from "@oviato/connect";
export default function PsbtSigner() {
const { session } = useOviConnect();
const [psbtInput, setPsbtInput] = useState("");
const [signInputs, setSignInputs] = useState("0");
const [broadcast, setBroadcast] = useState(false);
const [result, setResult] = useState<any>(null);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleSign = async () => {
if (!psbtInput.trim()) {
setError("Please enter PSBT");
return;
}
const indices = signInputs
.split(",")
.map((s) => parseInt(s.trim()))
.filter((n) => !isNaN(n));
if (indices.length === 0) {
setError("Please enter valid input indices");
return;
}
setIsLoading(true);
setError("");
setResult(null);
const signResult = await signPsbt({
psbt: psbtInput,
signInputs: indices,
broadcast,
});
if (signResult.status === "success") {
setResult(signResult.data);
} else {
setError(signResult.message || "Failed to sign PSBT");
}
setIsLoading(false);
};
if (!session) {
return (
<div>
<p>Please connect your wallet to sign PSBTs</p>
</div>
);
}
if (
session.network !== "bitcoinmainnet" &&
session.network !== "bitcointestnet"
) {
return (
<div>
<p>PSBT signing is only available on Bitcoin networks</p>
</div>
);
}
return (
<div className="space-y-4">
<div>
<h2>Sign PSBT</h2>
<p>Address: {session.address}</p>
</div>
<div>
<label htmlFor="psbt">PSBT (Base64):</label>
<textarea
id="psbt"
value={psbtInput}
onChange={(e) => setPsbtInput(e.target.value)}
placeholder="cHNidP8BAH0..."
rows={6}
className="w-full p-2 border rounded font-mono text-sm"
/>
</div>
<div>
<label htmlFor="inputs">Input Indices (comma-separated):</label>
<input
id="inputs"
type="text"
value={signInputs}
onChange={(e) => setSignInputs(e.target.value)}
placeholder="0,1,2"
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={broadcast}
onChange={(e) => setBroadcast(e.target.checked)}
/>
<span>Broadcast after signing</span>
</label>
</div>
<button
onClick={handleSign}
disabled={isLoading || !psbtInput}
className="px-4 py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{isLoading ? "Signing..." : "Sign PSBT"}
</button>
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded">
Error: {error}
</div>
)}
{result && (
<div className="p-3 bg-green-100 rounded">
<h3 className="font-bold">Signed!</h3>
<p className="break-all font-mono text-sm">PSBT: {result.psbt}</p>
{result.txid && (
<p className="break-all font-mono text-sm mt-2">
TX ID: {result.txid}
</p>
)}
</div>
)}
</div>
);
}PSBT Options
type SignPsbtOptions = {
psbt: string; // Base64 encoded PSBT
signInputs: number[]; // Input indices to sign [0, 1, 2]
broadcast?: boolean; // Auto-broadcast (default: false)
};Response Types
Transfer Response
type TransferResponse = {
status: "success" | "error";
data?: {
txid: string; // Transaction ID
explorerUrl: string; // Block explorer URL
};
message?: string; // Error message if failed
};PSBT Response
type PsbtResponse = {
status: "success" | "error";
data?: {
psbt: string; // Signed PSBT (Base64)
txid?: string; // TX ID if broadcast=true
};
message?: string; // Error message if failed
};Error Handling
Handle common transaction errors:
const result = await sendTransfer({
to: recipient,
amount: amount,
fee: fee,
});
if (result.status === "error") {
if (result.message?.includes("insufficient funds")) {
alert("Not enough balance");
} else if (result.message?.includes("invalid address")) {
alert("Invalid recipient address");
} else if (result.message?.includes("cancelled")) {
console.log("User cancelled transaction");
} else {
alert("Transfer failed: " + result.message);
}
}Security Best Practices
1. Always Validate Addresses
function isValidBitcoinAddress(address: string): boolean {
return /^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,62}$/.test(address);
}
function isValidEthereumAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address);
}
// Use before sending
if (!isValidBitcoinAddress(recipient)) {
setError("Invalid Bitcoin address");
return;
}2. Confirm Before Sending
Always show confirmation dialog:
const confirmed = confirm(
`Send ${amount} satoshis to ${recipient}?\n` +
`Fee: ${fee} satoshis\n` +
`Total: ${amount + fee} satoshis`
);
if (!confirmed) return;
await sendTransfer({ to: recipient, amount, fee });3. Check Balance First
import { rpc } from "@oviato/connect";
const balanceResult = await rpc.getBalance({
address: session.address,
});
const balance = balanceResult.result?.balance || 0;
if (balance < amount + fee) {
alert("Insufficient balance");
return;
}4. Show Transaction Status
const [status, setStatus] = useState("");
setStatus("Preparing transaction...");
const result = await sendTransfer({ to, amount, fee });
if (result.status === "success") {
setStatus("Transaction sent!");
console.log("View on explorer:", result.data.explorerUrl);
} else {
setStatus("Transaction failed");
}TypeScript
Full type safety with TypeScript:
import type { BridgeResponse } from "@oviato/connect";
type TransferData = {
txid: string;
explorerUrl: string;
};
const result: BridgeResponse<TransferData> = await sendTransfer({
to: "bc1q...",
amount: 10000,
fee: 1000,
});
if (result.status === "success") {
const { txid, explorerUrl } = result.data;
// TypeScript knows the types
}Troubleshooting
"Insufficient funds"
Check balance before sending:
const balance = await rpc.getBalance({ address: session.address });
if (balance.result.balance < amount + fee) {
alert("Not enough balance");
}"Invalid address"
Validate the recipient address format for the current network.
"User rejected transaction"
User cancelled the transaction in the signing modal.
"Network error"
Check network connectivity and try again.
Next Steps
- Signing Messages - Learn about message signing