@usebutr/react 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -45,7 +45,7 @@ declare const useWalletStoreContext: () => WalletStore;
45
45
  * the provider mounted. Empty when no `discovery` was passed. */
46
46
  declare const useDiscoveredWallets: () => ReadonlyArray<WalletAdapter>;
47
47
  //#endregion
48
- //#region src/hooks.d.ts
48
+ //#region src/hooks/selectors.d.ts
49
49
  /** Connection status: "idle" | "connecting" | "success" | "error". */
50
50
  declare const useConnectionStatus: () => import("@usebutr/core").ConnectionStatus;
51
51
  /** True iff `connectionStatus === "connecting"`. */
@@ -84,18 +84,58 @@ declare const useIsPlatformConnected: (chainPlatform: ChainPlatform) => boolean;
84
84
  * cleanly.
85
85
  */
86
86
  declare const useAccounts: (connectorId?: string | null) => ReadonlyArray<Account>;
87
+ /**
88
+ * Subscribe to the pool entry for a `connectorId`. Defaults to the
89
+ * active wallet when omitted. Re-renders only when the resolved
90
+ * wallet's identity (connectorId / address / chainId) changes.
91
+ *
92
+ * This used to live alongside the async hooks because `useSigner` /
93
+ * `useBalance` consume it internally; it belongs with the other
94
+ * selectors since its return shape is `ConnectedWallet | undefined`
95
+ * with no async wrapper.
96
+ */
97
+ declare const useWalletEntry: (connectorId: string | null | undefined) => ConnectedWallet | undefined;
87
98
  /** Stable accessor: `(connectorId) => ConnectedWallet | undefined`. */
88
99
  declare const useGetWallet: () => (connectorId: string) => ConnectedWallet | undefined;
89
100
  /** Stable accessor: `(platform) => ConnectedWallet | undefined`. */
90
101
  declare const useGetSelectedWallet: () => (chainPlatform: ChainPlatform) => ConnectedWallet | undefined;
91
102
  /** Stable accessor for raw connector instances. */
92
103
  declare const useGetConnectorInstance: () => (id: string) => ReturnType<import("@usebutr/core").WalletManagerConfig["createConnector"]>;
93
- declare const useConnectWallet: () => (connectorId: string, onSuccess?: (wallet: ConnectedWallet) => void, onError?: (error: Error) => void) => Promise<void>;
104
+ /**
105
+ * Direct access to the Zustand store for custom selectors. Uses shallow
106
+ * equality on the selector result, so returning an inline object or array
107
+ * is safe — no infinite re-render loop. Primitive selectors are
108
+ * unaffected (shallow falls back to `Object.is`).
109
+ *
110
+ * @example
111
+ * const { pool, activeConnectorId } = useWalletStore((state) => ({
112
+ * pool: state.pool,
113
+ * activeConnectorId: state.activeConnectorId,
114
+ * }));
115
+ */
116
+ declare const useWalletStore: <T>(selector: (state: WalletStoreState) => T) => T;
117
+ //#endregion
118
+ //#region src/hooks/actions.d.ts
119
+ /**
120
+ * Action hooks — dispatchers that mutate the wallet store. Each
121
+ * returns a stable function reference (Zustand's `useStore` against
122
+ * action selectors is identity-stable across renders), safe to pass
123
+ * to event handlers or effects without `useCallback`.
124
+ *
125
+ * No reactive subscription beyond identity stability — these don't
126
+ * re-render the component when store state changes. For reactive
127
+ * reads, see `./selectors.ts`.
128
+ */
129
+ /** Begin the connect flow for a connector id. Routes through the
130
+ * store's connect handler, which calls the connector's `connect()`,
131
+ * reads accounts, and writes the resulting `ConnectedWallet` into
132
+ * the pool. */
133
+ declare const useConnectWallet: () => (connectorId: string, onSuccess?: (wallet: import("@usebutr/core").ConnectedWallet) => void, onError?: (error: Error) => void) => Promise<void>;
94
134
  declare const useDisconnectWallet: () => (connectorId: string) => void;
95
135
  declare const useSetActiveConnector: () => (connectorId: string | null) => void;
96
- declare const useSetSelection: () => (chainPlatform: ChainPlatform, connectorId: string | null) => void;
136
+ declare const useSetSelection: () => (chainPlatform: import("@usebutr/core").ChainPlatform, connectorId: string | null) => void;
97
137
  declare const useResetWallet: () => () => void;
98
- declare const useUpdateWalletAccount: () => (connectorId: string, account: Account) => void;
138
+ declare const useUpdateWalletAccount: () => (connectorId: string, account: import("@usebutr/core").Account) => void;
99
139
  declare const useRefreshWallet: () => (connectorId: string) => void;
100
140
  /**
101
141
  * Open the wallet's account-selection UI (if the connector supports it),
@@ -112,21 +152,19 @@ declare const useRequestAccounts: () => (connectorId: string) => Promise<void>;
112
152
  * surfacing an error in UI and giving the user a "dismiss" affordance. */
113
153
  declare const useResetConnectionStatus: () => () => void;
114
154
  declare const useSetConnectionError: () => (error: import("@usebutr/core").ConnectionError | null) => void;
155
+ //#endregion
156
+ //#region src/hooks/async-resources.d.ts
115
157
  /**
116
- * Direct access to the Zustand store for custom selectors. Uses shallow
117
- * equality on the selector result, so returning an inline object or array
118
- * is safe no infinite re-render loop. Primitive selectors are
119
- * unaffected (shallow falls back to `Object.is`).
158
+ * Async-resource hooks return `AsyncState<T>` and run lifecycle
159
+ * effects under the hood. Each composes `useAsyncResource` (defined
160
+ * below) with a selector from `./selectors.ts` (`useWalletEntry`) plus
161
+ * a stable closure that calls the connector.
120
162
  *
121
- * @example
122
- * const { pool, activeConnectorId } = useWalletStore((state) => ({
123
- * pool: state.pool,
124
- * activeConnectorId: state.activeConnectorId,
125
- * }));
163
+ * Adding a new async hook is ~5 lines: import `useAsyncResource`,
164
+ * memoise the request closure on the wallet identity, return the
165
+ * state. The cancellation discipline lives once, in
166
+ * `useAsyncResource`.
126
167
  */
127
- declare const useWalletStore: <T>(selector: (state: WalletStoreState) => T) => T;
128
- //#endregion
129
- //#region src/hooks-async.d.ts
130
168
  type AsyncState<T> = {
131
169
  data: null;
132
170
  error: null;
@@ -144,9 +182,6 @@ type AsyncState<T> = {
144
182
  error: unknown;
145
183
  status: "error";
146
184
  };
147
- /** Subscribe to the pool entry for a connectorId. Re-renders only when the
148
- * resolved wallet's identity (connectorId / address / chainId) changes. */
149
- declare const useWalletEntry: (connectorId: string | null | undefined) => import("@usebutr/core").ConnectedWallet | undefined;
150
185
  /**
151
186
  * Cached signer for a connector. Invalidates when `connectorId`, account
152
187
  * address, or chain id changes — so a chain switch or account switch in the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.tsx","../src/hooks.ts","../src/hooks-async.ts"],"mappings":";;;;cAUM,kBAAA,EAAoB,KAAA,CAAM,OAAO,CAAC,WAAA;;AAFuC;;;;;KAgB1E,0BAAA;EACH,QAAA,EAAU,KAAA,CAAM,SAAA,EAfiC;EAiBjD,UAAA,GAAa,KAAA,CAAM,aAAA,GAHhB;EAKH,eAAA,IAAmB,EAAA,aAAe,aAAA;;;EAGlC,SAAA,GAAY,YAAA;EACZ,SAAA,GAAY,mBAAA;EACZ,cAAA,GAAiB,mBAAA;EACjB,YAAA,GAAe,mBAAA;EACf,UAAA,GAAa,mBAAA;EACb,OAAA,GAAU,mBAAA;EACV,aAAA,GAAgB,mBAAA;EAChB,cAAA,GAAiB,mBAAA;EACjB,sBAAA,GAAyB,mBAAA;EACzB,OAAA,GAAU,mBAAA;EACV,gBAAA,GAAmB,mBAAA;AAAA;;;;;;;;;;;cAmCf,qBAAA,EAAuB,KAAA,CAAM,EAAE,CAAC,0BAAA;;;cAgEhC,qBAAA,QAA4B,WAMjC;;;cAIK,oBAAA,QAA2B,aAAa,CAAC,aAAA;;;;cCnHzC,mBAAA,gCAAmB,gBAGxB;;cAGK,eAAA;;cAMA,wBAAA;;cAMA,oBAAA;;cAMA,kBAAA,gCAAkB,eAAA;;cAMlB,kBAAA;ADxD6C;AAAA,cC8D7C,aAAA;;;;cAQA,qBAAA;;cAUA,OAAA,QAAO,GAAA,SAAA,eAAA;;cAMP,mBAAA,QAA0B,KAAK,CAAC,eAAA;;cAMhC,YAAA,QAAY,GAAA,CAAA,aAAA;;cAMZ,eAAA,QAAsB,eAAe;;cAUrC,iBAAA,GAAqB,aAAA,EAAe,aAAA,YAAuB,eAAe;;cAgB1E,sBAAA,GAA0B,aAA4B,EAAb,aAAa;;;;;;;;cAYtD,WAAA,GAAe,WAAA,qBAA8B,aAAa,CAAC,OAAA;;cAkB3D,YAAA,SAEI,WAAA,aAAmB,eAAA;;cAIvB,oBAAA,SAEI,aAAA,EAAe,aAAA,KAAa,eAAA;;cAQhC,uBAAA,SAAuB,EAAA,aAAA,UAAA,yBAAA,mBAAA;AAAA,cASvB,gBAAA,SAAgB,WAAA,UAAA,SAAA,IAAA,MAAA,EAAA,eAAA,WAAA,OAAA,IAAA,KAAA,EAAA,KAAA,cAAA,OAAA;AAAA,cAKhB,mBAAA,SAAmB,WAAA;AAAA,cAKnB,qBAAA,SAAqB,WAAA;AAAA,cAKrB,eAAA,SAAe,aAAA,EAAA,aAAA,EAAA,WAAA;AAAA,cAKf,cAAA;AAAA,cAKA,sBAAA,SAAsB,WAAA,UAAA,OAAA,EAAA,OAAA;AAAA,cAKtB,gBAAA,SAAgB,WAAA;;;;;;;;;;;cAehB,kBAAA,SAAkB,WAAA,aAAA,OAAA;;;cAOlB,wBAAA;AAAA,cAKA,qBAAA,SAAqB,KAAA,0BAAA,eAAA;;;;;;;;ADzKqC;AAAA;;;;cC8L1D,cAAA,MAAqB,QAAA,GAAW,KAAA,EAAO,gBAAA,KAAqB,CAAA,KAAC,CAAA;;;KCpQ9D,UAAA;EACC,IAAA;EAAY,KAAA;EAAa,MAAA;AAAA;EACzB,IAAA;EAAY,KAAA;EAAa,MAAA;AAAA;EACzB,IAAA,EAAM,CAAC;EAAE,KAAA;EAAa,MAAA;AAAA;EACtB,IAAA;EAAY,KAAA;EAAgB,MAAA;AAAA;;;cAqF5B,cAAA,GAAkB,WAAA,wDAAsC,eAAA;;;;;;;;;;;;;cAwBxD,SAAA,GAAa,WAAA,qBAA8B,UAAU;AAAA,KAQtD,gBAAA,GAAmB,UAAU,CAAC,OAAA;EAAa,OAAA;AAAA;;;;;;;;;cAU1C,UAAA,GAAc,WAAA,kBAA6B,IAAA,cAAgB,gBAehE"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/context.tsx","../src/hooks/selectors.ts","../src/hooks/actions.ts","../src/hooks/async-resources.ts"],"mappings":";;;;cAUM,kBAAA,EAAoB,KAAA,CAAM,OAAO,CAAC,WAAA;;AAFuC;;;;;KAgB1E,0BAAA;EACH,QAAA,EAAU,KAAA,CAAM,SAAA,EAfiC;EAiBjD,UAAA,GAAa,KAAA,CAAM,aAAA,GAHhB;EAKH,eAAA,IAAmB,EAAA,aAAe,aAAA;;;EAGlC,SAAA,GAAY,YAAA;EACZ,SAAA,GAAY,mBAAA;EACZ,cAAA,GAAiB,mBAAA;EACjB,YAAA,GAAe,mBAAA;EACf,UAAA,GAAa,mBAAA;EACb,OAAA,GAAU,mBAAA;EACV,aAAA,GAAgB,mBAAA;EAChB,cAAA,GAAiB,mBAAA;EACjB,sBAAA,GAAyB,mBAAA;EACzB,OAAA,GAAU,mBAAA;EACV,gBAAA,GAAmB,mBAAA;AAAA;;;;;;;;;;;cAmCf,qBAAA,EAAuB,KAAA,CAAM,EAAE,CAAC,0BAAA;;;cAgEhC,qBAAA,QAA4B,WAMjC;;;cAIK,oBAAA,QAA2B,aAAa,CAAC,aAAA;;;;cCnGzC,mBAAA,gCAAmB,gBAGxB;;cAGK,eAAA;;cAMA,wBAAA;;cAMA,oBAAA;;cAMA,kBAAA,gCAAkB,eAAA;;cAMlB,kBAAA;ADxE6C;AAAA,cC8E7C,aAAA;;;;cAQA,qBAAA;;cAUA,OAAA,QAAO,GAAA,SAAA,eAAA;;cAMP,mBAAA,QAA0B,KAAK,CAAC,eAAA;;cAMhC,YAAA,QAAY,GAAA,CAAA,aAAA;;cAMZ,eAAA,QAAsB,eAAe;;cAUrC,iBAAA,GAAqB,aAAA,EAAe,aAAA,YAAuB,eAAe;;cAgB1E,sBAAA,GAA0B,aAA4B,EAAb,aAAa;;;;;;;;cAYtD,WAAA,GAAe,WAAA,qBAA8B,aAAa,CAAC,OAAA;;;;;;;;;;;cAuB3D,cAAA,GAAkB,WAAA,gCAAyC,eAAe;;cAiB1E,YAAA,SAEI,WAAA,aAAmB,eAAA;;cAIvB,oBAAA,SAEI,aAAA,EAAe,aAAA,KAAa,eAAA;;cAQhC,uBAAA,SAAuB,EAAA,aAAA,UAAA,yBAAA,mBAAA;;;;;;;;;;ADhLW;AAAA;;cCqMlC,cAAA,MAAqB,QAAA,GAAW,KAAA,EAAO,gBAAA,KAAqB,CAAA,KAAC,CAAA;;;;;;;ADvOY;;;;;;;;AAE5B;AAAA;cES7C,gBAAA,SAAgB,WAAA,UAAA,SAAA,IAAA,MAAA,0BAAA,eAAA,WAAA,OAAA,IAAA,KAAA,EAAA,KAAA,cAAA,OAAA;AAAA,cAKhB,mBAAA,SAAmB,WAAA;AAAA,cAKnB,qBAAA,SAAqB,WAAA;AAAA,cAKrB,eAAA,SAAe,aAAA,0BAAA,aAAA,EAAA,WAAA;AAAA,cAKf,cAAA;AAAA,cAKA,sBAAA,SAAsB,WAAA,UAAA,OAAA,0BAAA,OAAA;AAAA,cAKtB,gBAAA,SAAgB,WAAA;;;;;;;;;;;cAehB,kBAAA,SAAkB,WAAA,aAAA,OAAA;;;cAOlB,wBAAA;AAAA,cAKA,qBAAA,SAAqB,KAAA,0BAAA,eAAA;;;;;;AFpEoD;;;;;;;;KGS1E,UAAA;EACC,IAAA;EAAY,KAAA;EAAa,MAAA;AAAA;EACzB,IAAA;EAAY,KAAA;EAAa,MAAA;AAAA;EACzB,IAAA,EAAM,CAAC;EAAE,KAAA;EAAa,MAAA;AAAA;EACtB,IAAA;EAAY,KAAA;EAAgB,MAAA;AAAA;;;;;;;;;;;;;cA+F5B,SAAA,GAAa,WAAA,qBAA8B,UAAU;AAAA,KAQtD,gBAAA,GAAmB,UAAU,CAAC,OAAA;EAAa,OAAA;AAAA;;;;;;;;;cAU1C,UAAA,GAAc,WAAA,kBAA6B,IAAA,cAAgB,gBAehE"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createWalletStore as e,logError as t,walletEqual as n}from"@usebutr/core";import{createContext as r,use as i,useCallback as a,useEffect as o,useMemo as s,useReducer as c,useRef as ee,useState as l}from"react";import{jsx as u}from"react/jsx-runtime";import{useStore as d}from"zustand";import{shallow as f}from"zustand/shallow";import{useStoreWithEqualityFn as p}from"zustand/traditional";const m=r(null),h=[],g=r(h),_=(e,t)=>{let n=t.createConnector;return{connectors:t.connectors??[],createConnector:t=>e.get(t)??n?.(t)??null,onConnect:t.onConnect,onConnectError:t.onConnectError,onDisconnect:t.onDisconnect,onHydrated:t.onHydrated,onReset:t.onReset,onSlowConnect:t.onSlowConnect,onStorageError:t.onStorageError,slowConnectThresholdMs:t.slowConnectThresholdMs,storage:t.storage,storageKeyPrefix:t.storageKeyPrefix}},v=n=>{let{children:r,discovery:i}=n,[a]=l(()=>new Map),[s,c]=l(h),[d]=l(()=>e(_(a,n))),[f]=l(()=>i),p=ee(!1);return o(()=>{if(p.current)return;p.current=!0;let e=d.getState();(async()=>{try{await e.hydrateWallets()}catch(e){t(`[butr] failed to hydrate wallets:`,e)}})()},[d]),o(()=>{if(f)return f.subscribe(e=>{a.has(e.id)||(a.set(e.id,e),c(t=>[...t,e]),d.getState().tryRestoreFromPending(e.id))})},[a,f,d]),u(m.Provider,{value:d,children:u(g.Provider,{value:s,children:r})})},y=()=>{let e=i(m);if(!e)throw Error(`useWalletStoreContext must be used within WalletManagerProvider`);return e},te=()=>i(g),b=[],x=(e,t)=>{if(e===t)return!0;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(!r||!i||r.walletAddress!==i.walletAddress||r.chain.id!==i.chain.id)return!1}return!0},S=()=>d(y(),e=>e.connectionStatus),C=()=>d(y(),e=>e.connectionStatus===`connecting`),w=()=>d(y(),e=>e.connectingConnectorId),T=()=>d(y(),e=>e.activeConnectorId),E=()=>d(y(),e=>e.connectionError),D=()=>d(y(),e=>e.pool.size>0),O=()=>d(y(),e=>e.isHydrated),k=()=>d(y(),e=>e.isUserDisconnected),A=()=>d(y(),e=>e.pool),j=()=>{let e=A();return s(()=>[...e.values()],[e])},M=()=>d(y(),e=>e.selection),N=()=>p(y(),e=>e.activeConnectorId?e.pool.get(e.activeConnectorId):void 0,n),P=e=>p(y(),t=>{if(!e)return;let n=t.selection.get(e);return n?t.pool.get(n):void 0},n),F=e=>d(y(),t=>t.selection.has(e)),ne=e=>p(y(),t=>{let n=e??t.activeConnectorId,r=n?t.pool.get(n):void 0;return r?r.accounts:b},x),I=()=>{let e=y();return t=>e.getState().pool.get(t)},L=()=>{let e=y();return t=>{let n=e.getState(),r=n.selection.get(t);return r?n.pool.get(r):void 0}},R=()=>d(y(),e=>e.getConnectorInstance),z=()=>d(y(),e=>e.connectWallet),B=()=>d(y(),e=>e.disconnectWallet),V=()=>d(y(),e=>e.setActiveConnector),H=()=>d(y(),e=>e.setSelection),U=()=>d(y(),e=>e.reset),W=()=>d(y(),e=>e.updateWalletAccount),G=()=>d(y(),e=>e.refreshWallet),K=()=>d(y(),e=>e.requestAccounts),q=()=>d(y(),e=>e.resetConnectionStatus),J=()=>d(y(),e=>e.setConnectionError),Y=e=>p(y(),e,f),X=(e,t)=>{switch(t.type){case`reset`:return{data:null,error:null,status:`idle`};case`load`:return{data:null,error:null,status:`loading`};case`success`:return{data:t.data,error:null,status:`success`};case`error`:return{data:null,error:t.error,status:`error`};default:return{data:null,error:null,status:`idle`}}},Z={data:null,error:null,status:`idle`},Q=e=>{let[t,n]=c(X,Z);return o(()=>{if(!e){n({type:`reset`});return}n({type:`load`});let t=!1;return(async()=>{try{let r=await e();t||n({data:r,type:`success`})}catch(e){t||n({error:e,type:`error`})}})(),()=>{t=!0}},[e]),t},$=e=>p(y(),t=>{let n=e??t.activeConnectorId;return n?t.pool.get(n):void 0},n),re=e=>{let t=$(e);return Q(s(()=>t?()=>t.connector.getSigner():null,[t]))},ie=(e,t)=>{let n=$(e),[r,i]=c(e=>e+1,0),o=a(()=>{i()},[]);return{...Q(s(()=>n?()=>n.connector.getBalance(t):null,[n,t,r])),refetch:o}};export{v as WalletManagerProvider,m as WalletStoreContext,ne as useAccounts,T as useActiveConnectorId,N as useActiveWallet,ie as useBalance,z as useConnectWallet,j as useConnectedWallets,w as useConnectingConnectorId,E as useConnectionError,S as useConnectionStatus,B as useDisconnectWallet,te as useDiscoveredWallets,R as useGetConnectorInstance,L as useGetSelectedWallet,I as useGetWallet,C as useIsConnecting,O as useIsHydrated,F as useIsPlatformConnected,k as useIsUserDisconnected,A as usePool,G as useRefreshWallet,K as useRequestAccounts,q as useResetConnectionStatus,U as useResetWallet,P as useSelectedWallet,M as useSelection,V as useSetActiveConnector,J as useSetConnectionError,H as useSetSelection,re as useSigner,W as useUpdateWalletAccount,D as useWalletConnected,$ as useWalletEntry,Y as useWalletStore,y as useWalletStoreContext};
1
+ import{createWalletStore as e,logError as t,walletEqual as n}from"@usebutr/core";import{createContext as r,use as i,useCallback as a,useEffect as o,useMemo as s,useReducer as c,useRef as l,useState as u}from"react";import{jsx as d}from"react/jsx-runtime";import{useStore as f}from"zustand";import{shallow as p}from"zustand/shallow";import{useStoreWithEqualityFn as m}from"zustand/traditional";const h=r(null),g=[],_=r(g),v=(e,t)=>{let n=t.createConnector;return{connectors:t.connectors??[],createConnector:t=>e.get(t)??n?.(t)??null,onConnect:t.onConnect,onConnectError:t.onConnectError,onDisconnect:t.onDisconnect,onHydrated:t.onHydrated,onReset:t.onReset,onSlowConnect:t.onSlowConnect,onStorageError:t.onStorageError,slowConnectThresholdMs:t.slowConnectThresholdMs,storage:t.storage,storageKeyPrefix:t.storageKeyPrefix}},y=n=>{let{children:r,discovery:i}=n,[a]=u(()=>new Map),[s,c]=u(g),[f]=u(()=>e(v(a,n))),[p]=u(()=>i),m=l(!1);return o(()=>{if(m.current)return;m.current=!0;let e=f.getState();(async()=>{try{await e.hydrateWallets()}catch(e){t(`[butr] failed to hydrate wallets:`,e)}})()},[f]),o(()=>{if(p)return p.subscribe(e=>{a.has(e.id)||(a.set(e.id,e),c(t=>[...t,e]),f.getState().tryRestoreFromPending(e.id))})},[a,p,f]),d(h.Provider,{value:f,children:d(_.Provider,{value:s,children:r})})},b=()=>{let e=i(h);if(!e)throw Error(`useWalletStoreContext must be used within WalletManagerProvider`);return e},ee=()=>i(_),x=[],S=(e,t)=>{if(e===t)return!0;if(e.length!==t.length)return!1;for(let n=0;n<e.length;n+=1){let r=e[n],i=t[n];if(!r||!i||r.walletAddress!==i.walletAddress||r.chain.id!==i.chain.id)return!1}return!0},C=()=>f(b(),e=>e.connectionStatus),w=()=>f(b(),e=>e.connectionStatus===`connecting`),T=()=>f(b(),e=>e.connectingConnectorId),E=()=>f(b(),e=>e.activeConnectorId),D=()=>f(b(),e=>e.connectionError),O=()=>f(b(),e=>e.pool.size>0),k=()=>f(b(),e=>e.isHydrated),A=()=>f(b(),e=>e.isUserDisconnected),j=()=>f(b(),e=>e.pool),M=()=>{let e=j();return s(()=>[...e.values()],[e])},N=()=>f(b(),e=>e.selection),P=()=>m(b(),e=>e.activeConnectorId?e.pool.get(e.activeConnectorId):void 0,n),F=e=>m(b(),t=>{if(!e)return;let n=t.selection.get(e);return n?t.pool.get(n):void 0},n),I=e=>f(b(),t=>t.selection.has(e)),te=e=>m(b(),t=>{let n=e??t.activeConnectorId,r=n?t.pool.get(n):void 0;return r?r.accounts:x},S),L=e=>m(b(),t=>{let n=e??t.activeConnectorId;return n?t.pool.get(n):void 0},n),R=()=>{let e=b();return t=>e.getState().pool.get(t)},z=()=>{let e=b();return t=>{let n=e.getState(),r=n.selection.get(t);return r?n.pool.get(r):void 0}},B=()=>f(b(),e=>e.getConnectorInstance),V=e=>m(b(),e,p),H=()=>f(b(),e=>e.connectWallet),U=()=>f(b(),e=>e.disconnectWallet),W=()=>f(b(),e=>e.setActiveConnector),G=()=>f(b(),e=>e.setSelection),K=()=>f(b(),e=>e.reset),q=()=>f(b(),e=>e.updateWalletAccount),J=()=>f(b(),e=>e.refreshWallet),Y=()=>f(b(),e=>e.requestAccounts),X=()=>f(b(),e=>e.resetConnectionStatus),Z=()=>f(b(),e=>e.setConnectionError),Q=(e,t)=>{switch(t.type){case`reset`:return{data:null,error:null,status:`idle`};case`load`:return{data:null,error:null,status:`loading`};case`success`:return{data:t.data,error:null,status:`success`};case`error`:return{data:null,error:t.error,status:`error`};default:return{data:null,error:null,status:`idle`}}},ne={data:null,error:null,status:`idle`},$=e=>{let[t,n]=c(Q,ne);return o(()=>{if(!e){n({type:`reset`});return}n({type:`load`});let t=!1;return(async()=>{try{let r=await e();t||n({data:r,type:`success`})}catch(e){t||n({error:e,type:`error`})}})(),()=>{t=!0}},[e]),t},re=e=>{let t=L(e);return $(s(()=>t?()=>t.connector.getSigner():null,[t]))},ie=(e,t)=>{let n=L(e),[r,i]=c(e=>e+1,0),o=a(()=>{i()},[]);return{...$(s(()=>n?()=>n.connector.getBalance(t):null,[n,t,r])),refetch:o}};export{y as WalletManagerProvider,h as WalletStoreContext,te as useAccounts,E as useActiveConnectorId,P as useActiveWallet,ie as useBalance,H as useConnectWallet,M as useConnectedWallets,T as useConnectingConnectorId,D as useConnectionError,C as useConnectionStatus,U as useDisconnectWallet,ee as useDiscoveredWallets,B as useGetConnectorInstance,z as useGetSelectedWallet,R as useGetWallet,w as useIsConnecting,k as useIsHydrated,I as useIsPlatformConnected,A as useIsUserDisconnected,j as usePool,J as useRefreshWallet,Y as useRequestAccounts,X as useResetConnectionStatus,K as useResetWallet,F as useSelectedWallet,N as useSelection,W as useSetActiveConnector,Z as useSetConnectionError,G as useSetSelection,re as useSigner,q as useUpdateWalletAccount,O as useWalletConnected,L as useWalletEntry,V as useWalletStore,b as useWalletStoreContext};
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/context.tsx","../src/hooks.ts","../src/hooks-async.ts"],"sourcesContent":["import type {\n ConnectorMeta,\n WalletAdapter,\n WalletManagerConfig,\n WalletSource,\n WalletStore,\n} from \"@usebutr/core\";\nimport { createWalletStore, logError } from \"@usebutr/core\";\nimport React, { createContext, use, useEffect, useRef, useState } from \"react\";\n\nconst WalletStoreContext: React.Context<WalletStore | null> = createContext<WalletStore | null>(\n null,\n);\n\nconst EMPTY_DISCOVERED: ReadonlyArray<WalletAdapter> = [];\nconst DiscoveredWalletsContext: React.Context<ReadonlyArray<WalletAdapter>> =\n createContext<ReadonlyArray<WalletAdapter>>(EMPTY_DISCOVERED);\n\n/**\n * All props — especially `on*` lifecycle callbacks, `storage`, and `discovery` —\n * are captured once at mount via `useState` lazy initializers and are authoritative\n * for the provider's lifetime. Later prop changes are silently ignored, so consumers\n * must pass stable references: module-level values, `useRef`, or `useCallback`.\n */\ntype WalletManagerProviderProps = {\n children: React.ReactNode;\n /** Metadata for explicitly-registered connectors. */\n connectors?: Array<ConnectorMeta>;\n /** Explicit/manual connector factory. Resolved after `discovery`. */\n createConnector?: (id: string) => WalletAdapter | null;\n /** Auto-discovery source. Omit to skip auto-discovery; no\n * protocol code enters the bundle. */\n discovery?: WalletSource;\n onConnect?: WalletManagerConfig[\"onConnect\"];\n onConnectError?: WalletManagerConfig[\"onConnectError\"];\n onDisconnect?: WalletManagerConfig[\"onDisconnect\"];\n onHydrated?: WalletManagerConfig[\"onHydrated\"];\n onReset?: WalletManagerConfig[\"onReset\"];\n onSlowConnect?: WalletManagerConfig[\"onSlowConnect\"];\n onStorageError?: WalletManagerConfig[\"onStorageError\"];\n slowConnectThresholdMs?: WalletManagerConfig[\"slowConnectThresholdMs\"];\n storage?: WalletManagerConfig[\"storage\"];\n storageKeyPrefix?: WalletManagerConfig[\"storageKeyPrefix\"];\n};\n\n/** Build a WalletManagerConfig from flat provider props. */\nconst buildInitialConfig = (\n adapters: Map<string, WalletAdapter>,\n props: Omit<WalletManagerProviderProps, \"children\" | \"discovery\">,\n): WalletManagerConfig => {\n const userCreate = props.createConnector;\n return {\n connectors: props.connectors ?? [],\n createConnector: (id) => adapters.get(id) ?? userCreate?.(id) ?? null,\n onConnect: props.onConnect,\n onConnectError: props.onConnectError,\n onDisconnect: props.onDisconnect,\n onHydrated: props.onHydrated,\n onReset: props.onReset,\n onSlowConnect: props.onSlowConnect,\n onStorageError: props.onStorageError,\n slowConnectThresholdMs: props.slowConnectThresholdMs,\n storage: props.storage,\n storageKeyPrefix: props.storageKeyPrefix,\n };\n};\n\n/**\n * The butr provider. Pass `discovery` (a `WalletSource` such as\n * `autoDiscovery()` from `@usebutr/wallets`) for auto-discovered wallets,\n * and/or `createConnector` to register explicit adapters\n * (WalletConnect, Ledger, custom). When both are present an id is\n * resolved from discovered adapters first, then `createConnector`.\n *\n * The store and the discovery subscription are created once for the\n * provider's lifetime; props captured at mount are authoritative.\n */\nconst WalletManagerProvider: React.FC<WalletManagerProviderProps> = (props) => {\n const { children, discovery: discoveryProp } = props;\n\n // `adapters` is mutated in-place by the discovery subscription so the\n // store's `createConnector` closure always sees the latest discovered set.\n const [adapters] = useState<Map<string, WalletAdapter>>(() => new Map());\n const [discoveredList, setDiscoveredList] =\n useState<ReadonlyArray<WalletAdapter>>(EMPTY_DISCOVERED);\n\n // Store is created once; `buildInitialConfig` closes over `props` at first\n // render and `adapters` (the stable Map). Subsequent re-renders do not\n // re-run this initializer.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- captured once on mount\n const [store] = useState<WalletStore>(() =>\n createWalletStore(buildInitialConfig(adapters, props)),\n );\n\n // Discovery subscription ref is also locked to the first render value.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- captured once on mount\n const [discovery] = useState<WalletSource | undefined>(() => discoveryProp);\n\n const hasHydratedRef = useRef(false);\n\n useEffect(() => {\n if (hasHydratedRef.current) {\n return;\n }\n hasHydratedRef.current = true;\n const state = store.getState();\n void (async () => {\n try {\n await state.hydrateWallets();\n } catch (error: unknown) {\n logError(\"[butr] failed to hydrate wallets:\", error);\n }\n })();\n }, [store]);\n\n useEffect(() => {\n if (!discovery) {\n return;\n }\n const unsubscribe = discovery.subscribe((adapter) => {\n if (adapters.has(adapter.id)) {\n return;\n }\n adapters.set(adapter.id, adapter);\n setDiscoveredList((prev) => [...prev, adapter]);\n void store.getState().tryRestoreFromPending(adapter.id);\n });\n return unsubscribe;\n }, [adapters, discovery, store]);\n\n return (\n <WalletStoreContext.Provider value={store}>\n <DiscoveredWalletsContext.Provider value={discoveredList}>\n {children}\n </DiscoveredWalletsContext.Provider>\n </WalletStoreContext.Provider>\n );\n};\n\n/** Read the store from context. Exported for adapter packages building\n * custom discovery wiring. */\nconst useWalletStoreContext = (): WalletStore => {\n const store = use(WalletStoreContext);\n if (!store) {\n throw new Error(\"useWalletStoreContext must be used within WalletManagerProvider\");\n }\n return store;\n};\n\n/** Reactive list of wallets announced via the `discovery` source since\n * the provider mounted. Empty when no `discovery` was passed. */\nconst useDiscoveredWallets = (): ReadonlyArray<WalletAdapter> => use(DiscoveredWalletsContext);\n\nexport type { WalletManagerProviderProps };\nexport { WalletManagerProvider, WalletStoreContext, useDiscoveredWallets, useWalletStoreContext };\n","import type { Account, ChainPlatform, ConnectedWallet, WalletStoreState } from \"@usebutr/core\";\nimport { walletEqual } from \"@usebutr/core\";\nimport { useMemo } from \"react\";\nimport { useStore } from \"zustand\";\nimport { shallow } from \"zustand/shallow\";\nimport { useStoreWithEqualityFn } from \"zustand/traditional\";\n\nimport { useWalletStoreContext } from \"./context\";\n\nconst EMPTY_ACCOUNTS: ReadonlyArray<Account> = [];\n\nconst accountsEqual = (a: ReadonlyArray<Account>, b: ReadonlyArray<Account>) => {\n if (a === b) {\n return true;\n }\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i += 1) {\n const x = a[i];\n const y = b[i];\n if (!x || !y) {\n return false;\n }\n if (x.walletAddress !== y.walletAddress || x.chain.id !== y.chain.id) {\n return false;\n }\n }\n return true;\n};\n\n// ============================================================================\n// CONNECTION STATE HOOKS\n// ============================================================================\n\n/** Connection status: \"idle\" | \"connecting\" | \"success\" | \"error\". */\nconst useConnectionStatus = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionStatus);\n};\n\n/** True iff `connectionStatus === \"connecting\"`. */\nconst useIsConnecting = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionStatus === \"connecting\");\n};\n\n/** ID of the wallet currently in the connecting flight (null when idle). */\nconst useConnectingConnectorId = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectingConnectorId);\n};\n\n/** ID of the wallet currently focused as the global active selection. */\nconst useActiveConnectorId = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.activeConnectorId);\n};\n\n/** Last connection error message, if any. */\nconst useConnectionError = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionError);\n};\n\n/** True if at least one wallet is connected. */\nconst useWalletConnected = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.pool.size > 0);\n};\n\n/** Has the store finished its initial hydration pass? */\nconst useIsHydrated = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.isHydrated);\n};\n\n/** Session-scoped disconnect-intent flag. True after an explicit disconnect or\n * reset, false after a fresh session or a new connection. Consumers use this\n * to suppress auto-reconnect immediately after a manual disconnect. */\nconst useIsUserDisconnected = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.isUserDisconnected);\n};\n\n// ============================================================================\n// POOL / SELECTION / ACTIVE\n// ============================================================================\n\n/** Full pool of connected wallets, keyed by `connectorId`. Re-renders on any pool change. */\nconst usePool = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.pool);\n};\n\n/** Pool projected as an array. Stable when the pool reference is stable. */\nconst useConnectedWallets = (): Array<ConnectedWallet> => {\n const pool = usePool();\n return useMemo(() => [...pool.values()], [pool]);\n};\n\n/** Per-platform selection: `Map<ChainPlatform, connectorId>`. */\nconst useSelection = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.selection);\n};\n\n/** Globally active wallet (the one in front of the user right now). */\nconst useActiveWallet = (): ConnectedWallet | undefined => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => (state.activeConnectorId ? state.pool.get(state.activeConnectorId) : undefined),\n walletEqual,\n );\n};\n\n/** Reactive lookup of the wallet selected for a given platform. */\nconst useSelectedWallet = (chainPlatform: ChainPlatform | null): ConnectedWallet | undefined => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n if (!chainPlatform) {\n return undefined;\n }\n const id = state.selection.get(chainPlatform);\n return id ? state.pool.get(id) : undefined;\n },\n walletEqual,\n );\n};\n\n/** Reactive boolean: is there a selected wallet for a given platform? */\nconst useIsPlatformConnected = (chainPlatform: ChainPlatform): boolean => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.selection.has(chainPlatform));\n};\n\n/**\n * All known accounts on a wallet. Defaults to the active wallet when\n * `connectorId` is omitted. Re-renders only when the accounts list\n * actually changes (by address + chain id), so wallet-side\n * `accountChanged` events bridged via `Connector.subscribe` flow through\n * cleanly.\n */\nconst useAccounts = (connectorId?: string | null): ReadonlyArray<Account> => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n const id = connectorId ?? state.activeConnectorId;\n const wallet = id ? state.pool.get(id) : undefined;\n return wallet ? wallet.accounts : EMPTY_ACCOUNTS;\n },\n accountsEqual,\n );\n};\n\n// ============================================================================\n// STABLE ACCESSORS (return functions; safe to use in callbacks)\n// ============================================================================\n\n/** Stable accessor: `(connectorId) => ConnectedWallet | undefined`. */\nconst useGetWallet = () => {\n const store = useWalletStoreContext();\n return (connectorId: string) => store.getState().pool.get(connectorId);\n};\n\n/** Stable accessor: `(platform) => ConnectedWallet | undefined`. */\nconst useGetSelectedWallet = () => {\n const store = useWalletStoreContext();\n return (chainPlatform: ChainPlatform) => {\n const state = store.getState();\n const id = state.selection.get(chainPlatform);\n return id ? state.pool.get(id) : undefined;\n };\n};\n\n/** Stable accessor for raw connector instances. */\nconst useGetConnectorInstance = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.getConnectorInstance);\n};\n\n// ============================================================================\n// MUTATION HOOKS\n// ============================================================================\n\nconst useConnectWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectWallet);\n};\n\nconst useDisconnectWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.disconnectWallet);\n};\n\nconst useSetActiveConnector = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setActiveConnector);\n};\n\nconst useSetSelection = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setSelection);\n};\n\nconst useResetWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.reset);\n};\n\nconst useUpdateWalletAccount = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.updateWalletAccount);\n};\n\nconst useRefreshWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.refreshWallet);\n};\n\n/**\n * Open the wallet's account-selection UI (if the connector supports it),\n * then re-read `getAccounts()` and update the pool entry's `accounts`\n * array. EVM wallets call `wallet_requestPermissions` under the hood;\n * Wallet Standard wallets that don't expose a picker just refresh.\n *\n * Consumers should hide the trigger when\n * `wallet.connector.requestAccounts` is undefined — the action still\n * resolves cleanly, but no picker will open.\n */\nconst useRequestAccounts = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.requestAccounts);\n};\n\n/** Clear `connectionError` + reset `connectionStatus` to idle. Useful when\n * surfacing an error in UI and giving the user a \"dismiss\" affordance. */\nconst useResetConnectionStatus = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.resetConnectionStatus);\n};\n\nconst useSetConnectionError = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setConnectionError);\n};\n\n// ============================================================================\n// DIRECT STORE ACCESS\n// ============================================================================\n\n/**\n * Direct access to the Zustand store for custom selectors. Uses shallow\n * equality on the selector result, so returning an inline object or array\n * is safe — no infinite re-render loop. Primitive selectors are\n * unaffected (shallow falls back to `Object.is`).\n *\n * @example\n * const { pool, activeConnectorId } = useWalletStore((state) => ({\n * pool: state.pool,\n * activeConnectorId: state.activeConnectorId,\n * }));\n */\nconst useWalletStore = <T>(selector: (state: WalletStoreState) => T) => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(store, selector, shallow);\n};\n\nexport {\n useAccounts,\n useActiveConnectorId,\n useActiveWallet,\n useConnectedWallets,\n useConnectingConnectorId,\n useConnectionError,\n useConnectionStatus,\n useConnectWallet,\n useDisconnectWallet,\n useGetConnectorInstance,\n useGetSelectedWallet,\n useGetWallet,\n useIsConnecting,\n useIsHydrated,\n useIsPlatformConnected,\n useIsUserDisconnected,\n usePool,\n useRefreshWallet,\n useRequestAccounts,\n useResetConnectionStatus,\n useResetWallet,\n useSelectedWallet,\n useSelection,\n useSetActiveConnector,\n useSetConnectionError,\n useSetSelection,\n useUpdateWalletAccount,\n useWalletConnected,\n useWalletStore,\n};\n","import type { Balance } from \"@usebutr/core\";\nimport { walletEqual } from \"@usebutr/core\";\nimport { useCallback, useEffect, useMemo, useReducer } from \"react\";\nimport { useStoreWithEqualityFn } from \"zustand/traditional\";\n\nimport { useWalletStoreContext } from \"./context\";\n\ntype AsyncState<T> =\n | { data: null; error: null; status: \"idle\" }\n | { data: null; error: null; status: \"loading\" }\n | { data: T; error: null; status: \"success\" }\n | { data: null; error: unknown; status: \"error\" };\n\ntype AsyncAction<T> =\n | { type: \"reset\" }\n | { type: \"load\" }\n | { data: T; type: \"success\" }\n | { error: unknown; type: \"error\" };\n\n/** Pure async-lifecycle reducer. One dispatch per state transition\n * keeps `useEffect` clear of cascading setState calls — each effect\n * branch invokes the reducer exactly once. */\nconst asyncReducer = <T>(_state: AsyncState<T>, action: AsyncAction<T>): AsyncState<T> => {\n switch (action.type) {\n case \"reset\": {\n return { data: null, error: null, status: \"idle\" };\n }\n case \"load\": {\n return { data: null, error: null, status: \"loading\" };\n }\n case \"success\": {\n return { data: action.data, error: null, status: \"success\" };\n }\n case \"error\": {\n return { data: null, error: action.error, status: \"error\" };\n }\n default: {\n // Exhaustiveness check — TS errors here if `AsyncAction` grows\n // a variant without a case.\n const exhaustiveCheck: never = action;\n void exhaustiveCheck;\n return { data: null, error: null, status: \"idle\" };\n }\n }\n};\n\nconst IDLE: AsyncState<never> = { data: null, error: null, status: \"idle\" };\n\n/**\n * Generic async-resource hook. Encapsulates the load → fetch → cancel-\n * on-deps-change → dispatch-result lifecycle that every async wallet\n * read needs. `fn` is the request closure; pass `null` to stay idle.\n *\n * Invalidation is keyed on the identity of `fn` itself — callers\n * stabilise via `useMemo` and re-create the closure when they want a\n * refetch. This keeps the React-hooks exhaustive-deps lint rule happy\n * (the effect's deps list is the literal `[fn]`).\n *\n * Why factored out: every consumer (`useSigner`, `useBalance`, future\n * `useTokenBalance`, `useTransactionReceipt`, …) needs the exact same\n * cancellation discipline. Centralising it keeps the fragile parts\n * (`cancelled` flag, dispatch order) in one place — adding a new\n * async hook becomes a 3-line definition.\n */\nconst useAsyncResource = <T>(fn: (() => Promise<T>) | null): AsyncState<T> => {\n const [state, dispatch] = useReducer(asyncReducer<T>, IDLE);\n\n useEffect(() => {\n if (!fn) {\n dispatch({ type: \"reset\" });\n return;\n }\n dispatch({ type: \"load\" });\n let cancelled = false;\n void (async () => {\n try {\n const data = await fn();\n if (!cancelled) {\n dispatch({ data, type: \"success\" });\n }\n } catch (error: unknown) {\n if (!cancelled) {\n dispatch({ error, type: \"error\" });\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [fn]);\n\n return state;\n};\n\n/** Subscribe to the pool entry for a connectorId. Re-renders only when the\n * resolved wallet's identity (connectorId / address / chainId) changes. */\nconst useWalletEntry = (connectorId: string | null | undefined) => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n const id = connectorId ?? state.activeConnectorId;\n return id ? state.pool.get(id) : undefined;\n },\n walletEqual,\n );\n};\n\n/**\n * Cached signer for a connector. Invalidates when `connectorId`, account\n * address, or chain id changes — so a chain switch or account switch in the\n * wallet invalidates the cached signer automatically.\n *\n * If `connectorId` is omitted (or `null`/`undefined`), the active wallet's\n * signer is returned.\n *\n * Returns `{ data, error, status }`. `status` is `\"idle\"` when there's no\n * wallet, `\"loading\"` while the connector resolves, `\"success\"` once the\n * signer is available, `\"error\"` if `getSigner()` rejected.\n */\nconst useSigner = (connectorId?: string | null): AsyncState<unknown> => {\n const wallet = useWalletEntry(connectorId);\n // Stabilise the request closure so `useAsyncResource` only re-runs\n // when the resolved wallet identity changes (not on every render).\n const fn = useMemo(() => (wallet ? () => wallet.connector.getSigner() : null), [wallet]);\n return useAsyncResource(fn);\n};\n\ntype UseBalanceResult = AsyncState<Balance> & { refetch: () => void };\n\n/**\n * Cached balance for a connector. Invalidates on the same events as\n * `useSigner` (connectorId / address / chainId), plus an explicit `refetch`\n * handle for poll-on-demand or after-action refreshes.\n *\n * If `connectorId` is omitted, the active wallet's balance is returned.\n * `mint` is forwarded to the connector — semantics depend on the chain.\n */\nconst useBalance = (connectorId?: string | null, mint?: string): UseBalanceResult => {\n const wallet = useWalletEntry(connectorId);\n const [counter, bumpCounter] = useReducer((n: number) => n + 1, 0);\n const refetch = useCallback(() => {\n bumpCounter();\n }, []);\n // `counter` participates in the closure identity, so calling\n // `refetch()` produces a new `fn` and `useAsyncResource` re-runs.\n const fn = useMemo(\n () => (wallet ? () => wallet.connector.getBalance(mint) : null),\n // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps -- counter is the refetch trigger; not used inside the closure\n [wallet, mint, counter],\n );\n const state = useAsyncResource(fn);\n return { ...state, refetch };\n};\n\nexport type { AsyncState, UseBalanceResult };\nexport { useBalance, useSigner, useWalletEntry };\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/context.tsx","../src/hooks/selectors.ts","../src/hooks/actions.ts","../src/hooks/async-resources.ts"],"sourcesContent":["import type {\n ConnectorMeta,\n WalletAdapter,\n WalletManagerConfig,\n WalletSource,\n WalletStore,\n} from \"@usebutr/core\";\nimport { createWalletStore, logError } from \"@usebutr/core\";\nimport React, { createContext, use, useEffect, useRef, useState } from \"react\";\n\nconst WalletStoreContext: React.Context<WalletStore | null> = createContext<WalletStore | null>(\n null,\n);\n\nconst EMPTY_DISCOVERED: ReadonlyArray<WalletAdapter> = [];\nconst DiscoveredWalletsContext: React.Context<ReadonlyArray<WalletAdapter>> =\n createContext<ReadonlyArray<WalletAdapter>>(EMPTY_DISCOVERED);\n\n/**\n * All props — especially `on*` lifecycle callbacks, `storage`, and `discovery` —\n * are captured once at mount via `useState` lazy initializers and are authoritative\n * for the provider's lifetime. Later prop changes are silently ignored, so consumers\n * must pass stable references: module-level values, `useRef`, or `useCallback`.\n */\ntype WalletManagerProviderProps = {\n children: React.ReactNode;\n /** Metadata for explicitly-registered connectors. */\n connectors?: Array<ConnectorMeta>;\n /** Explicit/manual connector factory. Resolved after `discovery`. */\n createConnector?: (id: string) => WalletAdapter | null;\n /** Auto-discovery source. Omit to skip auto-discovery; no\n * protocol code enters the bundle. */\n discovery?: WalletSource;\n onConnect?: WalletManagerConfig[\"onConnect\"];\n onConnectError?: WalletManagerConfig[\"onConnectError\"];\n onDisconnect?: WalletManagerConfig[\"onDisconnect\"];\n onHydrated?: WalletManagerConfig[\"onHydrated\"];\n onReset?: WalletManagerConfig[\"onReset\"];\n onSlowConnect?: WalletManagerConfig[\"onSlowConnect\"];\n onStorageError?: WalletManagerConfig[\"onStorageError\"];\n slowConnectThresholdMs?: WalletManagerConfig[\"slowConnectThresholdMs\"];\n storage?: WalletManagerConfig[\"storage\"];\n storageKeyPrefix?: WalletManagerConfig[\"storageKeyPrefix\"];\n};\n\n/** Build a WalletManagerConfig from flat provider props. */\nconst buildInitialConfig = (\n adapters: Map<string, WalletAdapter>,\n props: Omit<WalletManagerProviderProps, \"children\" | \"discovery\">,\n): WalletManagerConfig => {\n const userCreate = props.createConnector;\n return {\n connectors: props.connectors ?? [],\n createConnector: (id) => adapters.get(id) ?? userCreate?.(id) ?? null,\n onConnect: props.onConnect,\n onConnectError: props.onConnectError,\n onDisconnect: props.onDisconnect,\n onHydrated: props.onHydrated,\n onReset: props.onReset,\n onSlowConnect: props.onSlowConnect,\n onStorageError: props.onStorageError,\n slowConnectThresholdMs: props.slowConnectThresholdMs,\n storage: props.storage,\n storageKeyPrefix: props.storageKeyPrefix,\n };\n};\n\n/**\n * The butr provider. Pass `discovery` (a `WalletSource` such as\n * `autoDiscovery()` from `@usebutr/wallets`) for auto-discovered wallets,\n * and/or `createConnector` to register explicit adapters\n * (WalletConnect, Ledger, custom). When both are present an id is\n * resolved from discovered adapters first, then `createConnector`.\n *\n * The store and the discovery subscription are created once for the\n * provider's lifetime; props captured at mount are authoritative.\n */\nconst WalletManagerProvider: React.FC<WalletManagerProviderProps> = (props) => {\n const { children, discovery: discoveryProp } = props;\n\n // `adapters` is mutated in-place by the discovery subscription so the\n // store's `createConnector` closure always sees the latest discovered set.\n const [adapters] = useState<Map<string, WalletAdapter>>(() => new Map());\n const [discoveredList, setDiscoveredList] =\n useState<ReadonlyArray<WalletAdapter>>(EMPTY_DISCOVERED);\n\n // Store is created once; `buildInitialConfig` closes over `props` at first\n // render and `adapters` (the stable Map). Subsequent re-renders do not\n // re-run this initializer.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- captured once on mount\n const [store] = useState<WalletStore>(() =>\n createWalletStore(buildInitialConfig(adapters, props)),\n );\n\n // Discovery subscription ref is also locked to the first render value.\n // eslint-disable-next-line react-hooks/exhaustive-deps -- captured once on mount\n const [discovery] = useState<WalletSource | undefined>(() => discoveryProp);\n\n const hasHydratedRef = useRef(false);\n\n useEffect(() => {\n if (hasHydratedRef.current) {\n return;\n }\n hasHydratedRef.current = true;\n const state = store.getState();\n void (async () => {\n try {\n await state.hydrateWallets();\n } catch (error: unknown) {\n logError(\"[butr] failed to hydrate wallets:\", error);\n }\n })();\n }, [store]);\n\n useEffect(() => {\n if (!discovery) {\n return;\n }\n const unsubscribe = discovery.subscribe((adapter) => {\n if (adapters.has(adapter.id)) {\n return;\n }\n adapters.set(adapter.id, adapter);\n setDiscoveredList((prev) => [...prev, adapter]);\n void store.getState().tryRestoreFromPending(adapter.id);\n });\n return unsubscribe;\n }, [adapters, discovery, store]);\n\n return (\n <WalletStoreContext.Provider value={store}>\n <DiscoveredWalletsContext.Provider value={discoveredList}>\n {children}\n </DiscoveredWalletsContext.Provider>\n </WalletStoreContext.Provider>\n );\n};\n\n/** Read the store from context. Exported for adapter packages building\n * custom discovery wiring. */\nconst useWalletStoreContext = (): WalletStore => {\n const store = use(WalletStoreContext);\n if (!store) {\n throw new Error(\"useWalletStoreContext must be used within WalletManagerProvider\");\n }\n return store;\n};\n\n/** Reactive list of wallets announced via the `discovery` source since\n * the provider mounted. Empty when no `discovery` was passed. */\nconst useDiscoveredWallets = (): ReadonlyArray<WalletAdapter> => use(DiscoveredWalletsContext);\n\nexport type { WalletManagerProviderProps };\nexport { WalletManagerProvider, WalletStoreContext, useDiscoveredWallets, useWalletStoreContext };\n","import type { Account, ChainPlatform, ConnectedWallet, WalletStoreState } from \"@usebutr/core\";\nimport { walletEqual } from \"@usebutr/core\";\nimport { useMemo } from \"react\";\nimport { useStore } from \"zustand\";\nimport { shallow } from \"zustand/shallow\";\nimport { useStoreWithEqualityFn } from \"zustand/traditional\";\n\nimport { useWalletStoreContext } from \"../context\";\n\n/**\n * Selector hooks — pure reactive reads of the wallet store. Each\n * subscribes via `useSyncExternalStore` (through Zustand's `useStore`)\n * and returns the value directly; no `AsyncState` wrapper, no\n * mutation side effects.\n *\n * Three sub-groups in this file:\n * - **Connection-state** scalars (status, error, flags)\n * - **Pool / selection / accounts** (reactive structures)\n * - **Stable accessors** (return functions safe for callbacks)\n *\n * The classification is \"what's returned\" rather than \"is it async.\"\n * For dispatchers, see `./actions.ts`. For async resources\n * (`useSigner`, `useBalance`), see `./async-resources.ts`.\n */\n\nconst EMPTY_ACCOUNTS: ReadonlyArray<Account> = [];\n\nconst accountsEqual = (a: ReadonlyArray<Account>, b: ReadonlyArray<Account>) => {\n if (a === b) {\n return true;\n }\n if (a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i += 1) {\n const x = a[i];\n const y = b[i];\n if (!x || !y) {\n return false;\n }\n if (x.walletAddress !== y.walletAddress || x.chain.id !== y.chain.id) {\n return false;\n }\n }\n return true;\n};\n\n// ============================================================================\n// Connection state\n// ============================================================================\n\n/** Connection status: \"idle\" | \"connecting\" | \"success\" | \"error\". */\nconst useConnectionStatus = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionStatus);\n};\n\n/** True iff `connectionStatus === \"connecting\"`. */\nconst useIsConnecting = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionStatus === \"connecting\");\n};\n\n/** ID of the wallet currently in the connecting flight (null when idle). */\nconst useConnectingConnectorId = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectingConnectorId);\n};\n\n/** ID of the wallet currently focused as the global active selection. */\nconst useActiveConnectorId = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.activeConnectorId);\n};\n\n/** Last connection error message, if any. */\nconst useConnectionError = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectionError);\n};\n\n/** True if at least one wallet is connected. */\nconst useWalletConnected = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.pool.size > 0);\n};\n\n/** Has the store finished its initial hydration pass? */\nconst useIsHydrated = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.isHydrated);\n};\n\n/** Session-scoped disconnect-intent flag. True after an explicit disconnect or\n * reset, false after a fresh session or a new connection. Consumers use this\n * to suppress auto-reconnect immediately after a manual disconnect. */\nconst useIsUserDisconnected = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.isUserDisconnected);\n};\n\n// ============================================================================\n// Pool / selection / accounts\n// ============================================================================\n\n/** Full pool of connected wallets, keyed by `connectorId`. Re-renders on any pool change. */\nconst usePool = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.pool);\n};\n\n/** Pool projected as an array. Stable when the pool reference is stable. */\nconst useConnectedWallets = (): Array<ConnectedWallet> => {\n const pool = usePool();\n return useMemo(() => [...pool.values()], [pool]);\n};\n\n/** Per-platform selection: `Map<ChainPlatform, connectorId>`. */\nconst useSelection = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.selection);\n};\n\n/** Globally active wallet (the one in front of the user right now). */\nconst useActiveWallet = (): ConnectedWallet | undefined => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => (state.activeConnectorId ? state.pool.get(state.activeConnectorId) : undefined),\n walletEqual,\n );\n};\n\n/** Reactive lookup of the wallet selected for a given platform. */\nconst useSelectedWallet = (chainPlatform: ChainPlatform | null): ConnectedWallet | undefined => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n if (!chainPlatform) {\n return undefined;\n }\n const id = state.selection.get(chainPlatform);\n return id ? state.pool.get(id) : undefined;\n },\n walletEqual,\n );\n};\n\n/** Reactive boolean: is there a selected wallet for a given platform? */\nconst useIsPlatformConnected = (chainPlatform: ChainPlatform): boolean => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.selection.has(chainPlatform));\n};\n\n/**\n * All known accounts on a wallet. Defaults to the active wallet when\n * `connectorId` is omitted. Re-renders only when the accounts list\n * actually changes (by address + chain id), so wallet-side\n * `accountChanged` events bridged via `Connector.subscribe` flow through\n * cleanly.\n */\nconst useAccounts = (connectorId?: string | null): ReadonlyArray<Account> => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n const id = connectorId ?? state.activeConnectorId;\n const wallet = id ? state.pool.get(id) : undefined;\n return wallet ? wallet.accounts : EMPTY_ACCOUNTS;\n },\n accountsEqual,\n );\n};\n\n/**\n * Subscribe to the pool entry for a `connectorId`. Defaults to the\n * active wallet when omitted. Re-renders only when the resolved\n * wallet's identity (connectorId / address / chainId) changes.\n *\n * This used to live alongside the async hooks because `useSigner` /\n * `useBalance` consume it internally; it belongs with the other\n * selectors since its return shape is `ConnectedWallet | undefined`\n * with no async wrapper.\n */\nconst useWalletEntry = (connectorId: string | null | undefined): ConnectedWallet | undefined => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(\n store,\n (state) => {\n const id = connectorId ?? state.activeConnectorId;\n return id ? state.pool.get(id) : undefined;\n },\n walletEqual,\n );\n};\n\n// ============================================================================\n// Stable accessors (return functions; safe to use in callbacks)\n// ============================================================================\n\n/** Stable accessor: `(connectorId) => ConnectedWallet | undefined`. */\nconst useGetWallet = () => {\n const store = useWalletStoreContext();\n return (connectorId: string) => store.getState().pool.get(connectorId);\n};\n\n/** Stable accessor: `(platform) => ConnectedWallet | undefined`. */\nconst useGetSelectedWallet = () => {\n const store = useWalletStoreContext();\n return (chainPlatform: ChainPlatform) => {\n const state = store.getState();\n const id = state.selection.get(chainPlatform);\n return id ? state.pool.get(id) : undefined;\n };\n};\n\n/** Stable accessor for raw connector instances. */\nconst useGetConnectorInstance = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.getConnectorInstance);\n};\n\n// ============================================================================\n// Direct store access (escape hatch)\n// ============================================================================\n\n/**\n * Direct access to the Zustand store for custom selectors. Uses shallow\n * equality on the selector result, so returning an inline object or array\n * is safe — no infinite re-render loop. Primitive selectors are\n * unaffected (shallow falls back to `Object.is`).\n *\n * @example\n * const { pool, activeConnectorId } = useWalletStore((state) => ({\n * pool: state.pool,\n * activeConnectorId: state.activeConnectorId,\n * }));\n */\nconst useWalletStore = <T>(selector: (state: WalletStoreState) => T) => {\n const store = useWalletStoreContext();\n return useStoreWithEqualityFn(store, selector, shallow);\n};\n\nexport {\n useAccounts,\n useActiveConnectorId,\n useActiveWallet,\n useConnectedWallets,\n useConnectingConnectorId,\n useConnectionError,\n useConnectionStatus,\n useGetConnectorInstance,\n useGetSelectedWallet,\n useGetWallet,\n useIsConnecting,\n useIsHydrated,\n useIsPlatformConnected,\n useIsUserDisconnected,\n usePool,\n useSelectedWallet,\n useSelection,\n useWalletConnected,\n useWalletEntry,\n useWalletStore,\n};\n","import { useStore } from \"zustand\";\n\nimport { useWalletStoreContext } from \"../context\";\n\n/**\n * Action hooks — dispatchers that mutate the wallet store. Each\n * returns a stable function reference (Zustand's `useStore` against\n * action selectors is identity-stable across renders), safe to pass\n * to event handlers or effects without `useCallback`.\n *\n * No reactive subscription beyond identity stability — these don't\n * re-render the component when store state changes. For reactive\n * reads, see `./selectors.ts`.\n */\n\n/** Begin the connect flow for a connector id. Routes through the\n * store's connect handler, which calls the connector's `connect()`,\n * reads accounts, and writes the resulting `ConnectedWallet` into\n * the pool. */\nconst useConnectWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.connectWallet);\n};\n\nconst useDisconnectWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.disconnectWallet);\n};\n\nconst useSetActiveConnector = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setActiveConnector);\n};\n\nconst useSetSelection = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setSelection);\n};\n\nconst useResetWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.reset);\n};\n\nconst useUpdateWalletAccount = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.updateWalletAccount);\n};\n\nconst useRefreshWallet = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.refreshWallet);\n};\n\n/**\n * Open the wallet's account-selection UI (if the connector supports it),\n * then re-read `getAccounts()` and update the pool entry's `accounts`\n * array. EVM wallets call `wallet_requestPermissions` under the hood;\n * Wallet Standard wallets that don't expose a picker just refresh.\n *\n * Consumers should hide the trigger when\n * `wallet.connector.requestAccounts` is undefined — the action still\n * resolves cleanly, but no picker will open.\n */\nconst useRequestAccounts = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.requestAccounts);\n};\n\n/** Clear `connectionError` + reset `connectionStatus` to idle. Useful when\n * surfacing an error in UI and giving the user a \"dismiss\" affordance. */\nconst useResetConnectionStatus = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.resetConnectionStatus);\n};\n\nconst useSetConnectionError = () => {\n const store = useWalletStoreContext();\n return useStore(store, (state) => state.setConnectionError);\n};\n\nexport {\n useConnectWallet,\n useDisconnectWallet,\n useRefreshWallet,\n useRequestAccounts,\n useResetConnectionStatus,\n useResetWallet,\n useSetActiveConnector,\n useSetConnectionError,\n useSetSelection,\n useUpdateWalletAccount,\n};\n","import type { Balance } from \"@usebutr/core\";\nimport { useCallback, useEffect, useMemo, useReducer } from \"react\";\n\nimport { useWalletEntry } from \"./selectors\";\n\n/**\n * Async-resource hooks — return `AsyncState<T>` and run lifecycle\n * effects under the hood. Each composes `useAsyncResource` (defined\n * below) with a selector from `./selectors.ts` (`useWalletEntry`) plus\n * a stable closure that calls the connector.\n *\n * Adding a new async hook is ~5 lines: import `useAsyncResource`,\n * memoise the request closure on the wallet identity, return the\n * state. The cancellation discipline lives once, in\n * `useAsyncResource`.\n */\n\ntype AsyncState<T> =\n | { data: null; error: null; status: \"idle\" }\n | { data: null; error: null; status: \"loading\" }\n | { data: T; error: null; status: \"success\" }\n | { data: null; error: unknown; status: \"error\" };\n\ntype AsyncAction<T> =\n | { type: \"reset\" }\n | { type: \"load\" }\n | { data: T; type: \"success\" }\n | { error: unknown; type: \"error\" };\n\n/** Pure async-lifecycle reducer. One dispatch per state transition\n * keeps `useEffect` clear of cascading setState calls — each effect\n * branch invokes the reducer exactly once. */\nconst asyncReducer = <T>(_state: AsyncState<T>, action: AsyncAction<T>): AsyncState<T> => {\n switch (action.type) {\n case \"reset\": {\n return { data: null, error: null, status: \"idle\" };\n }\n case \"load\": {\n return { data: null, error: null, status: \"loading\" };\n }\n case \"success\": {\n return { data: action.data, error: null, status: \"success\" };\n }\n case \"error\": {\n return { data: null, error: action.error, status: \"error\" };\n }\n default: {\n // Exhaustiveness check — TS errors here if `AsyncAction` grows\n // a variant without a case.\n const exhaustiveCheck: never = action;\n void exhaustiveCheck;\n return { data: null, error: null, status: \"idle\" };\n }\n }\n};\n\nconst IDLE: AsyncState<never> = { data: null, error: null, status: \"idle\" };\n\n/**\n * Generic async-resource hook. Encapsulates the load → fetch → cancel-\n * on-deps-change → dispatch-result lifecycle that every async wallet\n * read needs. `fn` is the request closure; pass `null` to stay idle.\n *\n * Invalidation is keyed on the identity of `fn` itself — callers\n * stabilise via `useMemo` and re-create the closure when they want a\n * refetch. This keeps the React-hooks exhaustive-deps lint rule happy\n * (the effect's deps list is the literal `[fn]`).\n *\n * Why factored out: every consumer (`useSigner`, `useBalance`, future\n * `useTokenBalance`, `useTransactionReceipt`, …) needs the exact same\n * cancellation discipline. Centralising it keeps the fragile parts\n * (`cancelled` flag, dispatch order) in one place — adding a new\n * async hook becomes a 3-line definition.\n */\nconst useAsyncResource = <T>(fn: (() => Promise<T>) | null): AsyncState<T> => {\n const [state, dispatch] = useReducer(asyncReducer<T>, IDLE);\n\n useEffect(() => {\n if (!fn) {\n dispatch({ type: \"reset\" });\n return;\n }\n dispatch({ type: \"load\" });\n let cancelled = false;\n void (async () => {\n try {\n const data = await fn();\n if (!cancelled) {\n dispatch({ data, type: \"success\" });\n }\n } catch (error: unknown) {\n if (!cancelled) {\n dispatch({ error, type: \"error\" });\n }\n }\n })();\n return () => {\n cancelled = true;\n };\n }, [fn]);\n\n return state;\n};\n\n/**\n * Cached signer for a connector. Invalidates when `connectorId`, account\n * address, or chain id changes — so a chain switch or account switch in the\n * wallet invalidates the cached signer automatically.\n *\n * If `connectorId` is omitted (or `null`/`undefined`), the active wallet's\n * signer is returned.\n *\n * Returns `{ data, error, status }`. `status` is `\"idle\"` when there's no\n * wallet, `\"loading\"` while the connector resolves, `\"success\"` once the\n * signer is available, `\"error\"` if `getSigner()` rejected.\n */\nconst useSigner = (connectorId?: string | null): AsyncState<unknown> => {\n const wallet = useWalletEntry(connectorId);\n // Stabilise the request closure so `useAsyncResource` only re-runs\n // when the resolved wallet identity changes (not on every render).\n const fn = useMemo(() => (wallet ? () => wallet.connector.getSigner() : null), [wallet]);\n return useAsyncResource(fn);\n};\n\ntype UseBalanceResult = AsyncState<Balance> & { refetch: () => void };\n\n/**\n * Cached balance for a connector. Invalidates on the same events as\n * `useSigner` (connectorId / address / chainId), plus an explicit `refetch`\n * handle for poll-on-demand or after-action refreshes.\n *\n * If `connectorId` is omitted, the active wallet's balance is returned.\n * `mint` is forwarded to the connector — semantics depend on the chain.\n */\nconst useBalance = (connectorId?: string | null, mint?: string): UseBalanceResult => {\n const wallet = useWalletEntry(connectorId);\n const [counter, bumpCounter] = useReducer((n: number) => n + 1, 0);\n const refetch = useCallback(() => {\n bumpCounter();\n }, []);\n // `counter` participates in the closure identity, so calling\n // `refetch()` produces a new `fn` and `useAsyncResource` re-runs.\n const fn = useMemo(\n () => (wallet ? () => wallet.connector.getBalance(mint) : null),\n // oxlint-disable-next-line eslint-plugin-react-hooks/exhaustive-deps -- counter is the refetch trigger; not used inside the closure\n [wallet, mint, counter],\n );\n const state = useAsyncResource(fn);\n return { ...state, refetch };\n};\n\nexport type { AsyncState, UseBalanceResult };\nexport { useBalance, useSigner };\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usebutr/react",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React provider and hooks for butr. Depends on @usebutr/core, react, zustand.",
5
5
  "license": "MIT",
6
6
  "author": "Pedro Filho <pedro@filho.me>",
@@ -25,7 +25,7 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@usebutr/core": "0.1.0"
28
+ "@usebutr/core": "0.2.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@testing-library/jest-dom": "^6.9.1",
@@ -40,7 +40,7 @@
40
40
  "zustand": "^5.0.0",
41
41
  "@repo/config-vitest": "0.0.0",
42
42
  "@repo/typescript-config": "0.0.0",
43
- "@usebutr/testing": "0.1.0"
43
+ "@usebutr/testing": "0.1.1"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": ">=18.0.0",