Event Querying Guide
Query DolphinPay's on-chain events, build payment history, and compute merchant statistics.
The Event Module uses Sui gRPC (listEvents on SuiGrpcClient). There is no GraphQL client, no custom indexer, and no WebSocket subscriptions — for near-real-time updates, poll.
Setup
The event module is available on any initialized client:
import { createClient } from '@dolphinpay/sdk';
const client = createClient({
network: 'testnet',
packageId: '<PACKAGE_ID>',
adminConfigId: '<ADMIN_CONFIG_ID>',
});
const events = client.events;
Query Parameters and Results
Every query* method takes the same optional parameters:
interface EventQueryParams {
cursor?: string; // endCursor of the previous page
limit?: number; // default 50 (servers cap page sizes)
descending?: boolean; // newest first
}
There is no server-side sender/address filter — events are filtered by event type only. To scope results to a merchant or payer, filter the returned payloads client-side (as getPaymentHistory does).
Results are typed:
interface EventQueryResult<T> {
events: Array<{
data: T; // the parsed Move event payload
sender: string; // transaction sender
transactionDigest: string;
checkpoint: string | null;
}>;
pageInfo: {
hasNextPage: boolean;
endCursor: string | null;
};
}
Each event payload carries its own timestamp field (epoch ms, as a string) — there is no transport-level event.timestamp.
Payment Events
Available methods and their payload types:
| Method | Payload fields (event.data) |
|---|---|
queryPaymentCreated | payment_id, merchant, amount, currency, description, timestamp |
queryPaymentCompleted | payment_id, merchant, payer, amount, currency, fee_amount, net_amount, tx_hash, timestamp |
queryPaymentFailed | payment_id, merchant, payer, reason, timestamp |
queryPaymentCancelled | payment_id, reason, timestamp |
queryPaymentExpired | payment_id, timestamp |
The current contract defines PaymentFailed and PaymentExpired, but no
current execution path emits either event. Their query methods normally return
no events. In particular, an expired execution aborts and rolls back its
attempted status update; detect expiry from the payment object's expiresAt
timestamp.
const created = await client.events.queryPaymentCreated({
limit: 20,
descending: true, // newest first
});
for (const event of created.events) {
console.log(`Payment ${event.data.payment_id}`);
console.log(` Merchant: ${event.data.merchant}`);
console.log(` Amount: ${event.data.amount} (USDC base units)`);
console.log(` Created: ${new Date(Number(event.data.timestamp)).toLocaleString()}`);
console.log(` Tx: ${event.transactionDigest}`);
}
const completed = await client.events.queryPaymentCompleted({ limit: 20 });
for (const event of completed.events) {
console.log(`Payment ${event.data.payment_id} completed`);
console.log(` Payer: ${event.data.payer}`);
console.log(` Fee: ${event.data.fee_amount}`); // "0" while the platform fee is 0 bps
console.log(` Net: ${event.data.net_amount}`);
}
Merchant Events
| Method | Payload fields (event.data) |
|---|---|
queryMerchantRegistered | merchant_id, owner, name, timestamp |
queryMerchantSettingsUpdated | merchant_id, updated_by, timestamp |
queryMerchantStatusChanged | merchant_id, is_active, changed_by, timestamp |
const registered = await client.events.queryMerchantRegistered({ limit: 10 });
for (const event of registered.events) {
console.log(`Merchant ${event.data.merchant_id} (${event.data.name})`);
console.log(` Owner: ${event.data.owner}`);
}
Payment History
getPaymentHistory aggregates the payment lifecycle events for one merchant into unified entries:
const history = await client.events.getPaymentHistory({
merchantAddress: '0xMERCHANT_ADDRESS', // the merchant address in the payment, not the object ID
limit: 50,
// cursor?: string — paginate over PaymentCreated events
});
for (const payment of history.payments) {
console.log(`Payment ${payment.id}: ${payment.status}`);
// status: 'pending' | 'completed' | 'failed' | 'expired' | 'cancelled'
console.log(` Amount: ${payment.amount} (USDC base units)`);
console.log(` Created: ${new Date(payment.createdAt).toLocaleString()}`);
if (payment.status === 'completed') {
console.log(` Payer: ${payment.payer}`);
console.log(` Net: ${payment.netAmount}, Fee: ${payment.feeAmount}`);
}
if (payment.failureReason) console.log(` Failed: ${payment.failureReason}`);
if (payment.cancellationReason) console.log(` Cancelled: ${payment.cancellationReason}`);
}
Pagination
import type { PaymentHistoryEntry } from '@dolphinpay/sdk';
let cursor: string | undefined;
const all: PaymentHistoryEntry[] = [];
do {
const page = await client.events.getPaymentHistory({
merchantAddress: '0xMERCHANT_ADDRESS',
limit: 100,
cursor,
});
all.push(...page.payments);
cursor = page.pageInfo.hasNextPage ? (page.pageInfo.endCursor ?? undefined) : undefined;
} while (cursor);
getPaymentHistory fetches PaymentCreated events by type and filters by merchant client-side, then resolves statuses by fetching up to the latest 200 completed/failed/cancelled/expired events each. The current contract does not emit failed or expired events, so those states are not produced by this aggregation. On a busy deployment, older completed or cancelled payments may show as pending if their terminal event falls outside that window. For production-grade reporting, run your own indexer.
Merchant Statistics
import { unitsToUsdc } from '@dolphinpay/sdk';
const stats = await client.events.getMerchantStatistics('0xMERCHANT_ADDRESS');
console.log(`Total payments: ${stats.totalPayments}`);
console.log(`Completed: ${stats.totalCompleted}`);
console.log(`Failed: ${stats.totalFailed}`);
console.log(`Cancelled: ${stats.totalCancelled}`);
console.log(`Expired: ${stats.totalExpired}`);
console.log(`Volume: ${unitsToUsdc(stats.totalVolume)} USDC`);
console.log(`Fees: ${unitsToUsdc(stats.totalFees)} USDC`); // 0 while the fee is 0 bps
const successRate = stats.totalPayments
? (stats.totalCompleted / stats.totalPayments) * 100
: 0;
console.log(`Success rate: ${successRate.toFixed(2)}%`);
getMerchantStatistics scans up to the latest 1000 events per lifecycle type and aggregates in memory. Deployments with more events than that will be undercounted — use an indexer for exact figures.
Near-Real-Time Updates (Polling)
There are no event subscriptions; poll on an interval instead:
import type { PaymentHistoryEntry } from '@dolphinpay/sdk';
function watchPayments(
merchantAddress: string,
onNewPayment: (p: PaymentHistoryEntry) => void,
intervalMs = 5000
) {
const seen = new Set<string>();
const timer = setInterval(async () => {
try {
const { payments } = await client.events.getPaymentHistory({
merchantAddress,
limit: 10,
});
for (const p of payments) {
if (!seen.has(p.id)) {
seen.add(p.id);
onNewPayment(p);
}
}
} catch (error) {
console.error('Polling error:', error);
}
}, intervalMs);
return () => clearInterval(timer);
}
Best Practices
- Paginate with
pageInfo.endCursorrather than requesting huge limits — the server caps page sizes. - Filter client-side by
event.data.merchant/event.data.payer; there is no address filter in the query API. - Cache aggregated results (history, statistics) — each call fans out into several event queries.
- Know the windows: history resolution scans 200 terminal events per type, statistics scan 1000 per type. Beyond that, build an indexer.
Next Steps
- Payment Operations — payment management
- Merchant Operations — merchant setup
- Basic Payment Example — complete integration example
- TypeScript SDK overview