Components/Wallet
QuickSendButton
A streamlined Bitcoin BSV sending component that provides instant access to send transactions without opening a full wallet interface
A streamlined Bitcoin BSV sending component that provides instant access to send transactions through an elegant modal interface, perfect for quick micropayments, tips, and instant transfers without the complexity of a full wallet interface.
Installation
npx bigblocks add quick-send-buttonImport
import { QuickSendButton } from 'bigblocks';Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| onSuccess | (txid: string, amount: number, address: string) => void | No | - | Callback when transaction succeeds |
| onError | (error: string) => void | No | - | Callback when transaction fails |
| defaultAmount | number | No | - | Pre-fill amount in satoshis |
| defaultAddress | string | No | - | Pre-fill recipient address |
| buttonText | string | No | 'Send BSV' | Custom button text |
| variant | 'solid' | 'outline' | 'ghost' | No | 'solid' | Button style variant |
| size | 'small' | 'medium' | 'large' | No | 'medium' | Button size |
| showBalanceHint | boolean | No | false | Show current balance hint |
| maxAmount | number | No | - | Maximum send amount in satoshis |
| disabled | boolean | No | false | Disable the button |
| className | string | No | - | Additional CSS classes |
Basic Usage
import { QuickSendButton } from 'bigblocks';
export default function BasicSendExample() {
return (
<div className="send-section">
<h3>Send BSV Instantly</h3>
<QuickSendButton
onSuccess={(txid, amount, address) => {
console.log(`Sent ${amount} satoshis to ${address}. TxID: ${txid}`);
}}
onError={(error) => {
console.error('Send failed:', error);
}}
/>
</div>
);
}Advanced Usage
Complete Send Interface
import { QuickSendButton } from 'bigblocks';
import { useState } from 'react';
export default function AdvancedSendInterface() {
const [transactionHistory, setTransactionHistory] = useState<Array<{
txid: string;
amount: number;
address: string;
timestamp: number;
}>>([]);
const handleSendSuccess = (txid: string, amount: number, address: string) => {
// Add to transaction history
const transaction = {
txid,
amount,
address,
timestamp: Date.now()
};
setTransactionHistory(prev => [transaction, ...prev]);
// Show success notification
console.log(`✅ Successfully sent ${amount} satoshis to ${address}`);
console.log(`Transaction ID: ${txid}`);
// Optional: Track analytics
fetch('/api/analytics/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount,
address,
txid,
timestamp: Date.now()
})
});
};
const handleSendError = (error: string) => {
console.error('❌ Send failed:', error);
// Show user-friendly error message
if (error.includes('insufficient')) {
alert('Insufficient balance. Please check your wallet balance.');
} else if (error.includes('invalid address')) {
alert('Invalid recipient address. Please check and try again.');
} else {
alert(`Transaction failed: ${error}`);
}
};
return (
<div className="advanced-send-interface">
<div className="send-controls">
<h2>Send Bitcoin SV</h2>
<QuickSendButton
onSuccess={handleSendSuccess}
onError={handleSendError}
showBalanceHint={true}
maxAmount={100000000} // 1 BSV maximum
buttonText="💰 Send BSV"
variant="solid"
size="large"
className="bg-gradient-to-r from-blue-500 to-purple-600 text-white font-bold"
/>
</div>
{transactionHistory.length > 0 && (
<div className="transaction-history mt-6">
<h3>Recent Transactions</h3>
<div className="space-y-2">
{transactionHistory.slice(0, 5).map((tx) => (
<div key={tx.txid} className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div>
<span className="font-mono text-sm">
{tx.address.slice(0, 10)}...{tx.address.slice(-6)}
</span>
<span className="ml-2 text-gray-600">
{tx.amount.toLocaleString()} sats
</span>
</div>
<a
href={`https://whatsonchain.com/tx/${tx.txid}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-500 hover:underline text-sm"
>
View
</a>
</div>
))}
</div>
</div>
)}
</div>
);
}Pre-filled Quick Send
import { QuickSendButton } from 'bigblocks';
export default function PrefilledSendExample() {
return (
<div className="prefilled-examples">
<h3>Quick Send Examples</h3>
{/* Pre-filled with amount */}
<div className="example-item">
<h4>Tip 1000 satoshis</h4>
<QuickSendButton
defaultAmount={1000}
buttonText="💡 Send Tip"
variant="outline"
onSuccess={(txid) => console.log('Tip sent:', txid)}
/>
</div>
{/* Pre-filled with address and amount */}
<div className="example-item">
<h4>Donate to Developer</h4>
<QuickSendButton
defaultAddress="1DeveloperAddressHere..."
defaultAmount={5000}
buttonText="🚀 Support Development"
variant="solid"
onSuccess={(txid, amount, address) => {
console.log(`Donated ${amount} satoshis to developer`);
// Show thank you message
alert('Thank you for supporting open source development!');
}}
/>
</div>
{/* Custom styling with balance hint */}
<div className="example-item">
<h4>Custom Send with Balance</h4>
<QuickSendButton
showBalanceHint={true}
buttonText="💸 Send Payment"
size="large"
className="border-2 border-green-500 hover:bg-green-50"
onSuccess={(txid, amount, address) => {
console.log(`Payment sent: ${amount} sats to ${address}`);
}}
/>
</div>
</div>
);
}Common Patterns
Tip Button for Content Creators
import { QuickSendButton } from 'bigblocks';
interface TipCreatorProps {
creatorAddress: string;
creatorName: string;
contentId?: string;
}
export default function TipCreator({ creatorAddress, creatorName, contentId }: TipCreatorProps) {
const tipAmounts = [500, 1000, 5000, 10000]; // Different tip amounts
const handleTip = (txid: string, amount: number) => {
console.log(`Tip sent to ${creatorName}: ${amount} satoshis`);
// Track tip for analytics
fetch('/api/tips/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
creatorAddress,
creatorName,
amount,
txid,
contentId,
timestamp: Date.now()
})
});
// Show thank you message
alert(`Thank you for tipping ${creatorName}! Transaction: ${txid.slice(0, 8)}...`);
};
return (
<div className="tip-creator-section">
<div className="creator-info mb-4">
<h4>Enjoyed this content?</h4>
<p>Tip {creatorName} to show your appreciation</p>
</div>
{/* Quick tip amounts */}
<div className="tip-amounts mb-4">
<span className="text-sm text-gray-600">Quick tips:</span>
<div className="flex gap-2 mt-1">
{tipAmounts.map((amount) => (
<QuickSendButton
key={amount}
defaultAddress={creatorAddress}
defaultAmount={amount}
buttonText={`${amount} sats`}
size="small"
variant="outline"
onSuccess={(txid) => handleTip(txid, amount)}
className="text-xs"
/>
))}
</div>
</div>
{/* Custom amount tip */}
<QuickSendButton
defaultAddress={creatorAddress}
buttonText={`💡 Tip ${creatorName}`}
variant="solid"
onSuccess={(txid, amount) => handleTip(txid, amount)}
className="bg-orange-500 hover:bg-orange-600 text-white"
/>
</div>
);
}Gaming Micropayments
import { QuickSendButton } from 'bigblocks';
interface GameItem {
id: string;
name: string;
price: number; // in satoshis
description: string;
icon: string;
}
interface GameShopProps {
playerId: string;
gamePaymentAddress: string;
}
export default function GameShop({ playerId, gamePaymentAddress }: GameShopProps) {
const items: GameItem[] = [
{ id: 'sword', name: 'Magic Sword', price: 2500, description: '+10 Attack', icon: '⚔️' },
{ id: 'shield', name: 'Shield', price: 1500, description: '+8 Defense', icon: '🛡️' },
{ id: 'potion', name: 'Health Potion', price: 500, description: 'Restore 50 HP', icon: '🧪' },
{ id: 'armor', name: 'Dragon Armor', price: 10000, description: '+20 Defense', icon: '🥼' }
];
const handleItemPurchase = async (item: GameItem, txid: string) => {
try {
// Award item to player
const response = await fetch('/api/game/purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
playerId,
itemId: item.id,
txid,
amount: item.price,
timestamp: Date.now()
})
});
if (response.ok) {
console.log(`${item.name} purchased for player ${playerId}`);
alert(`🎮 ${item.name} added to your inventory!`);
} else {
throw new Error('Failed to award item');
}
} catch (error) {
console.error('Purchase processing failed:', error);
alert('Purchase completed but item delivery failed. Contact support.');
}
};
return (
<div className="game-shop">
<h2>🏪 Game Shop</h2>
<p className="text-gray-600 mb-6">Buy items with Bitcoin SV</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{items.map((item) => (
<div key={item.id} className="shop-item p-4 border rounded-lg bg-white shadow">
<div className="flex items-center mb-2">
<span className="text-2xl mr-2">{item.icon}</span>
<div>
<h3 className="font-bold">{item.name}</h3>
<p className="text-sm text-gray-600">{item.description}</p>
</div>
</div>
<div className="flex items-center justify-between">
<span className="font-mono text-lg">
{item.price.toLocaleString()} sats
</span>
<QuickSendButton
defaultAmount={item.price}
defaultAddress={gamePaymentAddress}
buttonText="Buy"
size="small"
variant="solid"
onSuccess={(txid) => handleItemPurchase(item, txid)}
onError={(error) => {
console.error(`Purchase failed for ${item.name}:`, error);
alert('Purchase failed. Please try again.');
}}
className="bg-green-600 hover:bg-green-700 text-white"
/>
</div>
</div>
))}
</div>
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded">
<p className="text-sm text-blue-800">
💡 <strong>Pro tip:</strong> Items are delivered instantly upon transaction confirmation.
All purchases are recorded on the Bitcoin SV blockchain for permanent ownership proof.
</p>
</div>
</div>
);
}Split Payment Interface
import { QuickSendButton } from 'bigblocks';
import { useState } from 'react';
interface Recipient {
name: string;
address: string;
percentage?: number;
}
interface SplitPaymentProps {
recipients: Recipient[];
totalAmount: number;
}
export default function SplitPayment({ recipients, totalAmount }: SplitPaymentProps) {
const [selectedRecipients, setSelectedRecipients] = useState<Recipient[]>([]);
const splitAmount = selectedRecipients.length > 0
? Math.floor(totalAmount / selectedRecipients.length)
: 0;
const handleRecipientToggle = (recipient: Recipient, checked: boolean) => {
if (checked) {
setSelectedRecipients(prev => [...prev, recipient]);
} else {
setSelectedRecipients(prev => prev.filter(r => r.address !== recipient.address));
}
};
const handleSplitPayment = async (txid: string) => {
console.log(`Split payment initiated. Master TxID: ${txid}`);
try {
// Process individual payments to each recipient
const payments = await Promise.all(
selectedRecipients.map(async (recipient) => {
const response = await fetch('/api/wallet/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipientAddress: recipient.address,
amount: splitAmount,
metadata: {
type: 'split_payment',
masterTxid: txid,
recipientName: recipient.name
}
})
});
return response.json();
})
);
const successfulPayments = payments.filter(p => p.success);
console.log(`Split payment completed: ${successfulPayments.length}/${selectedRecipients.length} successful`);
alert(`💰 Split payment completed! ${successfulPayments.length} payments sent successfully.`);
} catch (error) {
console.error('Split payment processing failed:', error);
alert('Split payment processing failed. Please check individual transactions.');
}
};
return (
<div className="split-payment">
<h3>Split Payment: {totalAmount.toLocaleString()} sats</h3>
<div className="recipient-selection mb-4">
<h4 className="mb-2">Select Recipients:</h4>
{recipients.map((recipient) => (
<label key={recipient.address} className="flex items-center mb-2">
<input
type="checkbox"
checked={selectedRecipients.some(r => r.address === recipient.address)}
onChange={(e) => handleRecipientToggle(recipient, e.target.checked)}
className="mr-2"
/>
<span className="flex-1">
{recipient.name}
</span>
<span className="text-sm text-gray-500 font-mono">
{recipient.address.slice(0, 10)}...{recipient.address.slice(-6)}
</span>
</label>
))}
</div>
{selectedRecipients.length > 0 && (
<div className="split-summary mb-4 p-3 bg-gray-50 rounded">
<p><strong>Recipients:</strong> {selectedRecipients.length}</p>
<p><strong>Amount per recipient:</strong> {splitAmount.toLocaleString()} sats</p>
<p><strong>Total:</strong> {(splitAmount * selectedRecipients.length).toLocaleString()} sats</p>
</div>
)}
<QuickSendButton
buttonText={selectedRecipients.length > 0
? `Send to ${selectedRecipients.length} recipients`
: 'Select recipients first'
}
disabled={selectedRecipients.length === 0}
onSuccess={handleSplitPayment}
onError={(error) => {
console.error('Split payment failed:', error);
alert('Split payment initiation failed. Please try again.');
}}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-semibold"
/>
</div>
);
}Subscription Payment
import { QuickSendButton } from 'bigblocks';
import { useState } from 'react';
interface SubscriptionPlan {
id: string;
name: string;
price: number; // monthly price in satoshis
features: string[];
}
export default function SubscriptionPayment() {
const [selectedPlan, setSelectedPlan] = useState<SubscriptionPlan | null>(null);
const plans: SubscriptionPlan[] = [
{
id: 'basic',
name: 'Basic Plan',
price: 50000, // 50k satoshis/month
features: ['Feature A', 'Feature B', 'Email Support']
},
{
id: 'pro',
name: 'Pro Plan',
price: 100000, // 100k satoshis/month
features: ['Everything in Basic', 'Feature C', 'Feature D', 'Priority Support']
},
{
id: 'enterprise',
name: 'Enterprise Plan',
price: 250000, // 250k satoshis/month
features: ['Everything in Pro', 'Custom Features', 'Dedicated Support']
}
];
const handleSubscription = async (txid: string, amount: number) => {
if (!selectedPlan) return;
try {
// Create subscription
const response = await fetch('/api/subscriptions/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
planId: selectedPlan.id,
paymentTxid: txid,
amount,
startDate: Date.now()
})
});
if (response.ok) {
const subscription = await response.json();
console.log('Subscription created:', subscription);
alert(`🎉 Welcome to ${selectedPlan.name}! Your subscription is now active.`);
// Redirect to dashboard
window.location.href = '/dashboard';
} else {
throw new Error('Subscription creation failed');
}
} catch (error) {
console.error('Subscription setup failed:', error);
alert('Payment received but subscription setup failed. Please contact support.');
}
};
return (
<div className="subscription-payment">
<h2>Choose Your Plan</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
{plans.map((plan) => (
<div
key={plan.id}
className={`plan-card p-6 border-2 rounded-lg cursor-pointer transition-all ${
selectedPlan?.id === plan.id
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => setSelectedPlan(plan)}
>
<h3 className="font-bold text-xl mb-2">{plan.name}</h3>
<p className="text-2xl font-mono mb-4">
{plan.price.toLocaleString()} sats/mo
</p>
<ul className="text-sm space-y-1">
{plan.features.map((feature, index) => (
<li key={index} className="flex items-center">
<span className="text-green-500 mr-2">✓</span>
{feature}
</li>
))}
</ul>
</div>
))}
</div>
{selectedPlan && (
<div className="selected-plan p-4 bg-blue-50 border border-blue-200 rounded mb-4">
<h4>Selected: {selectedPlan.name}</h4>
<p>Monthly payment: {selectedPlan.price.toLocaleString()} satoshis</p>
</div>
)}
<QuickSendButton
defaultAmount={selectedPlan?.price}
buttonText={selectedPlan
? `Subscribe to ${selectedPlan.name}`
: 'Select a plan first'
}
disabled={!selectedPlan}
onSuccess={(txid, amount) => handleSubscription(txid, amount)}
onError={(error) => {
console.error('Subscription payment failed:', error);
alert('Payment failed. Please try again.');
}}
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3"
/>
{selectedPlan && (
<p className="text-sm text-gray-600 mt-2 text-center">
By subscribing, you agree to monthly payments of {selectedPlan.price.toLocaleString()} satoshis.
Cancel anytime in your account settings.
</p>
)}
</div>
);
}Authentication Requirements
The QuickSendButton requires Bitcoin authentication context for wallet operations:
import {
BitcoinAuthProvider,
BitcoinQueryProvider,
QuickSendButton
} from 'bigblocks';
function App() {
return (
<BitcoinQueryProvider>
<BitcoinAuthProvider config={{
apiUrl: '/api',
walletMode: 'integrated' // Enables wallet features
}}>
<QuickSendButton
buttonText="Send BSV"
showBalanceHint={true}
/>
</BitcoinAuthProvider>
</BitcoinQueryProvider>
);
}Features
- Inline Modal Interface: Quick popup form without full page navigation
- Balance Validation: Automatically checks sufficient funds before enabling send
- Address Validation: Validates Bitcoin addresses in real-time
- Amount Formatting: Smart satoshi/BSV conversion display
- Transaction Status: Real-time transaction broadcast status and confirmation
- Error Handling: User-friendly error messages with retry options
- Keyboard Shortcuts: ESC to close modal, Enter to send
- Responsive Design: Optimized for both mobile and desktop
- Security: Amount limits and address validation prevent errors
Best Practices
- Clear Intent: Use descriptive button text that explains what the payment is for
- Amount Validation: Set reasonable max amounts to prevent accidental large sends
- Address Verification: Always validate recipient addresses before enabling send
- Transaction Feedback: Provide clear success/error feedback with transaction IDs
- Balance Awareness: Show current balance to help users understand affordability
- Confirmation Steps: For larger amounts, consider additional confirmation prompts
Error Handling
Comprehensive Error Management
import { QuickSendButton } from 'bigblocks';
export default function RobustSendHandler() {
const handleSendError = (error: string) => {
console.error('Send failed:', error);
if (error.includes('insufficient funds')) {
// Show balance and suggest smaller amount
alert('Insufficient balance. Please check your wallet balance and try a smaller amount.');
} else if (error.includes('invalid address')) {
// Address format error
alert('Invalid recipient address. Please check the address format and try again.');
} else if (error.includes('amount too small')) {
// Below dust limit
alert('Amount too small. Minimum send amount is 546 satoshis.');
} else if (error.includes('network')) {
// Network connectivity issues
alert('Network error. Please check your connection and try again.');
} else if (error.includes('rate limit')) {
// Too many requests
alert('Too many transactions. Please wait a moment and try again.');
} else {
// Generic error
alert(`Transaction failed: ${error}`);
}
};
return (
<QuickSendButton
onError={handleSendError}
onSuccess={(txid, amount, address) => {
console.log(`✅ Sent ${amount} satoshis to ${address}`);
console.log(`Transaction: https://whatsonchain.com/tx/${txid}`);
alert('Transaction sent successfully!');
}}
maxAmount={10000000} // 0.1 BSV maximum
showBalanceHint={true}
/>
);
}Security Considerations
Amount Limits and Validation
import { QuickSendButton } from 'bigblocks';
export default function SecureSendButton() {
return (
<div className="secure-send">
<div className="security-notice mb-4 p-3 bg-yellow-50 border border-yellow-200 rounded">
<p className="text-sm text-yellow-800">
🔒 All transactions are signed with your private key and broadcast securely.
Maximum single transaction: 0.1 BSV.
</p>
</div>
<QuickSendButton
maxAmount={10000000} // 0.1 BSV limit
buttonText="🔐 Secure Send"
onSuccess={(txid, amount, address) => {
// Log transaction for audit trail
fetch('/api/audit/transaction', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
txid,
amount,
address,
timestamp: Date.now(),
userAgent: navigator.userAgent
})
});
console.log('Secure transaction completed:', txid);
}}
onError={(error) => {
// Log security-related errors
if (error.includes('amount exceeds')) {
console.warn('User attempted to exceed maximum amount');
}
console.error('Secure send failed:', error);
}}
/>
</div>
);
}Troubleshooting
Common Issues
Button appears disabled/unresponsive
- Ensure
BitcoinAuthProvideris properly configured with wallet access - Check that user has authenticated and unlocked their wallet
- Verify sufficient balance for minimum transaction (546 satoshis + fees)
Modal doesn't open when clicked
- Check for JavaScript errors in browser console
- Ensure required Bitcoin context providers are wrapped around the component
- Verify the component is not disabled via props
Transactions fail with "insufficient funds"
- User needs more BSV in their wallet
- Account for transaction fees (typically ~0.0001 BSV)
- Check if user has UTXOs available for spending
Address validation errors
- Ensure addresses are valid Bitcoin SV format
- Check for correct network (mainnet vs testnet)
- Verify addresses haven't been copy-pasted incorrectly
Debug Mode
import { QuickSendButton } from 'bigblocks';
export default function DebugSendButton() {
const debugSend = (txid: string, amount: number, address: string) => {
console.log('🐛 Debug send success:', {
txid,
amount,
address,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
});
};
const debugError = (error: string) => {
console.error('🐛 Debug send error:', {
error,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
});
};
return (
<div className="debug-send">
<h3>Debug Mode Send</h3>
<QuickSendButton
onSuccess={debugSend}
onError={debugError}
buttonText="🐛 Debug Send"
showBalanceHint={true}
className="border-2 border-gray-300"
/>
</div>
);
}Related Components
- SendBSVButton - Full-featured send interface
- QuickDonateButton - Quick donation interface
- WalletOverview - Complete wallet display
- CompactWalletOverview - Compact wallet info
API Reference
QuickSendButton Component
interface QuickSendButtonProps {
onSuccess?: (txid: string, amount: number, address: string) => void;
onError?: (error: string) => void;
defaultAmount?: number;
defaultAddress?: string;
buttonText?: string;
variant?: 'solid' | 'outline' | 'ghost';
size?: 'small' | 'medium' | 'large';
showBalanceHint?: boolean;
maxAmount?: number;
disabled?: boolean;
className?: string;
}Usage with React Query
import { useQuery, useMutation } from '@tanstack/react-query';
import { QuickSendButton } from 'bigblocks';
function QuickSendWithQuery() {
// Query for wallet balance
const { data: balance } = useQuery({
queryKey: ['wallet-balance'],
queryFn: () => fetch('/api/wallet/balance').then(res => res.json()),
refetchInterval: 30000 // Refresh every 30 seconds
});
// Mutation for sending transactions
const sendMutation = useMutation({
mutationFn: (sendData: { address: string; amount: number }) =>
fetch('/api/wallet/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sendData)
}).then(res => res.json()),
onSuccess: (data) => {
console.log('Transaction sent:', data.txid);
},
onError: (error) => {
console.error('Send failed:', error);
}
});
const maxSafeAmount = balance ? Math.floor(balance.satoshis * 0.9) : undefined;
return (
<QuickSendButton
maxAmount={maxSafeAmount}
showBalanceHint={true}
onSuccess={(txid, amount, address) => {
console.log('Success:', { txid, amount, address });
}}
onError={(error) => {
console.error('Error:', error);
}}
/>
);
}