@tuwaio/pulsar-core 0.6.1 → 0.6.3

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,53 @@ 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
+
87
138
  ### The Returned Store API
88
139
 
89
140
  The `createPulsarStore` function returns a vanilla Zustand store with the following state and actions:
@@ -96,7 +147,7 @@ The `createPulsarStore` function returns a vanilla Zustand store with the follow
96
147
 
97
148
  #### **Actions**
98
149
 
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.
150
+ - `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
151
  - `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
152
  - `addTxToPool(tx)`: Adds a new transaction directly to the tracking pool.
102
153
  - `updateTxParams(txKey, fields)`: Updates one or more properties of an existing transaction in the pool.
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,11 +214,20 @@ 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;
217
233
  } & SyncCallbacks<T>;
@@ -363,8 +379,9 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
363
379
  *
364
380
  * @param params The parameters for handling the transaction.
365
381
  * @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.
382
+ * @param params.params The metadata for the transaction. Title, description, and payload are validated before execution.
367
383
  * @param params.defaultTracker The default tracker to use if it cannot be determined automatically.
384
+ * @param params.beforeTxProcess Optional local preflight callback. When provided, it overrides the global callback from `createPulsarStore`.
368
385
  * @param params.onSuccess Callback to execute when the transaction is successfully submitted.
369
386
  * @param params.onError Callback to execute when the transaction fails.
370
387
  * @param params.onReplaced Callback to execute when the transaction is replaced.
@@ -373,6 +390,7 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
373
390
  actionFunction: () => Promise<ActionTxKey | undefined>;
374
391
  params: Omit<InitialTransactionParams, 'actionFunction'>;
375
392
  defaultTracker?: TransactionTracker;
393
+ beforeTxProcess?: BeforeTxProcess;
376
394
  } & TrackerCallbacks<T>) => Promise<void>;
377
395
  /**
378
396
  * Initializes trackers for all pending transactions in the pool.
@@ -543,7 +561,7 @@ declare function createTxInMemoryStore<T extends Transaction>({ localTransaction
543
561
  * @param options Configuration for the Zustand `persist` middleware.
544
562
  * @returns A fully configured Zustand store instance.
545
563
  */
546
- declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
564
+ declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, beforeTxProcess, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
547
565
  setState(partial: ITxTrackingStore<T> | Partial<ITxTrackingStore<T>> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T> | Partial<ITxTrackingStore<T>>), replace?: false | undefined): unknown;
548
566
  setState(state: ITxTrackingStore<T> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T>), replace: true): unknown;
549
567
  persist: {
@@ -661,4 +679,29 @@ type PollingTrackerConfig<R, T extends Transaction> = {
661
679
  */
662
680
  declare function initializePollingTracker<R, T extends Transaction>(config: PollingTrackerConfig<R, T>): void;
663
681
 
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 };
682
+ /** Maximum allowed length for each transaction title string. */
683
+ declare const MAX_TRANSACTION_TITLE_LENGTH = 100;
684
+ /** Maximum allowed length for each transaction description string. */
685
+ declare const MAX_TRANSACTION_DESCRIPTION_LENGTH = 300;
686
+ /** Maximum allowed serialized UTF-8 payload size in bytes. */
687
+ declare const MAX_TRANSACTION_PAYLOAD_BYTES: number;
688
+ /**
689
+ * Error thrown when transaction metadata fails Pulsar's safety limits.
690
+ */
691
+ declare class PulsarTransactionValidationError extends Error {
692
+ /** The transaction field that failed validation. */
693
+ readonly field: string;
694
+ constructor(field: string, message: string);
695
+ }
696
+ /**
697
+ * Validates metadata used before a transaction action is executed.
698
+ * Throws when title, description, or payload violates Pulsar safety limits.
699
+ */
700
+ declare function validateInitialTransactionParams(params: Omit<InitialTransactionParams, 'actionFunction'>): void;
701
+ /**
702
+ * Validates a complete transaction before it is persisted or synchronized.
703
+ * Throws when title, description, or payload violates Pulsar safety limits.
704
+ */
705
+ declare function validateTransaction<T extends Transaction>(tx: T): void;
706
+
707
+ 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,11 +214,20 @@ 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;
217
233
  } & SyncCallbacks<T>;
@@ -363,8 +379,9 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
363
379
  *
364
380
  * @param params The parameters for handling the transaction.
365
381
  * @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.
382
+ * @param params.params The metadata for the transaction. Title, description, and payload are validated before execution.
367
383
  * @param params.defaultTracker The default tracker to use if it cannot be determined automatically.
384
+ * @param params.beforeTxProcess Optional local preflight callback. When provided, it overrides the global callback from `createPulsarStore`.
368
385
  * @param params.onSuccess Callback to execute when the transaction is successfully submitted.
369
386
  * @param params.onError Callback to execute when the transaction fails.
370
387
  * @param params.onReplaced Callback to execute when the transaction is replaced.
@@ -373,6 +390,7 @@ type ITxTrackingStore<T extends Transaction> = IInitializeTxTrackingStore<T> & {
373
390
  actionFunction: () => Promise<ActionTxKey | undefined>;
374
391
  params: Omit<InitialTransactionParams, 'actionFunction'>;
375
392
  defaultTracker?: TransactionTracker;
393
+ beforeTxProcess?: BeforeTxProcess;
376
394
  } & TrackerCallbacks<T>) => Promise<void>;
377
395
  /**
378
396
  * Initializes trackers for all pending transactions in the pool.
@@ -543,7 +561,7 @@ declare function createTxInMemoryStore<T extends Transaction>({ localTransaction
543
561
  * @param options Configuration for the Zustand `persist` middleware.
544
562
  * @returns A fully configured Zustand store instance.
545
563
  */
546
- declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
564
+ declare function createPulsarStore<T extends Transaction>({ adapter, maxTransactions, onRemoteCreate, gelatoApiKey, beforeTxProcess, ...options }: PulsarAdapter<T> & PersistOptions<ITxTrackingStore<T>>): Omit<zustand.StoreApi<ITxTrackingStore<T>>, "setState" | "persist"> & {
547
565
  setState(partial: ITxTrackingStore<T> | Partial<ITxTrackingStore<T>> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T> | Partial<ITxTrackingStore<T>>), replace?: false | undefined): unknown;
548
566
  setState(state: ITxTrackingStore<T> | ((state: ITxTrackingStore<T>) => ITxTrackingStore<T>), replace: true): unknown;
549
567
  persist: {
@@ -661,4 +679,29 @@ type PollingTrackerConfig<R, T extends Transaction> = {
661
679
  */
662
680
  declare function initializePollingTracker<R, T extends Transaction>(config: PollingTrackerConfig<R, T>): void;
663
681
 
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 };
682
+ /** Maximum allowed length for each transaction title string. */
683
+ declare const MAX_TRANSACTION_TITLE_LENGTH = 100;
684
+ /** Maximum allowed length for each transaction description string. */
685
+ declare const MAX_TRANSACTION_DESCRIPTION_LENGTH = 300;
686
+ /** Maximum allowed serialized UTF-8 payload size in bytes. */
687
+ declare const MAX_TRANSACTION_PAYLOAD_BYTES: number;
688
+ /**
689
+ * Error thrown when transaction metadata fails Pulsar's safety limits.
690
+ */
691
+ declare class PulsarTransactionValidationError extends Error {
692
+ /** The transaction field that failed validation. */
693
+ readonly field: string;
694
+ constructor(field: string, message: string);
695
+ }
696
+ /**
697
+ * Validates metadata used before a transaction action is executed.
698
+ * Throws when title, description, or payload violates Pulsar safety limits.
699
+ */
700
+ declare function validateInitialTransactionParams(params: Omit<InitialTransactionParams, 'actionFunction'>): void;
701
+ /**
702
+ * Validates a complete transaction before it is persisted or synchronized.
703
+ * Throws when title, description, or payload violates Pulsar safety limits.
704
+ */
705
+ declare function validateTransaction<T extends Transaction>(tx: T): void;
706
+
707
+ 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:o,onRemoteCreate:t}){return (s,p)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:a=>{let e={...a,pending:true};s(n=>immer.produce(n,r=>{if(r.lastAddedTxKey=a.txKey,a.txKey){if(Object.keys(r.transactionsPool).length>=o){let l=Object.values(r.transactionsPool).sort((T,c)=>T.localTimestamp-c.localTimestamp);if(l.length>0){let T=l[0];delete r.transactionsPool[T.txKey];}}r.transactionsPool[a.txKey]=e;}})),t&&t(e).catch(n=>console.error("[Pulsar Sync] Create failed:",n));},updateTxParams:(a,e)=>{s(n=>immer.produce(n,r=>{let i=r.transactionsPool[a];i&&Object.assign(i,e);}));},removeTxFromPool:a=>{s(e=>immer.produce(e,n=>{delete n.transactionsPool[a];}));},closeTxTrackedModal:a=>{s(e=>immer.produce(e,n=>{if(a&&n.transactionsPool[a]){let r=n.transactionsPool[a];n.transactionsPool[a]={...r,isTrackedModalOpen:false};}n.initialTx=void 0;}));},getLastTxKey:()=>p().lastAddedTxKey})}var F=o=>Object.values(o).sort((t,s)=>Number(t.localTimestamp)-Number(s.localTimestamp)),Y=o=>F(o).filter(t=>t.pending),Z=(o,t)=>o[t],B=(o,t)=>F(o).filter(s=>s.from.toLowerCase()===t.toLowerCase()),ee=(o,t)=>B(o,t).filter(s=>s.pending);var N=(a=>(a.Ethereum="ethereum",a.Safe="safe",a.Gelato="gelato",a.Solana="solana",a))(N||{}),A=(p=>(p.Failed="Failed",p.Success="Success",p.Replaced="Replaced",p))(A||{});var $=o=>o==="Success"||o==="Replaced",M=(o,t)=>{let s=o[t.txKey];return s&&($(s.status)||s.pending)?false:(o[t.txKey]=t,true)};function Te({localTransactionsPool:o,getHistory:t,onHistoryFetched:s}){immer.setAutoFreeze(false);let p=e=>({isLoading:e,isError:false}),a=e=>e?(s&&queueMicrotask(()=>s(e.docs)),n=>immer.produce(n,r=>{let i=r.transactionsPool;for(let l of e.docs)M(i,l);r.currentPage=e.page,r.hasMore=e.hasNextPage,r.isLoading=false;})):n=>n;return vanilla.createStore()((e,n)=>({transactionsPool:o,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:r=>{e(i=>immer.produce(i,l=>{let T=l.transactionsPool;for(let c of Object.values(r))M(T,c);}));},fetchInitial:async r=>{if(!(!t||!r)){e(p(true));try{let i=await t({page:1,walletAddress:r});e(a(i));}catch(i){console.error("[Pulsar] Failed to fetch initial transaction history:",i),e({isLoading:false,isError:true});}}},fetchNextPage:async r=>{let{hasMore:i,isLoading:l,currentPage:T}=n();if(!(!t||!i||l||!r)){e(p(true));try{let c=T+1,m=await t({page:c,walletAddress:r});e(a(m));}catch(c){console.error(`[Pulsar] Failed to fetch transaction history page ${T+1}:`,c),e({isLoading:false,isError:true});}}}}))}function Ae({adapter:o,maxTransactions:t=50,onRemoteCreate:s,gelatoApiKey:p,...a}){return vanilla.createStore()(middleware.persist((e,n)=>({...E({maxTransactions:t,onRemoteCreate:s})(e,n),getAdapter:()=>o,initializeTransactionsPool:async()=>{let r=Object.values(n().transactionsPool).filter(i=>i.pending);await Promise.all(r.map(i=>orbitCore.selectAdapterByKey({adapterKey:i.adapter,adapter:o})?.checkAndInitializeTrackerInStore({tx:i,gelatoApiKey:p,...n()})));},injectExternalPendingTxs:async r=>{let l=n().getAdapter(),T=[];e(c=>immer.produce(c,m=>{let g=m.transactionsPool;r.forEach(d=>{let x=g[d.txKey];d.pending&&!x&&(g[d.txKey]=d,T.push(d));let S=d.status==="Success"||d.status==="Failed"||d.status==="Replaced";x?.pending&&S&&(x.status=d.status,x.pending=false,d.txKey&&(x.txKey=d.txKey),d.finishedTimestamp&&(x.finishedTimestamp=d.finishedTimestamp));});})),T.length>0&&await Promise.all(T.map(c=>orbitCore.selectAdapterByKey({adapterKey:c.adapter,adapter:l})?.checkAndInitializeTrackerInStore({tx:c,gelatoApiKey:p,...n()})));},executeTxAction:async({defaultTracker:r,actionFunction:i,params:l,...T})=>{let{desiredChainID:c,tracker:m,...g}=l,{onSuccess:d,onError:x,onReplaced:S}=T,K=D__default.default().unix();e({initialTx:{...l,actionFunction:i,localTimestamp:K,isInitializing:true}});let f=orbitCore.selectAdapterByKey({adapterKey:g.adapter,adapter:o}),w=u=>{e(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 i();if(!P){e({initialTx:void 0});return}let{tracker:R,txKey:h}=f.checkTransactionsTracker({actionTxKey:P,connectorType:u,tracker:m,gelatoApiKey:p}),O={...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:l.withTrackedModal};n().addTxToPool(O),e(L=>immer.produce(L,I=>{I.initialTx&&(I.initialTx.isInitializing=!1,I.initialTx.lastTxKey=h);}));let z=n().transactionsPool[h];await f.checkAndInitializeTrackerInStore({tx:z,onSuccess:d,onError:x,onReplaced:S,gelatoApiKey:p,...n()});}catch(u){throw w(u),u}}}),{...a}))}var we=(o=>t=>zustand.useStore(o,t));var V=5e3,X=10;function Ee(o){let{tx:t,fetcher:s,onInitialize:p,onSuccess:a,onFailure:e,onIntervalTick:n,onReplaced:r,removeTxFromPool:i,pollingInterval:l=V,maxRetries:T=X}=o;if(!t.pending)return;p?.();let c=T,m=true,g=x=>{m&&(m=false,i&&!x?.withoutRemoving&&i(t.txKey));};(async()=>{for(;m&&c>0;)try{if(await new Promise(x=>setTimeout(x,l)),!m)break;await s({tx:t,stopPolling:g,onSuccess:a,onFailure:e,onIntervalTick:n,onReplaced:r});}catch(x){console.error(`Polling fetcher for txKey ${t.txKey} threw an error:`,x),c--;}c<=0&&(console.warn(`Polling for txKey ${t.txKey} stopped after reaching the maximum number of retries.`),e(),g());})();}exports.TransactionStatus=A;exports.TransactionTracker=N;exports.createBoundedUseStore=we;exports.createPulsarStore=Ae;exports.createTxInMemoryStore=Te;exports.initializePollingTracker=Ee;exports.initializeTxTrackingStore=E;exports.selectAllTransactions=F;exports.selectAllTransactionsByActiveWallet=B;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'),ne=require('dayjs'),middleware=require('zustand/middleware'),zustand=require('zustand');function _interopDefault(e){return e&&e.__esModule?e:{default:e}}var ne__default=/*#__PURE__*/_interopDefault(ne);var z=100,B=300,N=10*1024,V=[/\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){k({field:"title",value:t.title,maxLength:z}),k({field:"description",value:t.description,maxLength:B}),_(t.payload);}function b(t){k({field:"title",value:t.title,maxLength:z}),k({field:"description",value:t.description,maxLength:B}),_(t.payload);}function k({field:t,value:e,maxLength:r}){if(e===void 0)return;(Array.isArray(e)?e:[e]).forEach((n,i)=>{let a=Array.isArray(e)?`${t}[${i}]`:t;if(typeof n!="string")throw new P(a,`${a} must be a string.`);if(n.length>r)throw new P(a,`${a} must be ${r} characters or less.`);E(a,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>N)throw new P("payload",`payload must be ${N} bytes or less when serialized.`);j(t);}function j(t,e="payload"){if(typeof t=="string"){E(e,t);return}t===null||typeof t!="object"||Object.entries(t).forEach(([r,l])=>{let n=`${e}.${r}`;E(n,r),j(l,n);});}function E(t,e){if(V.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=>{b(n);let i={...n,pending:true};r(a=>immer.produce(a,o=>{if(o.lastAddedTxKey=n.txKey,n.txKey){if(Object.keys(o.transactionsPool).length>=t){let d=Object.values(o.transactionsPool).sort((s,T)=>s.localTimestamp-T.localTimestamp);if(d.length>0){let s=d[0];delete o.transactionsPool[s.txKey];}}o.transactionsPool[n.txKey]=i;}})),e&&e(i).catch(a=>console.error("[Pulsar Sync] Create failed:",a));},updateTxParams:(n,i)=>{r(a=>immer.produce(a,o=>{let c=o.transactionsPool[n];c&&Object.assign(c,i);}));},removeTxFromPool:n=>{r(i=>immer.produce(i,a=>{delete a.transactionsPool[n];}));},closeTxTrackedModal:n=>{r(i=>immer.produce(i,a=>{if(n&&a.transactionsPool[n]){let o=a.transactionsPool[n];a.transactionsPool[n]={...o,isTrackedModalOpen:false};}a.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var G=t=>Object.values(t).sort((e,r)=>Number(e.localTimestamp)-Number(r.localTimestamp)),xe=t=>G(t).filter(e=>e.pending),ue=(t,e)=>t[e],Y=(t,e)=>G(t).filter(r=>r.from.toLowerCase()===e.toLowerCase()),me=(t,e)=>Y(t,e).filter(r=>r.pending);var Q=(n=>(n.Ethereum="ethereum",n.Safe="safe",n.Gelato="gelato",n.Solana="solana",n))(Q||{}),R=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(R||{});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 ve({localTransactionsPool:t,getHistory:e,onHistoryFetched:r}){immer.setAutoFreeze(false);let l=i=>({isLoading:i,isError:false}),n=i=>i?(r&&queueMicrotask(()=>r(i.docs)),a=>immer.produce(a,o=>{let c=o.transactionsPool;for(let d of i.docs)X(c,d);o.currentPage=i.page,o.hasMore=i.hasNextPage,o.isLoading=false;})):a=>a;return vanilla.createStore()((i,a)=>({transactionsPool:t,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:o=>{i(c=>immer.produce(c,d=>{let s=d.transactionsPool;for(let T of Object.values(o))X(s,T);}));},fetchInitial:async o=>{if(!(!e||!o)){i(l(true));try{let c=await e({page:1,walletAddress:o});i(n(c));}catch(c){console.error("[Pulsar] Failed to fetch initial transaction history:",c),i({isLoading:false,isError:true});}}},fetchNextPage:async o=>{let{hasMore:c,isLoading:d,currentPage:s}=a();if(!(!e||!c||d||!o)){i(l(true));try{let T=s+1,m=await e({page:T,walletAddress:o});i(n(m));}catch(T){console.error(`[Pulsar] Failed to fetch transaction history page ${s+1}:`,T),i({isLoading:false,isError:true});}}}}))}function _e({adapter:t,maxTransactions:e=50,onRemoteCreate:r,gelatoApiKey:l,beforeTxProcess:n,...i}){return vanilla.createStore()(middleware.persist((a,o)=>({...U({maxTransactions:e,onRemoteCreate:r})(a,o),getAdapter:()=>t,initializeTransactionsPool:async()=>{let d=Object.values(o().transactionsPool).filter(s=>s.pending).filter(s=>{try{return b(s),!0}catch(T){return console.warn("[Pulsar] Removed invalid persisted transaction:",T),o().removeTxFromPool(s.txKey),false}});await Promise.all(d.map(s=>orbitCore.selectAdapterByKey({adapterKey:s.adapter,adapter:t})?.checkAndInitializeTrackerInStore({tx:s,gelatoApiKey:l,...o()})));},injectExternalPendingTxs:async c=>{let s=o().getAdapter(),T=[],m=c.filter(x=>{try{return b(x),!0}catch(y){return console.warn("[Pulsar] Skipped invalid remote transaction:",y),false}});a(x=>immer.produce(x,y=>{let u=y.transactionsPool;m.forEach(p=>{let g=u[p.txKey];p.pending&&!g&&(u[p.txKey]=p,T.push(p));let v=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";g?.pending&&v&&(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(x=>orbitCore.selectAdapterByKey({adapterKey:x.adapter,adapter:s})?.checkAndInitializeTrackerInStore({tx:x,gelatoApiKey:l,...o()})));},executeTxAction:async({defaultTracker:c,actionFunction:d,params:s,beforeTxProcess:T,...m})=>{await(T??n)?.(),$(s);let{desiredChainID:x,tracker:y,...u}=s,{onSuccess:p,onError:g,onReplaced:v}=m,O=ne__default.default().unix();a({initialTx:{...s,actionFunction:d,localTimestamp:O,isInitializing:true}});let S=orbitCore.selectAdapterByKey({adapterKey:u.adapter,adapter:t}),L=f=>{a(I=>immer.produce(I,h=>{h.initialTx&&(h.initialTx.isInitializing=false,h.initialTx.error=orbitCore.normalizeError(f));}));};if(!S){let f=new Error("No adapter found for this transaction.");throw L(f),f}try{let{connectorType:f,walletAddress:I}=S.getConnectorInfo();await S.checkChainForTx(x);let h=await d();if(!h){a({initialTx:void 0});return}let{tracker:M,txKey:w}=S.checkTransactionsTracker({actionTxKey:h,connectorType:f,tracker:y,gelatoApiKey:l}),H={...u,connectorType:f,from:I,tracker:M||c,chainId:orbitCore.setChainId(x),localTimestamp:O,txKey:w,hash:M==="ethereum"?h:void 0,pending:!1,isTrackedModalOpen:s.withTrackedModal};o().addTxToPool(H),a(J=>immer.produce(J,K=>{K.initialTx&&(K.initialTx.isInitializing=!1,K.initialTx.lastTxKey=w);}));let W=o().transactionsPool[w];await S.checkAndInitializeTrackerInStore({tx:W,onSuccess:p,onError:g,onReplaced:v,gelatoApiKey:l,...o()});}catch(f){throw L(f),f}}}),{...i}))}var De=(t=>e=>zustand.useStore(t,e));var se=5e3,ce=10;function Xe(t){let{tx:e,fetcher:r,onInitialize:l,onSuccess:n,onFailure:i,onIntervalTick:a,onReplaced:o,removeTxFromPool:c,pollingInterval:d=se,maxRetries:s=ce}=t;if(!e.pending)return;l?.();let T=s,m=true,x=u=>{m&&(m=false,c&&!u?.withoutRemoving&&c(e.txKey));};(async()=>{for(;m&&T>0;)try{if(await new Promise(u=>setTimeout(u,d)),!m)break;await r({tx:e,stopPolling:x,onSuccess:n,onFailure:i,onIntervalTick:a,onReplaced:o});}catch(u){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,u),T--;}T<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),i(),x());})();}exports.MAX_TRANSACTION_DESCRIPTION_LENGTH=B;exports.MAX_TRANSACTION_PAYLOAD_BYTES=N;exports.MAX_TRANSACTION_TITLE_LENGTH=z;exports.PulsarTransactionValidationError=P;exports.TransactionStatus=R;exports.TransactionTracker=Q;exports.createBoundedUseStore=De;exports.createPulsarStore=_e;exports.createTxInMemoryStore=ve;exports.initializePollingTracker=Xe;exports.initializeTxTrackingStore=U;exports.selectAllTransactions=G;exports.selectAllTransactionsByActiveWallet=Y;exports.selectPendingTransactions=xe;exports.selectPendingTransactionsByActiveWallet=me;exports.selectTxByKey=ue;exports.validateInitialTransactionParams=$;exports.validateTransaction=b;
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:o,onRemoteCreate:t}){return (s,p)=>({transactionsPool:{},lastAddedTxKey:void 0,initialTx:void 0,addTxToPool:a=>{let e={...a,pending:true};s(n=>produce(n,r=>{if(r.lastAddedTxKey=a.txKey,a.txKey){if(Object.keys(r.transactionsPool).length>=o){let l=Object.values(r.transactionsPool).sort((T,c)=>T.localTimestamp-c.localTimestamp);if(l.length>0){let T=l[0];delete r.transactionsPool[T.txKey];}}r.transactionsPool[a.txKey]=e;}})),t&&t(e).catch(n=>console.error("[Pulsar Sync] Create failed:",n));},updateTxParams:(a,e)=>{s(n=>produce(n,r=>{let i=r.transactionsPool[a];i&&Object.assign(i,e);}));},removeTxFromPool:a=>{s(e=>produce(e,n=>{delete n.transactionsPool[a];}));},closeTxTrackedModal:a=>{s(e=>produce(e,n=>{if(a&&n.transactionsPool[a]){let r=n.transactionsPool[a];n.transactionsPool[a]={...r,isTrackedModalOpen:false};}n.initialTx=void 0;}));},getLastTxKey:()=>p().lastAddedTxKey})}var F=o=>Object.values(o).sort((t,s)=>Number(t.localTimestamp)-Number(s.localTimestamp)),Y=o=>F(o).filter(t=>t.pending),Z=(o,t)=>o[t],B=(o,t)=>F(o).filter(s=>s.from.toLowerCase()===t.toLowerCase()),ee=(o,t)=>B(o,t).filter(s=>s.pending);var N=(a=>(a.Ethereum="ethereum",a.Safe="safe",a.Gelato="gelato",a.Solana="solana",a))(N||{}),A=(p=>(p.Failed="Failed",p.Success="Success",p.Replaced="Replaced",p))(A||{});var $=o=>o==="Success"||o==="Replaced",M=(o,t)=>{let s=o[t.txKey];return s&&($(s.status)||s.pending)?false:(o[t.txKey]=t,true)};function Te({localTransactionsPool:o,getHistory:t,onHistoryFetched:s}){setAutoFreeze(false);let p=e=>({isLoading:e,isError:false}),a=e=>e?(s&&queueMicrotask(()=>s(e.docs)),n=>produce(n,r=>{let i=r.transactionsPool;for(let l of e.docs)M(i,l);r.currentPage=e.page,r.hasMore=e.hasNextPage,r.isLoading=false;})):n=>n;return createStore()((e,n)=>({transactionsPool:o,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:r=>{e(i=>produce(i,l=>{let T=l.transactionsPool;for(let c of Object.values(r))M(T,c);}));},fetchInitial:async r=>{if(!(!t||!r)){e(p(true));try{let i=await t({page:1,walletAddress:r});e(a(i));}catch(i){console.error("[Pulsar] Failed to fetch initial transaction history:",i),e({isLoading:false,isError:true});}}},fetchNextPage:async r=>{let{hasMore:i,isLoading:l,currentPage:T}=n();if(!(!t||!i||l||!r)){e(p(true));try{let c=T+1,m=await t({page:c,walletAddress:r});e(a(m));}catch(c){console.error(`[Pulsar] Failed to fetch transaction history page ${T+1}:`,c),e({isLoading:false,isError:true});}}}}))}function Ae({adapter:o,maxTransactions:t=50,onRemoteCreate:s,gelatoApiKey:p,...a}){return createStore()(persist((e,n)=>({...E({maxTransactions:t,onRemoteCreate:s})(e,n),getAdapter:()=>o,initializeTransactionsPool:async()=>{let r=Object.values(n().transactionsPool).filter(i=>i.pending);await Promise.all(r.map(i=>selectAdapterByKey({adapterKey:i.adapter,adapter:o})?.checkAndInitializeTrackerInStore({tx:i,gelatoApiKey:p,...n()})));},injectExternalPendingTxs:async r=>{let l=n().getAdapter(),T=[];e(c=>produce(c,m=>{let g=m.transactionsPool;r.forEach(d=>{let x=g[d.txKey];d.pending&&!x&&(g[d.txKey]=d,T.push(d));let S=d.status==="Success"||d.status==="Failed"||d.status==="Replaced";x?.pending&&S&&(x.status=d.status,x.pending=false,d.txKey&&(x.txKey=d.txKey),d.finishedTimestamp&&(x.finishedTimestamp=d.finishedTimestamp));});})),T.length>0&&await Promise.all(T.map(c=>selectAdapterByKey({adapterKey:c.adapter,adapter:l})?.checkAndInitializeTrackerInStore({tx:c,gelatoApiKey:p,...n()})));},executeTxAction:async({defaultTracker:r,actionFunction:i,params:l,...T})=>{let{desiredChainID:c,tracker:m,...g}=l,{onSuccess:d,onError:x,onReplaced:S}=T,K=D().unix();e({initialTx:{...l,actionFunction:i,localTimestamp:K,isInitializing:true}});let f=selectAdapterByKey({adapterKey:g.adapter,adapter:o}),w=u=>{e(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 i();if(!P){e({initialTx:void 0});return}let{tracker:R,txKey:h}=f.checkTransactionsTracker({actionTxKey:P,connectorType:u,tracker:m,gelatoApiKey:p}),O={...g,connectorType:u,from:k,tracker:R||r,chainId:setChainId(c),localTimestamp:K,txKey:h,hash:R==="ethereum"?P:void 0,pending:!1,isTrackedModalOpen:l.withTrackedModal};n().addTxToPool(O),e(L=>produce(L,I=>{I.initialTx&&(I.initialTx.isInitializing=!1,I.initialTx.lastTxKey=h);}));let z=n().transactionsPool[h];await f.checkAndInitializeTrackerInStore({tx:z,onSuccess:d,onError:x,onReplaced:S,gelatoApiKey:p,...n()});}catch(u){throw w(u),u}}}),{...a}))}var we=(o=>t=>useStore(o,t));var V=5e3,X=10;function Ee(o){let{tx:t,fetcher:s,onInitialize:p,onSuccess:a,onFailure:e,onIntervalTick:n,onReplaced:r,removeTxFromPool:i,pollingInterval:l=V,maxRetries:T=X}=o;if(!t.pending)return;p?.();let c=T,m=true,g=x=>{m&&(m=false,i&&!x?.withoutRemoving&&i(t.txKey));};(async()=>{for(;m&&c>0;)try{if(await new Promise(x=>setTimeout(x,l)),!m)break;await s({tx:t,stopPolling:g,onSuccess:a,onFailure:e,onIntervalTick:n,onReplaced:r});}catch(x){console.error(`Polling fetcher for txKey ${t.txKey} threw an error:`,x),c--;}c<=0&&(console.warn(`Polling for txKey ${t.txKey} stopped after reaching the maximum number of retries.`),e(),g());})();}export{A as TransactionStatus,N as TransactionTracker,we as createBoundedUseStore,Ae as createPulsarStore,Te as createTxInMemoryStore,Ee as initializePollingTracker,E as initializeTxTrackingStore,F as selectAllTransactions,B as selectAllTransactionsByActiveWallet,Y as selectPendingTransactions,ee as selectPendingTransactionsByActiveWallet,Z as selectTxByKey};
1
+ import {produce,setAutoFreeze}from'immer';import {createStore}from'zustand/vanilla';import {selectAdapterByKey,setChainId,normalizeError}from'@tuwaio/orbit-core';import ne from'dayjs';import {persist}from'zustand/middleware';import {useStore}from'zustand';var z=100,B=300,N=10*1024,V=[/\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){k({field:"title",value:t.title,maxLength:z}),k({field:"description",value:t.description,maxLength:B}),_(t.payload);}function b(t){k({field:"title",value:t.title,maxLength:z}),k({field:"description",value:t.description,maxLength:B}),_(t.payload);}function k({field:t,value:e,maxLength:r}){if(e===void 0)return;(Array.isArray(e)?e:[e]).forEach((n,i)=>{let a=Array.isArray(e)?`${t}[${i}]`:t;if(typeof n!="string")throw new P(a,`${a} must be a string.`);if(n.length>r)throw new P(a,`${a} must be ${r} characters or less.`);E(a,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>N)throw new P("payload",`payload must be ${N} bytes or less when serialized.`);j(t);}function j(t,e="payload"){if(typeof t=="string"){E(e,t);return}t===null||typeof t!="object"||Object.entries(t).forEach(([r,l])=>{let n=`${e}.${r}`;E(n,r),j(l,n);});}function E(t,e){if(V.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=>{b(n);let i={...n,pending:true};r(a=>produce(a,o=>{if(o.lastAddedTxKey=n.txKey,n.txKey){if(Object.keys(o.transactionsPool).length>=t){let d=Object.values(o.transactionsPool).sort((s,T)=>s.localTimestamp-T.localTimestamp);if(d.length>0){let s=d[0];delete o.transactionsPool[s.txKey];}}o.transactionsPool[n.txKey]=i;}})),e&&e(i).catch(a=>console.error("[Pulsar Sync] Create failed:",a));},updateTxParams:(n,i)=>{r(a=>produce(a,o=>{let c=o.transactionsPool[n];c&&Object.assign(c,i);}));},removeTxFromPool:n=>{r(i=>produce(i,a=>{delete a.transactionsPool[n];}));},closeTxTrackedModal:n=>{r(i=>produce(i,a=>{if(n&&a.transactionsPool[n]){let o=a.transactionsPool[n];a.transactionsPool[n]={...o,isTrackedModalOpen:false};}a.initialTx=void 0;}));},getLastTxKey:()=>l().lastAddedTxKey})}var G=t=>Object.values(t).sort((e,r)=>Number(e.localTimestamp)-Number(r.localTimestamp)),xe=t=>G(t).filter(e=>e.pending),ue=(t,e)=>t[e],Y=(t,e)=>G(t).filter(r=>r.from.toLowerCase()===e.toLowerCase()),me=(t,e)=>Y(t,e).filter(r=>r.pending);var Q=(n=>(n.Ethereum="ethereum",n.Safe="safe",n.Gelato="gelato",n.Solana="solana",n))(Q||{}),R=(l=>(l.Failed="Failed",l.Success="Success",l.Replaced="Replaced",l))(R||{});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 ve({localTransactionsPool:t,getHistory:e,onHistoryFetched:r}){setAutoFreeze(false);let l=i=>({isLoading:i,isError:false}),n=i=>i?(r&&queueMicrotask(()=>r(i.docs)),a=>produce(a,o=>{let c=o.transactionsPool;for(let d of i.docs)X(c,d);o.currentPage=i.page,o.hasMore=i.hasNextPage,o.isLoading=false;})):a=>a;return createStore()((i,a)=>({transactionsPool:t,isLoading:false,isError:false,hasMore:false,currentPage:1,syncWithLocalPool:o=>{i(c=>produce(c,d=>{let s=d.transactionsPool;for(let T of Object.values(o))X(s,T);}));},fetchInitial:async o=>{if(!(!e||!o)){i(l(true));try{let c=await e({page:1,walletAddress:o});i(n(c));}catch(c){console.error("[Pulsar] Failed to fetch initial transaction history:",c),i({isLoading:false,isError:true});}}},fetchNextPage:async o=>{let{hasMore:c,isLoading:d,currentPage:s}=a();if(!(!e||!c||d||!o)){i(l(true));try{let T=s+1,m=await e({page:T,walletAddress:o});i(n(m));}catch(T){console.error(`[Pulsar] Failed to fetch transaction history page ${s+1}:`,T),i({isLoading:false,isError:true});}}}}))}function _e({adapter:t,maxTransactions:e=50,onRemoteCreate:r,gelatoApiKey:l,beforeTxProcess:n,...i}){return createStore()(persist((a,o)=>({...U({maxTransactions:e,onRemoteCreate:r})(a,o),getAdapter:()=>t,initializeTransactionsPool:async()=>{let d=Object.values(o().transactionsPool).filter(s=>s.pending).filter(s=>{try{return b(s),!0}catch(T){return console.warn("[Pulsar] Removed invalid persisted transaction:",T),o().removeTxFromPool(s.txKey),false}});await Promise.all(d.map(s=>selectAdapterByKey({adapterKey:s.adapter,adapter:t})?.checkAndInitializeTrackerInStore({tx:s,gelatoApiKey:l,...o()})));},injectExternalPendingTxs:async c=>{let s=o().getAdapter(),T=[],m=c.filter(x=>{try{return b(x),!0}catch(y){return console.warn("[Pulsar] Skipped invalid remote transaction:",y),false}});a(x=>produce(x,y=>{let u=y.transactionsPool;m.forEach(p=>{let g=u[p.txKey];p.pending&&!g&&(u[p.txKey]=p,T.push(p));let v=p.status==="Success"||p.status==="Failed"||p.status==="Replaced";g?.pending&&v&&(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(x=>selectAdapterByKey({adapterKey:x.adapter,adapter:s})?.checkAndInitializeTrackerInStore({tx:x,gelatoApiKey:l,...o()})));},executeTxAction:async({defaultTracker:c,actionFunction:d,params:s,beforeTxProcess:T,...m})=>{await(T??n)?.(),$(s);let{desiredChainID:x,tracker:y,...u}=s,{onSuccess:p,onError:g,onReplaced:v}=m,O=ne().unix();a({initialTx:{...s,actionFunction:d,localTimestamp:O,isInitializing:true}});let S=selectAdapterByKey({adapterKey:u.adapter,adapter:t}),L=f=>{a(I=>produce(I,h=>{h.initialTx&&(h.initialTx.isInitializing=false,h.initialTx.error=normalizeError(f));}));};if(!S){let f=new Error("No adapter found for this transaction.");throw L(f),f}try{let{connectorType:f,walletAddress:I}=S.getConnectorInfo();await S.checkChainForTx(x);let h=await d();if(!h){a({initialTx:void 0});return}let{tracker:M,txKey:w}=S.checkTransactionsTracker({actionTxKey:h,connectorType:f,tracker:y,gelatoApiKey:l}),H={...u,connectorType:f,from:I,tracker:M||c,chainId:setChainId(x),localTimestamp:O,txKey:w,hash:M==="ethereum"?h:void 0,pending:!1,isTrackedModalOpen:s.withTrackedModal};o().addTxToPool(H),a(J=>produce(J,K=>{K.initialTx&&(K.initialTx.isInitializing=!1,K.initialTx.lastTxKey=w);}));let W=o().transactionsPool[w];await S.checkAndInitializeTrackerInStore({tx:W,onSuccess:p,onError:g,onReplaced:v,gelatoApiKey:l,...o()});}catch(f){throw L(f),f}}}),{...i}))}var De=(t=>e=>useStore(t,e));var se=5e3,ce=10;function Xe(t){let{tx:e,fetcher:r,onInitialize:l,onSuccess:n,onFailure:i,onIntervalTick:a,onReplaced:o,removeTxFromPool:c,pollingInterval:d=se,maxRetries:s=ce}=t;if(!e.pending)return;l?.();let T=s,m=true,x=u=>{m&&(m=false,c&&!u?.withoutRemoving&&c(e.txKey));};(async()=>{for(;m&&T>0;)try{if(await new Promise(u=>setTimeout(u,d)),!m)break;await r({tx:e,stopPolling:x,onSuccess:n,onFailure:i,onIntervalTick:a,onReplaced:o});}catch(u){console.error(`Polling fetcher for txKey ${e.txKey} threw an error:`,u),T--;}T<=0&&(console.warn(`Polling for txKey ${e.txKey} stopped after reaching the maximum number of retries.`),i(),x());})();}export{B as MAX_TRANSACTION_DESCRIPTION_LENGTH,N as MAX_TRANSACTION_PAYLOAD_BYTES,z as MAX_TRANSACTION_TITLE_LENGTH,P as PulsarTransactionValidationError,R as TransactionStatus,Q as TransactionTracker,De as createBoundedUseStore,_e as createPulsarStore,ve as createTxInMemoryStore,Xe as initializePollingTracker,U as initializeTxTrackingStore,G as selectAllTransactions,Y as selectAllTransactionsByActiveWallet,xe as selectPendingTransactions,me as selectPendingTransactionsByActiveWallet,ue as selectTxByKey,$ as validateInitialTransactionParams,b as validateTransaction};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuwaio/pulsar-core",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",
@@ -51,10 +51,12 @@
51
51
  "immer": "^11.1.8",
52
52
  "tsup": "^8.5.1",
53
53
  "typescript": "^6.0.3",
54
+ "vitest": "^4.1.5",
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
  }