Skip to main content

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:

MethodPayload fields (event.data)
queryPaymentCreatedpayment_id, merchant, amount, currency, description, timestamp
queryPaymentCompletedpayment_id, merchant, payer, amount, currency, fee_amount, net_amount, tx_hash, timestamp
queryPaymentFailedpayment_id, merchant, payer, reason, timestamp
queryPaymentCancelledpayment_id, reason, timestamp
queryPaymentExpiredpayment_id, timestamp
Defined but not emitted

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

MethodPayload fields (event.data)
queryMerchantRegisteredmerchant_id, owner, name, timestamp
queryMerchantSettingsUpdatedmerchant_id, updated_by, timestamp
queryMerchantStatusChangedmerchant_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);
Aggregation limitations

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)}%`);
Statistics limitation

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.endCursor rather 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