Basic Payment Integration 💳
This example demonstrates how to integrate DolphinPay for basic payment functionality in a web application.
Overview​
We'll create a simple payment integration that allows users to:
- Create payment requests
- Display payment information
- Execute payments through their wallet
- Track payment status
Project Setup​
1. Create a New Project​
# Create Next.js project
npx create-next-app@latest dolphinpay-integration
cd dolphinpay-integration
# Install dependencies
npm install @dolphinpay/sdk @mysten/dapp-kit @tanstack/react-query
2. Configure Environment​
Create .env.local:
# DolphinPay Configuration
NEXT_PUBLIC_DOLPHINPAY_PACKAGE_ID=0x9c7ca262d020b005e0e6b6a5d083b329d58716e0d80c07b46804324074468f9c
NEXT_PUBLIC_DOLPHINPAY_NETWORK=testnet
# Optional: Custom RPC
# NEXT_PUBLIC_SUI_RPC_URL=https://fullnode.testnet.sui.io:443
3. Set Up Wallet Provider​
Create src/providers/wallet-provider.tsx:
"use client"
import { WalletProvider } from "@mysten/dapp-kit"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
import { SuiClientProvider, WalletProvider } from "@mysten/dapp-kit"
import { getFullnodeUrl } from "@mysten/sui.js/client"
import { ReactNode } from "react"
const queryClient = new QueryClient()
interface ProvidersProps {
children: ReactNode
}
export function Providers({ children }: ProvidersProps) {
return (
<QueryClientProvider client={queryClient}>
<SuiClientProvider
networks={{
testnet: { url: getFullnodeUrl("testnet") },
mainnet: { url: getFullnodeUrl("mainnet") },
}}
defaultNetwork="testnet"
>
<WalletProvider>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
)
}
Basic Payment Component​
Create Payment Form​
Create src/components/payment-form.tsx:
"use client"
import { useState } from "react"
import { useWallet } from "@mysten/dapp-kit"
import { createClient, suiToMist } from "@dolphinpay/sdk"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
interface PaymentFormData {
merchantAddress: string
amount: string
description: string
}
export function PaymentForm() {
const { signAndExecuteTransactionBlock } = useWallet()
const [formData, setFormData] = useState<PaymentFormData>({
merchantAddress: "",
amount: "",
description: "",
})
const [loading, setLoading] = useState(false)
const [paymentId, setPaymentId] = useState<string | null>(null)
const client = createClient({
packageId: process.env.NEXT_PUBLIC_DOLPHINPAY_PACKAGE_ID!,
network: "testnet",
})
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!signAndExecuteTransactionBlock) {
alert("Please connect your wallet first")
return
}
setLoading(true)
try {
// Build payment creation transaction
const txb = client.payment.buildCreatePayment({
merchant: formData.merchantAddress,
amount: suiToMist(parseFloat(formData.amount)),
currency: "0x2::sui::SUI",
description: formData.description,
metadata: {
createdBy: "basic-integration-example",
},
expirySeconds: 3600, // 1 hour
})
// Execute transaction
const result = await signAndExecuteTransactionBlock({
transactionBlock: txb,
})
if (result.digest) {
// Extract payment object ID from transaction result
const paymentObject = result.effects?.created?.find((obj) =>
obj.type?.includes("Payment")
)
if (paymentObject) {
setPaymentId(paymentObject.reference.objectId)
}
alert(`Payment created successfully! TX: ${result.digest}`)
}
} catch (error) {
console.error("Payment creation failed:", error)
alert(`Payment creation failed: ${error.message}`)
} finally {
setLoading(false)
}
}
const handleInputChange = (field: keyof PaymentFormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
}
return (
<div className="max-w-md mx-auto space-y-6">
<Card>
<CardHeader>
<CardTitle>Create Payment</CardTitle>
<CardDescription>
Create a new payment request using DolphinPay
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="merchant">Merchant Address</Label>
<Input
id="merchant"
placeholder="0x..."
value={formData.merchantAddress}
onChange={(e) =>
handleInputChange("merchantAddress", e.target.value)
}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="amount">Amount (SUI)</Label>
<Input
id="amount"
type="number"
step="0.000001"
min="0.0001"
max="1000"
placeholder="10.0"
value={formData.amount}
onChange={(e) => handleInputChange("amount", e.target.value)}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Input
id="description"
placeholder="Payment for services"
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? "Creating Payment..." : "Create Payment"}
</Button>
</form>
</CardContent>
</Card>
{paymentId && (
<Card>
<CardHeader>
<CardTitle>Payment Created!</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-2">
Payment ID: {paymentId}
</p>
<p className="text-sm">
Share this payment ID with customers so they can execute the
payment.
</p>
</CardContent>
</Card>
)}
</div>
)
}
Create Payment Execution Component​
Create src/components/payment-execution.tsx:
"use client"
import { useState } from "react"
import { useWallet } from "@mysten/dapp-kit"
import { createClient } from "@dolphinpay/sdk"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
export function PaymentExecution() {
const { signAndExecuteTransactionBlock } = useWallet()
const [paymentId, setPaymentId] = useState("")
const [loading, setLoading] = useState(false)
const client = createClient({
packageId: process.env.NEXT_PUBLIC_DOLPHINPAY_PACKAGE_ID!,
network: "testnet",
})
const handleExecutePayment = async () => {
if (!signAndExecuteTransactionBlock || !paymentId) {
alert("Please connect wallet and enter payment ID")
return
}
setLoading(true)
try {
// Get payment details first
const payment = await client.payment.getPayment(paymentId)
if (payment.payment.status !== 0) {
// 0 = PENDING
alert("Payment is not in pending state")
return
}
// Get user's coins to find one with sufficient balance
const coins = await client.provider.getCoins({
owner: await client.provider.getAddress(),
coinType: payment.payment.currency,
})
const suitableCoin = coins.data.find(
(coin) => BigInt(coin.balance) >= BigInt(payment.payment.amount)
)
if (!suitableCoin) {
alert("Insufficient balance for this payment")
return
}
// Build payment execution transaction
const txb = client.payment.buildExecutePayment(
{
paymentId,
coinObjectId: suitableCoin.coinObjectId,
},
payment.payment.currency
)
// Execute transaction
const result = await signAndExecuteTransactionBlock({
transactionBlock: txb,
})
alert(`Payment executed successfully! TX: ${result.digest}`)
} catch (error) {
console.error("Payment execution failed:", error)
alert(`Payment execution failed: ${error.message}`)
} finally {
setLoading(false)
}
}
return (
<Card className="max-w-md mx-auto">
<CardHeader>
<CardTitle>Execute Payment</CardTitle>
<CardDescription>
Execute a pending payment using your wallet
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="payment-id">Payment ID</Label>
<Input
id="payment-id"
placeholder="0x..."
value={paymentId}
onChange={(e) => setPaymentId(e.target.value)}
required
/>
</div>
<Button
onClick={handleExecutePayment}
className="w-full"
disabled={loading || !paymentId}
>
{loading ? "Executing Payment..." : "Execute Payment"}
</Button>
</CardContent>
</Card>
)
}
Main Application Page​
Update the Main Page​
Edit src/app/page.tsx:
"use client"
import { useState } from "react"
import { ConnectButton } from "@mysten/dapp-kit"
import { PaymentForm } from "@/components/payment-form"
import { PaymentExecution } from "@/components/payment-execution"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
export default function Home() {
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b">
<div className="container mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold">DolphinPay Integration</h1>
<ConnectButton />
</div>
</header>
{/* Main Content */}
<main className="container mx-auto px-4 py-8">
<div className="text-center mb-8">
<h2 className="text-3xl font-bold mb-2">Basic Payment Integration</h2>
<p className="text-muted-foreground">
A simple example of integrating DolphinPay for cryptocurrency
payments
</p>
</div>
<Tabs defaultValue="create" className="max-w-2xl mx-auto">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="create">Create Payment</TabsTrigger>
<TabsTrigger value="execute">Execute Payment</TabsTrigger>
</TabsList>
<TabsContent value="create" className="mt-6">
<PaymentForm />
</TabsContent>
<TabsContent value="execute" className="mt-6">
<PaymentExecution />
</TabsContent>
</Tabs>
</main>
{/* Footer */}
<footer className="border-t mt-16">
<div className="container mx-auto px-4 py-8 text-center text-muted-foreground">
<p>
Built with{" "}
<a
href="https://github.com/dolphinslab/dolphin-pay"
className="text-primary hover:underline"
>
DolphinPay
</a>{" "}
on{" "}
<a href="https://sui.io" className="text-primary hover:underline">
Sui Blockchain
</a>
</p>
</div>
</footer>
</div>
)
}
UI Components​
Create Basic UI Components​
Create src/components/ui/button.tsx:
import { Button as ButtonPrimitive } from "@radix-ui/react-button"
import { cn } from "@/lib/utils"
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "default" | "outline" | "ghost"
size?: "default" | "sm" | "lg"
}
export function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonProps) {
return (
<ButtonPrimitive
className={cn(
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"disabled:pointer-events-none disabled:opacity-50",
{
"bg-primary text-primary-foreground hover:bg-primary/90":
variant === "default",
"border border-input hover:bg-accent hover:text-accent-foreground":
variant === "outline",
"hover:bg-accent hover:text-accent-foreground": variant === "ghost",
},
{
"h-10 px-4 py-2": size === "default",
"h-9 rounded-md px-3": size === "sm",
"h-11 rounded-md px-8": size === "lg",
},
className
)}
{...props}
/>
)
}
Create src/components/ui/input.tsx:
import { cn } from "@/lib/utils"
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
export function Input({ className, ...props }: InputProps) {
return (
<input
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm",
"ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium",
"placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed",
"disabled:opacity-50",
className
)}
{...props}
/>
)
}
Create src/components/ui/label.tsx:
import { Label as LabelPrimitive } from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {}
export function Label({ className, ...props }: LabelProps) {
return (
<LabelPrimitive
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed",
"peer-disabled:opacity-70",
className
)}
{...props}
/>
)
}
Create src/components/ui/card.tsx:
import { cn } from "@/lib/utils"
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
export function Card({ className, ...props }: CardProps) {
return (
<div
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
)
}
export function CardHeader({ className, ...props }: CardProps) {
return (
<div
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
)
}
export function CardTitle({ className, ...props }: CardProps) {
return (
<h3
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
)
}
export function CardDescription({ className, ...props }: CardProps) {
return (
<p className={cn("text-sm text-muted-foreground", className)} {...props} />
)
}
export function CardContent({ className, ...props }: CardProps) {
return <div className={cn("p-6 pt-0", className)} {...props} />
}
Utility Functions​
Create src/lib/utils.ts:
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Running the Example​
1. Install Dependencies​
npm install
2. Set Up Environment​
# Copy environment template
cp .env.example .env.local
# Edit .env.local with your settings
echo "NEXT_PUBLIC_DOLPHINPAY_PACKAGE_ID=0x9c7ca262d020b005e0e6b6a5d083b329d58716e0d80c07b46804324074468f9c" >> .env.local
echo "NEXT_PUBLIC_DOLPHINPAY_NETWORK=testnet" >> .env.local
3. Run Development Server​
npm run dev
4. Test the Integration​
- Connect Wallet: Click the "Connect Wallet" button and connect your Sui wallet
- Create Payment: Fill out the payment form and create a payment
- Execute Payment: Use the payment ID to execute the payment from another wallet
Testing with Testnet​
Get Testnet SUI​
- Join Sui Discord
- Use the faucet command:
!faucet <your-wallet-address> - Or visit the official faucet
Test the Flow​
- Create Payment: Use the form to create a payment (costs ~0.01 SUI)
- Copy Payment ID: Note the payment object ID from the transaction
- Execute Payment: Use the payment execution tab to pay (costs ~0.015 SUI)
- Verify: Check the transaction on SuiVision Explorer
Customization​
Add Fee Calculation​
import { calculateFeeBreakdown } from "@dolphinpay/sdk"
function FeeCalculator({ amount }: { amount: number }) {
const fees = calculateFeeBreakdown(suiToMist(amount))
return (
<div className="text-sm text-muted-foreground">
<p>Platform fee: {mistToSui(fees.platformFee)} SUI</p>
<p>Merchant fee: {mistToSui(fees.merchantFee)} SUI</p>
<p>You receive: {mistToSui(fees.netAmount)} SUI</p>
</div>
)
}
Add Payment Status Tracking​
import { useQuery } from "@tanstack/react-query"
function PaymentStatus({ paymentId }: { paymentId: string }) {
const { data: payment } = useQuery({
queryKey: ["payment", paymentId],
queryFn: () => client.payment.getPayment(paymentId),
refetchInterval: 5000, // Poll every 5 seconds
})
if (!payment) return <div>Loading...</div>
return (
<div className="text-sm">
Status:{" "}
{payment.payment.status === 0
? "Pending"
: payment.payment.status === 1
? "Completed"
: "Other"}
</div>
)
}
Next Steps​
This basic integration provides the foundation for more advanced features:
- Merchant Setup - Become a merchant and accept payments
- Payment Operations Guide - Advanced payment handling
- Merchant Operations Guide - Merchant management features
- Event Querying Guide - Track payment analytics
Troubleshooting​
Common Issues​
"Transaction failed"
- Check you have enough testnet SUI for gas
- Verify merchant address is valid (starts with 0x)
- Ensure amount is within limits (0.0001 - 1000 SUI)
"Wallet not connected"
- Make sure Sui Wallet extension is installed
- Check that wallet is connected to testnet
- Try refreshing the page
"Invalid payment ID"
- Verify the payment ID format (64-character hex string)
- Check that payment exists on testnet
- Ensure payment is in pending state
Get Help​
- Documentation: Check other sections of this guide
- GitHub Issues: https://github.com/dolphinslab/dolphin-pay/issues
- Community: Join Sui Discord for community support
Ready to build something more complex? Check out our merchant integration guide or SDK documentation for more features!