@tuwaio/satellite-evm 0.5.0 β†’ 0.6.0

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
@@ -1,154 +1,80 @@
1
- # Satellite Connect EVM
1
+ # @tuwaio/satellite-evm
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/@tuwaio/satellite-evm.svg)](https://www.npmjs.com/package/@tuwaio/satellite-evm)
4
- [![License](https://img.shields.io/npm/l/@tuwaio/satellite-evm.svg)](./LICENSE)
5
- [![Build Status](https://img.shields.io/github/actions/workflow/status/TuwaIO/satellite-connect/release.yml?branch=main)](https://github.com/TuwaIO/satellite-connect/actions)
4
+ [![License](https://img.shields.io/npm/l/@tuwaio/satellite-evm.svg)](https://github.com/TuwaIO/satellite-connect/blob/main/packages/satellite-evm/LICENSE)
6
5
 
7
- Low-level EVM wallet connectivity adapters built strictly on top of `viem` and `wagmi` primitives for the TUWA Ecosystem.
6
+ `@tuwaio/satellite-evm` is the EVM Layer 4 (L4) package of **Satellite Connect**, the wallet connection project of TUWA Stage 2 ("State & Connection", next to Pulsar). Built on **`@wagmi/core`**, **`viem`** and **`@tuwaio/orbit-evm`**, it provides the EVM adapter for [`@tuwaio/satellite-core`](https://satellite.docs.tuwa.io/packages/satellite-core), a watcher that keeps the store in sync with wagmi, and helpers for your wagmi config: default transports, Safe connector options and a connector that impersonates any address for testing. It does not use `ethers.js` or `web3.js`.
8
7
 
9
8
  ---
10
9
 
11
- ## πŸ›οΈ What is `@tuwaio/satellite-evm`?
10
+ ## πŸ›οΈ Core Capabilities
12
11
 
13
- `@tuwaio/satellite-evm` is the low-level EVM network connection adapter (Layer 4) of the Satellite framework. It implements native connection watchers, cryptographic signature handlers, and smart contract verification layers directly on top of `viem` and `@wagmi/core`.
14
-
15
- By bypassing proprietary wallet connection wrappers, this package ensures direct execution on raw client protocols and enforces strict RPC endpoint isolation.
16
-
17
- ---
18
-
19
- ## ✨ Engineering Features
20
-
21
- - **Direct Wagmi Alignment:** Deep integration with `@wagmi/core` configurations, avoiding wrapper bloat.
22
- - **Custom Connection Watchers:** Low-level event listener hooks tracking provider chain changes, account switches, and connection states.
23
- - **Contract Wallet Checks:** Native, caching verification utilities (`checkIsWalletAddressContract`) for verifying EIP-1271 signatures or contract accounts.
24
- - **RPC Transport Isolation:** Isolated transport rules with explicit path mapping using the `createDefaultTransports` controller.
12
+ - **Adapter:** `satelliteEVMAdapter(wagmiConfig, chains)` connects the connectors of your wagmi config (injected wallets found through EIP-6963, WalletConnect, Safe, Coinbase…) by connector type, such as `"evm:metamask"`. It asks the wallet to switch chains, reads native balances, builds block explorer links (`undefined` when the chain has no explorer), resolves ENS names and avatars on Ethereum Mainnet, and checks whether the account is a smart contract.
13
+ - **Watcher:** `createEVMConnectionsWatcher` subscribes to wagmi's `watchConnections` and copies account and chain changes made in the wallet into the store; it disconnects the store when the wallet disconnects. With a SIWX session it also disconnects when the sign-in is rejected or fails, or when the wallet switches to another account or chain than the session. Pass the store's `getState`, and the watcher reads the current state on every event. React apps use `EVMConnectorsWatcher` from [`@tuwaio/satellite-react/evm`](https://satellite.docs.tuwa.io/packages/satellite-react) instead.
14
+ - **Contract accounts:** `checkIsWalletAddressContract` reads the account's code on the requested chain and caches the result in memory per chain and address.
15
+ - **wagmi config helpers:** `createDefaultTransports(chains)` creates an `http()` transport for every chain; `safeSdkOptions` are the options for the `safe` connector of `@wagmi/connectors`, with anchored Safe{Wallet} origins; `impersonated()` is a development connector that acts as a wallet for the address saved in `localStorage` and forwards requests to your node.
25
16
 
26
17
  ---
27
18
 
28
19
  ## πŸ’Ύ Installation
29
20
 
30
- ### Requirements
31
-
32
- - Node.js 20-24
33
- - TypeScript 5.9+
34
-
35
21
  ```bash
36
22
  pnpm add @tuwaio/satellite-evm @tuwaio/satellite-core @tuwaio/orbit-core @tuwaio/orbit-evm @wagmi/core viem zustand immer
37
23
  ```
38
24
 
39
- ---
25
+ > [!IMPORTANT]
26
+ > `@tuwaio/satellite-core` (>=0.5), `@tuwaio/orbit-core` (>=0.3), `@tuwaio/orbit-evm` (>=0.3), `@wagmi/core` (3.x) and `viem` (2.x) are peer dependencies and must be installed alongside `@tuwaio/satellite-evm`. `zustand` and `immer` are the peer dependencies of `@tuwaio/satellite-core`. Add `@wagmi/connectors` if you use the `safe`, `walletConnect` or `coinbaseWallet` connectors.
40
27
 
41
- ## πŸš€ Quick Start
28
+ ---
42
29
 
43
- ### Basic Configuration
30
+ ## πŸš€ Usage
44
31
 
45
32
  ```typescript
46
- import { createDefaultTransports } from '@tuwaio/satellite-evm';
47
- import { createConfig } from '@wagmi/core';
48
- import { injected } from '@wagmi/connectors';
33
+ import { createDefaultTransports, impersonated, safeSdkOptions, satelliteEVMAdapter } from '@tuwaio/satellite-evm';
34
+ import { safe } from '@wagmi/connectors';
35
+ import { createConfig, injected } from '@wagmi/core';
49
36
  import { mainnet, sepolia } from 'viem/chains';
50
- import type { Chain } from 'viem/chains';
51
37
 
52
- export const appEVMChains = [mainnet, sepolia] as readonly [Chain, ...Chain[]];
38
+ export const appChains = [mainnet, sepolia] as const;
53
39
 
54
40
  export const wagmiConfig = createConfig({
55
- connectors: [injected()],
56
- transports: createDefaultTransports(appEVMChains), // Automatically creates http transports
57
- chains: appEVMChains,
58
- ssr: true, // Enable SSR support if needed (e.g., in Next.js)
41
+ chains: appChains,
42
+ connectors: [injected(), safe({ ...safeSdkOptions }), impersonated({})],
43
+ transports: createDefaultTransports(appChains), // public RPC URLs: pass your own http(url) transports in production
44
+ ssr: true,
59
45
  });
60
- ```
61
-
62
- ---
63
-
64
- ## πŸ”Œ Using the EVM Adapter
65
46
 
66
- The core of this package is the `satelliteEVMAdapter`. It bridges the Satellite Connect system with the underlying `wagmi` configuration and functionalities.
67
-
68
- ### Creating the Adapter
69
-
70
- You create the adapter by passing your `wagmiConfig` to the `satelliteEVMAdapter` function.
71
-
72
- ```typescript
73
- import { satelliteEVMAdapter } from '@tuwaio/satellite-evm';
74
- import { wagmiConfig } from './your-wagmi-config'; // Import your configured wagmiConfig
75
-
76
- export const evmAdapter = satelliteEVMAdapter(wagmiConfig, appEVMChains);
47
+ export const evmAdapter = satelliteEVMAdapter(wagmiConfig, appChains);
77
48
  ```
78
49
 
79
- ### Integrating with Satellite Connect Provider
80
-
81
- Use the created adapter within the `SatelliteConnectProvider` from `@tuwaio/satellite-react`.
82
-
83
- ```tsx
84
- import { SatelliteConnectProvider, EVMConnectorsWatcher } from '@tuwaio/satellite-react';
85
- import { WagmiProvider } from 'wagmi';
86
- import { satelliteEVMAdapter } from '@tuwaio/satellite-evm';
87
- import { wagmiConfig } from './your-wagmi-config';
88
- import { evmAdapter } from './your-evm-adapter';
89
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // Wagmi requires react-query
90
-
91
- const queryClient = new QueryClient();
92
-
93
- function AppProviders({ children }: { children: React.ReactNode }) {
94
- return (
95
- <WagmiProvider config={wagmiConfig}>
96
- <QueryClientProvider client={queryClient}>
97
- <SatelliteConnectProvider
98
- adapter={evmAdapter} // Pass the EVM adapter
99
- autoConnect={true} // Optional: enable auto-connect
100
- >
101
- <EVMConnectorsWatcher wagmiConfig={wagmiConfig} /> {/* Manages EVM wallet state */}
102
- {children}
103
- </SatelliteConnectProvider>
104
- </QueryClientProvider>
105
- </WagmiProvider>
106
- );
107
- }
108
- ```
50
+ Pass `evmAdapter` to `createSatelliteConnectStore` from [`@tuwaio/satellite-core`](https://satellite.docs.tuwa.io/packages/satellite-core) or to `SatelliteConnectProvider` from [`@tuwaio/satellite-react`](https://satellite.docs.tuwa.io/packages/satellite-react), and run the watcher next to it. The complete React setup is on the **[`@tuwaio/satellite-react`](https://satellite.docs.tuwa.io/packages/satellite-react)** page; the watcher without React is shown on the **[`createEVMConnectionsWatcher`](https://satellite.docs.tuwa.io/packages/satellite-evm/functions/createEVMConnectionsWatcher)** page.
109
51
 
110
52
  ---
111
53
 
112
- ## πŸ” Sign-In With X (SIWX) Session Integration
113
-
114
- The `EVMConnectorsWatcher` component accepts a `siwx` prop directly compatible with `@tuwaio/siwx-react`. It monitors session status and automatically handles wallet disconnections if the user switches accounts or chains without an active SIWX session.
54
+ ## πŸ—„οΈ Browser Storage
115
55
 
116
- ```tsx
117
- import { useSiwxSession } from '@tuwaio/siwx-react';
118
- import { SatelliteConnectProvider } from '@tuwaio/satellite-react';
119
- import { EVMConnectorsWatcher } from '@tuwaio/satellite-react/evm';
120
- import { satelliteEVMAdapter } from '@tuwaio/satellite-evm';
121
-
122
- function AppProviders({ children }: { children: ReactNode }) {
123
- const siwxSession = useSiwxSession();
124
-
125
- return (
126
- <SatelliteConnectProvider adapter={satelliteEVMAdapter(wagmiConfig, appEVMChains)} autoConnect={true}>
127
- <EVMConnectorsWatcher wagmiConfig={wagmiConfig} siwx={siwxSession} />
128
- {children}
129
- </SatelliteConnectProvider>
130
- );
131
- }
132
- ```
56
+ The package writes nothing to `localStorage`. The `impersonated` connector reads `satellite-connect:impersonatedAddress` (set with `impersonatedHelpers.setImpersonated` from `@tuwaio/orbit-core`, for example by Nova Connect) on every request; `@tuwaio/satellite-core` removes the key on `disconnectAll` and when the last connection is disconnected. The keys that the store writes for EVM connections are listed on the [`@tuwaio/satellite-core`](https://satellite.docs.tuwa.io/packages/satellite-core) page. wagmi keeps its own state in the `storage` of your wagmi config.
133
57
 
134
58
  ---
135
59
 
136
- ## πŸ› οΈ Core Utilities
60
+ ## 🌐 External Services
137
61
 
138
- - **`createDefaultTransports`**: Helper to create default `http` transports for each chain in your `wagmiConfig`.
139
- - **`checkIsWalletAddressContract`**: Utility to check if a connected address is a smart contract address. The result is cached in memory.
140
- - **`createEVMConnectionsWatcher`**: Framework-agnostic utility to monitor wagmi connection changes and synchronize them with the global state store. Handles account switches, network changes, disconnections, and optional SIWX session rejection scenarios. Returns a cleanup function.
62
+ | Helper | Host | Purpose |
63
+ | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
64
+ | Adapter `getBalance`, `checkIsWalletAddressContract` | The wagmi transport of the chain | `eth_getBalance`, `eth_getCode` |
65
+ | Adapter `getName`, `getAvatar`, `getAddress` | The default RPC URL of `mainnet` in your `chains` (viem's `mainnet` definition when it is missing), not your wagmi transports | ENS resolution on Ethereum Mainnet |
66
+ | Adapter `getAvatar` | The URL in the ENS `avatar` record, `ipfs.io` or `arweave.net` for IPFS and Arweave avatars | Avatar metadata (viem's `getEnsAvatar`) |
67
+ | `createDefaultTransports` | The default RPC URL of each chain definition | Every RPC request of the wagmi config |
68
+ | `impersonated` | The default RPC URL of the chain | Every request except accounts and chain switching, including signing |
141
69
 
142
- ---
143
-
144
- ## 🀝 Contributing & Support
70
+ The default RPC URLs of the chain definitions are public and rate-limited. Explorer links point to the `blockExplorers` of the chain definitions and are not requested by the package.
145
71
 
146
- Contributions are welcome! Please read our main **[Contribution Guidelines](https://github.com/TuwaIO/workflows/blob/main/CONTRIBUTING.md)**.
72
+ ---
147
73
 
148
- If you find this library useful, please consider supporting its development. Every contribution helps!
74
+ ## πŸ“š API Reference
149
75
 
150
- [**➑️ View Support Options**](https://github.com/TuwaIO/workflows/blob/main/Donation.md)
76
+ Every export, with signatures and types generated from the source, is documented at **[satellite.docs.tuwa.io/packages/satellite-evm](https://satellite.docs.tuwa.io/packages/satellite-evm)**.
151
77
 
152
78
  ## πŸ“„ License
153
79
 
154
- This project is licensed under the **Apache-2.0 License** - see the [LICENSE](./LICENSE) file for details.
80
+ Licensed under the **Apache-2.0 License**. See the [LICENSE](https://github.com/TuwaIO/satellite-connect/blob/main/packages/satellite-evm/LICENSE) file for details.
package/dist/index.d.mts CHANGED
@@ -3,183 +3,304 @@ import * as _wagmi_core from '@wagmi/core';
3
3
  import { Connector, CreateConnectorFn, Config, CreateConfigParameters } from '@wagmi/core';
4
4
  import { Chain } from 'viem/chains';
5
5
  import { Transport } from 'viem';
6
- import { ConnectorType } from '@tuwaio/orbit-core';
6
+ import { ConnectorType, TuwaErrorState } from '@tuwaio/orbit-core';
7
7
 
8
- /** EVM-specific connector type */
8
+ /** A wagmi `Connector` from `@wagmi/core`: the wallet connectors returned by `getConnectors` of the EVM adapter. */
9
9
  type ConnectorEVM = Connector<CreateConnectorFn>;
10
10
  /**
11
- * Extended wallet interface for EVM-specific properties
11
+ * An EVM connection in the Satellite Connect store: `BaseConnector` from `@tuwaio/satellite-core` plus the wagmi
12
+ * connector of the wallet. `chainId` is a numeric chain ID and `signMessage` signs with wagmi's `signMessage`.
12
13
  */
13
14
  interface EVMConnection extends BaseConnector {
14
- /** Connected Wallet Standard account */
15
+ /** The wagmi connector of the connected wallet. */
15
16
  connector?: ConnectorEVM;
16
17
  }
17
18
 
18
19
  /**
19
- * Creates an EVM-compatible adapter for Satellite
20
+ * Creates the EVM adapter for the Satellite Connect store (`createSatelliteConnectStore` from `@tuwaio/satellite-core`
21
+ * or `SatelliteConnectProvider` from `@tuwaio/satellite-react`). It implements `SatelliteAdapter` with `@wagmi/core`:
22
+ * - `getConnectors` returns the connectors of `config`. A connector matches a `connectorType` such as `"evm:metamask"`
23
+ * through `formatConnectorName` from `@tuwaio/orbit-core` (for example, the `Safe` connector is `"evm:safe"`).
24
+ * - `connect` runs wagmi's `connect` with the requested chain and returns an {@link EVMConnection}: the account (the
25
+ * zero address if wagmi reports none), the chain (`1` if unknown), the first default RPC URL of the chain, the
26
+ * connector icon and a `signMessage` that signs with wagmi's `signMessage`.
27
+ * - `disconnect` disconnects the given connection, or every connector of `config`.
28
+ * - `checkAndSwitchNetwork` asks the wallet to switch chains with `checkAndSwitchChain` from `@tuwaio/orbit-evm`.
29
+ * - `getBalance` reads the native balance through the wagmi transports and formats it with the chain's decimals.
30
+ * - `getExplorerUrl(url?, chainId?)` joins `url` to the block explorer of `chainId` (looked up in `chains`, then in
31
+ * `config`), or of the connected chain when `chainId` is omitted. It returns `undefined` when the chain has no
32
+ * explorer.
33
+ * - `getName`, `getAvatar` and `getAddress` resolve ENS names on Ethereum Mainnet with `@tuwaio/orbit-evm`, which
34
+ * caches the results in memory.
35
+ * - `checkIsContractAddress` is {@link checkIsWalletAddressContract}; `getSafeConnectorChainId` returns the chain of
36
+ * the `Safe` connector; `switchConnection` runs wagmi's `switchConnection`.
20
37
  *
21
- * @remarks
22
- * This adapter implements the SatelliteAdapter interface for Ethereum Virtual Machine (EVM) compatible chains.
23
- * It uses wagmi as the underlying library for connector connections and chain interactions.
24
- *
25
- * @param config - Wagmi configuration object containing chain and connector settings
26
- * @param chains - The list of chains to use for ENS client creation and other interactions
27
- * @param signInWithSiwe - Optional function for signing in with SIWE
28
- * @returns A configured SatelliteAdapter instance for EVM chains
29
- * @throws Error if config is not provided
38
+ * @param config - The wagmi config of the app.
39
+ * @param chains - The app chains. ENS lookups use the Ethereum Mainnet entry of this list (its default RPC URL), or
40
+ * viem's `mainnet` when it is missing.
41
+ * @returns The EVM adapter.
42
+ * @throws {Error} `Satellite EVM adapter requires a wagmi config object.` when `config` is missing.
30
43
  *
31
44
  * @example
32
- * ```typescript
33
- * const config = createConfig({
34
- * chains: [mainnet, polygon],
35
- * connectors: [injected()]
45
+ * ```ts
46
+ * import { satelliteEVMAdapter } from '@tuwaio/satellite-evm';
47
+ * import { createConfig, http, injected } from '@wagmi/core';
48
+ * import { mainnet, sepolia } from 'viem/chains';
49
+ *
50
+ * const chains = [mainnet, sepolia] as const;
51
+ * const wagmiConfig = createConfig({
52
+ * chains,
53
+ * connectors: [injected()],
54
+ * transports: { [mainnet.id]: http(), [sepolia.id]: http() },
36
55
  * });
37
56
  *
38
- * const chains = [mainnet, polygon];
39
- * const evmAdapter = satelliteEVMAdapter(config, chains);
57
+ * export const evmAdapter = satelliteEVMAdapter(wagmiConfig, chains);
40
58
  * ```
41
59
  */
42
60
  declare function satelliteEVMAdapter(config: Config, chains: readonly [Chain, ...Chain[]]): SatelliteAdapter<ConnectorEVM, EVMConnection>;
43
61
 
44
62
  /**
45
- * Configuration parameters for impersonated wallet connector
63
+ * Options of {@link impersonated}.
46
64
  */
47
65
  type ImpersonatedParameters = {
48
- /** Optional feature flags for testing error scenarios */
66
+ /**
67
+ * Simulated wallet errors. `true` throws a `UserRejectedRequestError` from `viem`; an `Error` is thrown as is.
68
+ */
49
69
  features?: {
50
- /** Simulate connection error */
70
+ /** Error thrown by `connect`. */
51
71
  connectError?: boolean | Error;
52
- /** Simulate chain switching error */
72
+ /** Error thrown by `wallet_switchEthereumChain`. */
53
73
  switchChainError?: boolean | Error;
54
- /** Simulate message signing error */
74
+ /** Error thrown by `personal_sign`. */
55
75
  signMessageError?: boolean | Error;
56
- /** Simulate typed data signing error */
76
+ /** Error thrown by `eth_signTypedData_v4`. */
57
77
  signTypedDataError?: boolean | Error;
58
- /** Enable reconnection behavior */
78
+ /** Not used by the connector. */
59
79
  reconnect?: boolean;
60
80
  };
61
81
  };
82
+ /**
83
+ * Creates a wagmi connector that acts as a wallet for any address, for development and testing (for example against
84
+ * an Anvil or Hardhat node that impersonates accounts). Add it to the `connectors` of your wagmi config; it appears
85
+ * in Satellite Connect as `"evm:impersonatedwallet"` and is never reconnected automatically.
86
+ *
87
+ * On every provider request the connector reads the address to impersonate from `localStorage`
88
+ * (`satellite-connect:impersonatedAddress`, set with `impersonatedHelpers.setImpersonated` from `@tuwaio/orbit-core`)
89
+ * and returns it for `eth_accounts` and `eth_requestAccounts` (no account when the key is empty). `eth_chainId` and
90
+ * `wallet_switchEthereumChain` are answered locally (only chains of the wagmi config can be selected). `personal_sign`
91
+ * is sent as `eth_sign`, and every other request (for example `eth_sendTransaction`) is sent as is to the first
92
+ * default RPC URL of the chain wagmi asks for (the first chain of the wagmi config by default). Public RPC nodes reject
93
+ * signing requests; use a local node that impersonates the address.
94
+ *
95
+ * @param parameters - Connector options. `features` simulate wallet errors.
96
+ * @returns A wagmi connector factory with the id `impersonated` and the name `Impersonated Connector`.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * import { impersonatedHelpers } from '@tuwaio/orbit-core';
101
+ * import { impersonated } from '@tuwaio/satellite-evm';
102
+ * import { createConfig, http } from '@wagmi/core';
103
+ * import { foundry } from 'viem/chains';
104
+ *
105
+ * export const wagmiConfig = createConfig({
106
+ * chains: [foundry],
107
+ * connectors: [impersonated({ features: { signMessageError: false } })],
108
+ * transports: { [foundry.id]: http() },
109
+ * });
110
+ *
111
+ * impersonatedHelpers.setImpersonated('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266');
112
+ * ```
113
+ */
62
114
  declare function impersonated(parameters: ImpersonatedParameters): _wagmi_core.CreateConnectorFn<provider, properties, storageItem>;
63
115
  declare namespace impersonated {
64
116
  var type: "impersonated";
65
117
  }
66
118
 
67
119
  /**
68
- * Configuration options for Safe SDK
69
- * @remarks
70
- * Defines allowed domains and debug mode for Safe integration
120
+ * Options for the wagmi `safe` connector: `safe({ ...safeSdkOptions })`. They are passed to the Safe Apps SDK, which
121
+ * accepts messages only from a parent window whose origin matches one of `allowedDomains`.
71
122
  */
72
123
  declare const safeSdkOptions: {
73
- /** Regular expressions for allowed Safe wallet domains */
124
+ /**
125
+ * Origins of the Safe{Wallet} apps allowed to host the dApp: `https://app.safe.global`, `https://gnosis-safe.io` and
126
+ * `https://metissafe.tech`, including their subdomains. The patterns are anchored, so look-alike domains such as
127
+ * `https://app-safe.global` are rejected.
128
+ */
74
129
  allowedDomains: RegExp[];
75
- /** Enable debug mode */
130
+ /** Debug logging of the Safe Apps SDK. */
76
131
  debug: boolean;
77
132
  };
78
133
 
79
134
  /**
80
- * Checks if a given wallet address is a smart contract by examining its bytecode
135
+ * Checks whether an address has contract code on a chain, for example to detect smart contract wallets such as Safe.
136
+ * The EVM adapter uses it as `checkIsContractAddress`, and the store saves the result in `isContractAddress`.
81
137
  *
82
- * @remarks
83
- * This function uses an in-memory cache to store results and avoid redundant blockchain requests.
84
- * The cache persists for the lifetime of the application session.
138
+ * Side effects: reads the code with wagmi's `getBytecode` through the wagmi transport of `chainId` (an RPC request),
139
+ * and caches the result in memory per chain and address for the lifetime of the page. Addresses with code include
140
+ * EIP-7702 delegated accounts.
85
141
  *
86
- * @param config - Wagmi configuration object
87
- * @param address - Ethereum address to check
88
- * @param chainId - ID of the blockchain network
89
- * @param chains - Array of supported chain configurations
90
- *
91
- * @returns Promise resolving to boolean indicating if the address is a contract
92
- * - true: Address is a smart contract
93
- * - false: Address is an EOA (Externally Owned Account) or client creation failed
142
+ * @param params - Address and chain to check.
143
+ * @param params.config - The wagmi config whose transports are used.
144
+ * @param params.address - The address to check.
145
+ * @param params.chainId - The chain to check it on, as a number or numeric string.
146
+ * @param params.chains - The app chains. When `chainId` is not in this list, nothing is requested and a warning is
147
+ * logged.
148
+ * @returns `true` when the address has code; `false` when it has none or `chainId` is not in `chains`.
149
+ * @throws {Error} When the RPC request fails (the failure is not cached).
94
150
  *
95
151
  * @example
96
- * ```typescript
152
+ * ```ts
153
+ * import { checkIsWalletAddressContract } from '@tuwaio/satellite-evm';
154
+ * import { type Config } from '@wagmi/core';
155
+ * import { mainnet } from 'viem/chains';
156
+ *
157
+ * declare const wagmiConfig: Config;
158
+ *
97
159
  * const isContract = await checkIsWalletAddressContract({
98
160
  * config: wagmiConfig,
99
- * address: "0x1234...",
100
- * chainId: 1,
101
- * chains: [mainnet, polygon]
161
+ * address: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
162
+ * chainId: mainnet.id,
163
+ * chains: [mainnet],
102
164
  * });
103
165
  * ```
104
- *
105
- * @throws Will throw an error if getBytecode request fails
106
166
  */
107
167
  declare function checkIsWalletAddressContract({ config, address, chainId, chains, }: {
108
- /** Wagmi configuration for blockchain interaction */
109
168
  config: Config;
110
- /** Ethereum address to check */
111
169
  address: string;
112
- /** Chain ID where the check should be performed */
113
170
  chainId: number | string;
114
- /** Array of supported chain configurations */
115
171
  chains: readonly [Chain, ...Chain[]];
116
172
  }): Promise<boolean>;
117
173
 
118
174
  /**
119
- * Creates default HTTP transports for each chain in the configuration
175
+ * Creates the `transports` of a wagmi config: a viem `http()` transport without a URL for every chain, so each chain
176
+ * uses the first default RPC URL of its definition (`rpcUrls.default.http[0]`). These are public, rate-limited
177
+ * endpoints: for production, pass `http(yourRpcUrl)` transports instead.
178
+ *
179
+ * @param chains - The chains of the wagmi config.
180
+ * @returns Transports by chain ID.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * import { createDefaultTransports } from '@tuwaio/satellite-evm';
185
+ * import { createConfig, injected } from '@wagmi/core';
186
+ * import { mainnet, sepolia } from 'viem/chains';
120
187
  *
121
- * @param chains - Array of chain configurations from wagmi
122
- * @returns Object mapping chain IDs to their corresponding HTTP transport instances
188
+ * const chains = [mainnet, sepolia] as const;
123
189
  *
124
- * @public
190
+ * export const wagmiConfig = createConfig({
191
+ * chains,
192
+ * connectors: [injected()],
193
+ * transports: createDefaultTransports(chains),
194
+ * });
195
+ * ```
125
196
  */
126
197
  declare const createDefaultTransports: (chains: CreateConfigParameters["chains"]) => Record<number, Transport>;
127
198
 
128
199
  /**
129
- * Callback functions interface for the EVM connections watcher.
130
- * These callbacks are used to interact with the global state store.
200
+ * Store state and actions used by {@link createEVMConnectionsWatcher}. Pass the store's `disconnect` and
201
+ * `updateActiveConnection` and its `getState`, so the watcher always reads the current state. Instead of `getState`
202
+ * you can pass the current `activeConnection` and `connectionError`; they are then read once, when the watcher is
203
+ * created.
131
204
  */
132
205
  interface EVMWatcherCallbacks {
133
- /** The currently active EVM connection from the global store */
134
- activeConnection: EVMConnection | undefined;
135
- /** Function to disconnect a specific connector type */
206
+ /** The active connection when the watcher is created. Ignored when `getState` is passed. */
207
+ activeConnection?: EVMConnection;
208
+ /**
209
+ * Disconnects a connection; the store's `disconnect`.
210
+ *
211
+ * @param connectorType - The connector to disconnect.
212
+ */
136
213
  disconnect: (connectorType: ConnectorType) => void;
137
- /** Current connection error state, if any */
138
- connectionError: string | undefined;
139
- /** Function to update the active connection's properties */
214
+ /**
215
+ * The store's `connectionError` when the watcher is created. While it is set, wallet changes are not copied to the
216
+ * store. Ignored when `getState` is passed.
217
+ */
218
+ connectionError?: TuwaErrorState | string;
219
+ /**
220
+ * Merges fields into the active connection; the store's `updateActiveConnection`.
221
+ *
222
+ * @param connection - Fields to merge.
223
+ */
140
224
  updateActiveConnection: (connection: Partial<EVMConnection>) => void;
225
+ /**
226
+ * Returns the current store state, for example the store's `getState`. The watcher calls it when it starts and on
227
+ * every wagmi event, so it does not have to be recreated when the active connection or the error changes.
228
+ *
229
+ * @returns The current `activeConnection` and `connectionError`.
230
+ */
231
+ getState?: () => {
232
+ /** The active connection. */
233
+ activeConnection?: EVMConnection;
234
+ /** The connection error. */
235
+ connectionError?: TuwaErrorState | string;
236
+ };
141
237
  }
142
238
  /**
143
- * Configuration interface for the EVM connections watcher.
239
+ * Configuration of {@link createEVMConnectionsWatcher}.
144
240
  */
145
241
  interface EVMWatcherConfig {
146
- /** Wagmi configuration object required for connection monitoring */
242
+ /** The wagmi config used by the EVM adapter. Its connections are watched. */
147
243
  wagmiConfig: Config;
148
- /** Optional Sign-In With X (SIWX) session state */
244
+ /** Optional SIWX session state. See `SatelliteSiwxState` from `@tuwaio/satellite-core`. */
149
245
  siwx?: SatelliteSiwxState;
150
- /** @deprecated Legacy SIWE prop alias for backwards compatibility */
246
+ /**
247
+ * Legacy SIWE state, used only when `siwx` is not passed.
248
+ *
249
+ * @deprecated Pass `siwx` instead.
250
+ */
151
251
  siwe?: {
252
+ /** `false` turns off the disconnect after a rejected sign-in. */
152
253
  enabled?: boolean;
254
+ /** Whether the user is signed in. */
153
255
  isSignedIn?: boolean;
256
+ /** Whether the sign-in was rejected. */
154
257
  isRejected?: boolean;
155
258
  };
156
259
  }
157
260
  /**
158
- * Creates and initializes an EVM connections watcher that monitors wagmi connection changes
159
- * and synchronizes them with the global state store.
261
+ * Keeps the Satellite Connect store in sync with wagmi, without a UI framework. `EVMConnectorsWatcher` from
262
+ * `@tuwaio/satellite-react/evm` runs it in React apps.
160
263
  *
161
- * This function provides a pure, framework-agnostic way to watch EVM connections
162
- * without being tied to React hooks or components.
264
+ * When created, it disconnects the active connection if the SIWX sign-in was rejected or failed (see
265
+ * `SatelliteSiwxState` from `@tuwaio/satellite-core`), and sets `signMessage` on an active EVM connection that wagmi
266
+ * reports as connected. Then it subscribes to wagmi's `watchConnections`. On every change, unless the active connection
267
+ * belongs to another chain family:
268
+ * - when wagmi has no connection left, the active connection is disconnected;
269
+ * - otherwise, unless `connectionError` is set: while the user is signed in with SIWX, the active connection is
270
+ * disconnected when the wallet account or chain no longer matches the session; in all other cases the connector
271
+ * type, address, chain, RPC URL and `signMessage` of the wagmi connection are merged into the store.
163
272
  *
164
- * @param config - Configuration object containing wagmi config and optional SIWX session settings
165
- * @param callbacks - Callback functions for interacting with the global state
166
- * @returns A cleanup function to stop watching connections
273
+ * Pass the store's `getState` as `callbacks.getState`, so the active connection and the error are read on every event.
274
+ * `config.siwx` is read when the watcher is created: create a new watcher when the SIWX state changes.
275
+ *
276
+ * @param config - The wagmi config and the optional SIWX state.
277
+ * @param callbacks - Store state and actions.
278
+ * @returns A function that unsubscribes from wagmi.
167
279
  *
168
280
  * @example
169
- * ```typescript
170
- * const unwatch = createEVMConnectionsWatcher(
171
- * { wagmiConfig, siwx: { enabled: true, isSignedIn: true, isRejected: false } },
172
- * { activeConnection, disconnect, connectionError, updateActiveConnection }
173
- * );
281
+ * ```ts
282
+ * import { createSatelliteConnectStore } from '@tuwaio/satellite-core';
283
+ * import {
284
+ * type ConnectorEVM,
285
+ * createEVMConnectionsWatcher,
286
+ * type EVMConnection,
287
+ * satelliteEVMAdapter,
288
+ * } from '@tuwaio/satellite-evm';
289
+ * import { type Config } from '@wagmi/core';
290
+ * import { mainnet } from 'viem/chains';
174
291
  *
175
- * // Later, when you need to stop watching:
176
- * unwatch();
177
- * ```
292
+ * declare const wagmiConfig: Config;
178
293
  *
179
- * @remarks
180
- * Evaluates session parity on account and network switches. If `siwx` is enabled and
181
- * the active session address or chainId does not match the newly connected wallet state,
182
- * it automatically triggers a `disconnect()` to prevent stale session attacks.
294
+ * const store = createSatelliteConnectStore<ConnectorEVM, EVMConnection>({
295
+ * adapter: satelliteEVMAdapter(wagmiConfig, [mainnet]),
296
+ * });
297
+ *
298
+ * const { disconnect, updateActiveConnection } = store.getState();
299
+ * export const unwatch = createEVMConnectionsWatcher(
300
+ * { wagmiConfig },
301
+ * { disconnect, updateActiveConnection, getState: store.getState },
302
+ * );
303
+ * ```
183
304
  */
184
305
  declare function createEVMConnectionsWatcher(config: EVMWatcherConfig, callbacks: EVMWatcherCallbacks): () => void;
185
306
 
package/dist/index.d.ts CHANGED
@@ -3,183 +3,304 @@ import * as _wagmi_core from '@wagmi/core';
3
3
  import { Connector, CreateConnectorFn, Config, CreateConfigParameters } from '@wagmi/core';
4
4
  import { Chain } from 'viem/chains';
5
5
  import { Transport } from 'viem';
6
- import { ConnectorType } from '@tuwaio/orbit-core';
6
+ import { ConnectorType, TuwaErrorState } from '@tuwaio/orbit-core';
7
7
 
8
- /** EVM-specific connector type */
8
+ /** A wagmi `Connector` from `@wagmi/core`: the wallet connectors returned by `getConnectors` of the EVM adapter. */
9
9
  type ConnectorEVM = Connector<CreateConnectorFn>;
10
10
  /**
11
- * Extended wallet interface for EVM-specific properties
11
+ * An EVM connection in the Satellite Connect store: `BaseConnector` from `@tuwaio/satellite-core` plus the wagmi
12
+ * connector of the wallet. `chainId` is a numeric chain ID and `signMessage` signs with wagmi's `signMessage`.
12
13
  */
13
14
  interface EVMConnection extends BaseConnector {
14
- /** Connected Wallet Standard account */
15
+ /** The wagmi connector of the connected wallet. */
15
16
  connector?: ConnectorEVM;
16
17
  }
17
18
 
18
19
  /**
19
- * Creates an EVM-compatible adapter for Satellite
20
+ * Creates the EVM adapter for the Satellite Connect store (`createSatelliteConnectStore` from `@tuwaio/satellite-core`
21
+ * or `SatelliteConnectProvider` from `@tuwaio/satellite-react`). It implements `SatelliteAdapter` with `@wagmi/core`:
22
+ * - `getConnectors` returns the connectors of `config`. A connector matches a `connectorType` such as `"evm:metamask"`
23
+ * through `formatConnectorName` from `@tuwaio/orbit-core` (for example, the `Safe` connector is `"evm:safe"`).
24
+ * - `connect` runs wagmi's `connect` with the requested chain and returns an {@link EVMConnection}: the account (the
25
+ * zero address if wagmi reports none), the chain (`1` if unknown), the first default RPC URL of the chain, the
26
+ * connector icon and a `signMessage` that signs with wagmi's `signMessage`.
27
+ * - `disconnect` disconnects the given connection, or every connector of `config`.
28
+ * - `checkAndSwitchNetwork` asks the wallet to switch chains with `checkAndSwitchChain` from `@tuwaio/orbit-evm`.
29
+ * - `getBalance` reads the native balance through the wagmi transports and formats it with the chain's decimals.
30
+ * - `getExplorerUrl(url?, chainId?)` joins `url` to the block explorer of `chainId` (looked up in `chains`, then in
31
+ * `config`), or of the connected chain when `chainId` is omitted. It returns `undefined` when the chain has no
32
+ * explorer.
33
+ * - `getName`, `getAvatar` and `getAddress` resolve ENS names on Ethereum Mainnet with `@tuwaio/orbit-evm`, which
34
+ * caches the results in memory.
35
+ * - `checkIsContractAddress` is {@link checkIsWalletAddressContract}; `getSafeConnectorChainId` returns the chain of
36
+ * the `Safe` connector; `switchConnection` runs wagmi's `switchConnection`.
20
37
  *
21
- * @remarks
22
- * This adapter implements the SatelliteAdapter interface for Ethereum Virtual Machine (EVM) compatible chains.
23
- * It uses wagmi as the underlying library for connector connections and chain interactions.
24
- *
25
- * @param config - Wagmi configuration object containing chain and connector settings
26
- * @param chains - The list of chains to use for ENS client creation and other interactions
27
- * @param signInWithSiwe - Optional function for signing in with SIWE
28
- * @returns A configured SatelliteAdapter instance for EVM chains
29
- * @throws Error if config is not provided
38
+ * @param config - The wagmi config of the app.
39
+ * @param chains - The app chains. ENS lookups use the Ethereum Mainnet entry of this list (its default RPC URL), or
40
+ * viem's `mainnet` when it is missing.
41
+ * @returns The EVM adapter.
42
+ * @throws {Error} `Satellite EVM adapter requires a wagmi config object.` when `config` is missing.
30
43
  *
31
44
  * @example
32
- * ```typescript
33
- * const config = createConfig({
34
- * chains: [mainnet, polygon],
35
- * connectors: [injected()]
45
+ * ```ts
46
+ * import { satelliteEVMAdapter } from '@tuwaio/satellite-evm';
47
+ * import { createConfig, http, injected } from '@wagmi/core';
48
+ * import { mainnet, sepolia } from 'viem/chains';
49
+ *
50
+ * const chains = [mainnet, sepolia] as const;
51
+ * const wagmiConfig = createConfig({
52
+ * chains,
53
+ * connectors: [injected()],
54
+ * transports: { [mainnet.id]: http(), [sepolia.id]: http() },
36
55
  * });
37
56
  *
38
- * const chains = [mainnet, polygon];
39
- * const evmAdapter = satelliteEVMAdapter(config, chains);
57
+ * export const evmAdapter = satelliteEVMAdapter(wagmiConfig, chains);
40
58
  * ```
41
59
  */
42
60
  declare function satelliteEVMAdapter(config: Config, chains: readonly [Chain, ...Chain[]]): SatelliteAdapter<ConnectorEVM, EVMConnection>;
43
61
 
44
62
  /**
45
- * Configuration parameters for impersonated wallet connector
63
+ * Options of {@link impersonated}.
46
64
  */
47
65
  type ImpersonatedParameters = {
48
- /** Optional feature flags for testing error scenarios */
66
+ /**
67
+ * Simulated wallet errors. `true` throws a `UserRejectedRequestError` from `viem`; an `Error` is thrown as is.
68
+ */
49
69
  features?: {
50
- /** Simulate connection error */
70
+ /** Error thrown by `connect`. */
51
71
  connectError?: boolean | Error;
52
- /** Simulate chain switching error */
72
+ /** Error thrown by `wallet_switchEthereumChain`. */
53
73
  switchChainError?: boolean | Error;
54
- /** Simulate message signing error */
74
+ /** Error thrown by `personal_sign`. */
55
75
  signMessageError?: boolean | Error;
56
- /** Simulate typed data signing error */
76
+ /** Error thrown by `eth_signTypedData_v4`. */
57
77
  signTypedDataError?: boolean | Error;
58
- /** Enable reconnection behavior */
78
+ /** Not used by the connector. */
59
79
  reconnect?: boolean;
60
80
  };
61
81
  };
82
+ /**
83
+ * Creates a wagmi connector that acts as a wallet for any address, for development and testing (for example against
84
+ * an Anvil or Hardhat node that impersonates accounts). Add it to the `connectors` of your wagmi config; it appears
85
+ * in Satellite Connect as `"evm:impersonatedwallet"` and is never reconnected automatically.
86
+ *
87
+ * On every provider request the connector reads the address to impersonate from `localStorage`
88
+ * (`satellite-connect:impersonatedAddress`, set with `impersonatedHelpers.setImpersonated` from `@tuwaio/orbit-core`)
89
+ * and returns it for `eth_accounts` and `eth_requestAccounts` (no account when the key is empty). `eth_chainId` and
90
+ * `wallet_switchEthereumChain` are answered locally (only chains of the wagmi config can be selected). `personal_sign`
91
+ * is sent as `eth_sign`, and every other request (for example `eth_sendTransaction`) is sent as is to the first
92
+ * default RPC URL of the chain wagmi asks for (the first chain of the wagmi config by default). Public RPC nodes reject
93
+ * signing requests; use a local node that impersonates the address.
94
+ *
95
+ * @param parameters - Connector options. `features` simulate wallet errors.
96
+ * @returns A wagmi connector factory with the id `impersonated` and the name `Impersonated Connector`.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * import { impersonatedHelpers } from '@tuwaio/orbit-core';
101
+ * import { impersonated } from '@tuwaio/satellite-evm';
102
+ * import { createConfig, http } from '@wagmi/core';
103
+ * import { foundry } from 'viem/chains';
104
+ *
105
+ * export const wagmiConfig = createConfig({
106
+ * chains: [foundry],
107
+ * connectors: [impersonated({ features: { signMessageError: false } })],
108
+ * transports: { [foundry.id]: http() },
109
+ * });
110
+ *
111
+ * impersonatedHelpers.setImpersonated('0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266');
112
+ * ```
113
+ */
62
114
  declare function impersonated(parameters: ImpersonatedParameters): _wagmi_core.CreateConnectorFn<provider, properties, storageItem>;
63
115
  declare namespace impersonated {
64
116
  var type: "impersonated";
65
117
  }
66
118
 
67
119
  /**
68
- * Configuration options for Safe SDK
69
- * @remarks
70
- * Defines allowed domains and debug mode for Safe integration
120
+ * Options for the wagmi `safe` connector: `safe({ ...safeSdkOptions })`. They are passed to the Safe Apps SDK, which
121
+ * accepts messages only from a parent window whose origin matches one of `allowedDomains`.
71
122
  */
72
123
  declare const safeSdkOptions: {
73
- /** Regular expressions for allowed Safe wallet domains */
124
+ /**
125
+ * Origins of the Safe{Wallet} apps allowed to host the dApp: `https://app.safe.global`, `https://gnosis-safe.io` and
126
+ * `https://metissafe.tech`, including their subdomains. The patterns are anchored, so look-alike domains such as
127
+ * `https://app-safe.global` are rejected.
128
+ */
74
129
  allowedDomains: RegExp[];
75
- /** Enable debug mode */
130
+ /** Debug logging of the Safe Apps SDK. */
76
131
  debug: boolean;
77
132
  };
78
133
 
79
134
  /**
80
- * Checks if a given wallet address is a smart contract by examining its bytecode
135
+ * Checks whether an address has contract code on a chain, for example to detect smart contract wallets such as Safe.
136
+ * The EVM adapter uses it as `checkIsContractAddress`, and the store saves the result in `isContractAddress`.
81
137
  *
82
- * @remarks
83
- * This function uses an in-memory cache to store results and avoid redundant blockchain requests.
84
- * The cache persists for the lifetime of the application session.
138
+ * Side effects: reads the code with wagmi's `getBytecode` through the wagmi transport of `chainId` (an RPC request),
139
+ * and caches the result in memory per chain and address for the lifetime of the page. Addresses with code include
140
+ * EIP-7702 delegated accounts.
85
141
  *
86
- * @param config - Wagmi configuration object
87
- * @param address - Ethereum address to check
88
- * @param chainId - ID of the blockchain network
89
- * @param chains - Array of supported chain configurations
90
- *
91
- * @returns Promise resolving to boolean indicating if the address is a contract
92
- * - true: Address is a smart contract
93
- * - false: Address is an EOA (Externally Owned Account) or client creation failed
142
+ * @param params - Address and chain to check.
143
+ * @param params.config - The wagmi config whose transports are used.
144
+ * @param params.address - The address to check.
145
+ * @param params.chainId - The chain to check it on, as a number or numeric string.
146
+ * @param params.chains - The app chains. When `chainId` is not in this list, nothing is requested and a warning is
147
+ * logged.
148
+ * @returns `true` when the address has code; `false` when it has none or `chainId` is not in `chains`.
149
+ * @throws {Error} When the RPC request fails (the failure is not cached).
94
150
  *
95
151
  * @example
96
- * ```typescript
152
+ * ```ts
153
+ * import { checkIsWalletAddressContract } from '@tuwaio/satellite-evm';
154
+ * import { type Config } from '@wagmi/core';
155
+ * import { mainnet } from 'viem/chains';
156
+ *
157
+ * declare const wagmiConfig: Config;
158
+ *
97
159
  * const isContract = await checkIsWalletAddressContract({
98
160
  * config: wagmiConfig,
99
- * address: "0x1234...",
100
- * chainId: 1,
101
- * chains: [mainnet, polygon]
161
+ * address: '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B',
162
+ * chainId: mainnet.id,
163
+ * chains: [mainnet],
102
164
  * });
103
165
  * ```
104
- *
105
- * @throws Will throw an error if getBytecode request fails
106
166
  */
107
167
  declare function checkIsWalletAddressContract({ config, address, chainId, chains, }: {
108
- /** Wagmi configuration for blockchain interaction */
109
168
  config: Config;
110
- /** Ethereum address to check */
111
169
  address: string;
112
- /** Chain ID where the check should be performed */
113
170
  chainId: number | string;
114
- /** Array of supported chain configurations */
115
171
  chains: readonly [Chain, ...Chain[]];
116
172
  }): Promise<boolean>;
117
173
 
118
174
  /**
119
- * Creates default HTTP transports for each chain in the configuration
175
+ * Creates the `transports` of a wagmi config: a viem `http()` transport without a URL for every chain, so each chain
176
+ * uses the first default RPC URL of its definition (`rpcUrls.default.http[0]`). These are public, rate-limited
177
+ * endpoints: for production, pass `http(yourRpcUrl)` transports instead.
178
+ *
179
+ * @param chains - The chains of the wagmi config.
180
+ * @returns Transports by chain ID.
181
+ *
182
+ * @example
183
+ * ```ts
184
+ * import { createDefaultTransports } from '@tuwaio/satellite-evm';
185
+ * import { createConfig, injected } from '@wagmi/core';
186
+ * import { mainnet, sepolia } from 'viem/chains';
120
187
  *
121
- * @param chains - Array of chain configurations from wagmi
122
- * @returns Object mapping chain IDs to their corresponding HTTP transport instances
188
+ * const chains = [mainnet, sepolia] as const;
123
189
  *
124
- * @public
190
+ * export const wagmiConfig = createConfig({
191
+ * chains,
192
+ * connectors: [injected()],
193
+ * transports: createDefaultTransports(chains),
194
+ * });
195
+ * ```
125
196
  */
126
197
  declare const createDefaultTransports: (chains: CreateConfigParameters["chains"]) => Record<number, Transport>;
127
198
 
128
199
  /**
129
- * Callback functions interface for the EVM connections watcher.
130
- * These callbacks are used to interact with the global state store.
200
+ * Store state and actions used by {@link createEVMConnectionsWatcher}. Pass the store's `disconnect` and
201
+ * `updateActiveConnection` and its `getState`, so the watcher always reads the current state. Instead of `getState`
202
+ * you can pass the current `activeConnection` and `connectionError`; they are then read once, when the watcher is
203
+ * created.
131
204
  */
132
205
  interface EVMWatcherCallbacks {
133
- /** The currently active EVM connection from the global store */
134
- activeConnection: EVMConnection | undefined;
135
- /** Function to disconnect a specific connector type */
206
+ /** The active connection when the watcher is created. Ignored when `getState` is passed. */
207
+ activeConnection?: EVMConnection;
208
+ /**
209
+ * Disconnects a connection; the store's `disconnect`.
210
+ *
211
+ * @param connectorType - The connector to disconnect.
212
+ */
136
213
  disconnect: (connectorType: ConnectorType) => void;
137
- /** Current connection error state, if any */
138
- connectionError: string | undefined;
139
- /** Function to update the active connection's properties */
214
+ /**
215
+ * The store's `connectionError` when the watcher is created. While it is set, wallet changes are not copied to the
216
+ * store. Ignored when `getState` is passed.
217
+ */
218
+ connectionError?: TuwaErrorState | string;
219
+ /**
220
+ * Merges fields into the active connection; the store's `updateActiveConnection`.
221
+ *
222
+ * @param connection - Fields to merge.
223
+ */
140
224
  updateActiveConnection: (connection: Partial<EVMConnection>) => void;
225
+ /**
226
+ * Returns the current store state, for example the store's `getState`. The watcher calls it when it starts and on
227
+ * every wagmi event, so it does not have to be recreated when the active connection or the error changes.
228
+ *
229
+ * @returns The current `activeConnection` and `connectionError`.
230
+ */
231
+ getState?: () => {
232
+ /** The active connection. */
233
+ activeConnection?: EVMConnection;
234
+ /** The connection error. */
235
+ connectionError?: TuwaErrorState | string;
236
+ };
141
237
  }
142
238
  /**
143
- * Configuration interface for the EVM connections watcher.
239
+ * Configuration of {@link createEVMConnectionsWatcher}.
144
240
  */
145
241
  interface EVMWatcherConfig {
146
- /** Wagmi configuration object required for connection monitoring */
242
+ /** The wagmi config used by the EVM adapter. Its connections are watched. */
147
243
  wagmiConfig: Config;
148
- /** Optional Sign-In With X (SIWX) session state */
244
+ /** Optional SIWX session state. See `SatelliteSiwxState` from `@tuwaio/satellite-core`. */
149
245
  siwx?: SatelliteSiwxState;
150
- /** @deprecated Legacy SIWE prop alias for backwards compatibility */
246
+ /**
247
+ * Legacy SIWE state, used only when `siwx` is not passed.
248
+ *
249
+ * @deprecated Pass `siwx` instead.
250
+ */
151
251
  siwe?: {
252
+ /** `false` turns off the disconnect after a rejected sign-in. */
152
253
  enabled?: boolean;
254
+ /** Whether the user is signed in. */
153
255
  isSignedIn?: boolean;
256
+ /** Whether the sign-in was rejected. */
154
257
  isRejected?: boolean;
155
258
  };
156
259
  }
157
260
  /**
158
- * Creates and initializes an EVM connections watcher that monitors wagmi connection changes
159
- * and synchronizes them with the global state store.
261
+ * Keeps the Satellite Connect store in sync with wagmi, without a UI framework. `EVMConnectorsWatcher` from
262
+ * `@tuwaio/satellite-react/evm` runs it in React apps.
160
263
  *
161
- * This function provides a pure, framework-agnostic way to watch EVM connections
162
- * without being tied to React hooks or components.
264
+ * When created, it disconnects the active connection if the SIWX sign-in was rejected or failed (see
265
+ * `SatelliteSiwxState` from `@tuwaio/satellite-core`), and sets `signMessage` on an active EVM connection that wagmi
266
+ * reports as connected. Then it subscribes to wagmi's `watchConnections`. On every change, unless the active connection
267
+ * belongs to another chain family:
268
+ * - when wagmi has no connection left, the active connection is disconnected;
269
+ * - otherwise, unless `connectionError` is set: while the user is signed in with SIWX, the active connection is
270
+ * disconnected when the wallet account or chain no longer matches the session; in all other cases the connector
271
+ * type, address, chain, RPC URL and `signMessage` of the wagmi connection are merged into the store.
163
272
  *
164
- * @param config - Configuration object containing wagmi config and optional SIWX session settings
165
- * @param callbacks - Callback functions for interacting with the global state
166
- * @returns A cleanup function to stop watching connections
273
+ * Pass the store's `getState` as `callbacks.getState`, so the active connection and the error are read on every event.
274
+ * `config.siwx` is read when the watcher is created: create a new watcher when the SIWX state changes.
275
+ *
276
+ * @param config - The wagmi config and the optional SIWX state.
277
+ * @param callbacks - Store state and actions.
278
+ * @returns A function that unsubscribes from wagmi.
167
279
  *
168
280
  * @example
169
- * ```typescript
170
- * const unwatch = createEVMConnectionsWatcher(
171
- * { wagmiConfig, siwx: { enabled: true, isSignedIn: true, isRejected: false } },
172
- * { activeConnection, disconnect, connectionError, updateActiveConnection }
173
- * );
281
+ * ```ts
282
+ * import { createSatelliteConnectStore } from '@tuwaio/satellite-core';
283
+ * import {
284
+ * type ConnectorEVM,
285
+ * createEVMConnectionsWatcher,
286
+ * type EVMConnection,
287
+ * satelliteEVMAdapter,
288
+ * } from '@tuwaio/satellite-evm';
289
+ * import { type Config } from '@wagmi/core';
290
+ * import { mainnet } from 'viem/chains';
174
291
  *
175
- * // Later, when you need to stop watching:
176
- * unwatch();
177
- * ```
292
+ * declare const wagmiConfig: Config;
178
293
  *
179
- * @remarks
180
- * Evaluates session parity on account and network switches. If `siwx` is enabled and
181
- * the active session address or chainId does not match the newly connected wallet state,
182
- * it automatically triggers a `disconnect()` to prevent stale session attacks.
294
+ * const store = createSatelliteConnectStore<ConnectorEVM, EVMConnection>({
295
+ * adapter: satelliteEVMAdapter(wagmiConfig, [mainnet]),
296
+ * });
297
+ *
298
+ * const { disconnect, updateActiveConnection } = store.getState();
299
+ * export const unwatch = createEVMConnectionsWatcher(
300
+ * { wagmiConfig },
301
+ * { disconnect, updateActiveConnection, getState: store.getState },
302
+ * );
303
+ * ```
183
304
  */
184
305
  declare function createEVMConnectionsWatcher(config: EVMWatcherConfig, callbacks: EVMWatcherCallbacks): () => void;
185
306
 
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- 'use strict';var orbitCore=require('@tuwaio/orbit-core'),orbitEvm=require('@tuwaio/orbit-evm'),core=require('@wagmi/core'),viem=require('viem'),chains=require('viem/chains'),utils=require('viem/utils');var y=new Map;async function A({config:o,address:s,chainId:e,chains:n}){if(y.has(s))return y.get(s);if(orbitEvm.createViemClient(e,n)){let r=!!await core.getBytecode(o,{address:s});return y.set(s,r),r}else return false}function Ie(o,s){if(!o)throw new Error("Satellite EVM adapter requires a wagmi config object.");return {key:orbitCore.OrbitAdapter.EVM,connect:async({connectorType:e,chainId:n})=>{let a=core.getConnectors(o).find(r=>orbitCore.getConnectorTypeFromName(orbitCore.OrbitAdapter.EVM,orbitCore.formatConnectorName(r.name))===e);if(!a)throw new Error("Cannot find connector with this wallet type");try{await core.connect(o,{connector:a,chainId:n});let r=core.getConnection(o);return {connectorType:e,address:r.address??viem.zeroAddress,chainId:r.chainId??chains.mainnet.id,rpcURL:r.chain?.rpcUrls.default.http[0]??chains.mainnet.rpcUrls.default.http[0],isConnected:r.isConnected,isContractAddress:!1,icon:a?.icon?.trim(),connector:a,signMessage:c=>core.signMessage(o,{message:c})}}catch(r){throw new Error(r instanceof Error?r.message:String(r),{cause:r})}},disconnect:async e=>{if(e&&e.isConnected)await core.disconnect(o,{connector:e?.connector});else {let n=core.getConnectors(o);await Promise.allSettled(n.map(async t=>{await core.disconnect(o,{connector:t});}));}},getConnectors:()=>{let e=core.getConnectors(o);return {adapter:orbitCore.OrbitAdapter.EVM,connectors:e.map(n=>n)}},checkAndSwitchNetwork:async e=>await orbitEvm.checkAndSwitchChain(Number(e),o),getBalance:async(e,n)=>{let t=await core.getBalance(o,{address:e,chainId:Number(n)});return {value:viem.formatUnits(t.value,t.decimals),symbol:t.symbol}},getExplorerUrl:e=>{let{chain:n}=core.getConnection(o),t=n?.blockExplorers?.default.url;return e?`${t}/${e}`:t},getName:e=>orbitEvm.getName(e,s),getAvatar:e=>orbitEvm.getAvatar(e,s),getAddress:e=>orbitEvm.getAddress(e,s),checkIsContractAddress:async({address:e,chainId:n})=>{let t=core.getChains(o);return await A({config:o,address:e,chainId:n,chains:t})},getSafeConnectorChainId:async()=>{let n=core.getConnectors(o).find(t=>t.name==="Safe");if(n)return await n.getChainId()},switchConnection:async e=>{let t=core.getConnectors(o).find(a=>orbitCore.getConnectorTypeFromName(orbitCore.OrbitAdapter.EVM,orbitCore.formatConnectorName(a.name))===e);if(!t)throw new Error(`Cannot find connector with type: ${e}`);try{await core.switchConnection(o,{connector:t});}catch(a){throw new Error(`Failed to switch to connector ${e}: ${a instanceof Error?a.message:String(a)}`,{cause:a})}}}}q.type="impersonated";function q(o){let s=o.features??{},e=false,n,t;return core.createConnector(a=>({id:"impersonated",name:"Impersonated Connector",type:q.type,async setup(){n=a.chains[0].id;},async connect({chainId:r}={}){if(s.connectError)throw typeof s.connectError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to connect.")):s.connectError;let{request:c}=await this.getProvider(),p=await c({method:"eth_requestAccounts"}),u=await this.getChainId();return r&&u!==r&&(u=(await this.switchChain({chainId:r})).id),e=true,{accounts:p,chainId:u}},async disconnect(){e=false,t=void 0;},async getAccounts(){if(!e)throw new Error("Not connected connector");let{request:r}=await this.getProvider();return (await r({method:"eth_accounts"})).map(viem.getAddress)},async getChainId(){let{request:r}=await this.getProvider(),c=await r({method:"eth_chainId"});return viem.fromHex(c,"number")},async isAuthorized(){return e?!!(await this.getAccounts()).length:false},async switchChain({chainId:r}){let c=a.chains.find(u=>u.id===r);if(!c)throw new viem.SwitchChainError(new core.ChainNotConfiguredError);let{request:p}=await this.getProvider();return await p({method:"wallet_switchEthereumChain",params:[{chainId:viem.numberToHex(r)}]}),c},onAccountsChanged(r){r.length===0?this.onDisconnect():a.emitter.emit("change",{accounts:r.map(viem.getAddress)});},onChainChanged(r){let c=Number(r);a.emitter.emit("change",{chainId:c});},async onDisconnect(){a.emitter.emit("disconnect"),e=false,t=void 0;},async getProvider({chainId:r}={}){t=orbitCore.impersonatedHelpers?.getImpersonated()?[orbitCore.impersonatedHelpers.getImpersonated()||viem.zeroAddress]:void 0;let p=(a.chains.find(d=>d.id===r)??a.chains[0]).rpcUrls.default.http[0];return viem.custom({request:async({method:d,params:h})=>{if(d==="eth_chainId")return viem.numberToHex(n);if(d==="eth_requestAccounts"||d==="eth_accounts")return t??[];if(d==="eth_signTypedData_v4"&&s.signTypedDataError)throw typeof s.signTypedDataError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to sign typed data.")):s.signTypedDataError;if(d==="wallet_switchEthereumChain"){if(s.switchChainError)throw typeof s.switchChainError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to switch chain.")):s.switchChainError;n=viem.fromHex(h[0].chainId,"number"),this.onChainChanged(n.toString());return}if(d==="personal_sign"){if(s.signMessageError)throw typeof s.signMessageError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to sign message.")):s.signMessageError;d="eth_sign",h=[h[1],h[0]];}let i={method:d,params:h},{error:l,result:m}=await utils.rpc.http(p,{body:i});if(l)throw new viem.RpcRequestError({body:i,error:l,url:p});return m}})({retryCount:1})}}))}var Pe={allowedDomains:[/gnosis-safe.io$/,/app.safe.global$/,/metissafe.tech$/],debug:false};var Ne=o=>o.reduce((s,e)=>{let n=e.id;return s[n]=viem.http(),s},{});function je(o,s){let{wagmiConfig:e}=o,n=o.siwx??(o.siwe?{enabled:o.siwe.enabled,isSignedIn:o.siwe.isSignedIn,isRejected:o.siwe.isRejected}:void 0),{activeConnection:t,disconnect:a,connectionError:r,updateActiveConnection:c}=s,p=()=>{let h=n?.isRejected||n?.status==="error",i=n?.isSignedIn??n?.isAuthenticated??false;n?.enabled!==false&&!i&&h&&t&&a(t.connectorType);},u=h=>{if(t&&orbitCore.getAdapterFromConnectorType(t.connectorType)!==orbitCore.OrbitAdapter.EVM)return;if(h.length===0){t&&a(t.connectorType);return}let i=core.getConnection(e);if(t&&orbitCore.getAdapterFromConnectorType(t.connectorType)!==orbitCore.OrbitAdapter.EVM||!i||r)return;let l=n?.address??n?.session?.address,m=n?.chainId??n?.session?.chainId;if((n?.isSignedIn??n?.isAuthenticated??false)&&t){let E=i.address&&l&&i.address.toLowerCase()!==l.toLowerCase()&&!l.toLowerCase().endsWith(i.address.toLowerCase()),$=i.chainId&&m&&String(i.chainId)!==String(m)&&!m.endsWith(`:${i.chainId}`);if(E||$){a(t.connectorType);return}}let I=i.connector,_={connectorType:I?`${orbitCore.OrbitAdapter.EVM}:${orbitCore.formatConnectorName(I.name)}`:t?.connectorType,address:i.address,chainId:i.chainId,rpcURL:i?.chain?.rpcUrls.default.http[0],isConnected:true,signMessage:E=>core.signMessage(e,{message:E})};c(_);};if(p(),t&&orbitCore.getAdapterFromConnectorType(t.connectorType)===orbitCore.OrbitAdapter.EVM){let h=core.getConnection(e);h&&h.isConnected&&c({signMessage:i=>core.signMessage(e,{message:i})});}return core.watchConnections(e,{onChange:u})}exports.checkIsWalletAddressContract=A;exports.createDefaultTransports=Ne;exports.createEVMConnectionsWatcher=je;exports.impersonated=q;exports.safeSdkOptions=Pe;exports.satelliteEVMAdapter=Ie;
1
+ 'use strict';var orbitCore=require('@tuwaio/orbit-core'),orbitEvm=require('@tuwaio/orbit-evm'),core=require('@wagmi/core'),viem=require('viem'),chains=require('viem/chains'),utils=require('viem/utils');var I=new Map;async function M({config:r,address:o,chainId:e,chains:n}){let s=`${Number(e)}:${o.toLowerCase()}`;if(I.has(s))return I.get(s);if(orbitEvm.createViemClient(Number(e),n)){let i=!!await core.getBytecode(r,{address:o,chainId:Number(e)});return I.set(s,i),i}else return false}function Se(r,o){if(!r)throw new Error("Satellite EVM adapter requires a wagmi config object.");return {key:orbitCore.OrbitAdapter.EVM,connect:async({connectorType:e,chainId:n})=>{let a=core.getConnectors(r).find(t=>orbitCore.getConnectorTypeFromName(orbitCore.OrbitAdapter.EVM,orbitCore.formatConnectorName(t.name))===e);if(!a)throw new Error("Cannot find connector with this wallet type");try{await core.connect(r,{connector:a,chainId:n});let t=core.getConnection(r);return {connectorType:e,address:t.address??viem.zeroAddress,chainId:t.chainId??chains.mainnet.id,rpcURL:t.chain?.rpcUrls.default.http[0]??chains.mainnet.rpcUrls.default.http[0],isConnected:t.isConnected,isContractAddress:!1,icon:a?.icon?.trim(),connector:a,signMessage:i=>core.signMessage(r,{message:i})}}catch(t){throw new Error(t instanceof Error?t.message:String(t),{cause:t})}},disconnect:async e=>{if(e&&e.isConnected)await core.disconnect(r,{connector:e?.connector});else {let n=core.getConnectors(r);await Promise.allSettled(n.map(async s=>{await core.disconnect(r,{connector:s});}));}},getConnectors:()=>{let e=core.getConnectors(r);return {adapter:orbitCore.OrbitAdapter.EVM,connectors:e.map(n=>n)}},checkAndSwitchNetwork:async e=>await orbitEvm.checkAndSwitchChain(Number(e),r),getBalance:async(e,n)=>{let s=await core.getBalance(r,{address:e,chainId:Number(n)});return {value:viem.formatUnits(s.value,s.decimals),symbol:s.symbol}},getExplorerUrl:(e,n)=>{let a=(n===void 0?core.getConnection(r).chain:o.find(t=>t.id===Number(n))??core.getChains(r).find(t=>t.id===Number(n)))?.blockExplorers?.default.url;if(a)return e?`${a.replace(/\/+$/,"")}/${e.replace(/^\/+/,"")}`:a},getName:e=>orbitEvm.getName(e,o),getAvatar:e=>orbitEvm.getAvatar(e,o),getAddress:e=>orbitEvm.getAddress(e,o),checkIsContractAddress:async({address:e,chainId:n})=>{let s=core.getChains(r);return await M({config:r,address:e,chainId:n,chains:s})},getSafeConnectorChainId:async()=>{let n=core.getConnectors(r).find(s=>s.name==="Safe");if(n)return await n.getChainId()},switchConnection:async e=>{let s=core.getConnectors(r).find(a=>orbitCore.getConnectorTypeFromName(orbitCore.OrbitAdapter.EVM,orbitCore.formatConnectorName(a.name))===e);if(!s)throw new Error(`Cannot find connector with type: ${e}`);try{await core.switchConnection(r,{connector:s});}catch(a){throw new Error(`Failed to switch to connector ${e}: ${a instanceof Error?a.message:String(a)}`,{cause:a})}}}}k.type="impersonated";function k(r){let o=r.features??{},e=false,n,s;return core.createConnector(a=>({id:"impersonated",name:"Impersonated Connector",type:k.type,async setup(){n=a.chains[0].id;},async connect({chainId:t}={}){if(o.connectError)throw typeof o.connectError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to connect.")):o.connectError;let{request:i}=await this.getProvider(),m=await i({method:"eth_requestAccounts"}),p=await this.getChainId();return t&&p!==t&&(p=(await this.switchChain({chainId:t})).id),e=true,{accounts:m,chainId:p}},async disconnect(){e=false,s=void 0;},async getAccounts(){if(!e)throw new Error("Not connected connector");let{request:t}=await this.getProvider();return (await t({method:"eth_accounts"})).map(viem.getAddress)},async getChainId(){let{request:t}=await this.getProvider(),i=await t({method:"eth_chainId"});return viem.fromHex(i,"number")},async isAuthorized(){return e?!!(await this.getAccounts()).length:false},async switchChain({chainId:t}){let i=a.chains.find(p=>p.id===t);if(!i)throw new viem.SwitchChainError(new core.ChainNotConfiguredError);let{request:m}=await this.getProvider();return await m({method:"wallet_switchEthereumChain",params:[{chainId:viem.numberToHex(t)}]}),i},onAccountsChanged(t){t.length===0?this.onDisconnect():a.emitter.emit("change",{accounts:t.map(viem.getAddress)});},onChainChanged(t){let i=Number(t);a.emitter.emit("change",{chainId:i});},async onDisconnect(){a.emitter.emit("disconnect"),e=false,s=void 0;},async getProvider({chainId:t}={}){s=orbitCore.impersonatedHelpers?.getImpersonated()?[orbitCore.impersonatedHelpers.getImpersonated()||viem.zeroAddress]:void 0;let m=(a.chains.find(h=>h.id===t)??a.chains[0]).rpcUrls.default.http[0];return viem.custom({request:async({method:h,params:u})=>{if(h==="eth_chainId")return viem.numberToHex(n);if(h==="eth_requestAccounts"||h==="eth_accounts")return s??[];if(h==="eth_signTypedData_v4"&&o.signTypedDataError)throw typeof o.signTypedDataError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to sign typed data.")):o.signTypedDataError;if(h==="wallet_switchEthereumChain"){if(o.switchChainError)throw typeof o.switchChainError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to switch chain.")):o.switchChainError;n=viem.fromHex(u[0].chainId,"number"),this.onChainChanged(n.toString());return}if(h==="personal_sign"){if(o.signMessageError)throw typeof o.signMessageError=="boolean"?new viem.UserRejectedRequestError(new Error("Failed to sign message.")):o.signMessageError;h="eth_sign",u=[u[1],u[0]];}let c={method:h,params:u},{error:l,result:d}=await utils.rpc.http(m,{body:c});if(l)throw new viem.RpcRequestError({body:c,error:l,url:m});return d}})({retryCount:1})}}))}var Re={allowedDomains:[/^https:\/\/([a-z0-9-]+\.)*gnosis-safe\.io$/,/^https:\/\/([a-z0-9-]+\.)*app\.safe\.global$/,/^https:\/\/([a-z0-9-]+\.)*metissafe\.tech$/],debug:false};var ke=r=>r.reduce((o,e)=>{let n=e.id;return o[n]=viem.http(),o},{});function ze(r,o){let{wagmiConfig:e}=r,n=r.siwx??(r.siwe?{enabled:r.siwe.enabled,isSignedIn:r.siwe.isSignedIn,isRejected:r.siwe.isRejected}:void 0),{disconnect:s,updateActiveConnection:a}=o,t=()=>o.getState?.()??o,i=()=>{let u=n?.isRejected||n?.status==="error",c=n?.isSignedIn??n?.isAuthenticated??false;if(n?.enabled!==false&&!c&&u){let{activeConnection:d}=t();d&&s(d.connectorType);}},m=u=>{let{activeConnection:c,connectionError:l}=t();if(c&&orbitCore.getAdapterFromConnectorType(c.connectorType)!==orbitCore.OrbitAdapter.EVM)return;if(u.length===0){c&&s(c.connectorType);return}let d=core.getConnection(e);if(c&&orbitCore.getAdapterFromConnectorType(c.connectorType)!==orbitCore.OrbitAdapter.EVM||!d||l)return;let E=n?.address??n?.session?.address,y=n?.chainId??n?.session?.chainId;if((n?.isSignedIn??n?.isAuthenticated??false)&&c){let b=d.address&&E&&d.address.toLowerCase()!==E.toLowerCase()&&!E.toLowerCase().endsWith(d.address.toLowerCase()),U=d.chainId&&y&&String(d.chainId)!==String(y)&&!y.endsWith(`:${d.chainId}`);if(b||U){s(c.connectorType);return}}let S=d.connector,D={connectorType:S?`${orbitCore.OrbitAdapter.EVM}:${orbitCore.formatConnectorName(S.name)}`:c?.connectorType,address:d.address,chainId:d.chainId,rpcURL:d?.chain?.rpcUrls.default.http[0],isConnected:true,signMessage:b=>core.signMessage(e,{message:b})};a(D);};i();let{activeConnection:p}=t();if(p&&orbitCore.getAdapterFromConnectorType(p.connectorType)===orbitCore.OrbitAdapter.EVM){let u=core.getConnection(e);u&&u.isConnected&&a({signMessage:c=>core.signMessage(e,{message:c})});}return core.watchConnections(e,{onChange:m})}exports.checkIsWalletAddressContract=M;exports.createDefaultTransports=ke;exports.createEVMConnectionsWatcher=ze;exports.impersonated=k;exports.safeSdkOptions=Re;exports.satelliteEVMAdapter=Se;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import {OrbitAdapter,getConnectorTypeFromName,formatConnectorName,impersonatedHelpers,getAdapterFromConnectorType}from'@tuwaio/orbit-core';import {createViemClient,getAddress,getAvatar,getName,checkAndSwitchChain}from'@tuwaio/orbit-evm';import {getBytecode,getConnectors,switchConnection,getChains,getConnection,getBalance,disconnect,connect,signMessage,createConnector,ChainNotConfiguredError,watchConnections}from'@wagmi/core';import {formatUnits,zeroAddress,custom,numberToHex,UserRejectedRequestError,fromHex,RpcRequestError,getAddress as getAddress$1,SwitchChainError,http}from'viem';import {mainnet}from'viem/chains';import {rpc}from'viem/utils';var y=new Map;async function A({config:o,address:s,chainId:e,chains:n}){if(y.has(s))return y.get(s);if(createViemClient(e,n)){let r=!!await getBytecode(o,{address:s});return y.set(s,r),r}else return false}function Ie(o,s){if(!o)throw new Error("Satellite EVM adapter requires a wagmi config object.");return {key:OrbitAdapter.EVM,connect:async({connectorType:e,chainId:n})=>{let a=getConnectors(o).find(r=>getConnectorTypeFromName(OrbitAdapter.EVM,formatConnectorName(r.name))===e);if(!a)throw new Error("Cannot find connector with this wallet type");try{await connect(o,{connector:a,chainId:n});let r=getConnection(o);return {connectorType:e,address:r.address??zeroAddress,chainId:r.chainId??mainnet.id,rpcURL:r.chain?.rpcUrls.default.http[0]??mainnet.rpcUrls.default.http[0],isConnected:r.isConnected,isContractAddress:!1,icon:a?.icon?.trim(),connector:a,signMessage:c=>signMessage(o,{message:c})}}catch(r){throw new Error(r instanceof Error?r.message:String(r),{cause:r})}},disconnect:async e=>{if(e&&e.isConnected)await disconnect(o,{connector:e?.connector});else {let n=getConnectors(o);await Promise.allSettled(n.map(async t=>{await disconnect(o,{connector:t});}));}},getConnectors:()=>{let e=getConnectors(o);return {adapter:OrbitAdapter.EVM,connectors:e.map(n=>n)}},checkAndSwitchNetwork:async e=>await checkAndSwitchChain(Number(e),o),getBalance:async(e,n)=>{let t=await getBalance(o,{address:e,chainId:Number(n)});return {value:formatUnits(t.value,t.decimals),symbol:t.symbol}},getExplorerUrl:e=>{let{chain:n}=getConnection(o),t=n?.blockExplorers?.default.url;return e?`${t}/${e}`:t},getName:e=>getName(e,s),getAvatar:e=>getAvatar(e,s),getAddress:e=>getAddress(e,s),checkIsContractAddress:async({address:e,chainId:n})=>{let t=getChains(o);return await A({config:o,address:e,chainId:n,chains:t})},getSafeConnectorChainId:async()=>{let n=getConnectors(o).find(t=>t.name==="Safe");if(n)return await n.getChainId()},switchConnection:async e=>{let t=getConnectors(o).find(a=>getConnectorTypeFromName(OrbitAdapter.EVM,formatConnectorName(a.name))===e);if(!t)throw new Error(`Cannot find connector with type: ${e}`);try{await switchConnection(o,{connector:t});}catch(a){throw new Error(`Failed to switch to connector ${e}: ${a instanceof Error?a.message:String(a)}`,{cause:a})}}}}q.type="impersonated";function q(o){let s=o.features??{},e=false,n,t;return createConnector(a=>({id:"impersonated",name:"Impersonated Connector",type:q.type,async setup(){n=a.chains[0].id;},async connect({chainId:r}={}){if(s.connectError)throw typeof s.connectError=="boolean"?new UserRejectedRequestError(new Error("Failed to connect.")):s.connectError;let{request:c}=await this.getProvider(),p=await c({method:"eth_requestAccounts"}),u=await this.getChainId();return r&&u!==r&&(u=(await this.switchChain({chainId:r})).id),e=true,{accounts:p,chainId:u}},async disconnect(){e=false,t=void 0;},async getAccounts(){if(!e)throw new Error("Not connected connector");let{request:r}=await this.getProvider();return (await r({method:"eth_accounts"})).map(getAddress$1)},async getChainId(){let{request:r}=await this.getProvider(),c=await r({method:"eth_chainId"});return fromHex(c,"number")},async isAuthorized(){return e?!!(await this.getAccounts()).length:false},async switchChain({chainId:r}){let c=a.chains.find(u=>u.id===r);if(!c)throw new SwitchChainError(new ChainNotConfiguredError);let{request:p}=await this.getProvider();return await p({method:"wallet_switchEthereumChain",params:[{chainId:numberToHex(r)}]}),c},onAccountsChanged(r){r.length===0?this.onDisconnect():a.emitter.emit("change",{accounts:r.map(getAddress$1)});},onChainChanged(r){let c=Number(r);a.emitter.emit("change",{chainId:c});},async onDisconnect(){a.emitter.emit("disconnect"),e=false,t=void 0;},async getProvider({chainId:r}={}){t=impersonatedHelpers?.getImpersonated()?[impersonatedHelpers.getImpersonated()||zeroAddress]:void 0;let p=(a.chains.find(d=>d.id===r)??a.chains[0]).rpcUrls.default.http[0];return custom({request:async({method:d,params:h})=>{if(d==="eth_chainId")return numberToHex(n);if(d==="eth_requestAccounts"||d==="eth_accounts")return t??[];if(d==="eth_signTypedData_v4"&&s.signTypedDataError)throw typeof s.signTypedDataError=="boolean"?new UserRejectedRequestError(new Error("Failed to sign typed data.")):s.signTypedDataError;if(d==="wallet_switchEthereumChain"){if(s.switchChainError)throw typeof s.switchChainError=="boolean"?new UserRejectedRequestError(new Error("Failed to switch chain.")):s.switchChainError;n=fromHex(h[0].chainId,"number"),this.onChainChanged(n.toString());return}if(d==="personal_sign"){if(s.signMessageError)throw typeof s.signMessageError=="boolean"?new UserRejectedRequestError(new Error("Failed to sign message.")):s.signMessageError;d="eth_sign",h=[h[1],h[0]];}let i={method:d,params:h},{error:l,result:m}=await rpc.http(p,{body:i});if(l)throw new RpcRequestError({body:i,error:l,url:p});return m}})({retryCount:1})}}))}var Pe={allowedDomains:[/gnosis-safe.io$/,/app.safe.global$/,/metissafe.tech$/],debug:false};var Ne=o=>o.reduce((s,e)=>{let n=e.id;return s[n]=http(),s},{});function je(o,s){let{wagmiConfig:e}=o,n=o.siwx??(o.siwe?{enabled:o.siwe.enabled,isSignedIn:o.siwe.isSignedIn,isRejected:o.siwe.isRejected}:void 0),{activeConnection:t,disconnect:a,connectionError:r,updateActiveConnection:c}=s,p=()=>{let h=n?.isRejected||n?.status==="error",i=n?.isSignedIn??n?.isAuthenticated??false;n?.enabled!==false&&!i&&h&&t&&a(t.connectorType);},u=h=>{if(t&&getAdapterFromConnectorType(t.connectorType)!==OrbitAdapter.EVM)return;if(h.length===0){t&&a(t.connectorType);return}let i=getConnection(e);if(t&&getAdapterFromConnectorType(t.connectorType)!==OrbitAdapter.EVM||!i||r)return;let l=n?.address??n?.session?.address,m=n?.chainId??n?.session?.chainId;if((n?.isSignedIn??n?.isAuthenticated??false)&&t){let E=i.address&&l&&i.address.toLowerCase()!==l.toLowerCase()&&!l.toLowerCase().endsWith(i.address.toLowerCase()),$=i.chainId&&m&&String(i.chainId)!==String(m)&&!m.endsWith(`:${i.chainId}`);if(E||$){a(t.connectorType);return}}let I=i.connector,_={connectorType:I?`${OrbitAdapter.EVM}:${formatConnectorName(I.name)}`:t?.connectorType,address:i.address,chainId:i.chainId,rpcURL:i?.chain?.rpcUrls.default.http[0],isConnected:true,signMessage:E=>signMessage(e,{message:E})};c(_);};if(p(),t&&getAdapterFromConnectorType(t.connectorType)===OrbitAdapter.EVM){let h=getConnection(e);h&&h.isConnected&&c({signMessage:i=>signMessage(e,{message:i})});}return watchConnections(e,{onChange:u})}export{A as checkIsWalletAddressContract,Ne as createDefaultTransports,je as createEVMConnectionsWatcher,q as impersonated,Pe as safeSdkOptions,Ie as satelliteEVMAdapter};
1
+ import {OrbitAdapter,getConnectorTypeFromName,formatConnectorName,impersonatedHelpers,getAdapterFromConnectorType}from'@tuwaio/orbit-core';import {createViemClient,getAddress,getAvatar,getName,checkAndSwitchChain}from'@tuwaio/orbit-evm';import {getBytecode,getConnectors,switchConnection,getChains,getConnection,getBalance,disconnect,connect,signMessage,createConnector,ChainNotConfiguredError,watchConnections}from'@wagmi/core';import {formatUnits,zeroAddress,custom,numberToHex,UserRejectedRequestError,fromHex,RpcRequestError,getAddress as getAddress$1,SwitchChainError,http}from'viem';import {mainnet}from'viem/chains';import {rpc}from'viem/utils';var I=new Map;async function M({config:r,address:o,chainId:e,chains:n}){let s=`${Number(e)}:${o.toLowerCase()}`;if(I.has(s))return I.get(s);if(createViemClient(Number(e),n)){let i=!!await getBytecode(r,{address:o,chainId:Number(e)});return I.set(s,i),i}else return false}function Se(r,o){if(!r)throw new Error("Satellite EVM adapter requires a wagmi config object.");return {key:OrbitAdapter.EVM,connect:async({connectorType:e,chainId:n})=>{let a=getConnectors(r).find(t=>getConnectorTypeFromName(OrbitAdapter.EVM,formatConnectorName(t.name))===e);if(!a)throw new Error("Cannot find connector with this wallet type");try{await connect(r,{connector:a,chainId:n});let t=getConnection(r);return {connectorType:e,address:t.address??zeroAddress,chainId:t.chainId??mainnet.id,rpcURL:t.chain?.rpcUrls.default.http[0]??mainnet.rpcUrls.default.http[0],isConnected:t.isConnected,isContractAddress:!1,icon:a?.icon?.trim(),connector:a,signMessage:i=>signMessage(r,{message:i})}}catch(t){throw new Error(t instanceof Error?t.message:String(t),{cause:t})}},disconnect:async e=>{if(e&&e.isConnected)await disconnect(r,{connector:e?.connector});else {let n=getConnectors(r);await Promise.allSettled(n.map(async s=>{await disconnect(r,{connector:s});}));}},getConnectors:()=>{let e=getConnectors(r);return {adapter:OrbitAdapter.EVM,connectors:e.map(n=>n)}},checkAndSwitchNetwork:async e=>await checkAndSwitchChain(Number(e),r),getBalance:async(e,n)=>{let s=await getBalance(r,{address:e,chainId:Number(n)});return {value:formatUnits(s.value,s.decimals),symbol:s.symbol}},getExplorerUrl:(e,n)=>{let a=(n===void 0?getConnection(r).chain:o.find(t=>t.id===Number(n))??getChains(r).find(t=>t.id===Number(n)))?.blockExplorers?.default.url;if(a)return e?`${a.replace(/\/+$/,"")}/${e.replace(/^\/+/,"")}`:a},getName:e=>getName(e,o),getAvatar:e=>getAvatar(e,o),getAddress:e=>getAddress(e,o),checkIsContractAddress:async({address:e,chainId:n})=>{let s=getChains(r);return await M({config:r,address:e,chainId:n,chains:s})},getSafeConnectorChainId:async()=>{let n=getConnectors(r).find(s=>s.name==="Safe");if(n)return await n.getChainId()},switchConnection:async e=>{let s=getConnectors(r).find(a=>getConnectorTypeFromName(OrbitAdapter.EVM,formatConnectorName(a.name))===e);if(!s)throw new Error(`Cannot find connector with type: ${e}`);try{await switchConnection(r,{connector:s});}catch(a){throw new Error(`Failed to switch to connector ${e}: ${a instanceof Error?a.message:String(a)}`,{cause:a})}}}}k.type="impersonated";function k(r){let o=r.features??{},e=false,n,s;return createConnector(a=>({id:"impersonated",name:"Impersonated Connector",type:k.type,async setup(){n=a.chains[0].id;},async connect({chainId:t}={}){if(o.connectError)throw typeof o.connectError=="boolean"?new UserRejectedRequestError(new Error("Failed to connect.")):o.connectError;let{request:i}=await this.getProvider(),m=await i({method:"eth_requestAccounts"}),p=await this.getChainId();return t&&p!==t&&(p=(await this.switchChain({chainId:t})).id),e=true,{accounts:m,chainId:p}},async disconnect(){e=false,s=void 0;},async getAccounts(){if(!e)throw new Error("Not connected connector");let{request:t}=await this.getProvider();return (await t({method:"eth_accounts"})).map(getAddress$1)},async getChainId(){let{request:t}=await this.getProvider(),i=await t({method:"eth_chainId"});return fromHex(i,"number")},async isAuthorized(){return e?!!(await this.getAccounts()).length:false},async switchChain({chainId:t}){let i=a.chains.find(p=>p.id===t);if(!i)throw new SwitchChainError(new ChainNotConfiguredError);let{request:m}=await this.getProvider();return await m({method:"wallet_switchEthereumChain",params:[{chainId:numberToHex(t)}]}),i},onAccountsChanged(t){t.length===0?this.onDisconnect():a.emitter.emit("change",{accounts:t.map(getAddress$1)});},onChainChanged(t){let i=Number(t);a.emitter.emit("change",{chainId:i});},async onDisconnect(){a.emitter.emit("disconnect"),e=false,s=void 0;},async getProvider({chainId:t}={}){s=impersonatedHelpers?.getImpersonated()?[impersonatedHelpers.getImpersonated()||zeroAddress]:void 0;let m=(a.chains.find(h=>h.id===t)??a.chains[0]).rpcUrls.default.http[0];return custom({request:async({method:h,params:u})=>{if(h==="eth_chainId")return numberToHex(n);if(h==="eth_requestAccounts"||h==="eth_accounts")return s??[];if(h==="eth_signTypedData_v4"&&o.signTypedDataError)throw typeof o.signTypedDataError=="boolean"?new UserRejectedRequestError(new Error("Failed to sign typed data.")):o.signTypedDataError;if(h==="wallet_switchEthereumChain"){if(o.switchChainError)throw typeof o.switchChainError=="boolean"?new UserRejectedRequestError(new Error("Failed to switch chain.")):o.switchChainError;n=fromHex(u[0].chainId,"number"),this.onChainChanged(n.toString());return}if(h==="personal_sign"){if(o.signMessageError)throw typeof o.signMessageError=="boolean"?new UserRejectedRequestError(new Error("Failed to sign message.")):o.signMessageError;h="eth_sign",u=[u[1],u[0]];}let c={method:h,params:u},{error:l,result:d}=await rpc.http(m,{body:c});if(l)throw new RpcRequestError({body:c,error:l,url:m});return d}})({retryCount:1})}}))}var Re={allowedDomains:[/^https:\/\/([a-z0-9-]+\.)*gnosis-safe\.io$/,/^https:\/\/([a-z0-9-]+\.)*app\.safe\.global$/,/^https:\/\/([a-z0-9-]+\.)*metissafe\.tech$/],debug:false};var ke=r=>r.reduce((o,e)=>{let n=e.id;return o[n]=http(),o},{});function ze(r,o){let{wagmiConfig:e}=r,n=r.siwx??(r.siwe?{enabled:r.siwe.enabled,isSignedIn:r.siwe.isSignedIn,isRejected:r.siwe.isRejected}:void 0),{disconnect:s,updateActiveConnection:a}=o,t=()=>o.getState?.()??o,i=()=>{let u=n?.isRejected||n?.status==="error",c=n?.isSignedIn??n?.isAuthenticated??false;if(n?.enabled!==false&&!c&&u){let{activeConnection:d}=t();d&&s(d.connectorType);}},m=u=>{let{activeConnection:c,connectionError:l}=t();if(c&&getAdapterFromConnectorType(c.connectorType)!==OrbitAdapter.EVM)return;if(u.length===0){c&&s(c.connectorType);return}let d=getConnection(e);if(c&&getAdapterFromConnectorType(c.connectorType)!==OrbitAdapter.EVM||!d||l)return;let E=n?.address??n?.session?.address,y=n?.chainId??n?.session?.chainId;if((n?.isSignedIn??n?.isAuthenticated??false)&&c){let b=d.address&&E&&d.address.toLowerCase()!==E.toLowerCase()&&!E.toLowerCase().endsWith(d.address.toLowerCase()),U=d.chainId&&y&&String(d.chainId)!==String(y)&&!y.endsWith(`:${d.chainId}`);if(b||U){s(c.connectorType);return}}let S=d.connector,D={connectorType:S?`${OrbitAdapter.EVM}:${formatConnectorName(S.name)}`:c?.connectorType,address:d.address,chainId:d.chainId,rpcURL:d?.chain?.rpcUrls.default.http[0],isConnected:true,signMessage:b=>signMessage(e,{message:b})};a(D);};i();let{activeConnection:p}=t();if(p&&getAdapterFromConnectorType(p.connectorType)===OrbitAdapter.EVM){let u=getConnection(e);u&&u.isConnected&&a({signMessage:c=>signMessage(e,{message:c})});}return watchConnections(e,{onChange:m})}export{M as checkIsWalletAddressContract,ke as createDefaultTransports,ze as createEVMConnectionsWatcher,k as impersonated,Re as safeSdkOptions,Se as satelliteEVMAdapter};
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tuwaio/satellite-evm",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "author": "Oleksandr Tkach",
6
6
  "license": "Apache-2.0",
7
- "description": "Layer 4 (L4) of the TUWA Ecosystem. Low-level EVM wallet connectivity adapters built strictly on top of viem and wagmi primitives.",
7
+ "description": "L4 EVM package of Satellite Connect (TUWA): wallet adapter and connections watcher for wagmi connectors, a contract account check and wagmi config helpers, built on @wagmi/core and viem.",
8
8
  "main": "./dist/index.js",
9
9
  "module": "./dist/index.mjs",
10
10
  "types": "./dist/index.d.ts",
@@ -16,13 +16,17 @@
16
16
  "socket.json"
17
17
  ],
18
18
  "keywords": [
19
+ "tuwa",
20
+ "satellite-connect",
21
+ "wallet-connection",
19
22
  "web3",
20
23
  "evm",
21
- "connect",
22
- "wallet",
23
24
  "wagmi",
24
25
  "viem",
25
- "zustand"
26
+ "ens",
27
+ "safe",
28
+ "typescript",
29
+ "headless"
26
30
  ],
27
31
  "repository": {
28
32
  "type": "git",
@@ -41,8 +45,6 @@
41
45
  ],
42
46
  "peerDependencies": {
43
47
  "@wagmi/core": "3.x.x",
44
- "immer": "11.x.x",
45
- "zustand": "5.x.x",
46
48
  "viem": "2.x.x",
47
49
  "@tuwaio/orbit-core": ">=0.3",
48
50
  "@tuwaio/orbit-evm": ">=0.3",
@@ -50,15 +52,13 @@
50
52
  },
51
53
  "devDependencies": {
52
54
  "@wagmi/core": "^3.6.5",
53
- "@tuwaio/orbit-core": "^0.3.0",
54
- "@tuwaio/orbit-evm": "^0.3.0",
55
- "@tuwaio/satellite-core": "^0.5.0",
56
- "immer": "^11.1.18",
55
+ "@tuwaio/orbit-core": "^0.3.1",
56
+ "@tuwaio/orbit-evm": "^0.3.1",
57
+ "@tuwaio/satellite-core": "^0.6.0",
57
58
  "tsup": "^8.5.1",
58
59
  "typescript": "6.0.3",
59
- "viem": "^2.56.3",
60
- "vitest": "^5.0.0",
61
- "zustand": "^5.0.15"
60
+ "viem": "^2.56.9",
61
+ "vitest": "^5.0.2"
62
62
  },
63
63
  "scripts": {
64
64
  "start": "tsup src/index.ts --watch",