Skip to main content

Payment Operations Guide

Create, execute, cancel, and query payments with the DolphinPay TypeScript SDK.

Payments are USDC only (native Circle USDC, 6 decimals) — the contract validates the coin type against the shared AdminConfig and aborts on anything else. The platform fee is 0 bps by policy, there is no merchant fee, and there is no refund flow.

All build* methods return a Transaction that you sign and execute with your own wallet (e.g. useSignAndExecuteTransaction from @mysten/dapp-kit). Query methods read chain state over gRPC.

Creating Payments

Basic Payment Creation

import { createClient, usdcToUnits, USDC_TYPES } from '@dolphinpay/sdk';

const client = createClient({
network: 'testnet',
packageId: '<PACKAGE_ID>',
adminConfigId: '<ADMIN_CONFIG_ID>',
});

const txb = client.payment.buildCreatePayment({
merchant: '0xMERCHANT_ADDRESS',
amount: usdcToUnits('10'), // "10000000" — USDC base units (6 decimals)
currencyType: USDC_TYPES.testnet, // native Circle USDC coin type
description: 'Premium subscription',
expirySeconds: 3600, // default 3600 (1 hour), max 1 year
});

// Sign and execute with your wallet
const result = await signAndExecute({ transaction: txb });
console.log('Payment created:', result.digest);

CreatePaymentParams:

FieldTypeNotes
merchantstringMerchant's receiving address
amountstring | numberUSDC base units; max 1000000000000 (1,000,000 USDC)
currencyTypestringFully qualified coin type — must be the network's native Circle USDC
descriptionstringMax 500 chars
metadata?Record<string, string>Up to 10 entries, inserted on-chain in record order
expirySeconds?numberDefault 3600

The created Payment object is shared on-chain so that any payer can execute it.

metadata is validated client-side (max 10 entries) and its entries are inserted into the on-chain VecMap in the order they appear in the record; omitted or empty metadata produces an empty map. Metadata is parsed back into payment.metadata when you query the payment.

const txb = client.payment.buildCreatePayment({
merchant: '0xMERCHANT_ADDRESS',
amount: usdcToUnits('10'),
currencyType: USDC_TYPES.testnet,
description: 'Premium subscription',
metadata: { orderId: 'ORDER-123', customerRef: 'CUST-42' },
expirySeconds: 3600,
});

Payment with Merchant Validation

Validates on-chain that the Merchant object exists and is active, and routes to its receiving address:

const txb = client.payment.buildCreatePaymentWithMerchant({
merchantObjectId: '0xMERCHANT_OBJECT_ID', // Merchant object, not an address
amount: usdcToUnits('50'),
currencyType: USDC_TYPES.testnet,
description: 'Service payment',
expirySeconds: 1800,
});

Executing Payments

Payment execution takes the parameters and the currency type as a separate second argument:

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

const txb = client.payment.buildExecutePayment(
{
paymentId: '0xPAYMENT_OBJECT_ID',
coinObjectId: '0xUSDC_COIN_OBJECT_ID', // a Coin<USDC> you own
amount: '10000000', // optional but recommended — see below
},
USDC_TYPES.testnet // currency type argument
);

const result = await signAndExecute({ transaction: txb });
Exact amount matching

The contract requires the coin value to exactly equal the payment amount. When you pass amount, the SDK splits exactly that value from coinObjectId inside the transaction and the remainder stays with you. If you omit amount, the whole coin is consumed and must already match exactly.

Execution with Merchant Routing

const txb = client.payment.buildExecutePaymentWithMerchant(
{
paymentId: '0xPAYMENT_OBJECT_ID',
merchantObjectId: '0xMERCHANT_OBJECT_ID',
coinObjectId: '0xUSDC_COIN_OBJECT_ID',
amount: '10000000',
},
USDC_TYPES.testnet
);

Finding a USDC Coin to Pay With

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

const { objects } = await client.suiClient.listOwnedObjects({
owner: payerAddress,
type: `0x2::coin::Coin<${USDC_TYPES.testnet}>`,
limit: 50,
include: { json: true },
});

const coin = objects.find(
(obj) => BigInt((obj.json as any).balance) >= BigInt(paymentAmount)
);
if (!coin) throw new Error('No USDC coin with sufficient balance');

Cancelling Payments

Only pending payments can be cancelled (by the merchant that created them):

const txb = client.payment.buildCancelPayment({
paymentId: '0xPAYMENT_OBJECT_ID',
reason: 'Order cancelled by customer',
});

const result = await signAndExecute({ transaction: txb });

There are no refunds: once a payment is completed, funds have been transferred and cannot be reversed on-chain.

Querying Payment Data

Get Payment Details

getPayment returns a PaymentQueryResult — the parsed Payment plus object metadata:

const result = await client.payment.getPayment('0xPAYMENT_OBJECT_ID');
const { payment } = result;

console.log('Amount (base units):', payment.amount);
console.log('Currency:', payment.currency);
console.log('Merchant:', payment.merchant);
console.log('Payer:', payment.payer); // null until executed
console.log('Status:', payment.status); // PaymentStatus enum value
console.log('Created:', new Date(Number(payment.createdAt)));
console.log('Expires:', new Date(Number(payment.expiresAt)));

console.log('Object version:', result.version, 'digest:', result.digest);
console.log('Metadata:', payment.metadata); // e.g. { orderId: 'ORDER-123' }

payment.metadata is strictly parsed from the on-chain VecMap back into a Record<string, string>; malformed on-chain data throws a diagnostic error naming the field path (e.g. Payment: invalid value at "metadata.contents[1]": …).

Check Payment Status

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

const status = await client.payment.getPaymentStatus('0xPAYMENT_OBJECT_ID');

// PaymentStatus: PENDING = 0, SUCCESS = 1, FAILED = 2, EXPIRED = 3, CANCELLED = 4
if (status === PaymentStatus.PENDING) {
console.log('Payment is pending');
} else if (status === PaymentStatus.SUCCESS) {
console.log('Payment completed');
}

Check Expiry

const isExpired = await client.payment.isPaymentExpired('0xPAYMENT_OBJECT_ID');

isPaymentExpired compares expiresAt with local time. Use it instead of waiting for PaymentStatus.EXPIRED: the current contract's expired execution path aborts, so its attempted status update is rolled back and the stored payment remains PENDING.

Dry Run Testing

Each builder has a dryRun* counterpart that simulates via gRPC:

const result = await client.payment.dryRunCreatePayment(
{
merchant: '0xMERCHANT_ADDRESS',
amount: usdcToUnits('10'),
currencyType: USDC_TYPES.testnet,
description: 'Test payment',
expirySeconds: 3600,
},
'0xSENDER_ADDRESS'
);

if (result.success) {
// result.effects, result.events, result.balanceChanges
const txb = client.payment.buildCreatePayment({ /* same params */ });
await signAndExecute({ transaction: txb });
} else {
console.error('Would fail:', result.error ?? result.status);
}

Also available: dryRunCreatePaymentWithMerchant(params, sender), dryRunExecutePayment(params, currencyType, sender), dryRunExecutePaymentWithMerchant(params, currencyType, sender), dryRunCancelPayment(params, sender).

Fees

The platform fee is 0 bps by policy, read from AdminConfig at execution. There is no merchant fee. To display a breakdown using the live rate:

import { calculateFeeBreakdown, usdcToUnits } from '@dolphinpay/sdk';

const { defaultPlatformFeeBps } = await client.admin.getAdminConfig();
const fees = calculateFeeBreakdown(usdcToUnits('100'), defaultPlatformFeeBps);
// With 0 bps: { amount: "100000000", platformFee: "0", totalFee: "0", netAmount: "100000000" }

Error Handling

Builders validate locally and throw before signing:

import { validatePaymentAmount, validateExpiry, usdcToUnits } from '@dolphinpay/sdk';

try {
validatePaymentAmount(usdcToUnits('10')); // throws if ≤ 0 or > 1,000,000 USDC
validateExpiry(3600); // throws if ≤ 0 or > 1 year

const txb = client.payment.buildCreatePayment({ /* ... */ });
const result = await signAndExecute({ transaction: txb });
} catch (error) {
console.error(error.message);
}

On-chain aborts map to PaymentErrorCode, e.g. E_INVALID_AMOUNT (100), E_PAYMENT_EXPIRED (101), E_AMOUNT_MISMATCH (104), E_CURRENCY_MISMATCH (105), E_MERCHANT_INACTIVE (111).

Next Steps