@tuwaio/pulsar-core 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -319
- package/dist/index.d.mts +611 -329
- package/dist/index.d.ts +611 -329
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -1,380 +1,152 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tuwaio/pulsar-core
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@tuwaio/pulsar-core)
|
|
4
|
-
[](
|
|
5
|
-
[](https://github.com/TuwaIO/pulsar-core/actions)
|
|
4
|
+
[](https://github.com/TuwaIO/pulsar-core/blob/main/packages/pulsar-core/LICENSE)
|
|
6
5
|
|
|
7
|
-
Layer 3 (L3) of the TUWA
|
|
6
|
+
`@tuwaio/pulsar-core` is the Layer 3 (L3) core package of **Pulsar**, the transaction tracking project of TUWA Stage 2 ("State & Connection", next to Satellite Connect). Built on **`zustand`** (with the `persist` middleware), **`immer`** and **`@tuwaio/orbit-core`**, it keeps the pool of tracked transactions, runs new transactions through chain adapters and restarts their trackers after a page reload. It has no chain logic, UI or network requests of its own: the chain adapters are [`@tuwaio/pulsar-evm`](https://pulsar.docs.tuwa.io/packages/pulsar-evm) and [`@tuwaio/pulsar-solana`](https://pulsar.docs.tuwa.io/packages/pulsar-solana).
|
|
8
7
|
|
|
9
8
|
---
|
|
10
9
|
|
|
11
|
-
## 🏛️
|
|
10
|
+
## 🏛️ Core Capabilities
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
## ✨ Key Features
|
|
22
|
-
|
|
23
|
-
- **Framework-Agnostic:** Orchestrate and integrate state logic into any environment (React, Vue, Svelte, or Node.js).
|
|
24
|
-
- **Multi-Chain by Design:** Isolated state machine adapter system to support heterogeneous blockchain networks.
|
|
25
|
-
- **Persistent State:** Client-side state persistence powered by browser storage engines to resume lifecycle tracking across refreshes.
|
|
26
|
-
- **Append-Only Ledger:** High-stability transaction pool management using FIFO eviction policies.
|
|
27
|
-
- **Type-Safe:** Zero-compromise TypeScript implementation with explicit interfaces.
|
|
12
|
+
- **Transaction store:** `createPulsarStore` returns a vanilla Zustand store. Its `executeTxAction` validates the metadata, sets `initialTx` for immediate UI feedback, checks the wallet's chain, runs the `beforeTxProcess` preflight, calls your `actionFunction` to sign and submit, adds the transaction to `transactionsPool` and starts its tracker.
|
|
13
|
+
- **Chain adapters:** pass one adapter or an array. Each transaction goes to the adapter whose `key` matches its `adapter` (the first adapter when none matches). Adapters implement the `TxAdapter` contract, so other chains can be added.
|
|
14
|
+
- **Persistence and resume:** the pool is saved to `localStorage` (see Browser Storage below). After a reload, `initializeTransactionsPool` restarts the trackers of pending transactions. The pool keeps at most `maxTransactions` (default 50) and evicts the oldest one.
|
|
15
|
+
- **Metadata safety:** each `title` string is limited to 100 characters, each `description` string to 300 and the JSON of `payload` to 10 KB, and strings must not contain executable-like patterns (`eval(`, `Function(`, `setTimeout`/`setInterval` with a string, `javascript:`). Invalid metadata throws `PulsarTransactionValidationError` before anything runs; invalid restored or remote transactions are dropped. This is a defensive gate, not a replacement for escaping output in your UI.
|
|
16
|
+
- **Remote sync:** `onRemoteCreate` sends every new transaction to your backend (for example Quasar) in the background, without its API keys, retries unconfirmed syncs later, and never delays or blocks tracking. `injectExternalPendingTxs` and `createTxInMemoryStore` bring the remote history back into the app; both drop transactions that fail validation.
|
|
17
|
+
- **Selectors and React binding:** `selectAllTransactions`, `selectPendingTransactions`, `selectTxByKey` and the `…ByActiveWallet` variants; `createBoundedUseStore` turns the store into a typed React hook.
|
|
18
|
+
- **Tracker building blocks:** `initializePollingTracker` (a polling loop with consecutive-failure retries) and `createTxUpdater` (keeps a tracker's copy of the transaction in sync with its store updates) for custom trackers.
|
|
28
19
|
|
|
29
20
|
---
|
|
30
21
|
|
|
31
22
|
## 💾 Installation
|
|
32
23
|
|
|
33
|
-
This package requires `zustand`, `immer` and `dayjs` as peer dependencies. You must install them alongside `@tuwaio/pulsar-core`.
|
|
34
|
-
|
|
35
24
|
```bash
|
|
36
|
-
# Using pnpm (recommended), but you can use npm, yarn or bun as well
|
|
37
25
|
pnpm add @tuwaio/pulsar-core @tuwaio/orbit-core zustand immer dayjs
|
|
38
26
|
```
|
|
39
27
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## 🚀 API & Usage
|
|
28
|
+
> [!IMPORTANT]
|
|
29
|
+
> `@tuwaio/orbit-core` (>=0.3), `zustand` (5.x), `immer` (11.x) and `dayjs` (1.x) are peer dependencies and must be installed alongside `@tuwaio/pulsar-core`. Add [`@tuwaio/pulsar-evm`](https://pulsar.docs.tuwa.io/packages/pulsar-evm) and/or [`@tuwaio/pulsar-solana`](https://pulsar.docs.tuwa.io/packages/pulsar-solana) for the chain adapters, and [`@tuwaio/pulsar-react`](https://pulsar.docs.tuwa.io/packages/pulsar-react) for React apps.
|
|
43
30
|
|
|
44
|
-
|
|
31
|
+
---
|
|
45
32
|
|
|
46
|
-
|
|
33
|
+
## 🚀 Usage
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
### Creating the store and sending a transaction
|
|
49
36
|
|
|
50
|
-
```
|
|
51
|
-
import {
|
|
37
|
+
```typescript
|
|
38
|
+
import { OrbitAdapter } from '@tuwaio/orbit-core';
|
|
39
|
+
import { createPulsarStore, type EvmTransaction, TransactionStatus } from '@tuwaio/pulsar-core';
|
|
52
40
|
import { pulsarEvmAdapter } from '@tuwaio/pulsar-evm';
|
|
41
|
+
import { createConfig, http, injected, sendTransaction } from '@wagmi/core';
|
|
42
|
+
import { sepolia } from 'viem/chains';
|
|
53
43
|
|
|
54
|
-
|
|
44
|
+
const wagmiConfig = createConfig({
|
|
45
|
+
chains: [sepolia],
|
|
46
|
+
connectors: [injected()],
|
|
47
|
+
transports: { [sepolia.id]: http() },
|
|
48
|
+
});
|
|
55
49
|
|
|
56
|
-
const
|
|
50
|
+
export const pulsarStore = createPulsarStore<EvmTransaction>({
|
|
51
|
+
name: 'pulsar-transactions', // localStorage key
|
|
52
|
+
adapter: pulsarEvmAdapter(wagmiConfig, [sepolia]),
|
|
53
|
+
});
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
// Once per page load, on the client: resume the transactions that were pending before a reload.
|
|
56
|
+
void pulsarStore.getState().initializeTransactionsPool();
|
|
57
|
+
|
|
58
|
+
export async function sendTip(to: `0x${string}`) {
|
|
59
|
+
await pulsarStore.getState().executeTxAction({
|
|
60
|
+
actionFunction: () => sendTransaction(wagmiConfig, { to, value: 1_000_000_000_000_000n }),
|
|
61
|
+
params: {
|
|
62
|
+
adapter: OrbitAdapter.EVM,
|
|
63
|
+
desiredChainID: sepolia.id,
|
|
64
|
+
type: 'tip',
|
|
65
|
+
title: ['Sending tip', 'Tip sent', 'Tip failed', 'Tip replaced'],
|
|
66
|
+
},
|
|
67
|
+
onSuccess: (tx) => console.log(tx.txKey, tx.status === TransactionStatus.Success),
|
|
68
|
+
});
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export type TransactionUnion = ExampleTx;
|
|
70
|
-
|
|
71
|
-
export const usePulsarStore = createBoundedUseStore(
|
|
72
|
-
createPulsarStore<TransactionUnion>({
|
|
73
|
-
name: storageName,
|
|
74
|
-
adapter: pulsarEvmAdapter(config, appChains),
|
|
75
|
-
maxTransactions: 100, // Optional: defaults to 50
|
|
76
|
-
beforeTxProcess: async () => {
|
|
77
|
-
// Optional global preflight. Throw here to block a transaction before wallet interaction.
|
|
78
|
-
await assertUserCanSubmitTransactions();
|
|
79
|
-
},
|
|
80
|
-
}),
|
|
81
|
-
);
|
|
71
|
+
// Read the state anywhere; `pulsarStore.subscribe` notifies you about changes.
|
|
72
|
+
pulsarStore.subscribe((state) => {
|
|
73
|
+
const pending = Object.values(state.transactionsPool).filter((tx) => tx.pending);
|
|
74
|
+
console.log(`${pending.length} pending transaction(s)`);
|
|
75
|
+
});
|
|
82
76
|
```
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
To prevent the `localStorage` from growing indefinitely, Pulsar Core implements a **FIFO (First-In, First-Out) Eviction Policy**.
|
|
78
|
+
`executeTxAction` rejects when the metadata is invalid, the wallet is on another chain and does not switch, `beforeTxProcess` throws (unless `abortOnTxError: false`) or the wallet rejects the transaction; `initialTx.error` holds the normalized error. For standard EVM transactions it resolves only when tracking has finished, so drive the UI from the store instead of awaiting it.
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
- **Eviction Process:** When the pool exceeds the `maxTransactions` limit, the oldest transaction (based on `localTimestamp`) is automatically removed from the state and storage when a new one is added.
|
|
80
|
+
The full React setup, with a wallet connector, typed transactions and the Solana variant, is in the **[React transaction tracking guide](https://docs.tuwa.io/guides/react-transaction-tracking)**.
|
|
90
81
|
|
|
91
|
-
###
|
|
82
|
+
### Preflight checks
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
`beforeTxProcess` runs after the chain check and before the wallet is asked to sign. Throw to block the transaction; a `beforeTxProcess` passed to `executeTxAction` replaces the global one for that transaction:
|
|
94
85
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
Invalid transactions are rejected before execution. Invalid pending transactions restored from persisted storage are removed during `initializeTransactionsPool()`. Invalid remote transactions passed to `injectExternalPendingTxs()` are skipped with a warning so the rest of the batch can still sync.
|
|
103
|
-
|
|
104
|
-
### `beforeTxProcess`
|
|
86
|
+
```typescript
|
|
87
|
+
import { OrbitAdapter } from '@tuwaio/orbit-core';
|
|
88
|
+
import { createPulsarStore, type EvmTransaction } from '@tuwaio/pulsar-core';
|
|
89
|
+
import { pulsarEvmAdapter } from '@tuwaio/pulsar-evm';
|
|
90
|
+
import { type Config } from '@wagmi/core';
|
|
91
|
+
import { sepolia } from 'viem/chains';
|
|
105
92
|
|
|
106
|
-
|
|
93
|
+
declare const wagmiConfig: Config;
|
|
94
|
+
declare function sendSwap(): Promise<`0x${string}`>;
|
|
107
95
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
await assertUserCanSubmitTransactions();
|
|
96
|
+
const pulsarStore = createPulsarStore<EvmTransaction>({
|
|
97
|
+
name: 'pulsar-transactions',
|
|
98
|
+
adapter: pulsarEvmAdapter(wagmiConfig, [sepolia]),
|
|
99
|
+
beforeTxProcess: () => {
|
|
100
|
+
if (!navigator.onLine) throw new Error('You are offline.');
|
|
114
101
|
},
|
|
115
102
|
});
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
You can override the global callback for one transaction by passing `beforeTxProcess` to `executeTxAction`.
|
|
119
103
|
|
|
120
|
-
|
|
121
|
-
await store.getState().executeTxAction({
|
|
104
|
+
await pulsarStore.getState().executeTxAction({
|
|
122
105
|
actionFunction: sendSwap,
|
|
123
106
|
beforeTxProcess: async () => {
|
|
124
|
-
await
|
|
125
|
-
|
|
126
|
-
params: {
|
|
127
|
-
adapter: OrbitAdapter.EVM,
|
|
128
|
-
desiredChainID: 1,
|
|
129
|
-
type: 'SWAP',
|
|
130
|
-
title: 'Swap',
|
|
131
|
-
description: 'Swap tokens',
|
|
107
|
+
const response = await fetch('/api/swaps/enabled');
|
|
108
|
+
if (!response.ok) throw new Error('Swaps are paused.');
|
|
132
109
|
},
|
|
110
|
+
params: { adapter: OrbitAdapter.EVM, desiredChainID: sepolia.id, type: 'swap', title: 'Swap' },
|
|
133
111
|
});
|
|
134
112
|
```
|
|
135
113
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
### `abortOnTxError`
|
|
139
|
-
|
|
140
|
-
Use the `abortOnTxError` parameter (defaults to `true`) to control error propagation during transaction preflight and remote synchronization:
|
|
141
|
-
|
|
142
|
-
- **Preflight hook (`beforeTxProcess`)**:
|
|
143
|
-
- When `abortOnTxError` is `true` (default), any error thrown in `beforeTxProcess` will populate the `initialTx.error` state and throw, aborting the transaction action immediately.
|
|
144
|
-
- When `abortOnTxError` is `false`, any error thrown in `beforeTxProcess` is caught, logged to the console as a warning, and the transaction execution continues.
|
|
145
|
-
- **Remote sync hook (`onRemoteCreate`)**:
|
|
146
|
-
- Errors in `onRemoteCreate` **always** abort the transaction flow (the transaction is not added to the local tracking pool, `initialTx.error` is populated, and the error is thrown) regardless of the `abortOnTxError` setting.
|
|
147
|
-
|
|
148
|
-
You can set `abortOnTxError` globally during store creation or override it locally in `executeTxAction`:
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
// Set globally (defaults to true if not provided)
|
|
152
|
-
const store = createPulsarStore<TransactionUnion>({
|
|
153
|
-
name: storageName,
|
|
154
|
-
adapter: pulsarEvmAdapter(config, appChains),
|
|
155
|
-
abortOnTxError: false, // Disable aborting for beforeTxProcess errors globally
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Override locally for a specific action
|
|
159
|
-
await store.getState().executeTxAction({
|
|
160
|
-
actionFunction: sendSwap,
|
|
161
|
-
abortOnTxError: true, // Force abort on beforeTxProcess errors for this action
|
|
162
|
-
params: {
|
|
163
|
-
adapter: OrbitAdapter.EVM,
|
|
164
|
-
desiredChainID: 1,
|
|
165
|
-
type: 'SWAP',
|
|
166
|
-
title: 'Swap',
|
|
167
|
-
description: 'Swap tokens',
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### The Returned Store API
|
|
173
|
-
|
|
174
|
-
The `createPulsarStore` function returns a vanilla Zustand store with the following state and actions:
|
|
175
|
-
|
|
176
|
-
#### **State**
|
|
177
|
-
|
|
178
|
-
- `transactionsPool: Record<string, T>`: The primary state object. This is a map of all tracked transactions, where the key is the transaction's unique `txKey` (e.g., a transaction hash).
|
|
179
|
-
- `initialTx?: InitialTransaction`: Holds the state of a transaction that is currently being initiated (e.g., waiting for a user's signature) but is not yet submitted to the network. Useful for providing instant UI feedback.
|
|
180
|
-
- `lastAddedTxKey?: string`: The `txKey` of the most recently added transaction.
|
|
181
|
-
|
|
182
|
-
#### **Actions**
|
|
183
|
-
|
|
184
|
-
- `executeTxAction(params)`: The primary, all-in-one function for initiating, sending, and tracking a new transaction. It runs `beforeTxProcess` and metadata validation before wallet interaction.
|
|
185
|
-
- `initializeTransactionsPool()`: An async function to re-initialize trackers for any pending transactions found in storage. **This is crucial for resuming tracking after a page reload.**
|
|
186
|
-
- `addTxToPool(tx)`: Adds a new transaction directly to the tracking pool.
|
|
187
|
-
- `updateTxParams(txKey, fields)`: Updates one or more properties of an existing transaction in the pool.
|
|
188
|
-
- `removeTxFromPool(txKey)`: Removes a transaction from the pool by its key.
|
|
189
|
-
- `closeTxTrackedModal(txKey?)`: A helper to manage UI state, which sets `isTrackedModalOpen` to `false` and clears the `initialTx` state.
|
|
190
|
-
- `getLastTxKey()`: Returns the `txKey` of the most recently added transaction.
|
|
191
|
-
|
|
192
|
-
#### **Selectors**
|
|
193
|
-
|
|
194
|
-
The package also provides a set of selector functions to help you efficiently query the transaction pool:
|
|
195
|
-
|
|
196
|
-
- `selectAllTransactions(pool)`: Returns all transactions sorted chronologically.
|
|
197
|
-
- `selectPendingTransactions(pool)`: Returns only transactions that are currently pending.
|
|
198
|
-
- `selectTxByKey(pool, txKey)`: Retrieves a specific transaction by its key.
|
|
199
|
-
- `selectAllTransactionsByActiveWallet(pool, address)`: Returns all transactions for a specific wallet.
|
|
200
|
-
- `selectPendingTransactionsByActiveWallet(pool, address)`: Returns pending transactions for a specific wallet.
|
|
201
|
-
|
|
202
|
-
---
|
|
203
|
-
|
|
204
|
-
### `createTxInMemoryStore({ ... })`
|
|
205
|
-
|
|
206
|
-
While `createPulsarStore` is the primary entry point for tracking _active_ transactions, `createTxInMemoryStore` provides an in-memory transaction store with synchronized local and remote sources. It is designed to keep a local transaction pool in sync with remote history, preserve terminal transaction states, and support paginated history loading.
|
|
207
|
-
|
|
208
|
-
#### **Configuration Example**
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
// src/app/actions - next js app routes example of server actions
|
|
212
|
-
'use server';
|
|
213
|
-
|
|
214
|
-
import { Quasar, Transaction } from '@tuwaio/quasar-sdk';
|
|
215
|
-
|
|
216
|
-
const quasar = new Quasar({
|
|
217
|
-
secretKey: process.env.QUASAR_SDK_SK ?? '',
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// --- Server Action for syncCreate ---
|
|
221
|
-
export async function syncTransaction(tx: Transaction) {
|
|
222
|
-
try {
|
|
223
|
-
console.log('Syncing tx to Quasar...', tx.txKey);
|
|
224
|
-
|
|
225
|
-
await quasar.pulsar.syncCreate(tx, RP_NAME);
|
|
226
|
-
|
|
227
|
-
return { success: true };
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error('Sync failed', error);
|
|
230
|
-
throw error;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// --- Server Action for getHistory ---
|
|
235
|
-
export async function getHistory(params?: {
|
|
236
|
-
walletAddress: string;
|
|
237
|
-
page?: number;
|
|
238
|
-
limit?: number;
|
|
239
|
-
chainId?: string;
|
|
240
|
-
status?: string;
|
|
241
|
-
txKey?: string;
|
|
242
|
-
appName?: string;
|
|
243
|
-
}) {
|
|
244
|
-
try {
|
|
245
|
-
const history = await quasar.pulsar.getHistory({
|
|
246
|
-
...params,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
return history;
|
|
250
|
-
} catch (error) {
|
|
251
|
-
console.error('Get history failed', error);
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
```ts
|
|
258
|
-
// src/store/pulsarStoreHook.ts - pulsar store hook example
|
|
259
|
-
|
|
260
|
-
'use client';
|
|
261
|
-
|
|
262
|
-
import { createBoundedUseStore, createPulsarStore, createTxInMemoryStore } from '@tuwaio/pulsar-core';
|
|
263
|
-
import { pulsarEvmAdapter } from '@tuwaio/pulsar-evm';
|
|
264
|
-
import { useSiwxSessionStore } from '@tuwaio/siwx-react';
|
|
265
|
-
|
|
266
|
-
import { appChains, config } from '@/configs/wagmiConfig';
|
|
267
|
-
import { getHistory, syncTransaction } from '@/app/actions';
|
|
114
|
+
With `abortOnTxError: false` (globally or per call), a `beforeTxProcess` error is logged and the transaction continues. `abortOnTxError` does not apply to `onRemoteCreate`.
|
|
268
115
|
|
|
269
|
-
|
|
116
|
+
### Remote sync and recovery
|
|
270
117
|
|
|
271
|
-
|
|
272
|
-
example = 'example',
|
|
273
|
-
}
|
|
118
|
+
Pass `onRemoteCreate` to send every new transaction to your backend, for example a server action that forwards it to [Quasar](https://sdk.docs.tuwa.io/quasar-cloud/overview). Pulsar keeps working when the backend is slow or down:
|
|
274
119
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
value: number;
|
|
279
|
-
};
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
export type TransactionUnion = ExampleTx;
|
|
283
|
-
|
|
284
|
-
const initialStore = createPulsarStore<TransactionUnion>({
|
|
285
|
-
name: storageName,
|
|
286
|
-
adapter: [pulsarEvmAdapter(config, appChains)],
|
|
287
|
-
onRemoteCreate: async (tx) => {
|
|
288
|
-
const auth = useSiwxSessionStore.getState().session;
|
|
289
|
-
await syncTransaction(tx, auth);
|
|
290
|
-
},
|
|
291
|
-
});
|
|
120
|
+
1. `addTxToPool` writes the transaction to the pool (and to `localStorage`) first, with `syncStatus: 'pending-sync'` and its key in `unsyncedTxKeys`, and the tracker starts right away. `onRemoteCreate` runs in the background: when it resolves, the transaction becomes `'synced'` and the key is removed; when it rejects, the error is logged and the key stays. Reject (throw) on failure: a resolved promise counts as synced.
|
|
121
|
+
2. The trackers keep following the transaction in the browser, so its status stays correct without the backend. A slow backend never delays tracking, and a sync interrupted by a closed tab is still listed after the reload.
|
|
122
|
+
3. `reconcileUnsyncedTransactions` calls `onRemoteCreate` again for every unsynced transaction that is not already being sent: at the start of every `executeTxAction`, when an unsynced transaction reaches a terminal status, and when `createTxInMemoryStore` loads the first history page. Successful ones become `'synced'`; failed ones stay listed, also across reloads.
|
|
292
123
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const pulsarInMemoryStore = createTxInMemoryStore<TransactionUnion>({
|
|
296
|
-
localTransactionsPool: initialStore.getState().transactionsPool,
|
|
297
|
-
reconcileUnsyncedTransactions: initialStore.getState().reconcileUnsyncedTransactions,
|
|
298
|
-
|
|
299
|
-
getHistory: async ({ page, walletAddress }) => {
|
|
300
|
-
try {
|
|
301
|
-
const auth = useSiwxSessionStore.getState().session;
|
|
302
|
-
const history = await getHistory(
|
|
303
|
-
{
|
|
304
|
-
walletAddress,
|
|
305
|
-
page,
|
|
306
|
-
limit: 10,
|
|
307
|
-
appName: 'Example App',
|
|
308
|
-
},
|
|
309
|
-
auth,
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
if (!history) {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
...history,
|
|
318
|
-
docs: history.docs as TransactionUnion[],
|
|
319
|
-
};
|
|
320
|
-
} catch (error) {
|
|
321
|
-
console.error('[PulsarHook] Failed to fetch history:', error);
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
},
|
|
124
|
+
`onRemoteCreate` receives a copy of the transaction without `pimlicoApiKey` and `gelatoApiKey`: configure provider keys on the backend instead (for Quasar, in the app settings). `bundlerUrl` is sent, so do not put API keys in it.
|
|
325
125
|
|
|
326
|
-
|
|
327
|
-
await initialStore.getState().injectExternalPendingTxs(remoteTxs);
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
initialStore.subscribe((state) => pulsarInMemoryStore.getState().syncWithLocalPool(state.transactionsPool));
|
|
332
|
-
|
|
333
|
-
export const usePulsarInMemoryStore = createBoundedUseStore(pulsarInMemoryStore);
|
|
334
|
-
```
|
|
126
|
+
To show the remote history, `createTxInMemoryStore` merges the pages returned by your `getHistory` with the local pool (terminal transactions are never overwritten by stale data, and transactions that fail validation are skipped), and `injectExternalPendingTxs` adds pending transactions from other devices to the local pool and tracks them. The complete Next.js + Quasar integration, with server actions and SIWX sessions, is in the **[TUWA SDK documentation](https://sdk.docs.tuwa.io/full-stack)**.
|
|
335
127
|
|
|
336
128
|
---
|
|
337
129
|
|
|
338
|
-
##
|
|
130
|
+
## 🗄️ Browser Storage
|
|
339
131
|
|
|
340
|
-
|
|
132
|
+
`createPulsarStore` saves its state with Zustand's `persist` middleware, by default in `localStorage`. Pass another `storage` (and other `persist` options such as `partialize` or `version`) in the same config object to change it:
|
|
341
133
|
|
|
342
|
-
|
|
343
|
-
|
|
134
|
+
| Key | Written by | Content |
|
|
135
|
+
| ---------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
136
|
+
| The `name` you pass (unique) | `createPulsarStore` | `{ state, version }` where `state` is `transactionsPool` (every tracked transaction), `lastAddedTxKey` and `unsyncedTxKeys` (keys not yet confirmed by `onRemoteCreate`) |
|
|
344
137
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (status === 'error') onFailure(status);
|
|
351
|
-
},
|
|
352
|
-
onSuccess: (status) => console.log('Success!', status),
|
|
353
|
-
onFailure: (status) => console.error('Failed!', status),
|
|
354
|
-
});
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
---
|
|
358
|
-
|
|
359
|
-
## ✨ How It Connects to the Ecosystem
|
|
360
|
-
|
|
361
|
-
Pulsar is a modular ecosystem. Here’s how the pieces fit together:
|
|
362
|
-
|
|
363
|
-
- **`@tuwaio/pulsar-core`:** Provides the generic, headless state machine (`createPulsarStore`). It knows _how_ to manage state but doesn't know anything about specific blockchains.
|
|
364
|
-
- **`@tuwaio/pulsar-evm`**: An adapter that plugs into the `adapters` config. It teaches the core store how to interact with EVM chains (e.g., how to check transaction receipts, get wallet info from Wagmi, etc.).
|
|
365
|
-
- **`@tuwaio/pulsar-solana`**: An adapter that plugs into the `adapters` config. It extends the core store to work with the Solana ecosystem, teaching it how to track transactions, get wallet info from `@wallet-ui/react`, and use Solana RPCs.
|
|
366
|
-
- **`@tuwaio/pulsar-react`**: Provides React bindings and hooks (like `useInitializeTransactionsPool`) to easily connect the Pulsar store to your React application's lifecycle.
|
|
138
|
+
- The state is written on every change: when a transaction is added, updated by its tracker or removed. `initialTx`, the transaction being signed, is neither saved nor restored, so a reload during signing leaves no stale signing state (pass your own `partialize` and `merge` to change that).
|
|
139
|
+
- In the browser the saved state is restored synchronously when the store is created, so the first client render already has the pool. Server rendering starts with an empty pool: render transaction lists on the client only. Where `localStorage` is unavailable, nothing is read or written.
|
|
140
|
+
- Nothing expires on its own. Transactions leave the pool only through the `maxTransactions` eviction or `removeTxFromPool`: the built-in trackers keep failed transactions as `Failed`. `store.persist.clearStorage()` deletes the saved state.
|
|
141
|
+
- Everything in a transaction is saved, including `payload` and, for ERC-4337, `bundlerUrl` and `pimlicoApiKey` (needed to resume tracking after a reload; a Pimlico key used in the browser is public anyway). Do not put secrets in `payload`.
|
|
142
|
+
- `createTxInMemoryStore` keeps its state in memory only.
|
|
367
143
|
|
|
368
144
|
---
|
|
369
145
|
|
|
370
|
-
##
|
|
371
|
-
|
|
372
|
-
Contributions are welcome! Please read our main **[Contribution Guidelines](https://github.com/TuwaIO/workflows/blob/main/CONTRIBUTING.md)**.
|
|
373
|
-
|
|
374
|
-
If you find this library useful, please consider supporting its development. Every contribution helps!
|
|
146
|
+
## 📚 API Reference
|
|
375
147
|
|
|
376
|
-
|
|
148
|
+
Every export, with signatures and types generated from the source, is documented at **[pulsar.docs.tuwa.io/packages/pulsar-core](https://pulsar.docs.tuwa.io/packages/pulsar-core)**.
|
|
377
149
|
|
|
378
150
|
## 📄 License
|
|
379
151
|
|
|
380
|
-
|
|
152
|
+
Licensed under the **Apache-2.0 License**. See the [LICENSE](https://github.com/TuwaIO/pulsar-core/blob/main/packages/pulsar-core/LICENSE) file for details.
|