Skip to main content

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​

  1. Connect Wallet: Click the "Connect Wallet" button and connect your Sui wallet
  2. Create Payment: Fill out the payment form and create a payment
  3. Execute Payment: Use the payment ID to execute the payment from another wallet

Testing with Testnet​

Get Testnet SUI​

  1. Join Sui Discord
  2. Use the faucet command: !faucet <your-wallet-address>
  3. Or visit the official faucet

Test the Flow​

  1. Create Payment: Use the form to create a payment (costs ~0.01 SUI)
  2. Copy Payment ID: Note the payment object ID from the transaction
  3. Execute Payment: Use the payment execution tab to pay (costs ~0.015 SUI)
  4. 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:

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​


Ready to build something more complex? Check out our merchant integration guide or SDK documentation for more features!