Skip to main content

Basic Payment Integration

This example shows a minimal React integration: create a USDC payment request, and execute it from the payer's wallet.

What you'll build

  • A form that creates a payment request (merchant side)
  • A component that executes a pending payment (payer side)
  • Status polling via the SDK's gRPC queries

Key facts to keep in mind:

  • Payments are native Circle USDC only (6 decimals); the contract aborts on any other coin type
  • Amounts are base units — convert with usdcToUnits / unitsToUsdc
  • The platform fee is 0 bps and there are no merchant fees — the merchant receives the full amount
  • SUI is used only for gas; there are no on-chain refunds
  • The DolphinPay SDK talks to Sui over gRPC only (no JSON-RPC, no websockets)

Project setup

npx create-next-app@latest dolphinpay-integration
cd dolphinpay-integration

npm install @mysten/dapp-kit @mysten/sui @tanstack/react-query

SDK dependency

@dolphinpay/sdk has no stable npm registry release yet — build it from source and add it as a local file dependency:

git clone https://github.com/DolphinsLab/dolphin-pay.git
cd dolphin-pay/sdk && bun install && bun run build

Then add the built sdk/ directory to your project's package.json (this is how the repository's own frontend/package.json consumes it):

{
"dependencies": {
"@dolphinpay/sdk": "file:../dolphin-pay/sdk"
}
}

The file: path is relative to your project's package.json — adjust it to wherever you cloned the repository.

Environment

The package is live on Sui testnet. Take the current IDs from DEPLOYMENT.md at the repository root — the source of truth for deployment status and IDs. Create .env.local:

NEXT_PUBLIC_SUI_NETWORK=testnet
NEXT_PUBLIC_PACKAGE_ID=<PACKAGE_ID>
NEXT_PUBLIC_ADMIN_CONFIG_ID=<ADMIN_CONFIG_ID>

Shared SDK client

Create src/lib/dolphinpay.ts:

import { createClient } from '@dolphinpay/sdk';

export const dolphinPay = createClient({
network: 'testnet',
packageId: process.env.NEXT_PUBLIC_PACKAGE_ID!,
adminConfigId: process.env.NEXT_PUBLIC_ADMIN_CONFIG_ID!,
});

Wallet provider

@mysten/dapp-kit is used for wallet plumbing only (connect, sign, execute). All DolphinPay reads go through the SDK's gRPC client. Create src/providers.tsx:

'use client';

import { SuiClientProvider, WalletProvider } from '@mysten/dapp-kit';
import { getFullnodeUrl } from '@mysten/sui/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type { ReactNode } from 'react';

const queryClient = new QueryClient();
const networks = { testnet: { url: getFullnodeUrl('testnet') } };

export function Providers({ children }: { children: ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<SuiClientProvider networks={networks} defaultNetwork="testnet">
<WalletProvider>{children}</WalletProvider>
</SuiClientProvider>
</QueryClientProvider>
);
}

Create a payment (merchant side)

Create src/components/payment-form.tsx:

'use client';

import { useState } from 'react';
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { usdcToUnits, USDC_TYPES } from '@dolphinpay/sdk';
import { dolphinPay } from '@/lib/dolphinpay';

export function PaymentForm() {
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();
const [merchant, setMerchant] = useState('');
const [amount, setAmount] = useState('');
const [description, setDescription] = useState('');
const [loading, setLoading] = useState(false);
const [digest, setDigest] = useState<string | null>(null);

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setLoading(true);
try {
// usdcToUnits converts the exact decimal string to USDC base units
// (6 decimals) — no float conversion, so no precision loss
const txb = dolphinPay.payment.buildCreatePayment({
merchant,
amount: usdcToUnits(amount),
currencyType: USDC_TYPES.testnet,
description,
expirySeconds: 3600, // 1 hour
});

const result = await signAndExecuteTransaction({ transaction: txb });
setDigest(result.digest);
// The Payment object ID is in the transaction's created objects;
// look it up from the digest or match the digest in
// dolphinPay.events.queryPaymentCreated().
} catch (error) {
console.error('Payment creation failed:', error);
} finally {
setLoading(false);
}
}

return (
<form onSubmit={handleSubmit}>
<label>
Merchant address
<input value={merchant} onChange={(e) => setMerchant(e.target.value)} placeholder="0x..." required />
</label>
<label>
Amount (USDC)
<input type="number" step="0.000001" min="0.000001" value={amount}
onChange={(e) => setAmount(e.target.value)} placeholder="10.00" required />
</label>
<label>
Description
<input value={description} onChange={(e) => setDescription(e.target.value)} required />
</label>
<button type="submit" disabled={loading}>
{loading ? 'Creating…' : 'Create payment'}
</button>
{digest && <p>Created! Transaction: {digest}</p>}
</form>
);
}

The created Payment is a shared object — share its ID (or a /pay/[paymentId] link) with the payer.

Execute a payment (payer side)

The payer pays with a USDC coin object from their wallet. Create src/components/payment-execution.tsx:

'use client';

import { useState } from 'react';
import { useSignAndExecuteTransaction } from '@mysten/dapp-kit';
import { USDC_TYPES } from '@dolphinpay/sdk';
import { dolphinPay } from '@/lib/dolphinpay';

export function PaymentExecution() {
const { mutateAsync: signAndExecuteTransaction } = useSignAndExecuteTransaction();
const [paymentId, setPaymentId] = useState('');
const [coinObjectId, setCoinObjectId] = useState('');
const [loading, setLoading] = useState(false);

async function handleExecute() {
setLoading(true);
try {
// Read payment state via the SDK's gRPC client
const { payment } = await dolphinPay.payment.getPayment(paymentId);

if (payment.status !== 0) { // 0 = pending (see the PaymentStatus enum)
alert('Payment is not pending');
return;
}
if (await dolphinPay.payment.isPaymentExpired(paymentId)) {
alert('Payment has expired');
return;
}

// Passing `amount` splits the exact value from the coin in-transaction,
// so the coin only needs a balance >= payment.amount.
const txb = dolphinPay.payment.buildExecutePayment(
{
paymentId,
coinObjectId, // a USDC coin owned by the payer
amount: payment.amount,
},
USDC_TYPES.testnet
);

const result = await signAndExecuteTransaction({ transaction: txb });
alert(`Payment executed! TX: ${result.digest}`);
} catch (error) {
console.error('Payment execution failed:', error);
} finally {
setLoading(false);
}
}

return (
<div>
<label>
Payment ID
<input value={paymentId} onChange={(e) => setPaymentId(e.target.value)} placeholder="0x..." />
</label>
<label>
USDC coin object ID
<input value={coinObjectId} onChange={(e) => setCoinObjectId(e.target.value)} placeholder="0x..." />
</label>
<button onClick={handleExecute} disabled={loading || !paymentId || !coinObjectId}>
{loading ? 'Executing…' : 'Execute payment'}
</button>
</div>
);
}

Poll payment status

import { useQuery } from '@tanstack/react-query';
import { unitsToUsdc } from '@dolphinpay/sdk';
import { dolphinPay } from '@/lib/dolphinpay';

function PaymentStatusView({ paymentId }: { paymentId: string }) {
const { data } = useQuery({
queryKey: ['payment', paymentId],
queryFn: () => dolphinPay.payment.getPayment(paymentId),
refetchInterval: 5000,
});

if (!data) return <div>Loading…</div>;

const { payment } = data;
return (
<div>
<p>Amount: {unitsToUsdc(payment.amount)} USDC</p>
<p>Status: {payment.status === 0 ? 'Pending' : 'Finalized'}</p>
</div>
);
}

There is no fee math to display: the platform fee is 0 bps and merchants have no fees, so the payer pays exactly payment.amount (plus SUI gas) and the merchant receives all of it.

Testing on testnet

  1. Get tokens — testnet USDC from https://faucet.circle.com (select Sui testnet); testnet SUI for gas from https://faucet.sui.io
  2. Create a payment with one wallet
  3. Execute it with another wallet holding USDC
  4. Verify the transaction on SuiVision

Use the dryRun* methods (e.g. dolphinPay.payment.dryRunExecutePayment(params, currencyType, sender)) to validate transactions without spending gas.

Troubleshooting

Transaction aborts on execution

  • The coin must be testnet native Circle USDC — SUI or any other coin type is rejected on-chain
  • Pass amount so the SDK splits the exact value; the contract requires the coin value to equal the payment amount
  • Check the payment hasn't expired or been cancelled

Objects not found

  • Verify NEXT_PUBLIC_PACKAGE_ID / NEXT_PUBLIC_ADMIN_CONFIG_ID against DEPLOYMENT.md; older packages are defunct

Wallet issues

  • Ensure the wallet extension is installed and set to testnet

Next steps