Skip to Content
ПосібникиСинхронізація транзакцій із Quasar

Синхронізація транзакцій із Quasar Cloud або Self-Hosted нодою

Цей посібник демонструє, як підключити хмарний або self-hosted індексатор Quasar до вашого Next.js застосунку. Завдяки Quasar транзакції користувача синхронізуються між усіма пристроями, відстежуються у фоні навіть після закриття вкладки та надсилаються на бекенд через криптографічно підписані вебхуки.


Перед початком

  • Застосунок із посібника Full-Stack React, налаштований із маршрутами SIWX та сховищем сесій. Браузер ніколи не бачить секретний ключ Quasar: запити надсилаються через безпечні Server Actions.
  • Секретний ключ Quasar у змінній QUASAR_SECRET_KEY на сервері. Для self-hosted ноди вкажіть її адресу в NEXT_PUBLIC_QUASAR_BASE_URL.

Крок 1: Встановлення клієнта Quasar

pnpm add @tuwaio/quasar-sdk

Крок 2: Додавання Server Actions

Server Actions перевіряють сесію SIWX із куки перед зверненням до Quasar API, захищаючи квоту додатку та приватність історії користувача:

src/app/quasarActions.ts
'use server'; import { Quasar, QuasarSDKError, type Transaction } from '@tuwaio/quasar-sdk';import { getSiwxServerSession, isSessionMatchingTarget } from '@tuwaio/sdk/siwx/server';import { cookies } from 'next/headers'; import { sessionStore } from '@/lib/authStores'; const quasar = new Quasar({ secretKey: process.env.QUASAR_SECRET_KEY ?? '', baseUrl: process.env.NEXT_PUBLIC_QUASAR_BASE_URL,}); const APP_NAME = 'my-app'; async function getSession() { return getSiwxServerSession({ cookieSource: await cookies(), sessionStore });} export async function syncTransaction(tx: Transaction): Promise<{ success: boolean; error?: string }> { const session = await getSession(); if (!session || !isSessionMatchingTarget(session, tx.from, tx.chainId)) { return { success: false, error: 'The signed-in wallet did not send this transaction.' }; } try { await quasar.pulsar.syncCreate(tx, APP_NAME); return { success: true }; } catch (error) { return { success: false, error: error instanceof QuasarSDKError ? error.message : 'Quasar is unavailable.' }; }} export async function getHistory(params: { walletAddress: string; page?: number }) { const session = await getSession(); if (!session || !isSessionMatchingTarget(session, params.walletAddress)) { return null; } return quasar.pulsar.getHistory({ walletAddress: params.walletAddress, page: params.page, limit: 10, appName: APP_NAME, });}

Крок 3: Синхронізація зі сховища транзакцій

Метод beforeTxProcess викликає preFlightTxCheck перед показом вікна підпису гаманця, а хук onRemoteCreate відправляє кожну нову транзакцію до Quasar через Server Action:

src/hooks/pulsarStore.ts
import { pulsarEvmAdapter } from '@tuwaio/evm-sdk/pulsar';import { preFlightTxCheck } from '@tuwaio/quasar-sdk/react';import { createBoundedUseStore, createPulsarStore, createTxInMemoryStore, type Transaction } from '@tuwaio/sdk/pulsar'; import { getHistory, syncTransaction } from '@/app/quasarActions';import { appChains, wagmiConfig } from '@/configs/appConfig'; export const pulsarStore = createPulsarStore<Transaction>({ name: 'my-app-transactions', adapter: [pulsarEvmAdapter(wagmiConfig, appChains)], beforeTxProcess: () => preFlightTxCheck(process.env.NEXT_PUBLIC_QUASAR_BASE_URL), onRemoteCreate: async (tx) => { const result = await syncTransaction(tx); if (!result.success) throw new Error(result.error); },}); export const usePulsarStore = createBoundedUseStore(pulsarStore); export const historyStore = createTxInMemoryStore<Transaction>({ localTransactionsPool: pulsarStore.getState().transactionsPool, reconcileUnsyncedTransactions: pulsarStore.getState().reconcileUnsyncedTransactions, getHistory: ({ page, walletAddress }) => getHistory({ walletAddress, page }), // Pending transactions sent from another device continue to be tracked here. onHistoryFetched: (remoteTxs) => pulsarStore.getState().injectExternalPendingTxs(remoteTxs),}); pulsarStore.subscribe((state) => historyStore.getState().syncWithLocalPool(state.transactionsPool)); export const useHistoryStore = createBoundedUseStore(historyStore);

Пакети викликів (EIP-5792)

Пакет, надісланий через wallet_sendCalls (tracker: TransactionTracker.EIP5792), синхронізується за ідентифікатором пакета, який знає лише гаманець. Щойно Pulsar дізнається хеш транзакції, що виконала пакет, надішліть його до Quasar: тоді Quasar відстежує пакет у мережі й надсилає його вебхук. Без хешу Quasar позначає пакет як Failed через годину після синхронізації. Потрібен @tuwaio/quasar-sdk 0.6 або новіший.

Додайте третій Server Action. Він читає пакет із Quasar, щоб перевірити, що його надіслав гаманець, з яким виконано вхід:

src/app/quasarActions.ts
export async function syncBatchHash(txKey: string, hash: `0x${string}`): Promise<{ success: boolean; error?: string }> { const session = await getSession(); const [batch] = session ? (await quasar.pulsar.getHistory({ txKey, appName: APP_NAME, limit: 1 })).docs : []; if (!session || !batch || !isSessionMatchingTarget(session, batch.from, batch.chainId)) { return { success: false, error: 'The signed-in wallet did not send this batch.' }; } try { await quasar.pulsar.syncHash(txKey, hash); return { success: true }; } catch (error) { return { success: false, error: error instanceof QuasarSDKError ? error.message : 'Quasar is unavailable.' }; }}

Потім стежте за хешами пакетів у сторі, поруч зі стором вкладки EVM. watchBatchHashes повідомляє про кожен пакет один раз, після його синхронізації; якщо дія не вдалася, він повторює спробу при наступній зміні стору:

src/hooks/pulsarStore.ts
import { watchBatchHashes } from '@tuwaio/quasar-sdk/pulsar'; import { syncBatchHash } from '@/app/quasarActions'; watchBatchHashes(pulsarStore, async (tx) => { const result = await syncBatchHash(tx.txKey, tx.hash); if (!result.success) throw new Error(result.error);});

Крок 4: Відображення історії

Передайте в Nova сховище історії замість локального пулу, налаштуйте пагінацію та завантажте першу сторінку після підписання входу:

src/providers/Providers.tsx
'use client'; import { EVMConnectorsWatcher } from '@tuwaio/evm-sdk/nova-connect';import { satelliteEVMAdapter } from '@tuwaio/evm-sdk/satellite';import { NovaConnectProvider, type NovaConnectProviderProps } from '@tuwaio/sdk/nova-connect';import { NovaTransactionsProvider } from '@tuwaio/sdk/nova-transactions/providers';import { getAdapterFromConnectorType } from '@tuwaio/sdk/orbit';import { type TxInMemoryPagination, useInitializeTransactionsPool } from '@tuwaio/sdk/pulsar';import { SatelliteConnectProvider, useSatelliteConnectStore } from '@tuwaio/sdk/satellite';import { isSessionMatchingTarget, useSiwxSessionStore } from '@tuwaio/sdk/siwx';import { type ReactNode, useEffect } from 'react'; import { appChains, wagmiConfig } from '@/configs/appConfig';import { useHistoryStore, usePulsarStore } from '@/hooks/pulsarStore'; const satelliteAdapter = satelliteEVMAdapter(wagmiConfig, appChains); const siwx: NovaConnectProviderProps['siwx'] = { getNonce: async () => { const res = await fetch('/api/siwx/nonce'); return ((await res.json()) as { nonce: string }).nonce; }, verifier: async (payload) => { const res = await fetch('/api/siwx/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); return res.ok ? res.json() : null; }, destroyer: async () => { await fetch('/api/siwx/logout', { method: 'POST' }); },}; // Loads the first page of the history when the connected wallet is signed in.function HistoryLoader() { const address = useSatelliteConnectStore((state) => state.activeConnection?.address); const session = useSiwxSessionStore((state) => state.session); const fetchInitial = useHistoryStore((state) => state.fetchInitial); const isSignedIn = Boolean(session && address && isSessionMatchingTarget(session, address)); useEffect(() => { if (isSignedIn && address) void fetchInitial(address); }, [isSignedIn, address, fetchInitial]); return null;} function useHistoryPagination(): TxInMemoryPagination { const isLoading = useHistoryStore((state) => state.isLoading); const isError = useHistoryStore((state) => state.isError); const currentPage = useHistoryStore((state) => state.currentPage); const hasMore = useHistoryStore((state) => state.hasMore); const fetchNextPage = useHistoryStore((state) => state.fetchNextPage); return { isLoading, isError, currentPage, hasMore, fetchNextPage };} function TransactionsUI() { const transactionsPool = useHistoryStore((state) => state.transactionsPool); const pagination = useHistoryPagination(); const initialTx = usePulsarStore((state) => state.initialTx); const closeTxTrackedModal = usePulsarStore((state) => state.closeTxTrackedModal); const executeTxAction = usePulsarStore((state) => state.executeTxAction); const initializeTransactionsPool = usePulsarStore((state) => state.initializeTransactionsPool); const getAdapter = usePulsarStore((state) => state.getAdapter); const activeConnection = useSatelliteConnectStore((state) => state.activeConnection); useInitializeTransactionsPool({ initializeTransactionsPool }); return ( <NovaTransactionsProvider transactionsPool={transactionsPool} pagination={pagination} initialTx={initialTx} closeTxTrackedModal={closeTxTrackedModal} executeTxAction={executeTxAction} connectedWalletAddress={activeConnection?.isConnected ? activeConnection.address : undefined} connectedAdapterType={getAdapterFromConnectorType(activeConnection?.connectorType ?? 'evm:')} adapter={getAdapter()} /> );} export function Providers({ children }: { children: ReactNode }) { const transactionsPool = useHistoryStore((state) => state.transactionsPool); const pagination = useHistoryPagination(); const getAdapter = usePulsarStore((state) => state.getAdapter); return ( <SatelliteConnectProvider adapter={satelliteAdapter} autoConnect> <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> <HistoryLoader /> <TransactionsUI /> <NovaConnectProvider appChains={appChains} transactionPool={transactionsPool} pagination={pagination} pulsarAdapter={getAdapter()} siwx={siwx} withBalance withChain > {children} </NovaConnectProvider> </SatelliteConnectProvider> );}

Надішліть транзакцію, відкрийте додаток у іншому браузері з тим самим гаманцем та увійдіть: транзакція миттєво з'явиться в історії та на панелі Quasar.


Серверні транзакції (виведення та виплати)

Для бекенд-виплат та релеєрів викликайте quasar.pulsar.syncCreate напряму без залучення браузера. Синхронізація є ідемпотентною за txKey, а Quasar надішле підписаний вебхук після фіналізації:

src/server/syncServerPayout.ts
import { Quasar } from '@tuwaio/quasar-sdk';import { TransactionTracker } from '@tuwaio/quasar-sdk/pulsar';import { OrbitAdapter } from '@tuwaio/sdk/orbit'; const quasar = new Quasar({ secretKey: process.env.QUASAR_SECRET_KEY ?? '', baseUrl: process.env.NEXT_PUBLIC_QUASAR_BASE_URL,}); export async function syncServerPayout(params: { hash: `0x${string}`; chainId: number; from: `0x${string}`; to: `0x${string}`; operationId: string;}) { return quasar.pulsar.syncCreate( { adapter: OrbitAdapter.EVM, tracker: TransactionTracker.Ethereum, txKey: params.hash, hash: params.hash, chainId: params.chainId, from: params.from, to: params.to, connectorType: 'evm:server', type: 'WITHDRAWAL', title: 'Withdrawal', pending: true, localTimestamp: Math.floor(Date.now() / 1000), payload: { operationId: params.operationId }, }, 'my-app', );}

📚 Наступні кроки

Востаннє оновлено