@tuwaio/pulsar-core 0.6.2 → 0.6.4

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 CHANGED
@@ -73,6 +73,10 @@ export const usePulsarStore = createBoundedUseStore(
73
73
  name: storageName,
74
74
  adapter: pulsarEvmAdapter(config, appChains),
75
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
+ },
76
80
  }),
77
81
  );
78
82
  ```
@@ -84,6 +88,87 @@ To prevent the `localStorage` from growing indefinitely, Pulsar Core implements
84
88
  - **Maximum Transactions:** By default, the store keeps the last **50** transactions. You can customize this via the `maxTransactions` property in the `createPulsarStore` config.
85
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.
86
90
 
91
+ ### Transaction Metadata Safety
92
+
93
+ Pulsar validates transaction metadata before it creates `initialTx`, calls the wallet action, writes to the local transaction pool, persists to `localStorage`, or calls `onRemoteCreate`.
94
+
95
+ - `title`: each string must be **100 characters or less**.
96
+ - `description`: each string must be **300 characters or less**.
97
+ - `payload`: must be JSON-serializable and **10KB or less** after UTF-8 JSON serialization.
98
+ - `title`, `description`, and payload string values reject executable-like patterns such as `eval(`, `Function(`, `setTimeout("...")`, `setInterval("...")`, and `javascript:`.
99
+
100
+ This validation is a defensive metadata gate, not a replacement for output escaping or HTML sanitization in UI code.
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`
105
+
106
+ Use `beforeTxProcess` for custom preflight policies such as auth checks, feature flags, rate limits, or application-level transaction guards. The callback receives no transaction metadata; throw an error to block the transaction before initialization or wallet interaction.
107
+
108
+ ```ts
109
+ const store = createPulsarStore<TransactionUnion>({
110
+ name: storageName,
111
+ adapter: pulsarEvmAdapter(config, appChains),
112
+ beforeTxProcess: async () => {
113
+ await assertUserCanSubmitTransactions();
114
+ },
115
+ });
116
+ ```
117
+
118
+ You can override the global callback for one transaction by passing `beforeTxProcess` to `executeTxAction`.
119
+
120
+ ```ts
121
+ await store.getState().executeTxAction({
122
+ actionFunction: sendSwap,
123
+ beforeTxProcess: async () => {
124
+ await assertSwapIsEnabled();
125
+ },
126
+ params: {
127
+ adapter: OrbitAdapter.EVM,
128
+ desiredChainID: 1,
129
+ type: 'SWAP',
130
+ title: 'Swap',
131
+ description: 'Swap tokens',
132
+ },
133
+ });
134
+ ```
135
+
136
+ When a local callback is provided to `executeTxAction`, it replaces the global callback for that action.
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
+
87
172
  ### The Returned Store API
88
173
 
89
174
  The `createPulsarStore` function returns a vanilla Zustand store with the following state and actions:
@@ -96,7 +181,7 @@ The `createPulsarStore` function returns a vanilla Zustand store with the follow
96
181
 
97
182
  #### **Actions**
98
183
 
99
- - `executeTxAction(params)`: The primary, all-in-one function for initiating, sending, and tracking a new transaction. This is the main action you will use.
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.
100
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.**
101
186
  - `addTxToPool(tx)`: Adds a new transaction directly to the tracking pool.
102
187
  - `updateTxParams(txKey, fields)`: Updates one or more properties of an existing transaction in the pool.
@@ -197,9 +282,7 @@ export type TransactionUnion = ExampleTx;
197
282
 
198
283
  const initialStore = createPulsarStore<TransactionUnion>({
199
284
  name: storageName,
200
- adapter: [
201
- pulsarEvmAdapter(config, appChains),
202
- ],
285
+ adapter: [pulsarEvmAdapter(config, appChains)],
203
286
  onRemoteCreate: async (tx) => {
204
287
  await syncTransaction(tx);
205
288
  },
package/dist/index.d.mts CHANGED
@@ -55,6 +55,8 @@ type BaseTransaction = {
55
55
  chainId: number | string;
56
56
  /**
57
57
  * User-facing description. Can be a single string for all states, or a tuple for specific states.
58
+ * Each string is validated before execution and persistence. It must be 300 characters or less and must not contain
59
+ * executable-like patterns such as `eval(` or `javascript:`.
58
60
  * @example
59
61
  * // A single description for all states
60
62
  * description: 'Swap 1 ETH for 1,500 USDC'
@@ -74,7 +76,10 @@ type BaseTransaction = {
74
76
  isTrackedModalOpen?: boolean;
75
77
  /** The local timestamp (in seconds) when the transaction was initiated by the user. */
76
78
  localTimestamp: number;
77
- /** Custom data (strings or numbers) to associate with the transaction. */
79
+ /**
80
+ * Custom JSON-serializable data (strings or numbers) to associate with the transaction.
81
+ * The serialized UTF-8 payload must be 10KB or less and string values must not contain executable-like patterns.
82
+ */
78
83
  payload?: Record<string, string | number>;
79
84
  /** A flag indicating if the transaction is still awaiting on-chain confirmation. */
80
85
  pending: boolean;
@@ -82,6 +87,8 @@ type BaseTransaction = {
82
87
  status?: TransactionStatus;
83
88
  /**
84
89
  * User-facing title. Can be a single string for all states, or a tuple for specific states.
90
+ * Each string is validated before execution and persistence. It must be 100 characters or less and must not contain
91
+ * executable-like patterns such as `eval(` or `javascript:`.
85
92
  * @example
86
93
  * // A single title for all states
87
94
  * title: 'ETH/USDC Swap'
@@ -207,13 +214,24 @@ interface SyncCallbacks<T extends Transaction> {
207
214
  */
208
215
  onRemoteCreate?: (tx: T) => Promise<void>;
209
216
  }
217
+ /**
218
+ * Callback executed before Pulsar initializes or submits a transaction.
219
+ *
220
+ * Throw an error from this function to block the transaction before `initialTx`, wallet interaction,
221
+ * persistence, or remote synchronization starts.
222
+ */
223
+ type BeforeTxProcess = () => Promise<void> | void;
210
224
  /**
211
225
  * The configuration object containing one or more transaction adapters.
212
226
  * @template T The specific transaction type.
213
227
  */
214
228
  type PulsarAdapter<T extends Transaction> = OrbitGenericAdapter<TxAdapter<T>> & {
229
+ /** Optional global preflight callback executed before every transaction unless locally overridden. */
230
+ beforeTxProcess?: BeforeTxProcess;
215
231
  maxTransactions?: number;
216
232
  gelatoApiKey?: string;
233
+ /** Optional setting to abort the transaction if the beforeTxProcess hook or remote creation fails. Defaults to true. */
234
+ abortOnTxError?: boolean;
217
235
  } & SyncCallbacks<T>;
218
236
  /**
219
237
  * Represents a tracker for a specific transaction tied to an action and a connector.
@@ -327,7 +345,7 @@ interface IInitializeTxTrackingStore<T extends Transaction> {
327
345
  * Adds a new transaction to the tracking pool and marks it as pending.
328
346
  * @param tx The transaction object to add.
329
347
  */
330
- addTxToPool: (tx: T) => void;
348
+ addTxToPool: (tx: T) => Promise<void>;
331
349
  /**
332
350
  * Updates one or more properties of an existing transaction in the pool.
333
351
  * @param txKey The key of the transaction to update.
@@ -363,8 +381,9 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
363
381
  *
364
382
  * @param params The parameters for handling the transaction.
365
383
  * @param params.actionFunction The async function to execute (e.g., a smart contract write call). Must return a unique key or undefined.
366
- * @param params.params The metadata for the transaction.
384
+ * @param params.params The metadata for the transaction. Title, description, and payload are validated before execution.
367
385
  * @param params.defaultTracker The default tracker to use if it cannot be determined automatically.
386
+ * @param params.beforeTxProcess Optional local preflight callback. When provided, it overrides the global callback from `createPulsarStore`.
368
387
  * @param params.onSuccess Callback to execute when the transaction is successfully submitted.
369
388
  * @param params.onError Callback to execute when the transaction fails.
370
389
  * @param params.onReplaced Callback to execute when the transaction is replaced.
@@ -373,6 +392,8 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
373
392
  actionFunction: () => Promise<ActionTxKey | undefined>;
374
393
  params: Omit<InitialTransactionParams, 'actionFunction'>;
375
394
  defaultTracker?: TransactionTracker;
395
+ beforeTxProcess?: BeforeTxProcess;
396
+ abortOnTxError?: boolean;
376
397
  } & TrackerCallbacks<T>) => Promise<void>;
377
398
  /**
378
399
  * Initializes trackers for all pending transactions in the pool.
@@ -543,7 +564,7 @@ declare function createTxInMemoryStore<T extends Transaction>({ localTransaction
543
564
  * @param options Configuration for the Zustand `persist` middleware.
544
565
  * @returns A fully configured Zustand store instance.
545
566
  */
546
- declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
567
+ declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, beforeTxProcess, abortOnTxError, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
547
568
  setState(partial: ITxTrackingStore<T> | Partial<ITxTrackingStore<T>> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T> | Partial<ITxTrackingStore<T>>), replace?: false | undefined): unknown;
548
569
  setState(state: ITxTrackingStore<T> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T>), replace: true): unknown;
549
570
  persist: {
@@ -661,4 +682,29 @@ type PollingTrackerConfig<R, T extends Transaction> = {
661
682
  */
662
683
  declare function initializePollingTracker<R, T extends Transaction>(config: PollingTrackerConfig<R, T>): void;
663
684
 
664
- export { type ActionTxKey, type BaseTransaction, type CheckTxTracker, type EvmTransaction, type IInitializeTxTrackingStore, type ITxInMemoryStore, type ITxInMemoryStoreParameters, type ITxTrackingStore, type InitialTransaction, type InitialTransactionParams, type PollingFetcherParams, type PollingTrackerConfig, type PulsarAdapter, type SolanaTransaction, type StarknetTransaction, type StoreSlice, type SyncCallbacks, type TrackerCallbacks, type Transaction, type TransactionPool, TransactionStatus, TransactionTracker, type TxAdapter, type TxInMemoryPagination, type UpdatableTransactionFields, createBoundedUseStore, createPulsarStore, createTxInMemoryStore, initializePollingTracker, initializeTxTrackingStore, selectAllTransactions, selectAllTransactionsByActiveWallet, selectPendingTransactions, selectPendingTransactionsByActiveWallet, selectTxByKey };
685
+ /** Maximum allowed length for each transaction title string. */
686
+ declare const MAX_TRANSACTION_TITLE_LENGTH = 100;
687
+ /** Maximum allowed length for each transaction description string. */
688
+ declare const MAX_TRANSACTION_DESCRIPTION_LENGTH = 300;
689
+ /** Maximum allowed serialized UTF-8 payload size in bytes. */
690
+ declare const MAX_TRANSACTION_PAYLOAD_BYTES: number;
691
+ /**
692
+ * Error thrown when transaction metadata fails Pulsar's safety limits.
693
+ */
694
+ declare class PulsarTransactionValidationError extends Error {
695
+ /** The transaction field that failed validation. */
696
+ readonly field: string;
697
+ constructor(field: string, message: string);
698
+ }
699
+ /**
700
+ * Validates metadata used before a transaction action is executed.
701
+ * Throws when title, description, or payload violates Pulsar safety limits.
702
+ */
703
+ declare function validateInitialTransactionParams(params: Omit<InitialTransactionParams, 'actionFunction'>): void;
704
+ /**
705
+ * Validates a complete transaction before it is persisted or synchronized.
706
+ * Throws when title, description, or payload violates Pulsar safety limits.
707
+ */
708
+ declare function validateTransaction<T extends Transaction>(tx: T): void;
709
+
710
+ export { type ActionTxKey, type BaseTransaction, type BeforeTxProcess, type CheckTxTracker, type EvmTransaction, type IInitializeTxTrackingStore, type ITxInMemoryStore, type ITxInMemoryStoreParameters, type ITxTrackingStore, type InitialTransaction, type InitialTransactionParams, MAX_TRANSACTION_DESCRIPTION_LENGTH, MAX_TRANSACTION_PAYLOAD_BYTES, MAX_TRANSACTION_TITLE_LENGTH, type PollingFetcherParams, type PollingTrackerConfig, type PulsarAdapter, PulsarTransactionValidationError, type SolanaTransaction, type StarknetTransaction, type StoreSlice, type SyncCallbacks, type TrackerCallbacks, type Transaction, type TransactionPool, TransactionStatus, TransactionTracker, type TxAdapter, type TxInMemoryPagination, type UpdatableTransactionFields, createBoundedUseStore, createPulsarStore, createTxInMemoryStore, initializePollingTracker, initializeTxTrackingStore, selectAllTransactions, selectAllTransactionsByActiveWallet, selectPendingTransactions, selectPendingTransactionsByActiveWallet, selectTxByKey, validateInitialTransactionParams, validateTransaction };
package/dist/index.d.ts CHANGED
@@ -55,6 +55,8 @@ type BaseTransaction = {
55
55
  chainId: number | string;
56
56
  /**
57
57
  * User-facing description. Can be a single string for all states, or a tuple for specific states.
58
+ * Each string is validated before execution and persistence. It must be 300 characters or less and must not contain
59
+ * executable-like patterns such as `eval(` or `javascript:`.
58
60
  * @example
59
61
  * // A single description for all states
60
62
  * description: 'Swap 1 ETH for 1,500 USDC'
@@ -74,7 +76,10 @@ type BaseTransaction = {
74
76
  isTrackedModalOpen?: boolean;
75
77
  /** The local timestamp (in seconds) when the transaction was initiated by the user. */
76
78
  localTimestamp: number;
77
- /** Custom data (strings or numbers) to associate with the transaction. */
79
+ /**
80
+ * Custom JSON-serializable data (strings or numbers) to associate with the transaction.
81
+ * The serialized UTF-8 payload must be 10KB or less and string values must not contain executable-like patterns.
82
+ */
78
83
  payload?: Record<string, string | number>;
79
84
  /** A flag indicating if the transaction is still awaiting on-chain confirmation. */
80
85
  pending: boolean;
@@ -82,6 +87,8 @@ type BaseTransaction = {
82
87
  status?: TransactionStatus;
83
88
  /**
84
89
  * User-facing title. Can be a single string for all states, or a tuple for specific states.
90
+ * Each string is validated before execution and persistence. It must be 100 characters or less and must not contain
91
+ * executable-like patterns such as `eval(` or `javascript:`.
85
92
  * @example
86
93
  * // A single title for all states
87
94
  * title: 'ETH/USDC Swap'
@@ -207,13 +214,24 @@ interface SyncCallbacks<T extends Transaction> {
207
214
  */
208
215
  onRemoteCreate?: (tx: T) => Promise<void>;
209
216
  }
217
+ /**
218
+ * Callback executed before Pulsar initializes or submits a transaction.
219
+ *
220
+ * Throw an error from this function to block the transaction before `initialTx`, wallet interaction,
221
+ * persistence, or remote synchronization starts.
222
+ */
223
+ type BeforeTxProcess = () => Promise<void> | void;
210
224
  /**
211
225
  * The configuration object containing one or more transaction adapters.
212
226
  * @template T The specific transaction type.
213
227
  */
214
228
  type PulsarAdapter<T extends Transaction> = OrbitGenericAdapter<TxAdapter<T>> & {
229
+ /** Optional global preflight callback executed before every transaction unless locally overridden. */
230
+ beforeTxProcess?: BeforeTxProcess;
215
231
  maxTransactions?: number;
216
232
  gelatoApiKey?: string;
233
+ /** Optional setting to abort the transaction if the beforeTxProcess hook or remote creation fails. Defaults to true. */
234
+ abortOnTxError?: boolean;
217
235
  } & SyncCallbacks<T>;
218
236
  /**
219
237
  * Represents a tracker for a specific transaction tied to an action and a connector.
@@ -327,7 +345,7 @@ interface IInitializeTxTrackingStore<T extends Transaction> {
327
345
  * Adds a new transaction to the tracking pool and marks it as pending.
328
346
  * @param tx The transaction object to add.
329
347
  */
330
- addTxToPool: (tx: T) => void;
348
+ addTxToPool: (tx: T) => Promise<void>;
331
349
  /**
332
350
  * Updates one or more properties of an existing transaction in the pool.
333
351
  * @param txKey The key of the transaction to update.
@@ -363,8 +381,9 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
363
381
  *
364
382
  * @param params The parameters for handling the transaction.
365
383
  * @param params.actionFunction The async function to execute (e.g., a smart contract write call). Must return a unique key or undefined.
366
- * @param params.params The metadata for the transaction.
384
+ * @param params.params The metadata for the transaction. Title, description, and payload are validated before execution.
367
385
  * @param params.defaultTracker The default tracker to use if it cannot be determined automatically.
386
+ * @param params.beforeTxProcess Optional local preflight callback. When provided, it overrides the global callback from `createPulsarStore`.
368
387
  * @param params.onSuccess Callback to execute when the transaction is successfully submitted.
369
388
  * @param params.onError Callback to execute when the transaction fails.
370
389
  * @param params.onReplaced Callback to execute when the transaction is replaced.
@@ -373,6 +392,8 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
373
392
  actionFunction: () => Promise<ActionTxKey | undefined>;
374
393
  params: Omit<InitialTransactionParams, 'actionFunction'>;
375
394
  defaultTracker?: TransactionTracker;
395
+ beforeTxProcess?: BeforeTxProcess;
396
+ abortOnTxError?: boolean;
376
397
  } & TrackerCallbacks<T>) => Promise<void>;
377
398
  /**
378
399
  * Initializes trackers for all pending transactions in the pool.
@@ -543,7 +564,7 @@ declare function createTxInMemoryStore<T extends Transaction>({ localTransaction
543
564
  * @param options Configuration for the Zustand `persist` middleware.
544
565
  * @returns A fully configured Zustand store instance.
545
566
  */
546
- declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
567
+ declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, beforeTxProcess, abortOnTxError, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
547
568
  setState(partial: ITxTrackingStore<T> | Partial<ITxTrackingStore<T>> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T> | Partial<ITxTrackingStore<T>>), replace?: false | undefined): unknown;
548
569
  setState(state: ITxTrackingStore<T> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T>), replace: true): unknown;
549
570
  persist: {
@@ -661,4 +682,29 @@ type PollingTrackerConfig<R, T extends Transaction> = {
661
682
  */
662
683
  declare function initializePollingTracker<R, T extends Transaction>(config: PollingTrackerConfig<R, T>): void;
663
684
 
664
- export { type ActionTxKey, type BaseTransaction, type CheckTxTracker, type EvmTransaction, type IInitializeTxTrackingStore, type ITxInMemoryStore, type ITxInMemoryStoreParameters, type ITxTrackingStore, type InitialTransaction, type InitialTransactionParams, type PollingFetcherParams, type PollingTrackerConfig, type PulsarAdapter, type SolanaTransaction, type StarknetTransaction, type StoreSlice, type SyncCallbacks, type TrackerCallbacks, type Transaction, type TransactionPool, TransactionStatus, TransactionTracker, type TxAdapter, type TxInMemoryPagination, type UpdatableTransactionFields, createBoundedUseStore, createPulsarStore, createTxInMemoryStore, initializePollingTracker, initializeTxTrackingStore, selectAllTransactions, selectAllTransactionsByActiveWallet, selectPendingTransactions, selectPendingTransactionsByActiveWallet, selectTxByKey };
685
+ /** Maximum allowed length for each transaction title string. */
686
+ declare const MAX_TRANSACTION_TITLE_LENGTH = 100;
687
+ /** Maximum allowed length for each transaction description string. */
688
+ declare const MAX_TRANSACTION_DESCRIPTION_LENGTH = 300;
689
+ /** Maximum allowed serialized UTF-8 payload size in bytes. */
690
+ declare const MAX_TRANSACTION_PAYLOAD_BYTES: number;
691
+ /**
692
+ * Error thrown when transaction metadata fails Pulsar's safety limits.
693
+ */
694
+ declare class PulsarTransactionValidationError extends Error {
695
+ /** The transaction field that failed validation. */
696
+ readonly field: string;
697
+ constructor(field: string, message: string);
698
+ }
699
+ /**
700
+ * Validates metadata used before a transaction action is executed.
701
+ * Throws when title, description, or payload violates Pulsar safety limits.
702
+ */
703
+ declare function validateInitialTransactionParams(params: Omit<InitialTransactionParams, 'actionFunction'>): void;
704
+ /**
705
+ * Validates a complete transaction before it is persisted or synchronized.
706
+ * Throws when title, description, or payload violates Pulsar safety limits.
707
+ */
708
+ declare function validateTransaction<T extends Transaction>(tx: T): void;
709
+
710
+ export { type ActionTxKey, type BaseTransaction, type BeforeTxProcess, type CheckTxTracker, type EvmTransaction, type IInitializeTxTrackingStore, type ITxInMemoryStore, type ITxInMemoryStoreParameters, type ITxTrackingStore, type InitialTransaction, type InitialTransactionParams, MAX_TRANSACTION_DESCRIPTION_LENGTH, MAX_TRANSACTION_PAYLOAD_BYTES, MAX_TRANSACTION_TITLE_LENGTH, type PollingFetcherParams, type PollingTrackerConfig, type PulsarAdapter, PulsarTransactionValidationError, type SolanaTransaction, type StarknetTransaction, type StoreSlice, type SyncCallbacks, type TrackerCallbacks, type Transaction, type TransactionPool, TransactionStatus, TransactionTracker, type TxAdapter, type TxInMemoryPagination, type UpdatableTransactionFields, createBoundedUseStore, createPulsarStore, createTxInMemoryStore, initializePollingTracker, initializeTxTrackingStore, selectAllTransactions, selectAllTransactionsByActiveWallet, selectPendingTransactions, selectPendingTransactionsByActiveWallet, selectTxByKey, validateInitialTransactionParams, validateTransaction };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- 'use strict';var immer=require('immer'),vanilla=require('zustand/vanilla'),orbitCore=require('@tuwaio/orbit-core'),D=require('dayjs'),middleware=require('zustand/middleware'),zustand=require('zustand');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var D__default=/*#__PURE__*/_interopDefault(D);function E({maxTransactions:n,onRemoteCreate:e}){return (i,l)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:a=>{let t={...a,pending:true};i(o=>immer.produce(o,r=>{if(r.lastAddedTxKey=a.txKey,a.txKey){if(Object.keys(r.transactionsPool).length>=n){let T=Object.values(r.transactionsPool).sort((d,c)=>d.localTimestamp-c.localTimestamp);if(T.length>0){let d=T[0];delete r.transactionsPool[d.txKey];}}r.transactionsPool[a.txKey]=t;}})),e&&e(t).catch(o=>console.error("[Pulsar Sync] Create failed:",o));},updateTxParams:(a,t)=>{i(o=>immer.produce(o,r=>{let s=r.transactionsPool[a];s&&Object.assign(s,t);}));},removeTxFromPool:a=>{i(t=>immer.produce(t,o=>{delete o.transactionsPool[a];}));},closeTxTrackedModal:a=>{i(t=>immer.produce(t,o=>{if(a&&o.transactionsPool[a]){let r=o.transactionsPool[a];o.transactionsPool[a]={...r,isTrackedModalOpen:false};}o.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var F=n=>Object.values(n).sort((e,i)=>Number(e.localTimestamp)-Number(i.localTimestamp)),Y=n=>F(n).filter(e=>e.pending),Z=(n,e)=>n[e],N=(n,e)=>F(n).filter(i=>i.from.toLowerCase()===e.toLowerCase()),ee=(n,e)=>N(n,e).filter(i=>i.pending);var j=(a=>(a.Ethereum="ethereum",a.Safe="safe",a.Gelato="gelato",a.Solana="solana",a))(j||{}),A=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(A||{});var M=n=>n==="Success"||n==="Replaced",O=(n,e)=>{let i=n[e.txKey];if(i){if(M(i.status))return false;if(i.pending){if(M(e.status))return n[e.txKey]={...i,...e},true;let l=typeof e.confirmations=="number"?e.confirmations:0,a=typeof i.confirmations=="number"?i.confirmations:0;return l>a?(n[e.txKey]={...i,...e},true):false}}return n[e.txKey]=e,true};function Te({localTransactionsPool:n,getHistory:e,onHistoryFetched:i}){immer.setAutoFreeze(false);let l=t=>({isLoading:t,isError:false}),a=t=>t?(i&&queueMicrotask(()=>i(t.docs)),o=>immer.produce(o,r=>{let s=r.transactionsPool;for(let T of t.docs)O(s,T);r.currentPage=t.page,r.hasMore=t.hasNextPage,r.isLoading=false;})):o=>o;return vanilla.createStore()((t,o)=>({transactionsPool:n,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:r=>{t(s=>immer.produce(s,T=>{let d=T.transactionsPool;for(let c of Object.values(r))O(d,c);}));},fetchInitial:async r=>{if(!(!e||!r)){t(l(true));try{let s=await e({page:1,walletAddress:r});t(a(s));}catch(s){console.error("[Pulsar] Failed to fetch initial transaction history:",s),t({isLoading:false,isError:true});}}},fetchNextPage:async r=>{let{hasMore:s,isLoading:T,currentPage:d}=o();if(!(!e||!s||T||!r)){t(l(true));try{let c=d+1,m=await e({page:c,walletAddress:r});t(a(m));}catch(c){console.error(`[Pulsar] Failed to fetch transaction history page ${d+1}:`,c),t({isLoading:false,isError:true});}}}}))}function Ae({adapter:n,maxTransactions:e=50,onRemoteCreate:i,gelatoApiKey:l,...a}){return vanilla.createStore()(middleware.persist((t,o)=>({...E({maxTransactions:e,onRemoteCreate:i})(t,o),getAdapter:()=>n,initializeTransactionsPool:async()=>{let r=Object.values(o().transactionsPool).filter(s=>s.pending);await Promise.all(r.map(s=>orbitCore.selectAdapterByKey({adapterKey:s.adapter,adapter:n})?.checkAndInitializeTrackerInStore({tx:s,gelatoApiKey:l,...o()})));},injectExternalPendingTxs:async r=>{let T=o().getAdapter(),d=[];t(c=>immer.produce(c,m=>{let g=m.transactionsPool;r.forEach(p=>{let x=g[p.txKey];p.pending&&!x&&(g[p.txKey]=p,d.push(p));let S=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";x?.pending&&S&&(x.status=p.status,x.pending=false,p.txKey&&(x.txKey=p.txKey),p.finishedTimestamp&&(x.finishedTimestamp=p.finishedTimestamp));});})),d.length>0&&await Promise.all(d.map(c=>orbitCore.selectAdapterByKey({adapterKey:c.adapter,adapter:T})?.checkAndInitializeTrackerInStore({tx:c,gelatoApiKey:l,...o()})));},executeTxAction:async({defaultTracker:r,actionFunction:s,params:T,...d})=>{let{desiredChainID:c,tracker:m,...g}=T,{onSuccess:p,onError:x,onReplaced:S}=d,K=D__default.default().unix();t({initialTx:{...T,actionFunction:s,localTimestamp:K,isInitializing:true}});let f=orbitCore.selectAdapterByKey({adapterKey:g.adapter,adapter:n}),w=u=>{t(k=>immer.produce(k,P=>{P.initialTx&&(P.initialTx.isInitializing=false,P.initialTx.error=orbitCore.normalizeError(u));}));};if(!f){let u=new Error("No adapter found for this transaction.");throw w(u),u}try{let{connectorType:u,walletAddress:k}=f.getConnectorInfo();await f.checkChainForTx(c);let P=await s();if(!P){t({initialTx:void 0});return}let{tracker:R,txKey:h}=f.checkTransactionsTracker({actionTxKey:P,connectorType:u,tracker:m,gelatoApiKey:l}),z={...g,connectorType:u,from:k,tracker:R||r,chainId:orbitCore.setChainId(c),localTimestamp:K,txKey:h,hash:R==="ethereum"?P:void 0,pending:!1,isTrackedModalOpen:T.withTrackedModal};o().addTxToPool(z),t(B=>immer.produce(B,I=>{I.initialTx&&(I.initialTx.isInitializing=!1,I.initialTx.lastTxKey=h);}));let L=o().transactionsPool[h];await f.checkAndInitializeTrackerInStore({tx:L,onSuccess:p,onError:x,onReplaced:S,gelatoApiKey:l,...o()});}catch(u){throw w(u),u}}}),{...a}))}var we=(n=>e=>zustand.useStore(n,e));var V=5e3,X=10;function Ee(n){let{tx:e,fetcher:i,onInitialize:l,onSuccess:a,onFailure:t,onIntervalTick:o,onReplaced:r,removeTxFromPool:s,pollingInterval:T=V,maxRetries:d=X}=n;if(!e.pending)return;l?.();let c=d,m=true,g=x=>{m&&(m=false,s&&!x?.withoutRemoving&&s(e.txKey));};(async()=>{for(;m&&c>0;)try{if(await new Promise(x=>setTimeout(x,T)),!m)break;await i({tx:e,stopPolling:g,onSuccess:a,onFailure:t,onIntervalTick:o,onReplaced:r});}catch(x){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,x),c--;}c<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),t(),g());})();}exports.TransactionStatus=A;exports.TransactionTracker=j;exports.createBoundedUseStore=we;exports.createPulsarStore=Ae;exports.createTxInMemoryStore=Te;exports.initializePollingTracker=Ee;exports.initializeTxTrackingStore=E;exports.selectAllTransactions=F;exports.selectAllTransactionsByActiveWallet=N;exports.selectPendingTransactions=Y;exports.selectPendingTransactionsByActiveWallet=ee;exports.selectTxByKey=Z;
1
+ 'use strict';var immer=require('immer'),vanilla=require('zustand/vanilla'),orbitCore=require('@tuwaio/orbit-core'),ie=require('dayjs'),middleware=require('zustand/middleware'),zustand=require('zustand');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var ie__default=/*#__PURE__*/_interopDefault(ie);var N=100,B=300,z=10*1024,ee=[/\beval\s*\(/i,/\bFunction\s*\(/,/\bset(?:Timeout|Interval)\s*\(\s*['"`]/i,/javascript\s*:/i],P=class extends Error{field;constructor(e,r){super(r),this.name="PulsarTransactionValidationError",this.field=e;}};function $(t){I({field:"title",value:t.title,maxLength:N}),I({field:"description",value:t.description,maxLength:B}),_(t.payload);}function k(t){I({field:"title",value:t.title,maxLength:N}),I({field:"description",value:t.description,maxLength:B}),_(t.payload);}function I({field:t,value:e,maxLength:r}){if(e===void 0)return;(Array.isArray(e)?e:[e]).forEach((n,o)=>{let c=Array.isArray(e)?`${t}[${o}]`:t;if(typeof n!="string")throw new P(c,`${c} must be a string.`);if(n.length>r)throw new P(c,`${c} must be ${r} characters or less.`);R(c,n);});}function _(t){if(t===void 0)return;let e;try{e=JSON.stringify(t);}catch{throw new P("payload","payload must be JSON-serializable.")}if(e===void 0)throw new P("payload","payload must be JSON-serializable.");if(new TextEncoder().encode(e).length>z)throw new P("payload",`payload must be ${z} bytes or less when serialized.`);j(t);}function j(t,e="payload"){if(typeof t=="string"){R(e,t);return}t===null||typeof t!="object"||Object.entries(t).forEach(([r,l])=>{let n=`${e}.${r}`;R(n,r),j(l,n);});}function R(t,e){if(ee.some(r=>r.test(e)))throw new P(t,`${t} contains a blocked executable-like pattern.`)}function U({maxTransactions:t,onRemoteCreate:e}){return (r,l)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:n=>{k(n);let o={...n,pending:true};return (async()=>{e&&await e(o),r(s=>immer.produce(s,a=>{if(a.lastAddedTxKey=n.txKey,n.txKey){if(Object.keys(a.transactionsPool).length>=t){let d=Object.values(a.transactionsPool).sort((i,T)=>i.localTimestamp-T.localTimestamp);if(d.length>0){let i=d[0];delete a.transactionsPool[i.txKey];}}a.transactionsPool[n.txKey]=o;}}));})()},updateTxParams:(n,o)=>{r(c=>immer.produce(c,s=>{let a=s.transactionsPool[n];a&&Object.assign(a,o);}));},removeTxFromPool:n=>{r(o=>immer.produce(o,c=>{delete c.transactionsPool[n];}));},closeTxTrackedModal:n=>{r(o=>immer.produce(o,c=>{if(n&&c.transactionsPool[n]){let s=c.transactionsPool[n];c.transactionsPool[n]={...s,isTrackedModalOpen:false};}c.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var G=t=>Object.values(t).sort((e,r)=>Number(e.localTimestamp)-Number(r.localTimestamp)),fe=t=>G(t).filter(e=>e.pending),ge=(t,e)=>t[e],te=(t,e)=>G(t).filter(r=>r.from.toLowerCase()===e.toLowerCase()),Pe=(t,e)=>te(t,e).filter(r=>r.pending);var re=(n=>(n.Ethereum="ethereum",n.Safe="safe",n.Gelato="gelato",n.Solana="solana",n))(re||{}),O=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(O||{});var q=t=>t==="Success"||t==="Replaced",X=(t,e)=>{let r=t[e.txKey];if(r){if(q(r.status))return false;if(r.pending){if(q(e.status))return t[e.txKey]={...r,...e},true;let l=typeof e.confirmations=="number"?e.confirmations:0,n=typeof r.confirmations=="number"?r.confirmations:0;return l>n?(t[e.txKey]={...r,...e},true):false}}return t[e.txKey]=e,true};function Ee({localTransactionsPool:t,getHistory:e,onHistoryFetched:r}){immer.setAutoFreeze(false);let l=o=>({isLoading:o,isError:false}),n=o=>o?(r&&queueMicrotask(()=>r(o.docs)),c=>immer.produce(c,s=>{let a=s.transactionsPool;for(let m of o.docs)X(a,m);s.currentPage=o.page,s.hasMore=o.hasNextPage,s.isLoading=false;})):c=>c;return vanilla.createStore()((o,c)=>({transactionsPool:t,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:s=>{o(a=>immer.produce(a,m=>{let d=m.transactionsPool;for(let i of Object.values(s))X(d,i);}));},fetchInitial:async s=>{if(!(!e||!s)){o(l(true));try{let a=await e({page:1,walletAddress:s});o(n(a));}catch(a){console.error("[Pulsar] Failed to fetch initial transaction history:",a),o({isLoading:false,isError:true});}}},fetchNextPage:async s=>{let{hasMore:a,isLoading:m,currentPage:d}=c();if(!(!e||!a||m||!s)){o(l(true));try{let i=d+1,T=await e({page:i,walletAddress:s});o(n(T));}catch(i){console.error(`[Pulsar] Failed to fetch transaction history page ${d+1}:`,i),o({isLoading:false,isError:true});}}}}))}function Ge({adapter:t,maxTransactions:e=50,onRemoteCreate:r,gelatoApiKey:l,beforeTxProcess:n,abortOnTxError:o,...c}){return vanilla.createStore()(middleware.persist((s,a)=>({...U({maxTransactions:e,onRemoteCreate:r})(s,a),getAdapter:()=>t,initializeTransactionsPool:async()=>{let d=Object.values(a().transactionsPool).filter(i=>i.pending).filter(i=>{try{return k(i),!0}catch(T){return console.warn("[Pulsar] Removed invalid persisted transaction:",T),a().removeTxFromPool(i.txKey),false}});await Promise.all(d.map(i=>orbitCore.selectAdapterByKey({adapterKey:i.adapter,adapter:t})?.checkAndInitializeTrackerInStore({tx:i,gelatoApiKey:l,...a()})));},injectExternalPendingTxs:async m=>{let i=a().getAdapter(),T=[],h=m.filter(f=>{try{return k(f),!0}catch(u){return console.warn("[Pulsar] Skipped invalid remote transaction:",u),false}});s(f=>immer.produce(f,u=>{let S=u.transactionsPool;h.forEach(p=>{let g=S[p.txKey];p.pending&&!g&&(S[p.txKey]=p,T.push(p));let A=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";g?.pending&&A&&(g.status=p.status,g.pending=false,p.txKey&&(g.txKey=p.txKey),p.finishedTimestamp&&(g.finishedTimestamp=p.finishedTimestamp));});})),T.length>0&&await Promise.all(T.map(f=>orbitCore.selectAdapterByKey({adapterKey:f.adapter,adapter:i})?.checkAndInitializeTrackerInStore({tx:f,gelatoApiKey:l,...a()})));},executeTxAction:async({defaultTracker:m,actionFunction:d,params:i,beforeTxProcess:T,abortOnTxError:h,...f})=>{let u=h??o??true,S=ie__default.default().unix();try{await(T??n)?.();}catch(x){if(u)throw s({initialTx:{...i,actionFunction:d,localTimestamp:S,isInitializing:false,error:orbitCore.normalizeError(x)}}),x;console.warn("[Pulsar] beforeTxProcess failed:",x);}$(i);let{desiredChainID:p,tracker:g,...A}=i,{onSuccess:W,onError:J,onReplaced:V}=f;s({initialTx:{...i,actionFunction:d,localTimestamp:S,isInitializing:true}});let b=orbitCore.selectAdapterByKey({adapterKey:A.adapter,adapter:t}),L=x=>{s(w=>immer.produce(w,y=>{y.initialTx&&(y.initialTx.isInitializing=false,y.initialTx.error=orbitCore.normalizeError(x));}));};if(!b){let x=new Error("No adapter found for this transaction.");throw L(x),x}try{let{connectorType:x,walletAddress:w}=b.getConnectorInfo();await b.checkChainForTx(p);let y=await d();if(!y){s({initialTx:void 0});return}let{tracker:M,txKey:E}=b.checkTransactionsTracker({actionTxKey:y,connectorType:x,tracker:g,gelatoApiKey:l}),Y={...A,connectorType:x,from:w,tracker:M||m,chainId:orbitCore.setChainId(p),localTimestamp:S,txKey:E,hash:M==="ethereum"?y:void 0,pending:!1,isTrackedModalOpen:i.withTrackedModal};await a().addTxToPool(Y),s(Z=>immer.produce(Z,K=>{K.initialTx&&(K.initialTx.isInitializing=!1,K.initialTx.lastTxKey=E);}));let Q=a().transactionsPool[E];await b.checkAndInitializeTrackerInStore({tx:Q,onSuccess:W,onError:J,onReplaced:V,gelatoApiKey:l,...a()});}catch(x){throw L(x),x}}}),{...c}))}var He=(t=>e=>zustand.useStore(t,e));var Te=5e3,de=10;function Je(t){let{tx:e,fetcher:r,onInitialize:l,onSuccess:n,onFailure:o,onIntervalTick:c,onReplaced:s,removeTxFromPool:a,pollingInterval:m=Te,maxRetries:d=de}=t;if(!e.pending)return;l?.();let i=d,T=true,h=u=>{T&&(T=false,a&&!u?.withoutRemoving&&a(e.txKey));};(async()=>{for(;T&&i>0;)try{if(await new Promise(u=>setTimeout(u,m)),!T)break;await r({tx:e,stopPolling:h,onSuccess:n,onFailure:o,onIntervalTick:c,onReplaced:s});}catch(u){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,u),i--;}i<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),o(),h());})();}exports.MAX_TRANSACTION_DESCRIPTION_LENGTH=B;exports.MAX_TRANSACTION_PAYLOAD_BYTES=z;exports.MAX_TRANSACTION_TITLE_LENGTH=N;exports.PulsarTransactionValidationError=P;exports.TransactionStatus=O;exports.TransactionTracker=re;exports.createBoundedUseStore=He;exports.createPulsarStore=Ge;exports.createTxInMemoryStore=Ee;exports.initializePollingTracker=Je;exports.initializeTxTrackingStore=U;exports.selectAllTransactions=G;exports.selectAllTransactionsByActiveWallet=te;exports.selectPendingTransactions=fe;exports.selectPendingTransactionsByActiveWallet=Pe;exports.selectTxByKey=ge;exports.validateInitialTransactionParams=$;exports.validateTransaction=k;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import {produce,setAutoFreeze}from'immer';import {createStore}from'zustand/vanilla';import {selectAdapterByKey,setChainId,normalizeError}from'@tuwaio/orbit-core';import D from'dayjs';import {persist}from'zustand/middleware';import {useStore}from'zustand';function E({maxTransactions:n,onRemoteCreate:e}){return (i,l)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:a=>{let t={...a,pending:true};i(o=>produce(o,r=>{if(r.lastAddedTxKey=a.txKey,a.txKey){if(Object.keys(r.transactionsPool).length>=n){let T=Object.values(r.transactionsPool).sort((d,c)=>d.localTimestamp-c.localTimestamp);if(T.length>0){let d=T[0];delete r.transactionsPool[d.txKey];}}r.transactionsPool[a.txKey]=t;}})),e&&e(t).catch(o=>console.error("[Pulsar Sync] Create failed:",o));},updateTxParams:(a,t)=>{i(o=>produce(o,r=>{let s=r.transactionsPool[a];s&&Object.assign(s,t);}));},removeTxFromPool:a=>{i(t=>produce(t,o=>{delete o.transactionsPool[a];}));},closeTxTrackedModal:a=>{i(t=>produce(t,o=>{if(a&&o.transactionsPool[a]){let r=o.transactionsPool[a];o.transactionsPool[a]={...r,isTrackedModalOpen:false};}o.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var F=n=>Object.values(n).sort((e,i)=>Number(e.localTimestamp)-Number(i.localTimestamp)),Y=n=>F(n).filter(e=>e.pending),Z=(n,e)=>n[e],N=(n,e)=>F(n).filter(i=>i.from.toLowerCase()===e.toLowerCase()),ee=(n,e)=>N(n,e).filter(i=>i.pending);var j=(a=>(a.Ethereum="ethereum",a.Safe="safe",a.Gelato="gelato",a.Solana="solana",a))(j||{}),A=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(A||{});var M=n=>n==="Success"||n==="Replaced",O=(n,e)=>{let i=n[e.txKey];if(i){if(M(i.status))return false;if(i.pending){if(M(e.status))return n[e.txKey]={...i,...e},true;let l=typeof e.confirmations=="number"?e.confirmations:0,a=typeof i.confirmations=="number"?i.confirmations:0;return l>a?(n[e.txKey]={...i,...e},true):false}}return n[e.txKey]=e,true};function Te({localTransactionsPool:n,getHistory:e,onHistoryFetched:i}){setAutoFreeze(false);let l=t=>({isLoading:t,isError:false}),a=t=>t?(i&&queueMicrotask(()=>i(t.docs)),o=>produce(o,r=>{let s=r.transactionsPool;for(let T of t.docs)O(s,T);r.currentPage=t.page,r.hasMore=t.hasNextPage,r.isLoading=false;})):o=>o;return createStore()((t,o)=>({transactionsPool:n,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:r=>{t(s=>produce(s,T=>{let d=T.transactionsPool;for(let c of Object.values(r))O(d,c);}));},fetchInitial:async r=>{if(!(!e||!r)){t(l(true));try{let s=await e({page:1,walletAddress:r});t(a(s));}catch(s){console.error("[Pulsar] Failed to fetch initial transaction history:",s),t({isLoading:false,isError:true});}}},fetchNextPage:async r=>{let{hasMore:s,isLoading:T,currentPage:d}=o();if(!(!e||!s||T||!r)){t(l(true));try{let c=d+1,m=await e({page:c,walletAddress:r});t(a(m));}catch(c){console.error(`[Pulsar] Failed to fetch transaction history page ${d+1}:`,c),t({isLoading:false,isError:true});}}}}))}function Ae({adapter:n,maxTransactions:e=50,onRemoteCreate:i,gelatoApiKey:l,...a}){return createStore()(persist((t,o)=>({...E({maxTransactions:e,onRemoteCreate:i})(t,o),getAdapter:()=>n,initializeTransactionsPool:async()=>{let r=Object.values(o().transactionsPool).filter(s=>s.pending);await Promise.all(r.map(s=>selectAdapterByKey({adapterKey:s.adapter,adapter:n})?.checkAndInitializeTrackerInStore({tx:s,gelatoApiKey:l,...o()})));},injectExternalPendingTxs:async r=>{let T=o().getAdapter(),d=[];t(c=>produce(c,m=>{let g=m.transactionsPool;r.forEach(p=>{let x=g[p.txKey];p.pending&&!x&&(g[p.txKey]=p,d.push(p));let S=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";x?.pending&&S&&(x.status=p.status,x.pending=false,p.txKey&&(x.txKey=p.txKey),p.finishedTimestamp&&(x.finishedTimestamp=p.finishedTimestamp));});})),d.length>0&&await Promise.all(d.map(c=>selectAdapterByKey({adapterKey:c.adapter,adapter:T})?.checkAndInitializeTrackerInStore({tx:c,gelatoApiKey:l,...o()})));},executeTxAction:async({defaultTracker:r,actionFunction:s,params:T,...d})=>{let{desiredChainID:c,tracker:m,...g}=T,{onSuccess:p,onError:x,onReplaced:S}=d,K=D().unix();t({initialTx:{...T,actionFunction:s,localTimestamp:K,isInitializing:true}});let f=selectAdapterByKey({adapterKey:g.adapter,adapter:n}),w=u=>{t(k=>produce(k,P=>{P.initialTx&&(P.initialTx.isInitializing=false,P.initialTx.error=normalizeError(u));}));};if(!f){let u=new Error("No adapter found for this transaction.");throw w(u),u}try{let{connectorType:u,walletAddress:k}=f.getConnectorInfo();await f.checkChainForTx(c);let P=await s();if(!P){t({initialTx:void 0});return}let{tracker:R,txKey:h}=f.checkTransactionsTracker({actionTxKey:P,connectorType:u,tracker:m,gelatoApiKey:l}),z={...g,connectorType:u,from:k,tracker:R||r,chainId:setChainId(c),localTimestamp:K,txKey:h,hash:R==="ethereum"?P:void 0,pending:!1,isTrackedModalOpen:T.withTrackedModal};o().addTxToPool(z),t(B=>produce(B,I=>{I.initialTx&&(I.initialTx.isInitializing=!1,I.initialTx.lastTxKey=h);}));let L=o().transactionsPool[h];await f.checkAndInitializeTrackerInStore({tx:L,onSuccess:p,onError:x,onReplaced:S,gelatoApiKey:l,...o()});}catch(u){throw w(u),u}}}),{...a}))}var we=(n=>e=>useStore(n,e));var V=5e3,X=10;function Ee(n){let{tx:e,fetcher:i,onInitialize:l,onSuccess:a,onFailure:t,onIntervalTick:o,onReplaced:r,removeTxFromPool:s,pollingInterval:T=V,maxRetries:d=X}=n;if(!e.pending)return;l?.();let c=d,m=true,g=x=>{m&&(m=false,s&&!x?.withoutRemoving&&s(e.txKey));};(async()=>{for(;m&&c>0;)try{if(await new Promise(x=>setTimeout(x,T)),!m)break;await i({tx:e,stopPolling:g,onSuccess:a,onFailure:t,onIntervalTick:o,onReplaced:r});}catch(x){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,x),c--;}c<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),t(),g());})();}export{A as TransactionStatus,j as TransactionTracker,we as createBoundedUseStore,Ae as createPulsarStore,Te as createTxInMemoryStore,Ee as initializePollingTracker,E as initializeTxTrackingStore,F as selectAllTransactions,N as selectAllTransactionsByActiveWallet,Y as selectPendingTransactions,ee as selectPendingTransactionsByActiveWallet,Z as selectTxByKey};
1
+ import {produce,setAutoFreeze}from'immer';import {createStore}from'zustand/vanilla';import {normalizeError,selectAdapterByKey,setChainId}from'@tuwaio/orbit-core';import ie from'dayjs';import {persist}from'zustand/middleware';import {useStore}from'zustand';var N=100,B=300,z=10*1024,ee=[/\beval\s*\(/i,/\bFunction\s*\(/,/\bset(?:Timeout|Interval)\s*\(\s*['"`]/i,/javascript\s*:/i],P=class extends Error{field;constructor(e,r){super(r),this.name="PulsarTransactionValidationError",this.field=e;}};function $(t){I({field:"title",value:t.title,maxLength:N}),I({field:"description",value:t.description,maxLength:B}),_(t.payload);}function k(t){I({field:"title",value:t.title,maxLength:N}),I({field:"description",value:t.description,maxLength:B}),_(t.payload);}function I({field:t,value:e,maxLength:r}){if(e===void 0)return;(Array.isArray(e)?e:[e]).forEach((n,o)=>{let c=Array.isArray(e)?`${t}[${o}]`:t;if(typeof n!="string")throw new P(c,`${c} must be a string.`);if(n.length>r)throw new P(c,`${c} must be ${r} characters or less.`);R(c,n);});}function _(t){if(t===void 0)return;let e;try{e=JSON.stringify(t);}catch{throw new P("payload","payload must be JSON-serializable.")}if(e===void 0)throw new P("payload","payload must be JSON-serializable.");if(new TextEncoder().encode(e).length>z)throw new P("payload",`payload must be ${z} bytes or less when serialized.`);j(t);}function j(t,e="payload"){if(typeof t=="string"){R(e,t);return}t===null||typeof t!="object"||Object.entries(t).forEach(([r,l])=>{let n=`${e}.${r}`;R(n,r),j(l,n);});}function R(t,e){if(ee.some(r=>r.test(e)))throw new P(t,`${t} contains a blocked executable-like pattern.`)}function U({maxTransactions:t,onRemoteCreate:e}){return (r,l)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:n=>{k(n);let o={...n,pending:true};return (async()=>{e&&await e(o),r(s=>produce(s,a=>{if(a.lastAddedTxKey=n.txKey,n.txKey){if(Object.keys(a.transactionsPool).length>=t){let d=Object.values(a.transactionsPool).sort((i,T)=>i.localTimestamp-T.localTimestamp);if(d.length>0){let i=d[0];delete a.transactionsPool[i.txKey];}}a.transactionsPool[n.txKey]=o;}}));})()},updateTxParams:(n,o)=>{r(c=>produce(c,s=>{let a=s.transactionsPool[n];a&&Object.assign(a,o);}));},removeTxFromPool:n=>{r(o=>produce(o,c=>{delete c.transactionsPool[n];}));},closeTxTrackedModal:n=>{r(o=>produce(o,c=>{if(n&&c.transactionsPool[n]){let s=c.transactionsPool[n];c.transactionsPool[n]={...s,isTrackedModalOpen:false};}c.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var G=t=>Object.values(t).sort((e,r)=>Number(e.localTimestamp)-Number(r.localTimestamp)),fe=t=>G(t).filter(e=>e.pending),ge=(t,e)=>t[e],te=(t,e)=>G(t).filter(r=>r.from.toLowerCase()===e.toLowerCase()),Pe=(t,e)=>te(t,e).filter(r=>r.pending);var re=(n=>(n.Ethereum="ethereum",n.Safe="safe",n.Gelato="gelato",n.Solana="solana",n))(re||{}),O=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(O||{});var q=t=>t==="Success"||t==="Replaced",X=(t,e)=>{let r=t[e.txKey];if(r){if(q(r.status))return false;if(r.pending){if(q(e.status))return t[e.txKey]={...r,...e},true;let l=typeof e.confirmations=="number"?e.confirmations:0,n=typeof r.confirmations=="number"?r.confirmations:0;return l>n?(t[e.txKey]={...r,...e},true):false}}return t[e.txKey]=e,true};function Ee({localTransactionsPool:t,getHistory:e,onHistoryFetched:r}){setAutoFreeze(false);let l=o=>({isLoading:o,isError:false}),n=o=>o?(r&&queueMicrotask(()=>r(o.docs)),c=>produce(c,s=>{let a=s.transactionsPool;for(let m of o.docs)X(a,m);s.currentPage=o.page,s.hasMore=o.hasNextPage,s.isLoading=false;})):c=>c;return createStore()((o,c)=>({transactionsPool:t,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:s=>{o(a=>produce(a,m=>{let d=m.transactionsPool;for(let i of Object.values(s))X(d,i);}));},fetchInitial:async s=>{if(!(!e||!s)){o(l(true));try{let a=await e({page:1,walletAddress:s});o(n(a));}catch(a){console.error("[Pulsar] Failed to fetch initial transaction history:",a),o({isLoading:false,isError:true});}}},fetchNextPage:async s=>{let{hasMore:a,isLoading:m,currentPage:d}=c();if(!(!e||!a||m||!s)){o(l(true));try{let i=d+1,T=await e({page:i,walletAddress:s});o(n(T));}catch(i){console.error(`[Pulsar] Failed to fetch transaction history page ${d+1}:`,i),o({isLoading:false,isError:true});}}}}))}function Ge({adapter:t,maxTransactions:e=50,onRemoteCreate:r,gelatoApiKey:l,beforeTxProcess:n,abortOnTxError:o,...c}){return createStore()(persist((s,a)=>({...U({maxTransactions:e,onRemoteCreate:r})(s,a),getAdapter:()=>t,initializeTransactionsPool:async()=>{let d=Object.values(a().transactionsPool).filter(i=>i.pending).filter(i=>{try{return k(i),!0}catch(T){return console.warn("[Pulsar] Removed invalid persisted transaction:",T),a().removeTxFromPool(i.txKey),false}});await Promise.all(d.map(i=>selectAdapterByKey({adapterKey:i.adapter,adapter:t})?.checkAndInitializeTrackerInStore({tx:i,gelatoApiKey:l,...a()})));},injectExternalPendingTxs:async m=>{let i=a().getAdapter(),T=[],h=m.filter(f=>{try{return k(f),!0}catch(u){return console.warn("[Pulsar] Skipped invalid remote transaction:",u),false}});s(f=>produce(f,u=>{let S=u.transactionsPool;h.forEach(p=>{let g=S[p.txKey];p.pending&&!g&&(S[p.txKey]=p,T.push(p));let A=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";g?.pending&&A&&(g.status=p.status,g.pending=false,p.txKey&&(g.txKey=p.txKey),p.finishedTimestamp&&(g.finishedTimestamp=p.finishedTimestamp));});})),T.length>0&&await Promise.all(T.map(f=>selectAdapterByKey({adapterKey:f.adapter,adapter:i})?.checkAndInitializeTrackerInStore({tx:f,gelatoApiKey:l,...a()})));},executeTxAction:async({defaultTracker:m,actionFunction:d,params:i,beforeTxProcess:T,abortOnTxError:h,...f})=>{let u=h??o??true,S=ie().unix();try{await(T??n)?.();}catch(x){if(u)throw s({initialTx:{...i,actionFunction:d,localTimestamp:S,isInitializing:false,error:normalizeError(x)}}),x;console.warn("[Pulsar] beforeTxProcess failed:",x);}$(i);let{desiredChainID:p,tracker:g,...A}=i,{onSuccess:W,onError:J,onReplaced:V}=f;s({initialTx:{...i,actionFunction:d,localTimestamp:S,isInitializing:true}});let b=selectAdapterByKey({adapterKey:A.adapter,adapter:t}),L=x=>{s(w=>produce(w,y=>{y.initialTx&&(y.initialTx.isInitializing=false,y.initialTx.error=normalizeError(x));}));};if(!b){let x=new Error("No adapter found for this transaction.");throw L(x),x}try{let{connectorType:x,walletAddress:w}=b.getConnectorInfo();await b.checkChainForTx(p);let y=await d();if(!y){s({initialTx:void 0});return}let{tracker:M,txKey:E}=b.checkTransactionsTracker({actionTxKey:y,connectorType:x,tracker:g,gelatoApiKey:l}),Y={...A,connectorType:x,from:w,tracker:M||m,chainId:setChainId(p),localTimestamp:S,txKey:E,hash:M==="ethereum"?y:void 0,pending:!1,isTrackedModalOpen:i.withTrackedModal};await a().addTxToPool(Y),s(Z=>produce(Z,K=>{K.initialTx&&(K.initialTx.isInitializing=!1,K.initialTx.lastTxKey=E);}));let Q=a().transactionsPool[E];await b.checkAndInitializeTrackerInStore({tx:Q,onSuccess:W,onError:J,onReplaced:V,gelatoApiKey:l,...a()});}catch(x){throw L(x),x}}}),{...c}))}var He=(t=>e=>useStore(t,e));var Te=5e3,de=10;function Je(t){let{tx:e,fetcher:r,onInitialize:l,onSuccess:n,onFailure:o,onIntervalTick:c,onReplaced:s,removeTxFromPool:a,pollingInterval:m=Te,maxRetries:d=de}=t;if(!e.pending)return;l?.();let i=d,T=true,h=u=>{T&&(T=false,a&&!u?.withoutRemoving&&a(e.txKey));};(async()=>{for(;T&&i>0;)try{if(await new Promise(u=>setTimeout(u,m)),!T)break;await r({tx:e,stopPolling:h,onSuccess:n,onFailure:o,onIntervalTick:c,onReplaced:s});}catch(u){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,u),i--;}i<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),o(),h());})();}export{B as MAX_TRANSACTION_DESCRIPTION_LENGTH,z as MAX_TRANSACTION_PAYLOAD_BYTES,N as MAX_TRANSACTION_TITLE_LENGTH,P as PulsarTransactionValidationError,O as TransactionStatus,re as TransactionTracker,He as createBoundedUseStore,Ge as createPulsarStore,Ee as createTxInMemoryStore,Je as initializePollingTracker,U as initializeTxTrackingStore,G as selectAllTransactions,te as selectAllTransactionsByActiveWallet,fe as selectPendingTransactions,Pe as selectPendingTransactionsByActiveWallet,ge as selectTxByKey,$ as validateInitialTransactionParams,k as validateTransaction};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuwaio/pulsar-core",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",
@@ -47,14 +47,16 @@
47
47
  },
48
48
  "devDependencies": {
49
49
  "@tuwaio/orbit-core": "^0.2.8",
50
- "dayjs": "^1.11.20",
50
+ "dayjs": "^1.11.21",
51
51
  "immer": "^11.1.8",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^6.0.3",
54
+ "vitest": "^4.1.7",
54
55
  "zustand": "^5.0.13"
55
56
  },
56
57
  "scripts": {
57
58
  "start": "tsup src/index.ts --watch",
58
- "build": "tsup"
59
+ "build": "tsup",
60
+ "test": "vitest"
59
61
  }
60
62
  }